diff --git a/src/Validation/src/ValidatablePropertyInfo.cs b/src/Validation/src/ValidatablePropertyInfo.cs
index 94d7eb606fa3..c95d124e2eb1 100644
--- a/src/Validation/src/ValidatablePropertyInfo.cs
+++ b/src/Validation/src/ValidatablePropertyInfo.cs
@@ -3,6 +3,9 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Validation;
@@ -13,12 +16,13 @@ namespace Microsoft.Extensions.Validation;
public abstract class ValidatablePropertyInfo : IValidatableInfo
{
private RequiredAttribute? _requiredAttribute;
+ private readonly bool _hasDisplayAttribute;
///
/// Creates a new instance of .
///
protected ValidatablePropertyInfo(
- [param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+ [param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)]
Type declaringType,
Type propertyType,
string name,
@@ -28,12 +32,18 @@ protected ValidatablePropertyInfo(
PropertyType = propertyType;
Name = name;
DisplayName = displayName;
+
+ // Cache the HasDisplayAttribute result to avoid repeated reflection calls
+ // We only check for the existence of the DisplayAttribute here and not the
+ // Name value itself since we rely on the source generator populating it
+ var property = DeclaringType.GetProperty(Name);
+ _hasDisplayAttribute = property is not null && HasDisplayAttribute(property);
}
///
/// Gets the member type.
///
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)]
internal Type DeclaringType { get; }
///
@@ -65,18 +75,24 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
var validationAttributes = GetValidationAttributes();
// Calculate and save the current path
+ var namingPolicy = context.SerializerOptions?.PropertyNamingPolicy;
+ var memberName = GetJsonPropertyName(Name, property, namingPolicy);
var originalPrefix = context.CurrentValidationPath;
if (string.IsNullOrEmpty(originalPrefix))
{
- context.CurrentValidationPath = Name;
+ context.CurrentValidationPath = memberName;
}
else
{
- context.CurrentValidationPath = $"{originalPrefix}.{Name}";
+ context.CurrentValidationPath = $"{originalPrefix}.{memberName}";
}
- context.ValidationContext.DisplayName = DisplayName;
- context.ValidationContext.MemberName = Name;
+ // Format the display name and member name according to JsonPropertyName attribute first, then naming policy
+ // If the property has a [Display] attribute (either on property or record parameter), use DisplayName directly without formatting
+ context.ValidationContext.DisplayName = _hasDisplayAttribute
+ ? DisplayName
+ : GetJsonPropertyName(DisplayName, property, namingPolicy);
+ context.ValidationContext.MemberName = memberName;
// Check required attribute first
if (_requiredAttribute is not null || validationAttributes.TryGetRequiredAttribute(out _requiredAttribute))
@@ -172,4 +188,61 @@ void ValidateValue(object? val, string name, string errorPrefix, ValidationAttri
}
}
}
+
+ ///
+ /// Gets the effective member name for JSON serialization, considering and naming policy.
+ ///
+ /// The target value to get the name for.
+ /// The property info to get the name for.
+ /// The JSON naming policy to apply if no is present.
+ /// The effective property name for JSON serialization.
+ private static string GetJsonPropertyName(string targetValue, PropertyInfo property, JsonNamingPolicy? namingPolicy)
+ {
+ var jsonPropertyName = property.GetCustomAttribute()?.Name;
+
+ if (jsonPropertyName is not null)
+ {
+ return jsonPropertyName;
+ }
+
+ if (namingPolicy is not null)
+ {
+ return namingPolicy.ConvertName(targetValue);
+ }
+
+ return targetValue;
+ }
+
+ ///
+ /// Determines whether the property has a , either directly on the property
+ /// or on the corresponding constructor parameter if the declaring type is a record.
+ ///
+ /// The property to check.
+ /// True if the property has a , false otherwise.
+ private bool HasDisplayAttribute(PropertyInfo property)
+ {
+ // Check if the property itself has the DisplayAttribute with a valid Name
+ if (property.GetCustomAttribute() is { Name: not null })
+ {
+ return true;
+ }
+
+ // Look for a constructor parameter matching the property name (case-insensitive)
+ // to account for the record scenario
+ foreach (var constructor in DeclaringType.GetConstructors())
+ {
+ foreach (var parameter in constructor.GetParameters())
+ {
+ if (string.Equals(parameter.Name, property.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (parameter.GetCustomAttribute() is { Name: not null })
+ {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/Validation/src/ValidatableTypeInfo.cs b/src/Validation/src/ValidatableTypeInfo.cs
index 8852f674a7e0..38d63f8ef20d 100644
--- a/src/Validation/src/ValidatableTypeInfo.cs
+++ b/src/Validation/src/ValidatableTypeInfo.cs
@@ -106,9 +106,13 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
// Create a validation error for each member name that is provided
foreach (var memberName in validationResult.MemberNames)
{
+ // Format the member name using JsonSerializerOptions naming policy if available
+ // Note: we don't respect [JsonPropertyName] here because we have no context of the property being validated.
+ var formattedMemberName = context.SerializerOptions?.PropertyNamingPolicy?.ConvertName(memberName) ?? memberName;
+
var key = string.IsNullOrEmpty(originalPrefix) ?
- memberName :
- $"{originalPrefix}.{memberName}";
+ formattedMemberName :
+ $"{originalPrefix}.{formattedMemberName}";
context.AddOrExtendValidationError(memberName, key, validationResult.ErrorMessage, value);
}
diff --git a/src/Validation/src/ValidateContext.cs b/src/Validation/src/ValidateContext.cs
index 5cce279c4be9..185edd51b7f9 100644
--- a/src/Validation/src/ValidateContext.cs
+++ b/src/Validation/src/ValidateContext.cs
@@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.Validation;
@@ -60,6 +62,52 @@ public sealed class ValidateContext
///
public int CurrentDepth { get; set; }
+ private JsonSerializerOptions? _cachedSerializerOptions;
+ private bool _serializerOptionsResolved;
+
+ internal JsonSerializerOptions? SerializerOptions
+ {
+ get
+ {
+ if (_serializerOptionsResolved)
+ {
+ return _cachedSerializerOptions;
+ }
+
+ _cachedSerializerOptions = ResolveSerializerOptions();
+ _serializerOptionsResolved = true;
+ return _cachedSerializerOptions;
+ }
+ }
+
+ ///
+ /// Attempts to resolve the used for serialization
+ /// using reflection to access JsonOptions from the ASP.NET Core shared framework.
+ ///
+ private JsonSerializerOptions? ResolveSerializerOptions()
+ {
+ var targetType = "Microsoft.AspNetCore.Http.Json.JsonOptions, Microsoft.AspNetCore.Http.Extensions";
+ var jsonOptionsType = Type.GetType(targetType, throwOnError: false);
+ if (jsonOptionsType is null)
+ {
+ return null;
+ }
+
+ var iOptionsType = typeof(IOptions<>).MakeGenericType(jsonOptionsType);
+
+ var optionsObj = ValidationContext.GetService(iOptionsType);
+ if (optionsObj is null)
+ {
+ return null;
+ }
+
+ var valueProp = iOptionsType.GetProperty("Value")!;
+ var jsonOptions = valueProp.GetValue(optionsObj);
+ var serializerProp = jsonOptionsType.GetProperty("SerializerOptions")!;
+
+ return serializerProp.GetValue(jsonOptions) as JsonSerializerOptions;
+ }
+
///
/// Optional event raised when a validation error is reported.
///
@@ -68,7 +116,6 @@ public sealed class ValidateContext
internal void AddValidationError(string propertyName, string key, string[] error, object? container)
{
ValidationErrors ??= [];
-
ValidationErrors[key] = error;
OnValidationError?.Invoke(new ValidationErrorContext
{
@@ -110,7 +157,7 @@ internal void AddOrExtendValidationError(string name, string key, string error,
if (ValidationErrors.TryGetValue(key, out var existingErrors) && !existingErrors.Contains(error))
{
- ValidationErrors[key] = [.. existingErrors, error];
+ ValidationErrors[key] = [..existingErrors, error];
}
else
{
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs
index 7bdf0bea29b2..08d98d7f3464 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs
@@ -126,8 +126,8 @@ async Task InvalidIntegerWithRangeProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithRange", kvp.Key);
- Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
+ Assert.Equal("integerWithRange", kvp.Key);
+ Assert.Equal("The field integerWithRange must be between 10 and 100.", kvp.Value.Single());
});
}
@@ -145,7 +145,7 @@ async Task InvalidIntegerWithRangeAndDisplayNameProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key);
+ Assert.Equal("integerWithRangeAndDisplayName", kvp.Key);
Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single());
});
}
@@ -164,8 +164,8 @@ async Task MissingRequiredSubtypePropertyProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("PropertyWithMemberAttributes", kvp.Key);
- Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single());
+ Assert.Equal("propertyWithMemberAttributes", kvp.Key);
+ Assert.Equal("The propertyWithMemberAttributes field is required.", kvp.Value.Single());
});
}
@@ -187,13 +187,13 @@ async Task InvalidRequiredSubtypePropertyProducesError(Endpoint endpoint)
Assert.Collection(problemDetails.Errors,
kvp =>
{
- Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key);
- Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
+ Assert.Equal("propertyWithMemberAttributes.requiredProperty", kvp.Key);
+ Assert.Equal("The requiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
- Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ Assert.Equal("propertyWithMemberAttributes.stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
@@ -216,18 +216,18 @@ async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint)
Assert.Collection(problemDetails.Errors,
kvp =>
{
- Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key);
- Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single());
+ Assert.Equal("propertyWithInheritance.emailString", kvp.Key);
+ Assert.Equal("The emailString field is not a valid e-mail address.", kvp.Value.Single());
},
kvp =>
{
- Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key);
- Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
+ Assert.Equal("propertyWithInheritance.requiredProperty", kvp.Key);
+ Assert.Equal("The requiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
- Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ Assert.Equal("propertyWithInheritance.stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
@@ -259,18 +259,18 @@ async Task InvalidListOfSubTypesProducesError(Endpoint endpoint)
Assert.Collection(problemDetails.Errors,
kvp =>
{
- Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key);
- Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
+ Assert.Equal("listOfSubTypes[0].requiredProperty", kvp.Key);
+ Assert.Equal("The requiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
- Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ Assert.Equal("listOfSubTypes[0].stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
},
kvp =>
{
- Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ Assert.Equal("listOfSubTypes[1].stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
@@ -288,7 +288,7 @@ async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint e
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithDerivedValidationAttribute", kvp.Key);
+ Assert.Equal("integerWithDerivedValidationAttribute", kvp.Key);
Assert.Equal("Value must be an even number", kvp.Value.Single());
});
}
@@ -297,7 +297,7 @@ async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint)
{
var payload = """
{
- "PropertyWithMultipleAttributes": 5
+ "propertyWithMultipleAttributes": 5
}
""";
var context = CreateHttpContextWithPayload(payload, serviceProvider);
@@ -307,15 +307,15 @@ async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("PropertyWithMultipleAttributes", kvp.Key);
+ Assert.Equal("propertyWithMultipleAttributes", kvp.Key);
Assert.Collection(kvp.Value,
error =>
{
- Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error);
+ Assert.Equal("The field propertyWithMultipleAttributes is invalid.", error);
},
error =>
{
- Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error);
+ Assert.Equal("The field propertyWithMultipleAttributes must be between 10 and 100.", error);
});
});
}
@@ -335,7 +335,7 @@ async Task InvalidPropertyWithCustomValidationProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithCustomValidation", kvp.Key);
+ Assert.Equal("integerWithCustomValidation", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("Can't use the same number value in two properties on the same class.", error);
});
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs
index 590195468298..3c0c1330df70 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs
@@ -128,23 +128,23 @@ async Task ValidateMethodCalledIfPropertyValidationsFail()
Assert.Collection(problemDetails.Errors,
error =>
{
- Assert.Equal("Value2", error.Key);
+ Assert.Equal("value2", error.Key);
Assert.Collection(error.Value,
- msg => Assert.Equal("The Value2 field is required.", msg));
+ msg => Assert.Equal("The value2 field is required.", msg));
},
error =>
{
- Assert.Equal("SubType.RequiredProperty", error.Key);
- Assert.Equal("The RequiredProperty field is required.", error.Value.Single());
+ Assert.Equal("subType.requiredProperty", error.Key);
+ Assert.Equal("The requiredProperty field is required.", error.Value.Single());
},
error =>
{
- Assert.Equal("SubType.Value3", error.Key);
+ Assert.Equal("subType.value3", error.Key);
Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single());
},
error =>
{
- Assert.Equal("Value1", error.Key);
+ Assert.Equal("value1", error.Key);
Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
});
}
@@ -169,12 +169,12 @@ async Task ValidateForSubtypeInvokedFirst()
Assert.Collection(problemDetails.Errors,
error =>
{
- Assert.Equal("SubType.Value3", error.Key);
+ Assert.Equal("subType.value3", error.Key);
Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single());
},
error =>
{
- Assert.Equal("Value1", error.Key);
+ Assert.Equal("value1", error.Key);
Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
});
}
@@ -199,7 +199,7 @@ async Task ValidateForTopLevelInvoked()
Assert.Collection(problemDetails.Errors,
error =>
{
- Assert.Equal("Value1", error.Key);
+ Assert.Equal("value1", error.Key);
Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
});
}
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.MultipleNamespaces.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.MultipleNamespaces.cs
index e08f8855a4a3..9d8180e6f08b 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.MultipleNamespaces.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.MultipleNamespaces.cs
@@ -69,8 +69,8 @@ async Task InvalidStringWithLengthProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ Assert.Equal("stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
@@ -106,8 +106,8 @@ async Task InvalidStringWithLengthProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 20.", kvp.Value.Single());
+ Assert.Equal("stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 20.", kvp.Value.Single());
});
}
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs
index ba176e622450..9780353d5aa5 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs
@@ -173,8 +173,8 @@ await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvi
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithRange", kvp.Key);
- Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
+ Assert.Equal("integerWithRange", kvp.Key);
+ Assert.Equal("The field integerWithRange must be between 10 and 100.", kvp.Value.Single());
});
});
}
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parsable.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parsable.cs
index 6ea212ebaa16..a7263344f7c2 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parsable.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parsable.cs
@@ -88,32 +88,32 @@ await VerifyEndpoint(compilation, "/complex-type-with-parsable-properties", asyn
Assert.Collection(problemDetails.Errors.OrderBy(kvp => kvp.Key),
error =>
{
- Assert.Equal("DateOnlyWithRange", error.Key);
+ Assert.Equal("dateOnlyWithRange", error.Key);
Assert.Contains("Date must be between 2023-01-01 and 2025-12-31", error.Value);
},
error =>
{
- Assert.Equal("DecimalWithRange", error.Key);
+ Assert.Equal("decimalWithRange", error.Key);
Assert.Contains("Amount must be between 0.1 and 100.5", error.Value);
},
error =>
{
- Assert.Equal("TimeOnlyWithRequiredValue", error.Key);
- Assert.Contains("The TimeOnlyWithRequiredValue field is required.", error.Value);
+ Assert.Equal("timeOnlyWithRequiredValue", error.Key);
+ Assert.Contains("The timeOnlyWithRequiredValue field is required.", error.Value);
},
error =>
{
- Assert.Equal("TimeSpanWithHourRange", error.Key);
+ Assert.Equal("timeSpanWithHourRange", error.Key);
Assert.Contains("Hours must be between 0 and 12", error.Value);
},
error =>
{
- Assert.Equal("Url", error.Key);
+ Assert.Equal("url", error.Key);
Assert.Contains("The field Url must be a valid URL.", error.Value);
},
error =>
{
- Assert.Equal("VersionWithRegex", error.Key);
+ Assert.Equal("versionWithRegex", error.Key);
Assert.Contains("Must be a valid version number (e.g. 1.0.0)", error.Value);
}
);
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs
index 39dbec97a78c..f72645dc7d6c 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs
@@ -96,18 +96,18 @@ await VerifyEndpoint(compilation, "/basic-polymorphism", async (endpoint, servic
Assert.Collection(problemDetails.Errors,
error =>
{
- Assert.Equal("Value3", error.Key);
- Assert.Equal("The Value3 field is not a valid Base64 encoding.", error.Value.Single());
+ Assert.Equal("value3", error.Key);
+ Assert.Equal("The value3 field is not a valid Base64 encoding.", error.Value.Single());
},
error =>
{
- Assert.Equal("Value1", error.Key);
+ Assert.Equal("value1", error.Key);
Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Value2", error.Key);
- Assert.Equal("The Value2 field is not a valid e-mail address.", error.Value.Single());
+ Assert.Equal("value2", error.Key);
+ Assert.Equal("The value2 field is not a valid e-mail address.", error.Value.Single());
});
});
@@ -127,12 +127,12 @@ await VerifyEndpoint(compilation, "/validatable-polymorphism", async (endpoint,
Assert.Collection(problemDetails.Errors,
error =>
{
- Assert.Equal("Value3", error.Key);
- Assert.Equal("The Value3 field is not a valid e-mail address.", error.Value.Single());
+ Assert.Equal("value3", error.Key);
+ Assert.Equal("The value3 field is not a valid e-mail address.", error.Value.Single());
},
error =>
{
- Assert.Equal("Value1", error.Key);
+ Assert.Equal("value1", error.Key);
Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single());
});
@@ -150,7 +150,7 @@ await VerifyEndpoint(compilation, "/validatable-polymorphism", async (endpoint,
Assert.Collection(problemDetails1.Errors,
error =>
{
- Assert.Equal("Value1", error.Key);
+ Assert.Equal("value1", error.Key);
Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single());
});
});
@@ -179,22 +179,22 @@ await VerifyEndpoint(compilation, "/polymorphism-container", async (endpoint, se
Assert.Collection(problemDetails.Errors,
error =>
{
- Assert.Equal("BaseType.Value3", error.Key);
- Assert.Equal("The Value3 field is not a valid Base64 encoding.", error.Value.Single());
+ Assert.Equal("baseType.value3", error.Key);
+ Assert.Equal("The value3 field is not a valid Base64 encoding.", error.Value.Single());
},
error =>
{
- Assert.Equal("BaseType.Value1", error.Key);
+ Assert.Equal("baseType.value1", error.Key);
Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("BaseType.Value2", error.Key);
- Assert.Equal("The Value2 field is not a valid e-mail address.", error.Value.Single());
+ Assert.Equal("baseType.value2", error.Key);
+ Assert.Equal("The value2 field is not a valid e-mail address.", error.Value.Single());
},
error =>
{
- Assert.Equal("BaseValidatableType.Value1", error.Key);
+ Assert.Equal("baseValidatableType.value1", error.Key);
Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single());
});
});
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs
index cfc1372b9a7a..59150bbca8b8 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs
@@ -113,8 +113,8 @@ async Task InvalidIntegerWithRangeProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithRange", kvp.Key);
- Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
+ Assert.Equal("integerWithRange", kvp.Key);
+ Assert.Equal("The field integerWithRange must be between 10 and 100.", kvp.Value.Single());
});
}
@@ -132,7 +132,7 @@ async Task InvalidIntegerWithRangeAndDisplayNameProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key);
+ Assert.Equal("integerWithRangeAndDisplayName", kvp.Key);
Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single());
});
}
@@ -151,18 +151,17 @@ async Task InvalidRequiredSubtypePropertyProducesError(Endpoint endpoint)
await endpoint.RequestDelegate(context);
- var problemDetails = await AssertBadRequest(context);
- Assert.Collection(problemDetails.Errors,
- kvp =>
- {
- Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key);
- Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
- },
- kvp =>
- {
- Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
- });
+ var problemDetails = await AssertBadRequest(context); Assert.Collection(problemDetails.Errors,
+ kvp =>
+ {
+ Assert.Equal("propertyWithMemberAttributes.requiredProperty", kvp.Key);
+ Assert.Equal("The requiredProperty field is required.", kvp.Value.Single());
+ },
+ kvp =>
+ {
+ Assert.Equal("propertyWithMemberAttributes.stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ });
}
async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint)
@@ -180,23 +179,22 @@ async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint)
await endpoint.RequestDelegate(context);
- var problemDetails = await AssertBadRequest(context);
- Assert.Collection(problemDetails.Errors,
- kvp =>
- {
- Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key);
- Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single());
- },
- kvp =>
- {
- Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key);
- Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
- },
- kvp =>
- {
- Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
- });
+ var problemDetails = await AssertBadRequest(context); Assert.Collection(problemDetails.Errors,
+ kvp =>
+ {
+ Assert.Equal("propertyWithInheritance.emailString", kvp.Key);
+ Assert.Equal("The emailString field is not a valid e-mail address.", kvp.Value.Single());
+ },
+ kvp =>
+ {
+ Assert.Equal("propertyWithInheritance.requiredProperty", kvp.Key);
+ Assert.Equal("The requiredProperty field is required.", kvp.Value.Single());
+ },
+ kvp =>
+ {
+ Assert.Equal("propertyWithInheritance.stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ });
}
async Task InvalidListOfSubTypesProducesError(Endpoint endpoint)
@@ -223,23 +221,22 @@ async Task InvalidListOfSubTypesProducesError(Endpoint endpoint)
await endpoint.RequestDelegate(context);
- var problemDetails = await AssertBadRequest(context);
- Assert.Collection(problemDetails.Errors,
- kvp =>
- {
- Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key);
- Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
- },
- kvp =>
- {
- Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
- },
- kvp =>
- {
- Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
- });
+ var problemDetails = await AssertBadRequest(context); Assert.Collection(problemDetails.Errors,
+ kvp =>
+ {
+ Assert.Equal("listOfSubTypes[0].requiredProperty", kvp.Key);
+ Assert.Equal("The requiredProperty field is required.", kvp.Value.Single());
+ },
+ kvp =>
+ {
+ Assert.Equal("listOfSubTypes[0].stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ },
+ kvp =>
+ {
+ Assert.Equal("listOfSubTypes[1].stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ });
}
async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint endpoint)
@@ -256,7 +253,7 @@ async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint e
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithDerivedValidationAttribute", kvp.Key);
+ Assert.Equal("integerWithDerivedValidationAttribute", kvp.Key);
Assert.Equal("Value must be an even number", kvp.Value.Single());
});
}
@@ -275,15 +272,15 @@ async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("PropertyWithMultipleAttributes", kvp.Key);
+ Assert.Equal("propertyWithMultipleAttributes", kvp.Key);
Assert.Collection(kvp.Value,
error =>
{
- Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error);
+ Assert.Equal("The field propertyWithMultipleAttributes is invalid.", error);
},
error =>
{
- Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error);
+ Assert.Equal("The field propertyWithMultipleAttributes must be between 10 and 100.", error);
});
});
}
@@ -303,7 +300,7 @@ async Task InvalidPropertyWithCustomValidationProducesError(Endpoint endpoint)
var problemDetails = await AssertBadRequest(context);
Assert.Collection(problemDetails.Errors, kvp =>
{
- Assert.Equal("IntegerWithCustomValidation", kvp.Key);
+ Assert.Equal("integerWithCustomValidation", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("Can't use the same number value in two properties on the same class.", error);
});
@@ -327,13 +324,13 @@ async Task InvalidPropertyOfSubtypeWithoutConstructorProducesError(Endpoint endp
Assert.Collection(problemDetails.Errors,
kvp =>
{
- Assert.Equal("PropertyOfSubtypeWithoutConstructor.RequiredProperty", kvp.Key);
- Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
+ Assert.Equal("propertyOfSubtypeWithoutConstructor.requiredProperty", kvp.Key);
+ Assert.Equal("The requiredProperty field is required.", kvp.Value.Single());
},
kvp =>
{
- Assert.Equal("PropertyOfSubtypeWithoutConstructor.StringWithLength", kvp.Key);
- Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
+ Assert.Equal("propertyOfSubtypeWithoutConstructor.stringWithLength", kvp.Key);
+ Assert.Equal("The field stringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
});
}
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Recursion.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Recursion.cs
index 7367e2919ae0..b3d3207fc317 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Recursion.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Recursion.cs
@@ -116,43 +116,43 @@ async Task ValidatesTypeWithLimitedNesting(Endpoint endpoint)
Assert.Collection(problemDetails.Errors,
error =>
{
- Assert.Equal("Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Next.Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("next.value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Next.Next.Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("next.next.value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Next.Next.Next.Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("next.next.next.value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Next.Next.Next.Next.Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("next.next.next.next.value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Next.Next.Next.Next.Next.Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("next.next.next.next.next.value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Next.Next.Next.Next.Next.Next.Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("next.next.next.next.next.next.value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
},
error =>
{
- Assert.Equal("Next.Next.Next.Next.Next.Next.Next.Value", error.Key);
- Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single());
+ Assert.Equal("next.next.next.next.next.next.next.value", error.Key);
+ Assert.Equal("The field value must be between 10 and 100.", error.Value.Single());
});
}
});
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj b/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj
index f74f56690452..5e152d09f131 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj
+++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj
@@ -8,6 +8,8 @@
+
+
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs
index 002ebb1582b0..2bd3143c1fd7 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs
@@ -6,6 +6,9 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.Validation.Tests;
@@ -586,6 +589,405 @@ public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_Beh
});
}
+ [Fact]
+ public async Task Validate_WithJsonPropertyNamingPolicy_FormatsNestedPropertyNames()
+ {
+ // Arrange
+ var addressType = new TestValidatableTypeInfo(
+ typeof(AddressWithNaming),
+ [
+ CreatePropertyInfo(typeof(AddressWithNaming), typeof(string), "StreetAddress", "StreetAddress",
+ [new RequiredAttribute()]),
+ CreatePropertyInfo(typeof(AddressWithNaming), typeof(string), "ZipCode", "ZipCode",
+ [new RequiredAttribute()])
+ ]);
+
+ var personType = new TestValidatableTypeInfo(
+ typeof(PersonWithNaming),
+ [
+ CreatePropertyInfo(typeof(PersonWithNaming), typeof(string), "FirstName", "FirstName",
+ [new RequiredAttribute()]),
+ CreatePropertyInfo(typeof(PersonWithNaming), typeof(AddressWithNaming), "HomeAddress", "HomeAddress",
+ [])
+ ]);
+
+ var validationOptions = new TestValidationOptions(new Dictionary
+ {
+ { typeof(PersonWithNaming), personType },
+ { typeof(AddressWithNaming), addressType }
+ });
+
+ var person = new PersonWithNaming
+ {
+ HomeAddress = new AddressWithNaming()
+ };
+
+ var serviceProvider = CreateServiceProviderWithJsonOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(person, serviceProvider, null)
+ };
+
+ // Act
+ await personType.ValidateAsync(person, context, default);
+
+ // Assert
+ Assert.NotNull(context.ValidationErrors);
+ Assert.Collection(context.ValidationErrors,
+ kvp =>
+ {
+ Assert.Equal("firstName", kvp.Key);
+ Assert.Equal("The firstName field is required.", kvp.Value.First());
+ },
+ kvp =>
+ {
+ Assert.Equal("homeAddress.streetAddress", kvp.Key);
+ Assert.Equal("The streetAddress field is required.", kvp.Value.First());
+ },
+ kvp =>
+ {
+ Assert.Equal("homeAddress.zipCode", kvp.Key);
+ Assert.Equal("The zipCode field is required.", kvp.Value.First());
+ });
+ }
+
+ [Theory]
+ [InlineData(typeof(JsonNamingPolicy), "CamelCase", "firstName")]
+ [InlineData(typeof(JsonNamingPolicy), "KebabCaseLower", "first-name")]
+ [InlineData(typeof(JsonNamingPolicy), "SnakeCaseLower", "first_name")]
+ public async Task Validate_WithDifferentNamingPolicies_FormatsPropertyNamesCorrectly(Type policyType, string policyName, string expectedPropertyName)
+ {
+ // Arrange
+ var namingPolicy = (JsonNamingPolicy?)policyType.GetProperty(policyName)?.GetValue(null);
+ var personType = new TestValidatableTypeInfo(
+ typeof(PersonWithNaming),
+ [
+ CreatePropertyInfo(typeof(PersonWithNaming), typeof(string), "FirstName", "FirstName",
+ [new RequiredAttribute()])
+ ]);
+
+ var validationOptions = new TestValidationOptions(new Dictionary
+ {
+ { typeof(PersonWithNaming), personType }
+ });
+
+ var serializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = namingPolicy
+ };
+ var serviceProvider = CreateServiceProviderWithJsonOptions(serializerOptions);
+
+ var person = new PersonWithNaming();
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(person, serviceProvider, null)
+ };
+
+ // Act
+ await personType.ValidateAsync(person, context, default);
+
+ // Assert
+ Assert.NotNull(context.ValidationErrors);
+ var error = Assert.Single(context.ValidationErrors);
+ Assert.Equal(expectedPropertyName, error.Key);
+ }
+
+ [Fact]
+ public async Task Validate_WithNamingPolicy_FormatsArrayIndices()
+ {
+ // Arrange
+ var orderItemType = new TestValidatableTypeInfo(
+ typeof(OrderItemWithNaming),
+ [
+ CreatePropertyInfo(typeof(OrderItemWithNaming), typeof(string), "ProductName", "ProductName",
+ [new RequiredAttribute()])
+ ]);
+
+ var orderType = new TestValidatableTypeInfo(
+ typeof(OrderWithNaming),
+ [
+ CreatePropertyInfo(typeof(OrderWithNaming), typeof(List), "OrderItems", "OrderItems",
+ [])
+ ]);
+ var serviceProvider = CreateServiceProviderWithJsonOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ var validationOptions = new TestValidationOptions(new Dictionary
+ {
+ { typeof(OrderWithNaming), orderType },
+ { typeof(OrderItemWithNaming), orderItemType }
+ });
+
+ var order = new OrderWithNaming
+ {
+ OrderItems = [new OrderItemWithNaming(), new OrderItemWithNaming()]
+ };
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(order, serviceProvider, null)
+ };
+
+ // Act
+ await orderType.ValidateAsync(order, context, default);
+
+ // Assert
+ Assert.NotNull(context.ValidationErrors);
+ Assert.Collection(context.ValidationErrors,
+ kvp =>
+ {
+ Assert.Equal("orderItems[0].productName", kvp.Key);
+ Assert.Equal("The productName field is required.", kvp.Value.First());
+ },
+ kvp =>
+ {
+ Assert.Equal("orderItems[1].productName", kvp.Key);
+ Assert.Equal("The productName field is required.", kvp.Value.First());
+ });
+ }
+
+ [Fact]
+ public async Task Validate_IValidatableObject_WithNamingPolicy_FormatsMemberNames()
+ {
+ // Arrange
+ var employeeType = new TestValidatableTypeInfo(
+ typeof(EmployeeWithNaming),
+ [
+ CreatePropertyInfo(typeof(EmployeeWithNaming), typeof(string), "FirstName", "FirstName", []),
+ CreatePropertyInfo(typeof(EmployeeWithNaming), typeof(string), "LastName", "LastName", []),
+ CreatePropertyInfo(typeof(EmployeeWithNaming), typeof(decimal), "AnnualSalary", "AnnualSalary", [])
+ ]);
+
+ var serviceProvider = CreateServiceProviderWithJsonOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+ var validationOptions = new TestValidationOptions(new Dictionary
+ {
+ { typeof(EmployeeWithNaming), employeeType }
+ });
+
+ var employee = new EmployeeWithNaming
+ {
+ FirstName = "John",
+ LastName = "Doe",
+ AnnualSalary = -50000
+ };
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(employee, serviceProvider, null)
+ };
+
+ // Act
+ await employeeType.ValidateAsync(employee, context, default);
+
+ // Assert
+ Assert.NotNull(context.ValidationErrors);
+ var error = Assert.Single(context.ValidationErrors);
+ Assert.Equal("annualSalary", error.Key);
+ Assert.Equal("Annual salary must be positive.", error.Value.First());
+ }
+
+ [Fact]
+ public async Task Validate_JsonPropertyNameAttribute_TakesPrecedenceOverNamingPolicy()
+ {
+ // Arrange
+ var personType = new TestValidatableTypeInfo(
+ typeof(PersonWithJsonPropertyName),
+ [
+ CreatePropertyInfo(typeof(PersonWithJsonPropertyName), typeof(string), "FirstName", "FirstName",
+ [new RequiredAttribute()]),
+ CreatePropertyInfo(typeof(PersonWithJsonPropertyName), typeof(string), "LastName", "LastName",
+ [new RequiredAttribute()])
+ ]);
+
+ var serviceProvider = CreateServiceProviderWithJsonOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+ var validationOptions = new TestValidationOptions(new Dictionary
+ {
+ { typeof(PersonWithJsonPropertyName), personType }
+ });
+
+ var person = new PersonWithJsonPropertyName();
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(person, serviceProvider, null)
+ };
+
+ // Act
+ await personType.ValidateAsync(person, context, default);
+
+ // Assert
+ Assert.NotNull(context.ValidationErrors);
+ Assert.Collection(context.ValidationErrors,
+ kvp =>
+ {
+ Assert.Equal("custom_first_name", kvp.Key); // JsonPropertyName takes precedence
+ Assert.Equal("The custom_first_name field is required.", kvp.Value.First());
+ },
+ kvp =>
+ {
+ Assert.Equal("lastName", kvp.Key); // Uses naming policy
+ Assert.Equal("The lastName field is required.", kvp.Value.First());
+ });
+ }
+
+ [Fact]
+ public async Task Validate_DisplayAttribute_PreventsNamingPolicyFormatting()
+ {
+ // Arrange
+ var personType = new TestValidatableTypeInfo(
+ typeof(PersonWithDisplay),
+ [
+ CreatePropertyInfo(typeof(PersonWithDisplay), typeof(string), "FirstName", "First Name",
+ [new RequiredAttribute()]),
+ CreatePropertyInfo(typeof(PersonWithDisplay), typeof(string), "LastName", "LastName",
+ [new RequiredAttribute()])
+ ]);
+
+ var serviceProvider = CreateServiceProviderWithJsonOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+ var validationOptions = new TestValidationOptions(new Dictionary
+ {
+ { typeof(PersonWithDisplay), personType }
+ });
+
+ var person = new PersonWithDisplay();
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(person, serviceProvider, null)
+ };
+
+ // Act
+ await personType.ValidateAsync(person, context, default);
+
+ // Assert
+ Assert.NotNull(context.ValidationErrors);
+ Assert.Collection(context.ValidationErrors,
+ kvp =>
+ {
+ Assert.Equal("firstName", kvp.Key);
+ Assert.Equal("The First Name field is required.", kvp.Value.First()); // Display attribute prevents formatting
+ },
+ kvp =>
+ {
+ Assert.Equal("lastName", kvp.Key);
+ Assert.Equal("The lastName field is required.", kvp.Value.First()); // Uses naming policy
+ });
+ }
+
+ [Fact]
+ public async Task Validate_WithCustomErrorMessage_FormatsPropertyNameCorrectly()
+ {
+ // Arrange
+ var personType = new TestValidatableTypeInfo(
+ typeof(PersonWithNaming),
+ [
+ CreatePropertyInfo(typeof(PersonWithNaming), typeof(string), "FirstName", "FirstName",
+ [new RequiredAttribute { ErrorMessage = "The {0} property is required." }])
+ ]);
+
+ var serviceProvider = CreateServiceProviderWithJsonOptions(new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+ var validationOptions = new TestValidationOptions(new Dictionary
+ {
+ { typeof(PersonWithNaming), personType }
+ });
+
+ var person = new PersonWithNaming();
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(person, serviceProvider, null)
+ };
+
+ // Act
+ await personType.ValidateAsync(person, context, default);
+
+ // Assert
+ Assert.NotNull(context.ValidationErrors);
+ var error = Assert.Single(context.ValidationErrors);
+ Assert.Equal("firstName", error.Key);
+ Assert.Equal("The firstName property is required.", error.Value.First());
+ }
+
+ // Test model classes for JsonSerializerOptions tests
+ private class PersonWithNaming
+ {
+ public string? FirstName { get; set; }
+ public AddressWithNaming? HomeAddress { get; set; }
+ }
+
+ private class AddressWithNaming
+ {
+ public string? StreetAddress { get; set; }
+ public string? ZipCode { get; set; }
+ }
+
+ private class OrderWithNaming
+ {
+ public List OrderItems { get; set; } = [];
+ }
+
+ private class OrderItemWithNaming
+ {
+ public string? ProductName { get; set; }
+ }
+
+ private class EmployeeWithNaming : IValidatableObject
+ {
+ public string? FirstName { get; set; }
+ public string? LastName { get; set; }
+ public decimal AnnualSalary { get; set; }
+
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ if (AnnualSalary < 0)
+ {
+ yield return new ValidationResult("Annual salary must be positive.", [nameof(AnnualSalary)]);
+ }
+ }
+ }
+
+ private class PersonWithJsonPropertyName
+ {
+ [JsonPropertyName("custom_first_name")]
+ public string? FirstName { get; set; }
+
+ public string? LastName { get; set; }
+ }
+
+ private class PersonWithDisplay
+ {
+ [Display(Name = "First Name")]
+ public string? FirstName { get; set; }
+
+ public string? LastName { get; set; }
+ }
+
// Returns no member names to validate https://github.com/dotnet/aspnetcore/issues/61739
private class GlobalErrorObject : IValidatableObject
{
@@ -762,6 +1164,38 @@ private class PasswordComplexityAttribute : ValidationAttribute
}
}
+ // Helper method to create a service provider with JsonOptions configured
+ private static IServiceProvider CreateServiceProviderWithJsonOptions(JsonSerializerOptions? serializerOptions = null)
+ {
+ var services = new ServiceCollection();
+
+ if (serializerOptions != null)
+ {
+ services.Configure(options =>
+ {
+ options.SerializerOptions.PropertyNamingPolicy = serializerOptions.PropertyNamingPolicy;
+ options.SerializerOptions.PropertyNameCaseInsensitive = serializerOptions.PropertyNameCaseInsensitive;
+ options.SerializerOptions.DictionaryKeyPolicy = serializerOptions.DictionaryKeyPolicy;
+ options.SerializerOptions.DefaultIgnoreCondition = serializerOptions.DefaultIgnoreCondition;
+ options.SerializerOptions.IgnoreReadOnlyProperties = serializerOptions.IgnoreReadOnlyProperties;
+ options.SerializerOptions.IgnoreReadOnlyFields = serializerOptions.IgnoreReadOnlyFields;
+ options.SerializerOptions.IncludeFields = serializerOptions.IncludeFields;
+ options.SerializerOptions.NumberHandling = serializerOptions.NumberHandling;
+ options.SerializerOptions.ReadCommentHandling = serializerOptions.ReadCommentHandling;
+ options.SerializerOptions.WriteIndented = serializerOptions.WriteIndented;
+ options.SerializerOptions.MaxDepth = serializerOptions.MaxDepth;
+ options.SerializerOptions.AllowTrailingCommas = serializerOptions.AllowTrailingCommas;
+ // Copy other relevant properties as needed
+ });
+ }
+ else
+ {
+ services.Configure(options => { }); // Register empty JsonOptions
+ }
+
+ return services.BuildServiceProvider();
+ }
+
// Test implementations
private class TestValidatablePropertyInfo : ValidatablePropertyInfo
{