Skip to content

Commit c68d3b7

Browse files
Use JSON Property Name attributes when creating ModelState Validation errors (dotnet#39008)
* Core functionality * Clean up * Moving to JsonOptions * Moving from abstract to virtual * Removing from JSonOptions * Updating constructor * Removing from default * Renaming as suggested during API Review * Renaming as suggested during API Review * Updating api * Update src/Mvc/Mvc.Core/src/ModelBinding/Metadata/SystemTextJsonValidationMetadataProvider.cs Co-authored-by: Pranav K <prkrishn@hotmail.com> * Adding SystemTextJsonValidationMetadataProvider unit test * Adding unittest * Adding unit test * Fixing coding style * Fix formatting Co-authored-by: Pranav K <prkrishn@hotmail.com>
1 parent ed0a740 commit c68d3b7

11 files changed

+408
-1
lines changed

Diff for: src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs

+5
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,11 @@ internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> BoundConstructorPrope
494494
/// </summary>
495495
internal virtual bool PropertyHasValidators => false;
496496

497+
/// <summary>
498+
/// Gets the name of a model, if specified explicitly, to be used on <see cref="ValidationEntry"/>
499+
/// </summary>
500+
internal virtual string? ValidationModelName { get; }
501+
497502
/// <summary>
498503
/// Throws if the ModelMetadata is for a record type with validation on properties.
499504
/// </summary>

Diff for: src/Mvc/Mvc.Core/src/ModelBinding/Metadata/DefaultModelMetadata.cs

+3
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,9 @@ public override bool? HasValidators
471471

472472
internal override bool PropertyHasValidators => ValidationMetadata.PropertyHasValidators;
473473

474+
/// <inheritdoc />
475+
internal override string? ValidationModelName => ValidationMetadata.ValidationModelName;
476+
474477
internal static bool CalculateHasValidators(HashSet<DefaultModelMetadata> visited, ModelMetadata metadata)
475478
{
476479
RuntimeHelpers.EnsureSufficientExecutionStack();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
using System.Linq;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
10+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
11+
12+
/// <summary>
13+
/// An implementation of <see cref="IDisplayMetadataProvider"/> and <see cref="IValidationMetadataProvider"/> for
14+
/// the System.Text.Json.Serialization attribute classes.
15+
/// </summary>
16+
public sealed class SystemTextJsonValidationMetadataProvider : IDisplayMetadataProvider, IValidationMetadataProvider
17+
{
18+
private readonly JsonNamingPolicy _jsonNamingPolicy;
19+
20+
/// <summary>
21+
/// Creates a new <see cref="SystemTextJsonValidationMetadataProvider"/> with the default <see cref="JsonNamingPolicy.CamelCase"/>
22+
/// </summary>
23+
public SystemTextJsonValidationMetadataProvider()
24+
: this(JsonNamingPolicy.CamelCase)
25+
{ }
26+
27+
/// <summary>
28+
/// Creates a new <see cref="SystemTextJsonValidationMetadataProvider"/> with an optional <see cref="JsonNamingPolicy"/>
29+
/// </summary>
30+
/// <param name="namingPolicy">The <see cref="JsonNamingPolicy"/> to be used to configure the metadata provider.</param>
31+
public SystemTextJsonValidationMetadataProvider(JsonNamingPolicy namingPolicy)
32+
{
33+
if (namingPolicy == null)
34+
{
35+
throw new ArgumentNullException(nameof(namingPolicy));
36+
}
37+
38+
_jsonNamingPolicy = namingPolicy;
39+
}
40+
41+
/// <inheritdoc />
42+
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
43+
{
44+
if (context == null)
45+
{
46+
throw new ArgumentNullException(nameof(context));
47+
}
48+
49+
var propertyName = ReadPropertyNameFrom(context.Attributes);
50+
51+
if (!string.IsNullOrEmpty(propertyName))
52+
{
53+
context.DisplayMetadata.DisplayName = () => propertyName;
54+
}
55+
}
56+
57+
/// <inheritdoc />
58+
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
59+
{
60+
if (context == null)
61+
{
62+
throw new ArgumentNullException(nameof(context));
63+
}
64+
65+
var propertyName = ReadPropertyNameFrom(context.Attributes);
66+
67+
if (string.IsNullOrEmpty(propertyName))
68+
{
69+
propertyName = _jsonNamingPolicy.ConvertName(context.Key.Name!);
70+
}
71+
72+
context.ValidationMetadata.ValidationModelName = propertyName;
73+
}
74+
75+
private static string? ReadPropertyNameFrom(IReadOnlyList<object> attributes)
76+
=> attributes?.OfType<JsonPropertyNameAttribute>().FirstOrDefault()?.Name;
77+
}

Diff for: src/Mvc/Mvc.Core/src/ModelBinding/Metadata/ValidationMetadata.cs

+5
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,9 @@ public class ValidationMetadata
5252
/// Gets or sets a value that determines if validators can be constructed using metadata on properties.
5353
/// </summary>
5454
internal bool PropertyHasValidators { get; set; }
55+
56+
/// <summary>
57+
/// Gets or sets a model name that will be used in <see cref="ValidationEntry"/>.
58+
/// </summary>
59+
public string? ValidationModelName { get; set; }
5560
}

Diff for: src/Mvc/Mvc.Core/src/ModelBinding/Validation/DefaultComplexObjectValidationStrategy.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public bool MoveNext()
105105
else
106106
{
107107
var property = _properties[_index - _parameters.Count];
108-
var propertyName = property.BinderModelName ?? property.PropertyName;
108+
var propertyName = property.ValidationModelName ?? property.BinderModelName ?? property.PropertyName;
109109
var key = ModelNames.CreatePropertyModelName(_key, propertyName);
110110

111111
if (_model == null)

Diff for: src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt

+7
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider
3+
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateDisplayMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DisplayMetadataProviderContext! context) -> void
4+
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateValidationMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadataProviderContext! context) -> void
5+
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.SystemTextJsonValidationMetadataProvider() -> void
6+
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.SystemTextJsonValidationMetadataProvider(System.Text.Json.JsonNamingPolicy! namingPolicy) -> void
7+
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.get -> string?
8+
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.set -> void
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
8+
9+
public class SystemTextJsonValidationMetadataProviderTest
10+
{
11+
[Fact]
12+
public void CreateValidationMetadata_SetValidationPropertyName_WithJsonPropertyNameAttribute()
13+
{
14+
var metadataProvider = new SystemTextJsonValidationMetadataProvider();
15+
var propertyName = "sample-data";
16+
17+
var key = ModelMetadataIdentity.ForProperty(typeof(SampleTestClass).GetProperty(nameof(SampleTestClass.NoAttributesProperty)), typeof(int), typeof(SampleTestClass));
18+
var modelAttributes = new ModelAttributes(Array.Empty<object>(), new[] { new JsonPropertyNameAttribute(propertyName) }, Array.Empty<object>());
19+
var context = new ValidationMetadataProviderContext(key, modelAttributes);
20+
21+
// Act
22+
metadataProvider.CreateValidationMetadata(context);
23+
24+
// Assert
25+
Assert.NotNull(context.ValidationMetadata.ValidationModelName);
26+
Assert.Equal(propertyName, context.ValidationMetadata.ValidationModelName);
27+
}
28+
29+
[Fact]
30+
public void CreateValidationMetadata_SetValidationPropertyName_CamelCaseWithDefaultNamingPolicy()
31+
{
32+
var metadataProvider = new SystemTextJsonValidationMetadataProvider();
33+
var propertyName = nameof(SampleTestClass.NoAttributesProperty);
34+
35+
var key = ModelMetadataIdentity.ForProperty(typeof(SampleTestClass).GetProperty(propertyName), typeof(int), typeof(SampleTestClass));
36+
var modelAttributes = new ModelAttributes(Array.Empty<object>(), Array.Empty<object>(), Array.Empty<object>());
37+
var context = new ValidationMetadataProviderContext(key, modelAttributes);
38+
39+
// Act
40+
metadataProvider.CreateValidationMetadata(context);
41+
42+
// Assert
43+
Assert.NotNull(context.ValidationMetadata.ValidationModelName);
44+
Assert.Equal(JsonNamingPolicy.CamelCase.ConvertName(propertyName), context.ValidationMetadata.ValidationModelName);
45+
}
46+
47+
[Theory]
48+
[MemberData(nameof(NamingPolicies))]
49+
public void CreateValidationMetadata_SetValidationPropertyName_WithJsonNamingPolicy(JsonNamingPolicy namingPolicy)
50+
{
51+
var metadataProvider = new SystemTextJsonValidationMetadataProvider(namingPolicy);
52+
var propertyName = nameof(SampleTestClass.NoAttributesProperty);
53+
54+
var key = ModelMetadataIdentity.ForProperty(typeof(SampleTestClass).GetProperty(propertyName), typeof(int), typeof(SampleTestClass));
55+
var modelAttributes = new ModelAttributes(Array.Empty<object>(), Array.Empty<object>(), Array.Empty<object>());
56+
var context = new ValidationMetadataProviderContext(key, modelAttributes);
57+
58+
// Act
59+
metadataProvider.CreateValidationMetadata(context);
60+
61+
// Assert
62+
Assert.NotNull(context.ValidationMetadata.ValidationModelName);
63+
Assert.Equal(namingPolicy.ConvertName(propertyName), context.ValidationMetadata.ValidationModelName);
64+
}
65+
66+
public static TheoryData<JsonNamingPolicy> NamingPolicies
67+
{
68+
get
69+
{
70+
return new TheoryData<JsonNamingPolicy>
71+
{
72+
UpperCaseJsonNamingPolicy.Instance,
73+
JsonNamingPolicy.CamelCase
74+
};
75+
}
76+
}
77+
78+
public class UpperCaseJsonNamingPolicy : System.Text.Json.JsonNamingPolicy
79+
{
80+
public static JsonNamingPolicy Instance = new UpperCaseJsonNamingPolicy();
81+
82+
public override string ConvertName(string name)
83+
{
84+
return name?.ToUpperInvariant();
85+
}
86+
}
87+
88+
public class SampleTestClass
89+
{
90+
public int NoAttributesProperty { get; set; }
91+
}
92+
}

