From 971554aea9cf012ea2930de5a1750888bb65e497 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Wed, 9 Jul 2025 16:15:19 +0200 Subject: [PATCH] Enhance validation for classes and records Updated `ShouldTransformSymbolWithAttribute` to support both class and record syntax nodes. Added a new test method `CanValidateRecordTypesWithAttribute` in `ValidatableType.cs` to validate record types with comprehensive scenarios for the `ComplexType` record. --- .../ValidationsGenerator.AttributeParser.cs | 2 +- .../ValidationsGenerator.ValidatableType.cs | 371 +++++++++++++++++- ...ute#ValidatableInfoResolver.g.verified.cs} | 0 ...bute#ValidatableInfoResolver.g.verified.cs | 238 +++++++++++ 4 files changed, 609 insertions(+), 2 deletions(-) rename src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/{ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs => ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs} (100%) create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs index 75566314d21b..6de21bac7d81 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs @@ -14,7 +14,7 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator { internal static bool ShouldTransformSymbolWithAttribute(SyntaxNode syntaxNode, CancellationToken cancellationToken) { - return syntaxNode is ClassDeclarationSyntax; + return syntaxNode is ClassDeclarationSyntax or RecordDeclarationSyntax; } internal ImmutableArray TransformValidatableTypeWithAttribute(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs index c471d99da183..f4041f62b979 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { [Fact] - public async Task CanValidateTypesWithAttribute() + public async Task CanValidateClassTypesWithAttribute() { var source = """ #pragma warning disable ASP0029 @@ -378,4 +378,373 @@ async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo) } }); } + + [Fact] + public async Task CanValidateRecordTypesWithAttribute() + { + var source = """ +#pragma warning disable ASP0029 + +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.Run(); + +[ValidatableType] +public record ComplexType +{ + [Range(10, 100)] + public int IntegerWithRange { get; set; } = 10; + + [Range(10, 100), Display(Name = "Valid identifier")] + public int IntegerWithRangeAndDisplayName { get; set; } = 50; + + [Required] + public SubType PropertyWithMemberAttributes { get; set; } = new SubType(); + + public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType(); + + public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance(); + + public List ListOfSubTypes { get; set; } = []; + + [CustomValidation(ErrorMessage = "Value must be an even number")] + public int IntegerWithCustomValidationAttribute { get; set; } + + [CustomValidation, Range(10, 100)] + public int PropertyWithMultipleAttributes { get; set; } = 10; +} + +public class CustomValidationAttribute : ValidationAttribute +{ + public override bool IsValid(object? value) => value is int number && number % 2 == 0; +} + +public record SubType +{ + [Required] + public string RequiredProperty { get; set; } = "some-value"; + + [StringLength(10)] + public string? StringWithLength { get; set; } +} + +public record SubTypeWithInheritance : SubType +{ + [EmailAddress] + public string? EmailString { get; set; } +} +"""; + await Verify(source, out var compilation); + VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) => + { + Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo)); + + await InvalidIntegerWithRangeProducesError(validatableTypeInfo); + await InvalidIntegerWithRangeAndDisplayNameProducesError(validatableTypeInfo); + await MissingRequiredSubtypePropertyProducesError(validatableTypeInfo); + await InvalidRequiredSubtypePropertyProducesError(validatableTypeInfo); + await InvalidSubTypeWithInheritancePropertyProducesError(validatableTypeInfo); + await InvalidListOfSubTypesProducesError(validatableTypeInfo); + await InvalidPropertyWithDerivedValidationAttributeProducesError(validatableTypeInfo); + await InvalidPropertyWithMultipleAttributesProducesError(validatableTypeInfo); + await InvalidPropertyWithCustomValidationProducesError(validatableTypeInfo); + await ValidInputProducesNoWarnings(validatableTypeInfo); + + async Task InvalidIntegerWithRangeProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithRange")?.SetValue(instance, 5); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithRange", kvp.Key); + Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task InvalidIntegerWithRangeAndDisplayNameProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 5); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key); + Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task MissingRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, null); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("PropertyWithMemberAttributes", kvp.Key); + Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single()); + }); + } + + async Task InvalidRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var subType = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType.GetType().GetProperty("RequiredProperty")?.SetValue(subType, ""); + subType.GetType().GetProperty("StringWithLength")?.SetValue(subType, "way-too-long"); + type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidSubTypeWithInheritancePropertyProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!); + inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, ""); + inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "way-too-long"); + inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "not-an-email"); + type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key); + Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidListOfSubTypesProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var subTypeList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!)); + + // Create first invalid item + var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, ""); + subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "way-too-long"); + + // Create second invalid item + var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid"); + subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "way-too-long"); + + // Create valid item + var subType3 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType3.GetType().GetProperty("RequiredProperty")?.SetValue(subType3, "valid"); + subType3.GetType().GetProperty("StringWithLength")?.SetValue(subType3, "valid"); + + // Add to list + subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType1]); + subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType2]); + subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType3]); + + type.GetProperty("ListOfSubTypes")?.SetValue(instance, subTypeList); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithDerivedValidationAttributeProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 5); // Odd number, should fail + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key); + Assert.Equal("Value must be an even number", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithMultipleAttributesProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 5); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("PropertyWithMultipleAttributes", kvp.Key); + Assert.Collection(kvp.Value, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error); + }, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error); + }); + }); + } + + async Task InvalidPropertyWithCustomValidationProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 3); // Odd number should fail + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key); + Assert.Equal("Value must be an even number", kvp.Value.Single()); + }); + } + + async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + + // Set all properties with valid values + type.GetProperty("IntegerWithRange")?.SetValue(instance, 50); + type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 50); + + // Create and set PropertyWithMemberAttributes + var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, "valid"); + subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "valid"); + type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType1); + + // Create and set PropertyWithoutMemberAttributes + var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid"); + subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "valid"); + type.GetProperty("PropertyWithoutMemberAttributes")?.SetValue(instance, subType2); + + // Create and set PropertyWithInheritance + var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!); + inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, "valid"); + inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "valid"); + inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "test@example.com"); + type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType); + + // Create empty list for ListOfSubTypes + var emptyList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!)); + type.GetProperty("ListOfSubTypes")?.SetValue(instance, emptyList); + + // Set custom validation attributes + type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 2); // Even number should pass + type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 12); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Null(context.ValidationErrors); + } + }); + } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs similarity index 100% rename from src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..52220c7918fa --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,238 @@ +//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.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)] + 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::SubType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "RequiredProperty", + displayName: "RequiredProperty" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + if (type == typeof(global::SubTypeWithInheritance)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubTypeWithInheritance), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubTypeWithInheritance), + propertyType: typeof(string), + name: "EmailString", + displayName: "EmailString" + ), + ] + ); + return true; + } + if (type == typeof(global::ComplexType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithRange", + displayName: "IntegerWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithRangeAndDisplayName", + displayName: "Valid identifier" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubType), + name: "PropertyWithMemberAttributes", + displayName: "PropertyWithMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubType), + name: "PropertyWithoutMemberAttributes", + displayName: "PropertyWithoutMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubTypeWithInheritance), + name: "PropertyWithInheritance", + displayName: "PropertyWithInheritance" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::System.Collections.Generic.List), + name: "ListOfSubTypes", + displayName: "ListOfSubTypes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithCustomValidationAttribute", + displayName: "IntegerWithCustomValidationAttribute" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "PropertyWithMultipleAttributes", + displayName: "PropertyWithMultipleAttributes" + ), + ] + ); + 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([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] 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.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