diff --git a/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs b/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs index aea74a7ab19c..45e30b2efcd1 100644 --- a/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs +++ b/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs @@ -63,6 +63,32 @@ public static IncrementalValuesProvider Concat( }); } + public static IncrementalValuesProvider Concat( + this IncrementalValuesProvider> first, + IncrementalValuesProvider second) + { + return first.Collect() + .Combine(second.Collect()) + .SelectMany((tuple, _) => + { + if (tuple.Left.IsEmpty) + { + return tuple.Right; + } + + var results = ImmutableArray.CreateBuilder(tuple.Left.Length + tuple.Right.Length); + for (var i = 0; i < tuple.Left.Length; i++) + { + results.AddRange(tuple.Left[i]); + } + for (var i = 0; i < tuple.Right.Length; i++) + { + results.AddRange(tuple.Right[i]); + } + return results.DrainToImmutable(); + }); + } + private sealed class ImmutableArrayEqualityComparer : IEqualityComparer> { public static readonly ImmutableArrayEqualityComparer Instance = new(); diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs index 6de21bac7d81..23d7474ee63b 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs @@ -1,6 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -24,7 +23,7 @@ internal ImmutableArray TransformValidatableTypeWithAttribute(G var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation); if (TryExtractValidatableType((ITypeSymbol)context.TargetSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes)) { - return [..validatableTypes]; + return [.. validatableTypes]; } return []; } diff --git a/src/Validation/gen/ValidationsGenerator.cs b/src/Validation/gen/ValidationsGenerator.cs index 5f7c2c878ac5..512f1a37f61b 100644 --- a/src/Validation/gen/ValidationsGenerator.cs +++ b/src/Validation/gen/ValidationsGenerator.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Immutable; using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; namespace Microsoft.Extensions.Validation; @@ -16,24 +20,40 @@ public void Initialize(IncrementalGeneratorInitializationContext context) predicate: FindAddValidation, transform: TransformAddValidation ); - // Extract types that have been marked with [ValidatableType]. - var validatableTypesWithAttribute = context.SyntaxProvider.ForAttributeWithMetadataName( + + // Extract types that have been marked with framework [ValidatableType]. + var frameworkValidatableTypes = context.SyntaxProvider.ForAttributeWithMetadataName( "Microsoft.Extensions.Validation.ValidatableTypeAttribute", predicate: ShouldTransformSymbolWithAttribute, transform: TransformValidatableTypeWithAttribute ); + + // Extract types that have been marked with generated [ValidatableType]. + var generatedValidatableTypes = context.SyntaxProvider.ForAttributeWithMetadataName( + "Microsoft.Extensions.Validation.Embedded.ValidatableTypeAttribute", + predicate: ShouldTransformSymbolWithAttribute, + transform: TransformValidatableTypeWithAttribute + ); + + // Combine both sources of validatable types + var validatableTypesWithAttribute = frameworkValidatableTypes.Concat(generatedValidatableTypes); + // Extract all minimal API endpoints in the application. var endpoints = context.SyntaxProvider .CreateSyntaxProvider( predicate: FindEndpoints, transform: TransformEndpoints) .Where(endpoint => endpoint is not null); + // Extract validatable types from all endpoints. var validatableTypesFromEndpoints = endpoints .Select(ExtractValidatableEndpoint); + // Join all validatable types encountered in the type graph. - var validatableTypes = validatableTypesWithAttribute - .Concat(validatableTypesFromEndpoints) + var allValidatableTypesProviders = validatableTypesFromEndpoints + .Concat(validatableTypesWithAttribute); + + var validatableTypes = allValidatableTypesProviders .Distinct(ValidatableTypeComparer.Instance) .Collect(); @@ -42,6 +62,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Emit the IValidatableInfo resolver injection and // ValidatableTypeInfo for all validatable types. - context.RegisterSourceOutput(emitInputs, Emit); + context.RegisterSourceOutput(emitInputs, (context, emitInputs) => + Emit(context, (emitInputs.Left, emitInputs.Right))); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.AutoGeneratedAttribute.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.AutoGeneratedAttribute.cs new file mode 100644 index 000000000000..788d488e49b1 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.AutoGeneratedAttribute.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Validation.GeneratorTests; +using VerifyXunit; +using Xunit; + +namespace Microsoft.Extensions.Validation.GeneratorTests; + +[UsesVerify] +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + private const string GeneratedAttributeSource = """ + // + namespace Microsoft.CodeAnalysis + { + [global::System.AttributeUsage(global::System.AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class EmbeddedAttribute : global::System.Attribute + { + } + } + + namespace Microsoft.Extensions.Validation.Embedded + { + [global::Microsoft.CodeAnalysis.EmbeddedAttribute] + [global::System.AttributeUsage(global::System.AttributeTargets.Class)] + internal sealed class ValidatableTypeAttribute : global::System.Attribute + { + } + } + """; + + [Fact] + public async Task CanDiscoverGeneratedValidatableTypeAttribute() + { + var source = """ + + namespace MyApp + { + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using System.ComponentModel.DataAnnotations; + using Microsoft.Extensions.Validation.Embedded; + + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddValidation(); + var app = builder.Build(); + app.MapPost("/customers", (Customer customer) => "OK"); + app.Run(); + } + } + + [ValidatableType] + public class Customer + { + [Required] + public string Name { get; set; } = ""; + + [EmailAddress] + public string Email { get; set; } = ""; + } + } + """; + + // Combine the generated attribute with the user's source + var combinedSource = GeneratedAttributeSource + "\n" + source; + + await Verify(combinedSource, out var compilation); + } + + [Fact] + public async Task CanUseBothFrameworkAndGeneratedValidatableTypeAttributes() + { + var source = """ + namespace MyApp + { + using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.DependencyInjection; + using System.ComponentModel.DataAnnotations; + using Microsoft.Extensions.Validation.Embedded; + + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddValidation(); + var app = builder.Build(); + app.MapPost("/customers", (Customer customer) => "OK"); + app.Run(); + } + } + + // Using framework attribute + [Microsoft.Extensions.Validation.ValidatableType] + public class Customer + { + [Required] + public string Name { get; set; } = ""; + + [EmailAddress] + public string Email { get; set; } = ""; + } + + // Using generated attribute + [ValidatableType] + public class Product + { + [Required] + public string ProductName { get; set; } = ""; + + [Range(0, double.MaxValue)] + public decimal Price { get; set; } + } + } + """; + + // Combine the generated attribute with the user's source + var combinedSource = GeneratedAttributeSource + "\n" + source; + + await Verify(combinedSource, out var compilation); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..226fc69f7d3a --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,170 @@ +//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, + string displayName) : base(containingType, propertyType, name, displayName) + { + 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.GetValidationAttributes(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) : base(type, members) { } + } + + [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::MyApp.Customer)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::MyApp.Customer), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::MyApp.Customer), + propertyType: typeof(string), + name: "Name", + displayName: "Name" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::MyApp.Customer), + propertyType: typeof(string), + name: "Email", + displayName: "Email" + ), + ] + ); + 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 _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [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 _cache.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(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..20a3fe742bd2 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,191 @@ +//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, + string displayName) : base(containingType, propertyType, name, displayName) + { + 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.GetValidationAttributes(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) : base(type, members) { } + } + + [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::MyApp.Customer)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::MyApp.Customer), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::MyApp.Customer), + propertyType: typeof(string), + name: "Name", + displayName: "Name" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::MyApp.Customer), + propertyType: typeof(string), + name: "Email", + displayName: "Email" + ), + ] + ); + return true; + } + if (type == typeof(global::MyApp.Product)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::MyApp.Product), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::MyApp.Product), + propertyType: typeof(string), + name: "ProductName", + displayName: "ProductName" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::MyApp.Product), + propertyType: typeof(decimal), + name: "Price", + displayName: "Price" + ), + ] + ); + 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 _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [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 _cache.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(); + }); + } + } +} \ No newline at end of file