diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 8cb332fa9f20..85273ab111e7 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -28,6 +28,8 @@ Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> Sy Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.set -> void Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.get -> System.Collections.Generic.Dictionary? Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.set -> void +Microsoft.AspNetCore.Http.Validation.ValidateContext.SerializerOptions.get -> System.Text.Json.JsonSerializerOptions? +Microsoft.AspNetCore.Http.Validation.ValidateContext.SerializerOptions.set -> void Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.get -> Microsoft.AspNetCore.Http.Validation.ValidationOptions! Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.set -> void Microsoft.AspNetCore.Http.Validation.ValidationOptions diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs index 0b16e34d1dc9..710d07c6fa81 100644 --- a/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs +++ b/src/Http/Http.Abstractions/src/Validation/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.AspNetCore.Http.Validation; @@ -13,12 +16,13 @@ namespace Microsoft.AspNetCore.Http.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,16 @@ protected ValidatablePropertyInfo( PropertyType = propertyType; Name = name; DisplayName = displayName; + + // Cache the HasDisplayAttribute result to avoid repeated reflection calls + 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 +73,23 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context, var validationAttributes = GetValidationAttributes(); // Calculate and save the current path + var memberName = GetJsonPropertyName(Name, property, context.SerializerOptions?.PropertyNamingPolicy); 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, context.SerializerOptions?.PropertyNamingPolicy); + context.ValidationContext.MemberName = memberName; // Check required attribute first if (_requiredAttribute is not null || validationAttributes.TryGetRequiredAttribute(out _requiredAttribute)) @@ -170,4 +183,61 @@ void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] valida } } } + + /// + /// Gets the effective member name for JSON serialization, considering JsonPropertyName attribute 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 JsonPropertyName attribute 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 DisplayAttribute, 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 DisplayAttribute, 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/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs index 6245c43c1b69..535e930cf943 100644 --- a/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs +++ b/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs @@ -106,9 +106,17 @@ 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 = memberName; + if (context.SerializerOptions?.PropertyNamingPolicy != null) + { + formattedMemberName = context.SerializerOptions.PropertyNamingPolicy.ConvertName(memberName); + } + var key = string.IsNullOrEmpty(originalPrefix) ? - memberName : - $"{originalPrefix}.{memberName}"; + formattedMemberName : + $"{originalPrefix}.{formattedMemberName}"; context.AddOrExtendValidationError(key, validationResult.ErrorMessage); } diff --git a/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs b/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs index d38ada2ddeb1..1c0e710311f1 100644 --- a/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs +++ b/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; namespace Microsoft.AspNetCore.Http.Validation; @@ -60,11 +62,18 @@ public sealed class ValidateContext /// public int CurrentDepth { get; set; } - internal void AddValidationError(string key, string[] error) + /// + /// Gets or sets the JSON serializer options to use for property name formatting. + /// When available, property names in validation errors will be formatted according to the + /// PropertyNamingPolicy and JsonPropertyName attributes. + /// + public JsonSerializerOptions? SerializerOptions { get; set; } + + internal void AddValidationError(string key, string[] errors) { ValidationErrors ??= []; - ValidationErrors[key] = error; + ValidationErrors[key] = errors; } internal void AddOrExtendValidationErrors(string key, string[] errors) @@ -90,7 +99,7 @@ internal void AddOrExtendValidationError(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/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs b/src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs index a6123bb11c67..94997f896b48 100644 --- a/src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs +++ b/src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs @@ -6,6 +6,8 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.Http.Validation.Tests; @@ -81,7 +83,210 @@ [new RequiredAttribute()]) } [Fact] - public async Task Validate_HandlesIValidatableObject_Implementation() + public async Task Validate_ValidatesComplexType_WithNestedProperties_AppliesJsonPropertyNamingPolicy() + { + // Arrange + var personType = new TestValidatableTypeInfo( + typeof(Person), + [ + CreatePropertyInfo(typeof(Person), typeof(string), "Name", "Name", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Person), typeof(int), "Age", "Age", + [new RangeAttribute(0, 120)]), + CreatePropertyInfo(typeof(Person), typeof(Address), "Address", "Address", + []) + ]); + + var addressType = new TestValidatableTypeInfo( + typeof(Address), + [ + CreatePropertyInfo(typeof(Address), typeof(string), "Street", "Street", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Address), typeof(string), "City", "City", + [new RequiredAttribute()]) + ]); + + var validationOptions = new TestValidationOptions(new Dictionary + { + { typeof(Person), personType }, + { typeof(Address), addressType } + }); + + var personWithMissingRequiredFields = new Person + { + Age = 150, // Invalid age + Address = new Address() // Missing required City and Street + }; + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(personWithMissingRequiredFields), + SerializerOptions = jsonOptions + }; + + // Act + await personType.ValidateAsync(personWithMissingRequiredFields, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("name", kvp.Key); + Assert.Equal("The name field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("age", kvp.Key); + Assert.Equal("The field age must be between 0 and 120.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("address.street", kvp.Key); + Assert.Equal("The street field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("address.city", kvp.Key); + Assert.Equal("The city field is required.", kvp.Value.First()); + }); + } + + [Theory] + [InlineData("CamelCase", "firstName", "lastName")] + [InlineData("KebabCaseLower", "first-name", "last-name")] + [InlineData("SnakeCaseLower", "first_name", "last_name")] + public async Task Validate_AppliesJsonPropertyNamingPolicy_ForDifferentNamingPolicies(string policy, string expectedFirstName, string expectedLastName) + { + // Arrange + var personType = new TestValidatableTypeInfo( + typeof(PersonWithJsonNames), + [ + CreatePropertyInfo(typeof(PersonWithJsonNames), typeof(string), "FirstName", "FirstName", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(PersonWithJsonNames), typeof(string), "LastName", "LastName", + [new RequiredAttribute()]) + ]); + + var validationOptions = new TestValidationOptions(new Dictionary + { + { typeof(PersonWithJsonNames), personType } + }); + + var person = new PersonWithJsonNames(); // Missing required fields + + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.PropertyNamingPolicy = policy switch + { + "CamelCase" => JsonNamingPolicy.CamelCase, + "KebabCaseLower" => JsonNamingPolicy.KebabCaseLower, + "SnakeCaseLower" => JsonNamingPolicy.SnakeCaseLower, + _ => null + }; + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(person), + SerializerOptions = jsonOptions + }; + + // Act + await personType.ValidateAsync(person, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal(expectedFirstName, kvp.Key); + Assert.Equal($"The {expectedFirstName} field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal(expectedLastName, kvp.Key); + Assert.Equal($"The {expectedLastName} field is required.", kvp.Value.First()); + }); + } + + [Fact] + public async Task Validate_HandlesArrayIndices_WithJsonPropertyNamingPolicy() + { + // Arrange + var orderType = new TestValidatableTypeInfo( + typeof(Order), + [ + CreatePropertyInfo(typeof(Order), typeof(string), "OrderNumber", "OrderNumber", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Order), typeof(List), "Items", "Items", + []) + ]); + + var itemType = new TestValidatableTypeInfo( + typeof(OrderItem), + [ + CreatePropertyInfo(typeof(OrderItem), typeof(string), "ProductName", "ProductName", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(OrderItem), typeof(int), "Quantity", "Quantity", + [new RangeAttribute(1, 100)]) + ]); + + var order = new Order + { + // Missing OrderNumber + Items = + [ + new OrderItem { /* Missing ProductName */ Quantity = 0 }, // Invalid quantity + ] + }; + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(OrderItem), itemType }, + { typeof(Order), orderType } + }), + ValidationContext = new ValidationContext(order), + SerializerOptions = jsonOptions + }; + + // Act + await orderType.ValidateAsync(order, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("orderNumber", kvp.Key); + Assert.Equal("The orderNumber field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("items[0].productName", kvp.Key); + Assert.Equal("The productName field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("items[0].quantity", kvp.Key); + Assert.Equal("The field quantity must be between 1 and 100.", kvp.Value.First()); + }); + } + + [Fact] + public async Task Validate_HandlesIValidatableObject_WithJsonPropertyNamingPolicy() { // Arrange var employeeType = new TestValidatableTypeInfo( @@ -101,13 +306,20 @@ [new RequiredAttribute()]), Department = "IT", Salary = -5000 // Negative salary will trigger IValidatableObject validation }; + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + var context = new ValidateContext { ValidationOptions = new TestValidationOptions(new Dictionary { { typeof(Employee), employeeType } }), - ValidationContext = new ValidationContext(employee) + ValidationContext = new ValidationContext(employee), + SerializerOptions = jsonOptions }; // Act @@ -116,10 +328,51 @@ [new RequiredAttribute()]), // Assert Assert.NotNull(context.ValidationErrors); var error = Assert.Single(context.ValidationErrors); - Assert.Equal("Salary", error.Key); + Assert.Equal("salary", error.Key); Assert.Equal("Salary must be a positive value.", error.Value.First()); } + [Fact] + public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_WithJsonNamingPolicy() + { + var multiType = new TestValidatableTypeInfo( + typeof(MultiMemberErrorObject), + [ + CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "FirstName", "FirstName", []), + CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "LastName", "LastName", []) + ]); + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(MultiMemberErrorObject), multiType } + }), + ValidationContext = new ValidationContext(new MultiMemberErrorObject { FirstName = "", LastName = "" }), + SerializerOptions = jsonOptions + }; + + await multiType.ValidateAsync(context.ValidationContext.ObjectInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("firstName", kvp.Key); + Assert.Equal("FirstName and LastName are required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("lastName", kvp.Key); + Assert.Equal("FirstName and LastName are required.", kvp.Value.First()); + }); + } + [Fact] public async Task Validate_HandlesPolymorphicTypes_WithSubtypes() { @@ -598,6 +851,70 @@ private class Person public Address? Address { get; set; } } + private class PersonWithJsonNames + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + } + +[Fact] + public async Task Validate_RespectsJsonPropertyNameAttribute_ForValidationErrors() + { + // Arrange + var modelType = new TestValidatableTypeInfo( + typeof(ModelWithJsonPropertyNames), + [ + CreatePropertyInfo(typeof(ModelWithJsonPropertyNames), typeof(string), "UserName", "UserName", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(ModelWithJsonPropertyNames), typeof(string), "EmailAddress", "EmailAddress", + [new EmailAddressAttribute()]) + ]); + + var model = new ModelWithJsonPropertyNames { EmailAddress = "invalid-email" }; // Missing username and invalid email + + var jsonOptions = new JsonSerializerOptions(); + // Add a custom converter that knows about JsonPropertyName attributes + jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(ModelWithJsonPropertyNames), modelType } + }), + ValidationContext = new ValidationContext(model), + SerializerOptions = jsonOptions + }; + + // Act + await modelType.ValidateAsync(model, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + // Use [JsonPropertyName] over naming policy + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("username", kvp.Key); + Assert.Equal("The username field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("email", kvp.Key); + Assert.Equal("The email field is not a valid e-mail address.", kvp.Value.First()); + }); + } + + private class ModelWithJsonPropertyNames + { + [JsonPropertyName("username")] + public string? UserName { get; set; } + + [JsonPropertyName("email")] + [EmailAddress] + public string? EmailAddress { get; set; } + } + private class Address { public string? Street { get; set; } @@ -787,4 +1104,77 @@ public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNull } } } + + [Fact] + public void Validate_FormatsErrorMessagesWithPropertyNamingPolicy() + { + // Arrange + var validationOptions = new ValidationOptions(); + + var address = new Address(); + var validationContext = new ValidationContext(address); + var validateContext = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = validationContext, + SerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + } + }; + + var propertyInfo = CreatePropertyInfo( + typeof(Address), + typeof(string), + "Street", + "Street", + [new RequiredAttribute()]); + + // Act + propertyInfo.ValidateAsync(address, validateContext, CancellationToken.None); + + // Assert + var error = Assert.Single(validateContext.ValidationErrors!); + Assert.Equal("street", error.Key); + Assert.Equal("The street field is required.", error.Value.First()); + } + + [Fact] + public void Validate_PreservesCustomErrorMessagesWithPropertyNamingPolicy() + { + // Arrange + var validationOptions = new ValidationOptions(); + + var model = new TestModel(); + var validationContext = new ValidationContext(model); + var validateContext = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = validationContext, + SerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + } + }; + + var propertyInfo = CreatePropertyInfo( + typeof(TestModel), + typeof(string), + "CustomProperty", + "CustomProperty", + [new RequiredAttribute { ErrorMessage = "Custom message without standard format." }]); + + // Act + propertyInfo.ValidateAsync(model, validateContext, CancellationToken.None); + + // Assert + var error = Assert.Single(validateContext.ValidationErrors!); + Assert.Equal("customProperty", error.Key); + Assert.Equal("Custom message without standard format.", error.Value.First()); // Custom message without formatting + } + + private class TestModel + { + public string? CustomProperty { get; set; } + } } diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs index 50cd7eca1769..01b337ef16c7 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs @@ -124,8 +124,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()); }); } @@ -143,7 +143,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()); }); } @@ -162,8 +162,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()); }); } @@ -185,13 +185,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()); }); } @@ -214,18 +214,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()); }); } @@ -257,18 +257,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()); }); } @@ -286,7 +286,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()); }); } @@ -305,15 +305,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); }); }); } @@ -333,7 +333,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/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs index 70f1d725bc70..2997c861dabf 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs @@ -128,23 +128,24 @@ 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); + // The error message is generated using nameof(Value1) in the IValidateObject implementation Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); }); } @@ -169,12 +170,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 +200,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/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs index 58478ece957e..bfc7f55e03a8 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs @@ -67,8 +67,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()); }); } @@ -104,8 +104,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/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs index 410c74a5cecc..2e9ccfbf79f2 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs @@ -170,8 +170,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/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs index 6cebd6df8584..947d747b3c6a 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs @@ -88,32 +88,33 @@ 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); + // Message provided explicitly in attribute 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/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Polymorphism.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Polymorphism.cs index 54148e784a0a..c256fae5ff03 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Polymorphism.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/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/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs index 4f296c66d648..0a22d452bb0b 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs @@ -111,8 +111,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()); }); } @@ -130,7 +130,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()); }); } @@ -153,13 +153,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()); }); } @@ -182,18 +182,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()); }); } @@ -225,18 +225,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()); }); } @@ -254,7 +254,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()); }); } @@ -273,15 +273,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); }); }); } @@ -301,7 +301,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); }); @@ -325,13 +325,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/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs index 4affa35f8997..81ff47f0ac49 100644 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs +++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs @@ -114,43 +114,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/Http/Routing/src/ValidationEndpointFilterFactory.cs b/src/Http/Routing/src/ValidationEndpointFilterFactory.cs index 6b86c9582f89..d960e6e73356 100644 --- a/src/Http/Routing/src/ValidationEndpointFilterFactory.cs +++ b/src/Http/Routing/src/ValidationEndpointFilterFactory.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Mime; using System.Reflection; +using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -60,6 +61,10 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context { ValidateContext? validateContext = null; + // Get JsonOptions from DI + var jsonOptions = context.HttpContext.RequestServices.GetService>(); + var serializerOptions = jsonOptions?.Value?.SerializerOptions; + foreach (var entry in validatableParameters) { if (entry.Index >= context.Arguments.Count) @@ -80,7 +85,8 @@ public static EndpointFilterDelegate Create(EndpointFilterFactoryContext context validateContext = new ValidateContext { ValidationOptions = options, - ValidationContext = validationContext + ValidationContext = validationContext, + SerializerOptions = serializerOptions }; } else