Skip to content

Commit 2324b69

Browse files
authored
Merge pull request #1253 from json-api-dotnet/fix-modelstate-validation-non-required
Fixed: ModelState validation failed when [Range] does not include property default value
2 parents 5ffa4fb + 7d08a55 commit 2324b69

File tree

6 files changed

+27
-36
lines changed

6 files changed

+27
-36
lines changed

src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsPro
3232
protected override ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
3333
{
3434
var metadata = (DefaultModelMetadata)base.CreateModelMetadata(entry);
35-
36-
if (metadata.ValidationMetadata.IsRequired == true)
37-
{
38-
metadata.ValidationMetadata.PropertyValidationFilter = _jsonApiValidationFilter;
39-
}
35+
metadata.ValidationMetadata.PropertyValidationFilter = _jsonApiValidationFilter;
4036

4137
return metadata;
4238
}

src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using JsonApiDotNetCore.Middleware;
22
using JsonApiDotNetCore.Resources;
33
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
45
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
56
using Microsoft.Extensions.DependencyInjection;
67

@@ -23,15 +24,13 @@ public JsonApiValidationFilter(IHttpContextAccessor httpContextAccessor)
2324
/// <inheritdoc />
2425
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
2526
{
26-
IServiceProvider serviceProvider = GetScopedServiceProvider();
27-
28-
var request = serviceProvider.GetRequiredService<IJsonApiRequest>();
29-
30-
if (IsId(entry.Key))
27+
if (entry.Metadata.MetadataKind == ModelMetadataKind.Type || IsId(entry.Key))
3128
{
3229
return true;
3330
}
3431

32+
IServiceProvider serviceProvider = GetScopedServiceProvider();
33+
var request = serviceProvider.GetRequiredService<IJsonApiRequest>();
3534
bool isTopResourceInPrimaryRequest = string.IsNullOrEmpty(parentEntry.Key) && IsAtPrimaryEndpoint(request);
3635

3736
if (!isTopResourceInPrimaryRequest)

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ internal sealed class ModelStateFakers : FakerContainer
1717
new Faker<SystemFile>()
1818
.UseSeed(GetFakerSeed())
1919
.RuleFor(systemFile => systemFile.FileName, faker => faker.System.FileName())
20+
.RuleFor(systemFile => systemFile.Attributes, faker => faker.Random.Enum(FileAttributes.Normal, FileAttributes.Hidden, FileAttributes.ReadOnly))
2021
.RuleFor(systemFile => systemFile.SizeInBytes, faker => faker.Random.Long(0, 1_000_000)));
2122

2223
private readonly Lazy<Faker<SystemDirectory>> _lazySystemDirectoryFaker = new(() =>
2324
new Faker<SystemDirectory>()
2425
.UseSeed(GetFakerSeed())
25-
.RuleFor(systemDirectory => systemDirectory.Name, faker => faker.Address.City())
26-
.RuleFor(systemDirectory => systemDirectory.IsCaseSensitive, faker => faker.Random.Bool())
27-
.RuleFor(systemDirectory => systemDirectory.SizeInBytes, faker => faker.Random.Long(0, 1_000_000)));
26+
.RuleFor(systemDirectory => systemDirectory.Name, faker => Path.GetFileNameWithoutExtension(faker.System.FileName()))
27+
.RuleFor(systemDirectory => systemDirectory.IsCaseSensitive, faker => faker.Random.Bool()));
2828

2929
public Faker<SystemVolume> SystemVolume => _lazySystemVolumeFaker.Value;
3030
public Faker<SystemFile> SystemFile => _lazySystemFileFaker.Value;

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

+14-17
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,6 @@ public async Task Cannot_create_resource_with_multiple_violations()
166166
type = "systemDirectories",
167167
attributes = new
168168
{
169-
isCaseSensitive = false,
170-
sizeInBytes = -1
171169
}
172170
}
173171
};
@@ -192,9 +190,9 @@ public async Task Cannot_create_resource_with_multiple_violations()
192190
ErrorObject error2 = responseDocument.Errors[1];
193191
error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
194192
error2.Title.Should().Be("Input validation failed.");
195-
error2.Detail.Should().Be("The field SizeInBytes must be between 0 and 9223372036854775807.");
193+
error2.Detail.Should().Be("The IsCaseSensitive field is required.");
196194
error2.Source.ShouldNotBeNull();
197-
error2.Source.Pointer.Should().Be("/data/attributes/sizeInBytes");
195+
error2.Source.Pointer.Should().Be("/data/attributes/isCaseSensitive");
198196
}
199197