Diff for: src/Mvc/Mvc.Core/test/ModelBinding/Validation/DefaultComplexObjectValidationStrategyTest.cs

+48
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
55

6+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
7+
68
public class DefaultComplexObjectValidationStrategyTest
79
{
810
[Fact]
@@ -45,6 +47,46 @@ public void GetChildren_ReturnsExpectedElements()
4547
});
4648
}
4749

50+
[Fact]
51+
public void GetChildren_ReturnsExpectedElements_WithValidationModelName()
52+
{
53+
// Arrange
54+
var model = new Person()
55+
{
56+
Age = 23,
57+
Id = 1,
58+
Name = "Joey",
59+
};
60+
61+
var metadata = TestModelMetadataProvider.CreateDefaultProvider(new List<IMetadataDetailsProvider> { new TestValidationModelNameProvider() }).GetMetadataForType(typeof(Person));
62+
var strategy = DefaultComplexObjectValidationStrategy.Instance;
63+
64+
// Act
65+
var enumerator = strategy.GetChildren(metadata, "prefix", model);
66+
67+
// Assert
68+
Assert.Collection(
69+
BufferEntries(enumerator).OrderBy(e => e.Key),
70+
entry =>
71+
{
72+
Assert.Equal("prefix.AGE", entry.Key);
73+
Assert.Equal(23, entry.Model);
74+
Assert.Same(metadata.Properties["Age"], entry.Metadata);
75+
},
76+
entry =>
77+
{
78+
Assert.Equal("prefix.ID", entry.Key);
79+
Assert.Equal(1, entry.Model);
80+
Assert.Same(metadata.Properties["Id"], entry.Metadata);
81+
},
82+
entry =>
83+
{
84+
Assert.Equal("prefix.NAME", entry.Key);
85+
Assert.Equal("Joey", entry.Model);
86+
Assert.Same(metadata.Properties["Name"], entry.Metadata);
87+
});
88+
}
89+
4890
[Fact]
4991
public void GetChildren_SetsModelNull_IfContainerNull()
5092
{
@@ -149,4 +191,10 @@ public LazyPerson(string input)
149191

150192
public string Name => _string.Substring(3, 5);
151193
}
194+
195+
private class TestValidationModelNameProvider : IValidationMetadataProvider
196+
{
197+
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
198+
=> context.ValidationMetadata.ValidationModelName = context.Key.Name?.ToUpperInvariant();
199+
}
152200
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
6+
using Newtonsoft.Json;
7+
using Newtonsoft.Json.Serialization;
8+
9+
namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson;
10+
11+
/// <summary>
12+
/// An implementation of <see cref="IDisplayMetadataProvider"/> and <see cref="IValidationMetadataProvider"/> for
13+
/// the Newtonsoft.Json attribute classes.
14+
/// </summary>
15+
public sealed class NewtonsoftJsonValidationMetadataProvider : IDisplayMetadataProvider, IValidationMetadataProvider
16+
{
17+
private readonly NamingStrategy _jsonNamingPolicy;
18+
19+
/// <summary>
20+
/// Creates a new <see cref="NewtonsoftJsonValidationMetadataProvider"/> with the default <see cref="CamelCaseNamingStrategy"/>
21+
/// </summary>
22+
public NewtonsoftJsonValidationMetadataProvider()
23+
: this(new CamelCaseNamingStrategy())
24+
{ }
25+
26+
/// <summary>
27+
/// Initializes a new instance of <see cref="NewtonsoftJsonValidationMetadataProvider"/> with an optional <see cref="NamingStrategy"/>
28+
/// </summary>
29+
/// <param name="namingStrategy">The <see cref="NamingStrategy"/> to be used to configure the metadata provider.</param>
30+
public NewtonsoftJsonValidationMetadataProvider(NamingStrategy namingStrategy)
31+
{
32+
if (namingStrategy == null)
33+
{
34+
throw new ArgumentNullException(nameof(namingStrategy));
35+
}
36+
37+
_jsonNamingPolicy = namingStrategy;
38+
}
39+
40+
/// <inheritdoc />
41+
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
42+
{
43+
if (context == null)
44+
{
45+
throw new ArgumentNullException(nameof(context));
46+
}
47+
48+
var propertyName = ReadPropertyNameFrom(context.Attributes);
49+
50+
if (!string.IsNullOrEmpty(propertyName))
51+
{
52+
context.DisplayMetadata.DisplayName = () => propertyName;
53+
}
54+
}
55+
56+
/// <inheritdoc />
57+
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
58+
{
59+
if (context == null)
60+
{
61+
throw new ArgumentNullException(nameof(context));
62+
}
63+
64+
var propertyName = ReadPropertyNameFrom(context.Attributes);
65+
66+
if (string.IsNullOrEmpty(propertyName))
67+
{
68+
propertyName = _jsonNamingPolicy.GetPropertyName(context.Key.Name!, false);
69+
}
70+
71+
context.ValidationMetadata.ValidationModelName = propertyName!;
72+
}
73+
74+
private static string? ReadPropertyNameFrom(IReadOnlyList<object> attributes)
75+
=> attributes?.OfType<JsonPropertyAttribute>().FirstOrDefault()?.PropertyName;
76+
}
+5
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider
3+
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.CreateDisplayMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DisplayMetadataProviderContext! context) -> void
4+
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.CreateValidationMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadataProviderContext! context) -> void
5+
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.NewtonsoftJsonValidationMetadataProvider() -> void
6+
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.NewtonsoftJsonValidationMetadataProvider(Newtonsoft.Json.Serialization.NamingStrategy! namingStrategy) -> void

0 commit comments

Comments
 (0)