diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index 1685591afd5f..c232310dfaf1 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -75,6 +75,25 @@ internal static bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, W return false; } + // Type parameters (e.g., TRequest from a generic MapCommand() extension) + // have DeclaredAccessibility == NotApplicable. The concrete type is only known at + // call sites, not inside the generic method body where the endpoint delegate is + // defined. Walk constraint types to discover any validatable types reachable + // through type constraints. + if (typeSymbol is ITypeParameterSymbol typeParam) + { + // Add to visitedTypes BEFORE iterating constraints to prevent + // infinite recursion through circular constraints such as + // where T : class, IEnumerable. + visitedTypes.Add(typeSymbol); + var foundValidatable = false; + foreach (var constraintType in typeParam.ConstraintTypes) + { + foundValidatable |= TryExtractValidatableType(constraintType, wellKnownTypes, validatableTypes, visitedTypes); + } + return foundValidatable; + } + // Skip types that are not accessible from generated code if (typeSymbol.DeclaredAccessibility is not Accessibility.Public) { @@ -209,6 +228,16 @@ private static ImmutableArray ExtractValidatableMembers(ITy validatableTypes, visitedTypes); + // Skip properties whose type is a type parameter (e.g., TSelf + // from CRTP pattern RequestBase). The emitter would + // generate typeof(TSelf) which is not valid C#. Concrete + // validatable types reachable through constraints are already + // discovered by TryExtractValidatableType above. + if (ContainsTypeParameter(correspondingProperty.Type)) + { + continue; + } + // Record primary-constructor parameters can carry [Display]/[DisplayName] too. // Prefer the parameter's attribute over the property's. var (paramLiteral, paramHasResource) = parameter.GetDisplayInfo(displayAttributeSymbol, displayNameAttributeSymbol); @@ -276,6 +305,14 @@ private static ImmutableArray ExtractValidatableMembers(ITy continue; } + // Skip properties whose type is a type parameter (e.g., TSelf + // from CRTP pattern RequestBase). The emitter would + // generate typeof(TSelf) which is not valid C#. + if (ContainsTypeParameter(member.Type)) + { + continue; + } + var (memberLiteral, memberHasResource) = member.GetDisplayInfo(displayAttributeSymbol, displayNameAttributeSymbol); members.Add(new ValidatableProperty( @@ -310,4 +347,40 @@ internal static bool HasIValidatableObjectInterface(ITypeSymbol typeSymbol, Well var validatableObjectSymbol = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_IValidatableObject); return typeSymbol.ImplementsInterface(validatableObjectSymbol); } + + /// + /// Returns true if the given type symbol contains an unresolved type parameter + /// anywhere in its type tree. This catches not only bare T but also + /// constructed types like List<T>, T[], T?, and + /// Dictionary<string, T> — all of which would produce invalid + /// typeof(...) expressions in the emitted code. + /// + private static bool ContainsTypeParameter(ITypeSymbol type) + { + // Bare type parameter: T, TSelf, TSelf? + if (type is ITypeParameterSymbol) + { + return true; + } + + // Array: T[], T[,], List[] + if (type is IArrayTypeSymbol arrayType) + { + return ContainsTypeParameter(arrayType.ElementType); + } + + // Constructed generic: List, Dictionary, Nullable, Func + if (type is INamedTypeSymbol { IsGenericType: true } namedType) + { + foreach (var typeArg in namedType.TypeArguments) + { + if (ContainsTypeParameter(typeArg)) + { + return true; + } + } + } + + return false; + } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.TypeParameter.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.TypeParameter.cs new file mode 100644 index 000000000000..fe383ad7caf6 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.TypeParameter.cs @@ -0,0 +1,373 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.Validation.GeneratorTests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + // Repro for the silent-skip half of issue dotnet/aspnetcore#65418. When the + // route-handler delegate parameter resolves to an open ITypeParameterSymbol — + // as happens inside the body of a generic endpoint extension method like + // MapValidated(this IEndpointRouteBuilder, string) — TryExtractValidatableType + // on main hits the DeclaredAccessibility check (NotApplicable for type parameters) + // and silently returns false. The concrete constraint type is therefore never + // discovered and no typeof(...) check is emitted for it, even though it carries + // [Required] / [Range] attributes. With the fix the generator walks the type + // parameter's ConstraintTypes and discovers the concrete validatable type. + // + // The snapshot is the bug witness: on main the resolver body is empty (no + // typeof(global::UserRequest) check); with the fix the resolver contains the + // type and its members. + [Fact] + public async Task CanValidateOpenTypeParameterReachableThroughConstraint() + { + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Validation; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapValidated("/users"); + +app.Run(); + +public class UserRequest +{ + [Required] + public string Name { get; set; } = "default"; + + [Range(1, 120)] + public int Age { get; set; } = 25; +} + +public static class GenericEndpointExtensions +{ + public static RouteHandlerBuilder MapValidated(this IEndpointRouteBuilder endpoints, string pattern) + where T : UserRequest + => endpoints.MapPost(pattern, (T req) => Results.Ok()); +} +"""; + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/users", async (endpoint, serviceProvider) => + { + await InvalidNameProducesError(endpoint); + await InvalidAgeProducesError(endpoint); + await ValidInputProducesNoErrors(endpoint); + + async Task InvalidNameProducesError(Endpoint endpoint) + { + var payload = """ + { + "Name": "", + "Age": 30 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("Name", kvp.Key); + Assert.Equal("The Name field is required.", kvp.Value.Single()); + }); + } + + async Task InvalidAgeProducesError(Endpoint endpoint) + { + var payload = """ + { + "Name": "Alice", + "Age": 0 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("Age", kvp.Key); + Assert.Equal("The field Age must be between 1 and 120.", kvp.Value.Single()); + }); + } + + async Task ValidInputProducesNoErrors(Endpoint endpoint) + { + var payload = """ + { + "Name": "Alice", + "Age": 30 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + await endpoint.RequestDelegate(context); + + Assert.Equal(200, context.Response.StatusCode); + } + }); + } + + [Fact] + public async Task CanValidateTypesWithGenericBaseClass() + { + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapPost("/crtp-class", (CreateOrderCommand request) => Results.Ok("Passed"!)); +app.MapPost("/crtp-record", (CreateRecordCommand request) => Results.Ok("Passed"!)); + +app.Run(); + +public class CommandBase where TSelf : CommandBase +{ + [Required] + public string Name { get; set; } = "default"; +} + +public class CreateOrderCommand : CommandBase +{ + [Range(1, 1000)] + public int Quantity { get; set; } = 1; +} + +public record RecordCommandBase where TSelf : RecordCommandBase +{ + [Required] + public string Title { get; set; } = "default"; +} + +public record CreateRecordCommand : RecordCommandBase +{ + [Range(1, 100)] + public int Count { get; set; } = 1; +} +"""; + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/crtp-class", async (endpoint, serviceProvider) => + { + await InvalidNameProducesError(endpoint); + await InvalidQuantityProducesError(endpoint); + await ValidInputProducesNoErrors(endpoint); + + async Task InvalidNameProducesError(Endpoint endpoint) + { + var payload = """ + { + "Name": "", + "Quantity": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("Name", kvp.Key); + Assert.Equal("The Name field is required.", kvp.Value.Single()); + }); + } + + async Task InvalidQuantityProducesError(Endpoint endpoint) + { + var payload = """ + { + "Name": "valid", + "Quantity": 0 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("Quantity", kvp.Key); + Assert.Equal("The field Quantity must be between 1 and 1000.", kvp.Value.Single()); + }); + } + + async Task ValidInputProducesNoErrors(Endpoint endpoint) + { + var payload = """ + { + "Name": "valid", + "Quantity": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + await endpoint.RequestDelegate(context); + + Assert.Equal(200, context.Response.StatusCode); + } + }); + + await VerifyEndpoint(compilation, "/crtp-record", async (endpoint, serviceProvider) => + { + await InvalidTitleProducesError(endpoint); + await InvalidCountProducesError(endpoint); + await ValidRecordInputProducesNoErrors(endpoint); + + async Task InvalidTitleProducesError(Endpoint endpoint) + { + var payload = """ + { + "Title": "", + "Count": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("Title", kvp.Key); + Assert.Equal("The Title field is required.", kvp.Value.Single()); + }); + } + + async Task InvalidCountProducesError(Endpoint endpoint) + { + var payload = """ + { + "Title": "valid", + "Count": 0 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("Count", kvp.Key); + Assert.Equal("The field Count must be between 1 and 100.", kvp.Value.Single()); + }); + } + + async Task ValidRecordInputProducesNoErrors(Endpoint endpoint) + { + var payload = """ + { + "Title": "valid", + "Count": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + await endpoint.RequestDelegate(context); + + Assert.Equal(200, context.Response.StatusCode); + } + }); + } + + // Coverage for the ContainsTypeParameter guard — the typeof(TSelf) compile-error half of + // issue dotnet/aspnetcore#65418. A generic endpoint helper constrains its request type to a + // CRTP base (where T : Node) whose only validatable member is itself of the type + // parameter. Reached through the constraint walk as an open type, that member's type is the + // unresolved parameter, so ContainsTypeParameter is true and the member is dropped — at the + // regular-property guard for the class (Node.Next) and at the record primary-constructor + // guard for the record (RecordNode(TSelf Link)). With the member dropped, the base has no + // validatable members and is never emitted as a ValidatableType, so the snapshot resolver + // contains neither Node nor RecordNode. + // + // Without the guard the generator keeps the member and adds the open base with + // typeof(global::Node) — T is not in scope in the generated resolver — and the generated + // code fails to compile (CS0246). VerifyEndpoint re-emits source + generated code and asserts + // the emit succeeds, so this test is exactly that compile-error repro. + [Fact] + public async Task DoesNotEmitTypeofForTypeParameterMembersReachedThroughConstraint() + { + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Validation; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapValidatedNode("/nodes"); +app.MapValidatedRecord("/records"); + +app.Run(); + +public class Node where TSelf : Node +{ + [Required] + public TSelf Next { get; set; } = default!; +} + +public class ConcreteNode : Node +{ +} + +public record RecordNode(TSelf Link) where TSelf : RecordNode; + +public record ConcreteRecord() : RecordNode(default!); + +public static class GenericCrtpEndpointExtensions +{ + public static RouteHandlerBuilder MapValidatedNode(this IEndpointRouteBuilder endpoints, string pattern) + where T : Node + => endpoints.MapPost(pattern, (T req) => Results.Ok()); + + public static RouteHandlerBuilder MapValidatedRecord(this IEndpointRouteBuilder endpoints, string pattern) + where T : RecordNode + => endpoints.MapPost(pattern, (T req) => Results.Ok()); +} +"""; + await Verify(source, out var compilation); + + // Reaching either callback proves source + generated code compiled and the host started; + // without the guard the generated resolver references typeof(global::Node) and fails to + // compile at the VerifyEndpoint emit step. + await VerifyEndpoint(compilation, "/nodes", (endpoint, _) => + { + Assert.NotNull(endpoint); + return Task.CompletedTask; + }); + + await VerifyEndpoint(compilation, "/records", (endpoint, _) => + { + Assert.NotNull(endpoint); + return Task.CompletedTask; + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateOpenTypeParameterReachableThroughConstraint#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateOpenTypeParameterReachableThroughConstraint#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..78366ec8cde5 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateOpenTypeParameterReachableThroughConstraint#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,319 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(containingType, propertyType, name, displayNameInfo) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members, + global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(type, members, displayNameInfo) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::UserRequest)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::UserRequest), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::UserRequest), + propertyType: typeof(string), + name: "Name", + displayNameInfo: null + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::UserRequest), + propertyType: typeof(int), + name: "Age", + displayNameInfo: null + ), + ], + displayNameInfo: null + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class LiteralDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + private readonly string _literal; + + public LiteralDisplayName(string literal) + { + _literal = literal; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + { + var localizer = context.ValidationOptions.Localizer; + if (localizer is null) + { + return _literal; + } + return localizer.ResolveDisplayName(new global::Microsoft.Extensions.Validation.DisplayNameLocalizationContext + { + DeclaringType = declaringType, + DisplayName = _literal, + MemberName = memberName, + }) ?? _literal; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class PropertyResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + private readonly global::System.Type _containingType; + private readonly string _propertyName; + + public PropertyResourceDisplayName( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + _containingType = containingType; + _propertyName = propertyName; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + => DisplayAttributeCache.GetPropertyDisplayAttribute(_containingType, _propertyName)?.GetName(); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class TypeResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + private readonly global::System.Type _type; + + public TypeResourceDisplayName( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type) + { + _type = type; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + => DisplayAttributeCache.GetTypeDisplayAttribute(_type)?.GetName(); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class DisplayAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _typeCache = new(); + + public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetPropertyDisplayAttribute( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + // Check primary-constructor parameters first to handle record scenarios where + // [Display(ResourceType = ..., Name = ...)] is on the parameter rather than the property. + foreach (var constructor in k.ContainingType.GetConstructors()) + { + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.Ordinal)); + + if (parameter != null) + { + var paramDisplayAttr = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(parameter); + if (paramDisplayAttr is not null) + { + return paramDisplayAttr; + } + + break; + } + } + + var property = k.ContainingType.GetProperty(k.PropertyName); + return property is null + ? null + : global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(property, inherit: true); + }); + } + + public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetTypeDisplayAttribute( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type) + { + return _typeCache.GetOrAdd(type, static t => + global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(t, inherit: true)); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithGenericBaseClass#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithGenericBaseClass#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..2c635bbb167e --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithGenericBaseClass#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,361 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(containingType, propertyType, name, displayNameInfo) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members, + global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(type, members, displayNameInfo) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::CommandBase)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::CommandBase), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::CommandBase), + propertyType: typeof(string), + name: "Name", + displayNameInfo: null + ), + ], + displayNameInfo: null + ); + return true; + } + if (type == typeof(global::CreateOrderCommand)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::CreateOrderCommand), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::CreateOrderCommand), + propertyType: typeof(int), + name: "Quantity", + displayNameInfo: null + ), + ], + displayNameInfo: null + ); + return true; + } + if (type == typeof(global::RecordCommandBase)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::RecordCommandBase), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::RecordCommandBase), + propertyType: typeof(string), + name: "Title", + displayNameInfo: null + ), + ], + displayNameInfo: null + ); + return true; + } + if (type == typeof(global::CreateRecordCommand)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::CreateRecordCommand), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::CreateRecordCommand), + propertyType: typeof(int), + name: "Count", + displayNameInfo: null + ), + ], + displayNameInfo: null + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class LiteralDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + private readonly string _literal; + + public LiteralDisplayName(string literal) + { + _literal = literal; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + { + var localizer = context.ValidationOptions.Localizer; + if (localizer is null) + { + return _literal; + } + return localizer.ResolveDisplayName(new global::Microsoft.Extensions.Validation.DisplayNameLocalizationContext + { + DeclaringType = declaringType, + DisplayName = _literal, + MemberName = memberName, + }) ?? _literal; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class PropertyResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + private readonly global::System.Type _containingType; + private readonly string _propertyName; + + public PropertyResourceDisplayName( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + _containingType = containingType; + _propertyName = propertyName; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + => DisplayAttributeCache.GetPropertyDisplayAttribute(_containingType, _propertyName)?.GetName(); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class TypeResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + private readonly global::System.Type _type; + + public TypeResourceDisplayName( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type) + { + _type = type; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + => DisplayAttributeCache.GetTypeDisplayAttribute(_type)?.GetName(); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class DisplayAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _typeCache = new(); + + public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetPropertyDisplayAttribute( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + // Check primary-constructor parameters first to handle record scenarios where + // [Display(ResourceType = ..., Name = ...)] is on the parameter rather than the property. + foreach (var constructor in k.ContainingType.GetConstructors()) + { + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.Ordinal)); + + if (parameter != null) + { + var paramDisplayAttr = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(parameter); + if (paramDisplayAttr is not null) + { + return paramDisplayAttr; + } + + break; + } + } + + var property = k.ContainingType.GetProperty(k.PropertyName); + return property is null + ? null + : global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(property, inherit: true); + }); + } + + public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetTypeDisplayAttribute( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type) + { + return _typeCache.GetOrAdd(type, static t => + global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(t, inherit: true)); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitTypeofForTypeParameterMembersReachedThroughConstraint#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitTypeofForTypeParameterMembersReachedThroughConstraint#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..729bc96a5156 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitTypeofForTypeParameterMembersReachedThroughConstraint#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,297 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(containingType, propertyType, name, displayNameInfo) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members, + global::Microsoft.Extensions.Validation.DisplayNameInfo? displayNameInfo = null) : base(type, members, displayNameInfo) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class LiteralDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + private readonly string _literal; + + public LiteralDisplayName(string literal) + { + _literal = literal; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + { + var localizer = context.ValidationOptions.Localizer; + if (localizer is null) + { + return _literal; + } + return localizer.ResolveDisplayName(new global::Microsoft.Extensions.Validation.DisplayNameLocalizationContext + { + DeclaringType = declaringType, + DisplayName = _literal, + MemberName = memberName, + }) ?? _literal; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class PropertyResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + private readonly global::System.Type _containingType; + private readonly string _propertyName; + + public PropertyResourceDisplayName( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + _containingType = containingType; + _propertyName = propertyName; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + => DisplayAttributeCache.GetPropertyDisplayAttribute(_containingType, _propertyName)?.GetName(); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class TypeResourceDisplayName : global::Microsoft.Extensions.Validation.DisplayNameInfo + { + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + private readonly global::System.Type _type; + + public TypeResourceDisplayName( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type) + { + _type = type; + } + + public override string? GetDisplayName(global::Microsoft.Extensions.Validation.ValidateContext context, string memberName, global::System.Type? declaringType) + => DisplayAttributeCache.GetTypeDisplayAttribute(_type)?.GetName(); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class DisplayAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _typeCache = new(); + + public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetPropertyDisplayAttribute( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + // Check primary-constructor parameters first to handle record scenarios where + // [Display(ResourceType = ..., Name = ...)] is on the parameter rather than the property. + foreach (var constructor in k.ContainingType.GetConstructors()) + { + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.Ordinal)); + + if (parameter != null) + { + var paramDisplayAttr = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(parameter); + if (paramDisplayAttr is not null) + { + return paramDisplayAttr; + } + + break; + } + } + + var property = k.ContainingType.GetProperty(k.PropertyName); + return property is null + ? null + : global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(property, inherit: true); + }); + } + + public static global::System.ComponentModel.DataAnnotations.DisplayAttribute? GetTypeDisplayAttribute( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type) + { + return _typeCache.GetOrAdd(type, static t => + global::System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(t, inherit: true)); + } + } +} \ No newline at end of file