Skip to content

Commit 6becd1f

Browse files
author
Bart Koelman
committed
Enable ASP.NET ModelState validation by default
Due to recent enhancements, this produces better error messages for missing required relationships and avoids 500 errors due to foreign key constraint violations.
1 parent 6fc7825 commit 6becd1f

39 files changed

+203
-107
lines changed

docs/usage/options.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ options.SerializerOptions.DictionaryKeyPolicy = null;
100100
Because we copy resource properties into an intermediate object before serialization, JSON annotations such as `[JsonPropertyName]` and `[JsonIgnore]` on `[Attr]` properties are ignored.
101101

102102

103-
## Enable ModelState Validation
103+
## ModelState Validation
104104

105-
If you would like to use ASP.NET ModelState validation into your controllers when creating / updating resources, set `ValidateModelState` to `true`. By default, no model validation is performed.
105+
[ASP.NET ModelState validation](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) can be used to validate incoming request bodies when creating and updating resources. Since v5.0, this is enabled by default.
106+
When `ValidateModelState` is set to `false`, no model validation is performed.
106107

107108
How nullability affects ModelState validation is described [here](~/usage/resources/nullability.md).
108109

@@ -117,5 +118,8 @@ public class Person : Identifiable<int>
117118
[Required]
118119
[MinLength(3)]
119120
public string FirstName { get; set; }
121+
122+
[Required]
123+
public LoginAccount Account : get; set; }
120124
}
121125
```

docs/usage/resources/nullability.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Properties on a resource class can be declared as nullable or non-nullable. This affects both ASP.NET ModelState validation and the way Entity Framework Core generates database columns.
44

5-
Note that ModelState validation is turned off by default. It can be enabled in [options](~/usage/options.md#enable-modelstate-validation).
5+
ModelState validation is enabled by default since v5.0. In earlier versions, it can be enabled in [options](~/usage/options.md#enable-modelstate-validation).
66

77
# Value types
88

src/Examples/JsonApiDotNetCoreExample/Startup.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public void ConfigureServices(IServiceCollection services)
4949
{
5050
options.Namespace = "api/v1";
5151
options.UseRelativeLinks = true;
52-
options.ValidateModelState = true;
5352
options.IncludeTotalResourceCount = true;
5453
options.SerializerOptions.WriteIndented = true;
5554
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());

src/JsonApiDotNetCore/Configuration/IInverseNavigationResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Configuration
55
{
66
/// <summary>
77
/// Responsible for populating <see cref="RelationshipAttribute.InverseNavigationProperty" />. This service is instantiated in the configure phase of the
8-
/// application. When using a data access layer different from EF Core, you will need to implement and register this service, or set
8+
/// application. When using a data access layer different from Entity Framework Core, you will need to implement and register this service, or set
99
/// <see cref="RelationshipAttribute.InverseNavigationProperty" /> explicitly.
1010
/// </summary>
1111
[PublicAPI]

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public interface IJsonApiOptions
103103
PageNumber? MaximumPageNumber { get; }
104104

105105
/// <summary>
106-
/// Whether or not to enable ASP.NET Core model state validation. False by default.
106+
/// Whether or not to enable ASP.NET ModelState validation. True by default.
107107
/// </summary>
108108
bool ValidateModelState { get; }
109109

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<Reso
9696
}
9797

9898
/// <summary>
99-
/// Configures built-in ASP.NET Core MVC components. Most of this configuration can be adjusted for the developers' need.
99+
/// Configures built-in ASP.NET MVC components. Most of this configuration can be adjusted for the developers' need.
100100
/// </summary>
101101
public void ConfigureMvc()
102102
{

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public sealed class JsonApiOptions : IJsonApiOptions
6666
public PageNumber? MaximumPageNumber { get; set; }
6767

6868
/// <inheritdoc />
69-
public bool ValidateModelState { get; set; }
69+
public bool ValidateModelState { get; set; } = true;
7070

7171
/// <inheritdoc />
7272
public bool AllowClientGeneratedIds { get; set; }

src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
namespace JsonApiDotNetCore.Configuration
1010
{
1111
/// <summary>
12-
/// Validation filter that blocks ASP.NET Core ModelState validation on data according to the JSON:API spec.
12+
/// Validation filter that blocks ASP.NET ModelState validation on data according to the JSON:API spec.
1313
/// </summary>
1414
internal sealed class JsonApiValidationFilter : IPropertyValidationFilter
1515
{

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ public ResourceGraphBuilder Add(DbContext dbContext)
6666

6767
private static bool IsImplicitManyToManyJoinEntity(IEntityType entityType)
6868
{
69-
#pragma warning disable EF1001 // Internal EF Core API usage.
69+
#pragma warning disable EF1001 // Internal Entity Framework Core API usage.
7070
return entityType is EntityType { IsImplicitlyCreatedJoinEntityType: true };
71-
#pragma warning restore EF1001 // Internal EF Core API usage.
71+
#pragma warning restore EF1001 // Internal Entity Framework Core API usage.
7272
}
7373

7474
/// <summary>

src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace JsonApiDotNetCore.Controllers.Annotations
88
{
99
/// <summary>
10-
/// Used on an ASP.NET Core controller class to indicate which query string parameters are blocked.
10+
/// Used on an ASP.NET controller class to indicate which query string parameters are blocked.
1111
/// </summary>
1212
/// <example><![CDATA[
1313
/// [DisableQueryString(JsonApiQueryStringParameters.Sort | JsonApiQueryStringParameters.Page)]

src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace JsonApiDotNetCore.Controllers.Annotations
55
{
66
/// <summary>
7-
/// Used on an ASP.NET Core controller class to indicate that a custom route is used instead of the built-in routing convention.
7+
/// Used on an ASP.NET controller class to indicate that a custom route is used instead of the built-in routing convention.
88
/// </summary>
99
/// <example><![CDATA[
1010
/// [DisableRoutingConvention, Route("some/custom/route/to/customers")]

src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace JsonApiDotNetCore.Controllers.Annotations
44
{
55
/// <summary>
6-
/// Used on an ASP.NET Core controller class to indicate write actions must be blocked.
6+
/// Used on an ASP.NET controller class to indicate write actions must be blocked.
77
/// </summary>
88
/// <example><![CDATA[
99
/// [HttpReadOnly]

src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace JsonApiDotNetCore.Controllers.Annotations
44
{
55
/// <summary>
6-
/// Used on an ASP.NET Core controller class to indicate the DELETE verb must be blocked.
6+
/// Used on an ASP.NET controller class to indicate the DELETE verb must be blocked.
77
/// </summary>
88
/// <example><![CDATA[
99
/// [NoHttpDelete]

src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace JsonApiDotNetCore.Controllers.Annotations
44
{
55
/// <summary>
6-
/// Used on an ASP.NET Core controller class to indicate the PATCH verb must be blocked.
6+
/// Used on an ASP.NET controller class to indicate the PATCH verb must be blocked.
77
/// </summary>
88
/// <example><![CDATA[
99
/// [NoHttpPatch]

src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace JsonApiDotNetCore.Controllers.Annotations
44
{
55
/// <summary>
6-
/// Used on an ASP.NET Core controller class to indicate the POST verb must be blocked.
6+
/// Used on an ASP.NET controller class to indicate the POST verb must be blocked.
77
/// </summary>
88
/// <example><![CDATA[
99
/// [NoHttpost]

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace JsonApiDotNetCore.Controllers
1414
{
1515
/// <summary>
16-
/// Implements the foundational ASP.NET Core controller layer in the JsonApiDotNetCore architecture that delegates to a Resource Service.
16+
/// Implements the foundational ASP.NET controller layer in the JsonApiDotNetCore architecture that delegates to a Resource Service.
1717
/// </summary>
1818
/// <typeparam name="TResource">
1919
/// The resource type.

src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
namespace JsonApiDotNetCore.Controllers
1717
{
1818
/// <summary>
19-
/// Implements the foundational ASP.NET Core controller layer in the JsonApiDotNetCore architecture for handling atomic:operations requests. See
19+
/// Implements the foundational ASP.NET controller layer in the JsonApiDotNetCore architecture for handling atomic:operations requests. See
2020
/// https://jsonapi.org/ext/atomic/ for details. Delegates work to <see cref="IOperationsProcessor" />.
2121
/// </summary>
2222
[PublicAPI]

src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
namespace JsonApiDotNetCore.Errors
1616
{
1717
/// <summary>
18-
/// The error that is thrown when model state validation fails.
18+
/// The error that is thrown when ASP.NET ModelState validation fails.
1919
/// </summary>
2020
[PublicAPI]
2121
public sealed class InvalidModelStateException : JsonApiException

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<PropertyGroup>
99
<PackageTags>jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net</PackageTags>
10-
<Description>A framework for building JSON:API compliant REST APIs using .NET Core and Entity Framework Core. Includes support for Atomic Operations. The ultimate goal of this library is to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination. You just need to focus on defining the resources and implementing your custom business logic. This library has been designed around dependency injection making extensibility incredibly easy.</Description>
10+
<Description>A framework for building JSON:API compliant REST APIs using ASP.NET and Entity Framework Core. Includes support for Atomic Operations. The ultimate goal of this library is to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination. You just need to focus on defining the resources and implementing your custom business logic. This library has been designed around dependency injection making extensibility incredibly easy.</Description>
1111
<PackageProjectUrl>https://www.jsonapi.net/</PackageProjectUrl>
1212
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1313
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>

src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,8 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke
330330

331331
foreach (RelationshipAttribute relationship in _resourceGraph.GetResourceType<TResource>().Relationships)
332332
{
333-
// Loads the data of the relationship, if in EF Core it is configured in such a way that loading the related
334-
// entities into memory is required for successfully executing the selected deletion behavior.
333+
// Loads the data of the relationship, if in Entity Framework Core it is configured in such a way that loading
334+
// the related entities into memory is required for successfully executing the selected deletion behavior.
335335
if (RequiresLoadOfRelationshipForDeletion(relationship))
336336
{
337337
NavigationEntry navigation = GetNavigationEntry(resourceTracked, relationship);
@@ -475,7 +475,7 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
475475
{
476476
var leftResourceTracked = (TResource)_dbContext.GetTrackedOrAttach(leftResource);
477477

478-
// Make EF Core believe any additional resources added from ResourceDefinition already exist in database.
478+
// Make Entity Framework Core believe any additional resources added from ResourceDefinition already exist in database.
479479
IIdentifiable[] extraResourceIdsToRemove = rightResourceIdsToRemove.Where(rightId => !rightResourceIds.Contains(rightId)).ToArray();
480480

481481
object? rightValueStored = relationship.GetValue(leftResource);

src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute
3232
internal Type? RightClrType { get; set; }
3333

3434
/// <summary>
35-
/// The <see cref="PropertyInfo" /> of the EF Core inverse navigation, which may or may not exist. Even if it exists, it may not be exposed as a JSON:API
36-
/// relationship.
35+
/// The <see cref="PropertyInfo" /> of the Entity Framework Core inverse navigation, which may or may not exist. Even if it exists, it may not be exposed
36+
/// as a JSON:API relationship.
3737
/// </summary>
3838
/// <example>
3939
/// <code><![CDATA[

src/JsonApiDotNetCore/Serialization/Request/IJsonApiReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace JsonApiDotNetCore.Serialization.Request
66
{
77
/// <summary>
8-
/// Deserializes the incoming JSON:API request body and converts it to models, which are passed to controller actions by ASP.NET Core on `FromBody`
8+
/// Deserializes the incoming JSON:API request body and converts it to models, which are passed to controller actions by ASP.NET on `FromBody`
99
/// parameters.
1010
/// </summary>
1111
[PublicAPI]

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
33
using System.Threading.Tasks;
44
using FluentAssertions;
55
using JsonApiDotNetCore.Serialization.Objects;
6-
using JsonApiDotNetCoreTests.Startups;
76
using Microsoft.EntityFrameworkCore;
87
using TestBuildingBlocks;
98
using Xunit;
109

1110
namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.ModelStateValidation
1211
{
13-
public sealed class AtomicModelStateValidationTests
14-
: IClassFixture<IntegrationTestContext<ModelStateValidationStartup<OperationsDbContext>, OperationsDbContext>>
12+
public sealed class AtomicModelStateValidationTests : IClassFixture<IntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext>>
1513
{
16-
private readonly IntegrationTestContext<ModelStateValidationStartup<OperationsDbContext>, OperationsDbContext> _testContext;
14+
private readonly IntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext> _testContext;
1715
private readonly OperationsFakers _fakers = new();
1816

19-
public AtomicModelStateValidationTests(IntegrationTestContext<ModelStateValidationStartup<OperationsDbContext>, OperationsDbContext> testContext)
17+
public AtomicModelStateValidationTests(IntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext> testContext)
2018
{
2119
_testContext = testContext;
2220

test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44
using FluentAssertions;
55
using JsonApiDotNetCore.Configuration;
66
using JsonApiDotNetCore.Serialization.Objects;
7-
using JsonApiDotNetCoreTests.Startups;
87
using Microsoft.EntityFrameworkCore;
98
using TestBuildingBlocks;
109
using Xunit;
1110

1211
namespace JsonApiDotNetCoreTests.IntegrationTests.EagerLoading
1312
{
14-
public sealed class EagerLoadingTests : IClassFixture<IntegrationTestContext<ModelStateValidationStartup<EagerLoadingDbContext>, EagerLoadingDbContext>>
13+
public sealed class EagerLoadingTests : IClassFixture<IntegrationTestContext<TestableStartup<EagerLoadingDbContext>, EagerLoadingDbContext>>
1514
{
16-
private readonly IntegrationTestContext<ModelStateValidationStartup<EagerLoadingDbContext>, EagerLoadingDbContext> _testContext;
15+
private readonly IntegrationTestContext<TestableStartup<EagerLoadingDbContext>, EagerLoadingDbContext> _testContext;
1716
private readonly EagerLoadingFakers _fakers = new();
1817

19-
public EagerLoadingTests(IntegrationTestContext<ModelStateValidationStartup<EagerLoadingDbContext>, EagerLoadingDbContext> testContext)
18+
public EagerLoadingTests(IntegrationTestContext<TestableStartup<EagerLoadingDbContext>, EagerLoadingDbContext> testContext)
2019
{
2120
_testContext = testContext;
2221

test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateDbContext.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.InputValidation.ModelState
88
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
99
public sealed class ModelStateDbContext : DbContext
1010
{
11+
public DbSet<SystemVolume> Volumes => Set<SystemVolume>();
1112
public DbSet<SystemDirectory> Directories => Set<SystemDirectory>();
1213
public DbSet<SystemFile> Files => Set<SystemFile>();
1314

@@ -18,6 +19,12 @@ public ModelStateDbContext(DbContextOptions<ModelStateDbContext> options)
1819

1920
protected override void OnModelCreating(ModelBuilder builder)
2021
{
22+
builder.Entity<SystemVolume>()
23+
.HasOne(systemVolume => systemVolume.RootDirectory)
24+
.WithOne()
25+
.HasForeignKey<SystemVolume>("RootDirectoryId")
26+
.IsRequired();
27+
2128
builder.Entity<SystemDirectory>()
2229
.HasMany(systemDirectory => systemDirectory.Subdirectories)
2330
.WithOne(systemDirectory => systemDirectory.Parent!);

test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateFakers.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.InputValidation.ModelState
99
{
1010
internal sealed class ModelStateFakers : FakerContainer
1111
{
12+
private readonly Lazy<Faker<SystemVolume>> _lazySystemVolumeFaker = new(() =>
13+
new Faker<SystemVolume>()
14+
.UseSeed(GetFakerSeed())
15+
.RuleFor(systemVolume => systemVolume.Name, faker => faker.Lorem.Word()));
16+
1217
private readonly Lazy<Faker<SystemFile>> _lazySystemFileFaker = new(() =>
1318
new Faker<SystemFile>()
1419
.UseSeed(GetFakerSeed())
@@ -22,6 +27,7 @@ internal sealed class ModelStateFakers : FakerContainer
2227
.RuleFor(systemDirectory => systemDirectory.IsCaseSensitive, faker => faker.Random.Bool())
2328
.RuleFor(systemDirectory => systemDirectory.SizeInBytes, faker => faker.Random.Long(0, 1_000_000)));
2429

30+
public Faker<SystemVolume> SystemVolume => _lazySystemVolumeFaker.Value;
2531
public Faker<SystemFile> SystemFile => _lazySystemFileFaker.Value;
2632
public Faker<SystemDirectory> SystemDirectory => _lazySystemDirectoryFaker.Value;
2733
}

test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@
44
using System.Threading.Tasks;
55
using FluentAssertions;
66
using JsonApiDotNetCore.Serialization.Objects;
7-
using JsonApiDotNetCoreTests.Startups;
87
using TestBuildingBlocks;
98
using Xunit;
109

1110
namespace JsonApiDotNetCoreTests.IntegrationTests.InputValidation.ModelState
1211
{
13-
public sealed class ModelStateValidationTests : IClassFixture<IntegrationTestContext<ModelStateValidationStartup<ModelStateDbContext>, ModelStateDbContext>>
12+
public sealed class ModelStateValidationTests : IClassFixture<IntegrationTestContext<TestableStartup<ModelStateDbContext>, ModelStateDbContext>>
1413
{
15-
private readonly IntegrationTestContext<ModelStateValidationStartup<ModelStateDbContext>, ModelStateDbContext> _testContext;
14+
private readonly IntegrationTestContext<TestableStartup<ModelStateDbContext>, ModelStateDbContext> _testContext;
1615
private readonly ModelStateFakers _fakers = new();
1716

18-
public ModelStateValidationTests(IntegrationTestContext<ModelStateValidationStartup<ModelStateDbContext>, ModelStateDbContext> testContext)
17+
public ModelStateValidationTests(IntegrationTestContext<TestableStartup<ModelStateDbContext>, ModelStateDbContext> testContext)
1918
{
2019
_testContext = testContext;
2120

0 commit comments

Comments
 (0)