diff --git a/src/Shared/RoslynUtils/WellKnownTypeData.cs b/src/Shared/RoslynUtils/WellKnownTypeData.cs index 4311eed4c6ac..38bc35ac8f90 100644 --- a/src/Shared/RoslynUtils/WellKnownTypeData.cs +++ b/src/Shared/RoslynUtils/WellKnownTypeData.cs @@ -120,6 +120,7 @@ public enum WellKnownType System_AttributeUsageAttribute, System_Text_Json_Serialization_JsonDerivedTypeAttribute, System_Text_Json_Serialization_JsonIgnoreAttribute, + System_Text_Json_Serialization_JsonPropertyNameAttribute, System_ComponentModel_DataAnnotations_DisplayAttribute, System_ComponentModel_DataAnnotations_ValidationAttribute, System_ComponentModel_DataAnnotations_RequiredAttribute, @@ -243,6 +244,7 @@ public enum WellKnownType "System.AttributeUsageAttribute", "System.Text.Json.Serialization.JsonDerivedTypeAttribute", "System.Text.Json.Serialization.JsonIgnoreAttribute", + "System.Text.Json.Serialization.JsonPropertyNameAttribute", "System.ComponentModel.DataAnnotations.DisplayAttribute", "System.ComponentModel.DataAnnotations.ValidationAttribute", "System.ComponentModel.DataAnnotations.RequiredAttribute", diff --git a/src/Validation/gen/Extensions/ISymbolExtensions.cs b/src/Validation/gen/Extensions/ISymbolExtensions.cs index a770b7260656..798536766e6f 100644 --- a/src/Validation/gen/Extensions/ISymbolExtensions.cs +++ b/src/Validation/gen/Extensions/ISymbolExtensions.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.Validation; internal static class ISymbolExtensions { - public static string GetDisplayName(this ISymbol property, INamedTypeSymbol displayAttribute) + public static string? GetDisplayName(this ISymbol property, INamedTypeSymbol displayAttribute) { var displayNameAttribute = property.GetAttributes() .FirstOrDefault(attribute => @@ -25,13 +25,38 @@ attribute.AttributeClass is { } attributeClass && { if (string.Equals(namedArgument.Key, "Name", StringComparison.Ordinal)) { - return namedArgument.Value.Value?.ToString() ?? property.Name; + if (namedArgument.Value.Value?.ToString() is { } name) + { + return name; + } } } } } - return property.Name; + return null; + } + + public static string? GetJsonPropertyName(this ISymbol property, INamedTypeSymbol nameAttribute) + { + var jsonPropertyNameAttribute = property.GetAttributes() + .FirstOrDefault(attribute => + attribute.AttributeClass is { } attributeClass && + SymbolEqualityComparer.Default.Equals(attributeClass, nameAttribute)); + + if (jsonPropertyNameAttribute is not null) + { + if (jsonPropertyNameAttribute.ConstructorArguments.Length is 1) + { + var arg = jsonPropertyNameAttribute.ConstructorArguments[0]; + if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string name) + { + return name; + } + } + } + + return null; } public static bool IsEqualityContract(this IPropertySymbol prop, WellKnownTypes wellKnownTypes) => diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index 7544c9d72feb..e489da6d72e6 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -5,7 +5,6 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.AspNetCore.Analyzers.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; using Microsoft.CodeAnalysis; @@ -195,12 +194,19 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb ref validatableTypes, ref visitedTypes); + var displayName = + parameter.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)) ?? + correspondingProperty.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)) ?? + parameter.GetJsonPropertyName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonPropertyNameAttribute)) ?? + correspondingProperty.GetJsonPropertyName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonPropertyNameAttribute)) ?? + parameter.Name ?? + correspondingProperty.Name; + members.Add(new ValidatableProperty( ContainingType: correspondingProperty.ContainingType, Type: correspondingProperty.Type, Name: correspondingProperty.Name, - DisplayName: parameter.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)) ?? - correspondingProperty.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)), + DisplayName: displayName, Attributes: [])); } } @@ -252,11 +258,16 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb continue; } + var displayName = + member.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)) ?? + member.GetJsonPropertyName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonPropertyNameAttribute)) ?? + member.Name; + members.Add(new ValidatableProperty( ContainingType: member.ContainingType, Type: member.Type, Name: member.Name, - DisplayName: member.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)), + DisplayName: displayName, Attributes: attributes)); } 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 d4bd0ef579f5..c7ff824dc142 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs @@ -15,6 +15,7 @@ public async Task CanValidateComplexTypesWithJsonIgnore() using System; using System.ComponentModel.DataAnnotations; using System.Collections.Generic; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -22,7 +23,6 @@ public async Task CanValidateComplexTypesWithJsonIgnore() using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Mvc; -using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(); @@ -32,6 +32,7 @@ public async Task CanValidateComplexTypesWithJsonIgnore() app.MapPost("/complex-type-with-json-ignore", (ComplexTypeWithJsonIgnore complexType) => Results.Ok("Passed"!)); app.MapPost("/record-type-with-json-ignore", (RecordTypeWithJsonIgnore recordType) => Results.Ok("Passed"!)); +app.MapPost("/complex-type-with-json-property-name", (ComplexTypeWithJsonPropertyName complexType) => Results.Ok("Passed"!)); app.Run(); @@ -76,6 +77,21 @@ public record CircularReferenceRecord public string Name { get; set; } = "test"; } + +public class ComplexTypeWithJsonPropertyName +{ + [Range(10, 100)] + public int DefaultPropertyName { get; set; } = 10; + + [Range(10, 100)] + [JsonPropertyName("custom-property-name")] + public int CustomJsonPropertyName { get; set; } = 20; + + [Display(Name = "display-name")] + [Range(10, 100)] + [JsonPropertyName("custom-property-name-with-display-name")] + public int CustomJsonPropertyNameWithDisplayName { get; set; } = 30; +} """; await Verify(source, out var compilation); await VerifyEndpoint(compilation, "/complex-type-with-json-ignore", async (endpoint, serviceProvider) => 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 a5a4b2e09ed0..af223cf495bb 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs @@ -15,6 +15,7 @@ public async Task CanValidateRecordTypes() using System; using System.ComponentModel.DataAnnotations; using System.Collections.Generic; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -31,6 +32,7 @@ public async Task CanValidateRecordTypes() var app = builder.Build(); app.MapPost("/validatable-record", (ValidatableRecord validatableRecord) => Results.Ok("Passed"!)); +app.MapPost("/validatable-record-with-json-property-name", (ValidatableRecordWithJsonPropertyNames validatableRecord) => Results.Ok("Passed"!)); app.Run(); @@ -95,7 +97,20 @@ public record ValidatableRecord( [DerivedValidation, Range(10, 100)] int PropertyWithMultipleAttributes = 10, [FromServices] [Required] TestService ServiceProperty = null!, // This should be ignored because of [FromServices] - [FromKeyedServices("serviceKey")] [Range(10, 100)] int KeyedServiceProperty = 5 // This should be ignored because of [FromKeyedServices] + [FromKeyedServices("serviceKey")] [Range(10, 100)] int KeyedServiceProperty = 5, // This should be ignored because of [FromKeyedServices] + [Display(Name = "display-name")] [Range(10, 100)] int IntegerWithRangeAndDisplay = 10 +); + +public record ValidatableRecordWithJsonPropertyNames( + [Range(10, 100)] + int DefaultPropertyName = 10, + [Range(10, 100)] + [property: JsonPropertyName("custom-property-name")] + int CustomJsonPropertyName = 20, + [Display(Name = "display-name")] + [Range(10, 100)] + [property: JsonPropertyName("custom-property-name-with-display-name")] + int CustomJsonPropertyNameWithDisplayName = 30 ); """; await Verify(source, out var compilation); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index c6957d8a67ec..1e5cd616ba4c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -101,6 +101,33 @@ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System. ); return true; } + if (type == typeof(global::ComplexTypeWithJsonPropertyName)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexTypeWithJsonPropertyName), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithJsonPropertyName), + propertyType: typeof(int), + name: "DefaultPropertyName", + displayName: "DefaultPropertyName" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithJsonPropertyName), + propertyType: typeof(int), + name: "CustomJsonPropertyName", + displayName: "custom-property-name" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithJsonPropertyName), + propertyType: typeof(int), + name: "CustomJsonPropertyNameWithDisplayName", + displayName: "display-name" + ), + ] + ); + return true; + } return false; } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index e308777967aa..8bc91d6fc814 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -193,6 +193,39 @@ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System. name: "PropertyWithMultipleAttributes", displayName: "PropertyWithMultipleAttributes" ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(int), + name: "IntegerWithRangeAndDisplay", + displayName: "display-name" + ), + ] + ); + return true; + } + if (type == typeof(global::ValidatableRecordWithJsonPropertyNames)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ValidatableRecordWithJsonPropertyNames), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecordWithJsonPropertyNames), + propertyType: typeof(int), + name: "DefaultPropertyName", + displayName: "DefaultPropertyName" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecordWithJsonPropertyNames), + propertyType: typeof(int), + name: "CustomJsonPropertyName", + displayName: "custom-property-name" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecordWithJsonPropertyNames), + propertyType: typeof(int), + name: "CustomJsonPropertyNameWithDisplayName", + displayName: "display-name" + ), ] ); return true;