200198
[Fact]
@@ -205,15 +203,14 @@ public async Task Does_not_exceed_MaxModelValidationErrors()
205203
{
206204
data = new
207205
{
208-
type = "systemDirectories",
206+
type = "systemFiles",
209207
attributes = new
210208
{
211-
sizeInBytes = -1
212209
}
213210
}
214211
};
215212

216-
const string route = "/systemDirectories";
213+
const string route = "/systemFiles";
217214

218215
// Act
219216
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
@@ -232,16 +229,16 @@ public async Task Does_not_exceed_MaxModelValidationErrors()
232229
ErrorObject error2 = responseDocument.Errors[1];
233230
error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
234231
error2.Title.Should().Be("Input validation failed.");
235-
error2.Detail.Should().Be("The Name field is required.");
232+
error2.Detail.Should().Be("The FileName field is required.");
236233
error2.Source.ShouldNotBeNull();
237-
error2.Source.Pointer.Should().Be("/data/attributes/directoryName");
234+
error2.Source.Pointer.Should().Be("/data/attributes/fileName");
238235

239236
ErrorObject error3 = responseDocument.Errors[2];
240237
error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
241238
error3.Title.Should().Be("Input validation failed.");
242-
error3.Detail.Should().Be("The IsCaseSensitive field is required.");
239+
error3.Detail.Should().Be("The Attributes field is required.");
243240
error3.Source.ShouldNotBeNull();
244-
error3.Source.Pointer.Should().Be("/data/attributes/isCaseSensitive");
241+
error3.Source.Pointer.Should().Be("/data/attributes/attributes");
245242
}
246243

247244
[Fact]
@@ -360,30 +357,30 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
360357
public async Task Can_update_resource_with_omitted_required_attribute_value()
361358
{
362359
// Arrange
363-
SystemDirectory existingDirectory = _fakers.SystemDirectory.Generate();
360+
SystemFile existingFile = _fakers.SystemFile.Generate();
364361

365-
long newSizeInBytes = _fakers.SystemDirectory.Generate().SizeInBytes;
362+
long? newSizeInBytes = _fakers.SystemFile.Generate().SizeInBytes;
366363

367364
await _testContext.RunOnDatabaseAsync(async dbContext =>
368365
{
369-
dbContext.Directories.Add(existingDirectory);
366+
dbContext.Files.Add(existingFile);
370367
await dbContext.SaveChangesAsync();
371368
});
372369

373370
var requestBody = new
374371
{
375372
data = new
376373
{
377-
type = "systemDirectories",
378-
id = existingDirectory.StringId,
374+
type = "systemFiles",
375+
id = existingFile.StringId,
379376
attributes = new
380377
{
381378
sizeInBytes = newSizeInBytes
382379
}
383380
}
384381
};
385382

386-
string route = $"/systemDirectories/{existingDirectory.StringId}";
383+
string route = $"/systemFiles/{existingFile.StringId}";
387384

388385
// Act
389386
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync<string>(route, requestBody);

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

-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ public sealed class SystemDirectory : Identifiable<int>
2020
[Required]
2121
public bool? IsCaseSensitive { get; set; }
2222

23-
[Attr]
24-
[Range(typeof(long), "0", "9223372036854775807")]
25-
public long SizeInBytes { get; set; }
26-
2723
[HasMany]
2824
public ICollection<SystemDirectory> Subdirectories { get; set; } = new List<SystemDirectory>();
2925

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public sealed class SystemFile : Identifiable<int>
1515

1616
[Attr]
1717
[Required]
18-
[Range(typeof(long), "0", "9223372036854775807")]
19-
public long? SizeInBytes { get; set; }
18+
public FileAttributes? Attributes { get; set; }
19+
20+
[Attr]
21+
[Range(typeof(long), "1", "9223372036854775807")]
22+
public long SizeInBytes { get; set; }
2023
}

0 commit comments

Comments
 (0)