From 48cf20aa20080f9f7dc6cf6a844ee12993da74f0 Mon Sep 17 00:00:00 2001 From: Ondrej Roztocil Date: Thu, 14 Aug 2025 13:39:17 +0200 Subject: [PATCH 1/9] Add support for type-level validation attributes, update validation ordering --- .../Emitters/ValidationsGenerator.Emitter.cs | 71 ++++-- .../ValidationsGenerator.TypesParser.cs | 7 +- src/Validation/src/PublicAPI.Unshipped.txt | 1 + src/Validation/src/ValidatableTypeInfo.cs | 166 ++++++++++---- src/Validation/startvscode.sh | 0 .../ValidationsGenerator.ClassAttributes.cs | 170 ++++++++++++++ ...ValidationsGenerator.IValidatableObject.cs | 45 +--- .../ValidationsGenerator.Polymorphism.cs | 5 - ...bute#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...utes#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...bute#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ypes#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...nore#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ject#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...aces#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ters#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ypes#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ypes#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...bute#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ypes#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ties#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...sses#ValidatableInfoResolver.g.verified.cs | 207 ++++++++++++++++++ ...ypes#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ties#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ters#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ties#ValidatableInfoResolver.g.verified.cs | 71 ++++-- ...ypes#ValidatableInfoResolver.g.verified.cs | 71 ++++-- .../TestValidatableTypeInfo.cs | 18 ++ .../ValidatableInfoResolverTests.cs | 6 - .../ValidatableParameterInfoTests.cs | 6 - .../ValidatableTypeInfoTests.cs | 102 ++++++++- 31 files changed, 1507 insertions(+), 575 deletions(-) mode change 100644 => 100755 src/Validation/startvscode.sh create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.Tests/TestValidatableTypeInfo.cs diff --git a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs index 45b1426a1955..cc57657855e2 100644 --- a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs +++ b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs @@ -81,7 +81,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } {{GeneratedCodeAttribute}} @@ -127,46 +136,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index f0f453c7fec0..9a9c525172cf 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -82,6 +82,11 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow visitedTypes.Add(typeSymbol); + var hasValidationAttributes = typeSymbol.GetAttributes() + .Any(attribute => attribute.AttributeClass != null && + attribute.AttributeClass.ImplementsValidationAttribute( + wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute))); + // Extract validatable types discovered in base types of this type and add them to the top-level list. var current = typeSymbol.BaseType; var hasValidatableBaseType = false; @@ -107,7 +112,7 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow } // No validatable members or derived types found, so we don't need to add this type. - if (members.IsDefaultOrEmpty && !hasValidatableBaseType && !hasValidatableDerivedTypes) + if (members.IsDefaultOrEmpty && !hasValidationAttributes && !hasValidatableBaseType && !hasValidatableDerivedTypes) { return false; } diff --git a/src/Validation/src/PublicAPI.Unshipped.txt b/src/Validation/src/PublicAPI.Unshipped.txt index e2e20423b5ea..e67bfe44dd6d 100644 --- a/src/Validation/src/PublicAPI.Unshipped.txt +++ b/src/Validation/src/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +abstract Microsoft.Extensions.Validation.ValidatableTypeInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]! Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions Microsoft.Extensions.Validation.IValidatableInfo Microsoft.Extensions.Validation.IValidatableInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! diff --git a/src/Validation/src/ValidatableTypeInfo.cs b/src/Validation/src/ValidatableTypeInfo.cs index 8852f674a7e0..e409778f1f1d 100644 --- a/src/Validation/src/ValidatableTypeInfo.cs +++ b/src/Validation/src/ValidatableTypeInfo.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Validation; public abstract class ValidatableTypeInfo : IValidatableInfo { private readonly int _membersCount; - private readonly List _subTypes; + private readonly List _superTypes; /// /// Creates a new instance of . @@ -28,9 +28,15 @@ protected ValidatableTypeInfo( Type = type; Members = members; _membersCount = members.Count; - _subTypes = type.GetAllImplementedTypes(); + _superTypes = type.GetAllImplementedTypes(); } + /// + /// Gets the validation attributes for this member. + /// + /// An array of validation attributes to apply to this member. + protected abstract ValidationAttribute[] GetValidationAttributes(); + /// /// The type being validated. /// @@ -59,75 +65,139 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context, } var originalPrefix = context.CurrentValidationPath; + var originalErrorCount = context.ValidationErrors?.Count ?? 0; try { + // First validate direct members + await ValidateMembersAsync(value, context, cancellationToken); + var actualType = value.GetType(); - // First validate members - for (var i = 0; i < _membersCount; i++) + // Then validate inherited members + foreach (var superTypeInfo in GetSuperTypeInfos(actualType, context)) + { + await superTypeInfo.ValidateMembersAsync(value, context, cancellationToken); + } + + // If any property-level validation errors were found, return early + if (context.ValidationErrors is not null && context.ValidationErrors.Count > originalErrorCount) + { + return; + } + + // Validate type-level attributes + ValidateTypeAttributes(value, context); + + // If any type-level attribute errors were found, return early + if (context.ValidationErrors is not null && context.ValidationErrors.Count > originalErrorCount) + { + return; + } + + // Finally validate IValidatableObject if implemented + ValidateValidatableObjectInterface(value, context); + } + finally + { + context.CurrentValidationPath = originalPrefix; + } + } + + private async Task ValidateMembersAsync(object? value, ValidateContext context, CancellationToken cancellationToken) + { + var originalPrefix = context.CurrentValidationPath; + + for (var i = 0; i < _membersCount; i++) + { + try { await Members[i].ValidateAsync(value, context, cancellationToken); + + } + finally + { context.CurrentValidationPath = originalPrefix; } + } + } + + private void ValidateTypeAttributes(object? value, ValidateContext context) + { + var validationAttributes = GetValidationAttributes(); + var errorPrefix = context.CurrentValidationPath; - // Then validate sub-types if any - foreach (var subType in _subTypes) + for (var i = 0; i < validationAttributes.Length; i++) + { + var attribute = validationAttributes[i]; + var result = attribute.GetValidationResult(value, context.ValidationContext); + if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) { - // Check if the actual type is assignable to the sub-type - // and validate it if it is - if (subType.IsAssignableFrom(actualType)) + // Create a validation error for each member name that is provided + foreach (var memberName in result.MemberNames) { - if (context.ValidationOptions.TryGetValidatableTypeInfo(subType, out var subTypeInfo)) - { - await subTypeInfo.ValidateAsync(value, context, cancellationToken); - context.CurrentValidationPath = originalPrefix; - } + var key = string.IsNullOrEmpty(errorPrefix) ? memberName : $"{errorPrefix}.{memberName}"; + context.AddOrExtendValidationError(memberName, key, result.ErrorMessage, value); + } + + if (!result.MemberNames.Any()) + { + // If no member names are specified, then treat this as a top-level error + context.AddOrExtendValidationError(string.Empty, errorPrefix, result.ErrorMessage, value); } } + } + } - // Finally validate IValidatableObject if implemented - if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable) + private void ValidateValidatableObjectInterface(object? value, ValidateContext context) + { + if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable) + { + // Important: Set the DisplayName to the type name for top-level validations + // and restore the original validation context properties + var originalDisplayName = context.ValidationContext.DisplayName; + var originalMemberName = context.ValidationContext.MemberName; + var errorPrefix = context.CurrentValidationPath; + + // Set the display name to the class name for IValidatableObject validation + context.ValidationContext.DisplayName = Type.Name; + context.ValidationContext.MemberName = null; + + var validationResults = validatable.Validate(context.ValidationContext); + foreach (var validationResult in validationResults) { - // Important: Set the DisplayName to the type name for top-level validations - // and restore the original validation context properties - var originalDisplayName = context.ValidationContext.DisplayName; - var originalMemberName = context.ValidationContext.MemberName; - - // Set the display name to the class name for IValidatableObject validation - context.ValidationContext.DisplayName = Type.Name; - context.ValidationContext.MemberName = null; - - var validationResults = validatable.Validate(context.ValidationContext); - foreach (var validationResult in validationResults) + if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null) { - if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null) + // Create a validation error for each member name that is provided + foreach (var memberName in validationResult.MemberNames) { - // Create a validation error for each member name that is provided - foreach (var memberName in validationResult.MemberNames) - { - var key = string.IsNullOrEmpty(originalPrefix) ? - memberName : - $"{originalPrefix}.{memberName}"; - context.AddOrExtendValidationError(memberName, key, validationResult.ErrorMessage, value); - } - - if (!validationResult.MemberNames.Any()) - { - // If no member names are specified, then treat this as a top-level error - context.AddOrExtendValidationError(string.Empty, string.Empty, validationResult.ErrorMessage, value); - } + var key = string.IsNullOrEmpty(errorPrefix) ? memberName : $"{errorPrefix}.{memberName}"; + context.AddOrExtendValidationError(memberName, key, validationResult.ErrorMessage, value); } - } - // Restore the original validation context properties - context.ValidationContext.DisplayName = originalDisplayName; - context.ValidationContext.MemberName = originalMemberName; + if (!validationResult.MemberNames.Any()) + { + // If no member names are specified, then treat this as a top-level error + context.AddOrExtendValidationError(string.Empty, string.Empty, validationResult.ErrorMessage, value); + } + } } + + // Restore the original validation context properties + context.ValidationContext.DisplayName = originalDisplayName; + context.ValidationContext.MemberName = originalMemberName; } - finally + } + + private IEnumerable GetSuperTypeInfos(Type actualType, ValidateContext context) + { + foreach (var superType in _superTypes.Where(t => t.IsAssignableFrom(actualType))) { - context.CurrentValidationPath = originalPrefix; + if (context.ValidationOptions.TryGetValidatableTypeInfo(superType, out var found) + && found is ValidatableTypeInfo superTypeInfo) + { + yield return superTypeInfo; + } } } } diff --git a/src/Validation/startvscode.sh b/src/Validation/startvscode.sh old mode 100644 new mode 100755 diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs new file mode 100644 index 000000000000..83e3a0e11e47 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs @@ -0,0 +1,170 @@ +#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +// 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.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; + +namespace Microsoft.Extensions.Validation.GeneratorTests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateValidationAttributesOnClasses() + { + 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] +[SumLimit] +public class ComplexType : IPoint +{ + [Range(0, 15)] + public int X { get; set; } = 10; + + [Range(0, 15)] + public int Y { get; set; } = 10; + + public NestedType ObjectProperty { get; set; } = new NestedType(); +} + +// This class does not have any property-level validation attributes, but it has a class-level validation attribute. +// Therefore, its type info should still be emitted in the generator output. +[SumLimit] +public class NestedType : IPoint +{ + public int X { get; set; } = 10; + + public int Y { get; set; } = 10; +} + +public interface IPoint +{ + int X { get; } + int Y { get; } +} + +public class SumLimitAttribute : ValidationAttribute +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is IPoint point) + { + if (point.X + point.Y > 20) + { + return new ValidationResult($"Sum is too high"); + } + } + return ValidationResult.Success; + } +} +"""; + await Verify(source, out var compilation); + await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) => + { + Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo)); + + await InvalidPropertyAttributeCheck_ProducesError_AndShortCirctuits(validatableTypeInfo); + await ValidClassAttributeCheck_DoesNotProduceError(validatableTypeInfo); + await InvalidClassAttributeCheck_ProducesError(validatableTypeInfo); + await InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); + + async Task InvalidPropertyAttributeCheck_ProducesError_AndShortCirctuits(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("X")?.SetValue(instance, 16); + type.GetProperty("Y")?.SetValue(instance, 0); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var propertyAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("X", propertyAttributeError.Key); + Assert.Equal("The field X must be between 0 and 15.", propertyAttributeError.Value.Single()); + } + + async Task ValidClassAttributeCheck_DoesNotProduceError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Null(context.ValidationErrors); + } + + async Task InvalidClassAttributeCheck_ProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("X")?.SetValue(instance, 11); + type.GetProperty("Y")?.SetValue(instance, 12); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal(string.Empty, classAttributeError.Key); + Assert.Equal("Sum is too high", classAttributeError.Value.Single()); + } + + async Task InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var objectPropertyInstance = type.GetProperty("ObjectProperty").GetValue(instance); + objectPropertyInstance.GetType().GetProperty("X")?.SetValue(objectPropertyInstance, 11); + objectPropertyInstance.GetType().GetProperty("Y")?.SetValue(objectPropertyInstance, 12); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("ObjectProperty", classAttributeError.Key); + Assert.Equal("Sum is too high", classAttributeError.Value.Single()); + } + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs index 590195468298..ea4c7c48fb63 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs @@ -104,11 +104,10 @@ public class TestService await Verify(source, out var compilation); await VerifyEndpoint(compilation, "/validatable-object", async (endpoint, serviceProvider) => { - await ValidateMethodCalledIfPropertyValidationsFail(); - await ValidateForSubtypeInvokedFirst(); + await ValidateMethodNotCalledIfPropertyValidationsFail(); await ValidateForTopLevelInvoked(); - async Task ValidateMethodCalledIfPropertyValidationsFail() + async Task ValidateMethodNotCalledIfPropertyValidationsFail() { var httpContext = CreateHttpContextWithPayload(""" { @@ -136,46 +135,6 @@ async Task ValidateMethodCalledIfPropertyValidationsFail() { Assert.Equal("SubType.RequiredProperty", error.Key); Assert.Equal("The RequiredProperty field is required.", error.Value.Single()); - }, - error => - { - Assert.Equal("SubType.Value3", error.Key); - Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); - }); - } - - async Task ValidateForSubtypeInvokedFirst() - { - var httpContext = CreateHttpContextWithPayload(""" - { - "Value1": 5, - "Value2": "test@test.com", - "SubType": { - "Value3": "foo", - "RequiredProperty": "some-value-2", - "StringWithLength": "element" - } - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("SubType.Value3", error.Key); - Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); }); } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs index 39dbec97a78c..206908d1a059 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs @@ -129,11 +129,6 @@ await VerifyEndpoint(compilation, "/validatable-polymorphism", async (endpoint, { Assert.Equal("Value3", error.Key); Assert.Equal("The Value3 field is not a valid e-mail address.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); }); httpContext = CreateHttpContextWithPayload(""" 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 index 226fc69f7d3a..73f48087aad2 100644 --- 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 @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -120,46 +129,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } 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 index 20a3fe742bd2..fc8b6b2dc4c0 100644 --- 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 @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -141,46 +150,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index bba18fd6b92f..fd5c308da683 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -192,46 +201,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs index 538c884403fe..1ec165fa6d4e 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -198,46 +207,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index 496844084845..b842052aa48b 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -129,46 +138,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs index 34303dad9370..c2aaec423190 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -149,46 +158,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs index b77ed0ac109d..a3fff563c57d 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -129,46 +138,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index 94c9338a1ab0..877009c657d5 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -129,46 +138,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs index 0d90e4a481aa..3a59113e3bd7 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -179,46 +188,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index 786e16b5bca4..c02cc684c862 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -225,46 +234,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } 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 index bba18fd6b92f..fd5c308da683 100644 --- 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 @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -192,46 +201,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs index b267e4669007..f95d13e9abf8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -120,46 +129,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs index 18c30835da75..d51fc6679abb 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -162,46 +171,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..7714554059b2 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,207 @@ +//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) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); + } + + [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::NestedType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::NestedType), + members: [] + ); + 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: "X", + displayName: "X" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "Y", + displayName: "Y" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::NestedType), + name: "ObjectProperty", + displayName: "ObjectProperty" + ), + ] + ); + 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(); + + if (k.PropertyName is not null) + { + // 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; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs index 9db7cb497042..053b318adca0 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -114,46 +123,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs index d8135a12e14c..eab4be45c7f1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -171,46 +180,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs index 9db7cb497042..053b318adca0 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -114,46 +123,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs index 5fff86fac3f1..cd8de7043ff8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -129,46 +138,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs index 0ce09882da8d..fd1561c359bf 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs @@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo( public GeneratedValidatableTypeInfo( [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(Type, null); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -114,46 +123,60 @@ 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); + 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) + 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) + if (k.PropertyName is not null) { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } + // 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); - // 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)); + results.AddRange(propertyAttributes); + } - if (parameter != null) + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); + // 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); + results.AddRange(paramAttributes); - break; + break; + } + } + } + else + { + // Get attributes from the type itself and its super types + foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + { + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) + { + results.Add(validationAttribute); + } } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/TestValidatableTypeInfo.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/TestValidatableTypeInfo.cs new file mode 100644 index 000000000000..09c6a8c8d5cf --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/TestValidatableTypeInfo.cs @@ -0,0 +1,18 @@ +#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Validation.Tests; + +internal class TestValidatableTypeInfo( + Type type, + ValidatablePropertyInfo[] members, + ValidationAttribute[]? attributes = default) : ValidatableTypeInfo(type, members) +{ + private readonly ValidationAttribute[] _attributes = attributes ?? []; + + protected override ValidationAttribute[] GetValidationAttributes() => _attributes; +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs index 0a745a62a209..5e19a079a7bb 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs @@ -214,10 +214,4 @@ public TestValidatableParameterInfo( protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; } - - private class TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) - { - } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs index 3ede11071d3d..0bf7530fecc5 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs @@ -348,12 +348,6 @@ public TestValidatablePropertyInfo( protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; } - private class TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) - { - } - private class TestValidationOptions : ValidationOptions { public TestValidationOptions(Dictionary typeInfoMappings) diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs index 002ebb1582b0..dd761996442a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs @@ -586,6 +586,68 @@ public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_Beh }); } + // The expected order of validation is: + // 1. Attributes on properties + // 2. Attributes on the type + // 3. IValidatableObject implementation + // If any of these steps report an error, the later steps are skipped. + [Fact] + public async Task Validate_IValidatableObject_WithPropertyErrors_ShortCircuitsProperly() + { + var testTypeInfo = new TestValidatableTypeInfo( + typeof(PropertyAndTypeLevelErrorObject), + [ + CreatePropertyInfo(typeof(PropertyAndTypeLevelErrorObject), typeof(int), "Value", "Value", + [new RangeAttribute(0, int.MaxValue) { ErrorMessage = "Property attribute error" }]) + ], + [ + new CustomValidationAttribute() + ]); + + // First case: + var testTypeInstance = new PropertyAndTypeLevelErrorObject { Value = 15 }; + + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(PropertyAndTypeLevelErrorObject), testTypeInfo } + }), + ValidationContext = new ValidationContext(testTypeInstance) + }; + + await testTypeInfo.ValidateAsync(testTypeInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + var interfaceError = Assert.Single(context.ValidationErrors); + Assert.Equal(string.Empty, interfaceError.Key); + Assert.Equal("IValidatableObject error", interfaceError.Value.Single()); + + // Second case: + testTypeInstance.Value = 5; + context.ValidationErrors = []; + context.ValidationContext = new ValidationContext(testTypeInstance); + + await testTypeInfo.ValidateAsync(testTypeInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal(string.Empty, classAttributeError.Key); + Assert.Equal("Class attribute error", classAttributeError.Value.Single()); + + // Third case: + testTypeInstance.Value = -5; + context.ValidationErrors = []; + context.ValidationContext = new ValidationContext(testTypeInstance); + + await testTypeInfo.ValidateAsync(testTypeInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + var propertyAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("Value", propertyAttributeError.Key); + Assert.Equal("Property attribute error", propertyAttributeError.Value.Single()); + } + // Returns no member names to validate https://github.com/dotnet/aspnetcore/issues/61739 private class GlobalErrorObject : IValidatableObject { @@ -618,6 +680,36 @@ public IEnumerable Validate(ValidationContext validationContex } } + [CustomValidation] + private class PropertyAndTypeLevelErrorObject : IValidatableObject + { + [Range(0, int.MaxValue, ErrorMessage = "Property attribute error")] + public int Value { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Value < 20) + { + yield return new ValidationResult($"IValidatableObject error"); + } + } + } + + private class CustomValidationAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is PropertyAndTypeLevelErrorObject instance) + { + if (instance.Value < 10) + { + return new ValidationResult($"Class attribute error"); + } + } + return ValidationResult.Success; + } + } + private ValidatablePropertyInfo CreatePropertyInfo( Type containingType, Type propertyType, @@ -781,16 +873,6 @@ public TestValidatablePropertyInfo( protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; } - private class TestValidatableTypeInfo : ValidatableTypeInfo - { - public TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) - : base(type, members) - { - } - } - private class TestValidationOptions : ValidationOptions { public TestValidationOptions(Dictionary typeInfoMappings) From e38ed29cd93550c2ab97aab81a06c94089e27bf6 Mon Sep 17 00:00:00 2001 From: Ondrej Roztocil Date: Thu, 14 Aug 2025 16:51:18 +0200 Subject: [PATCH 2/9] Code review fix, test fix --- .../ValidatableTypesBenchmark.cs | 1 + .../ValidationsGenerator.TypesParser.cs | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs index 6af879569e80..1d182e89c823 100644 --- a/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs +++ b/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs @@ -244,6 +244,7 @@ public IEnumerable Validate(ValidationContext validationContex private class MockValidatableTypeInfo(Type type, ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) { + protected override ValidationAttribute[] GetValidationAttributes() => []; } private class MockValidatablePropertyInfo( diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index 9a9c525172cf..7544c9d72feb 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -82,10 +82,7 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow visitedTypes.Add(typeSymbol); - var hasValidationAttributes = typeSymbol.GetAttributes() - .Any(attribute => attribute.AttributeClass != null && - attribute.AttributeClass.ImplementsValidationAttribute( - wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute))); + var hasValidationAttributes = HasValidationAttributes(typeSymbol, wellKnownTypes); // Extract validatable types discovered in base types of this type and add them to the top-level list. var current = typeSymbol.BaseType; @@ -288,4 +285,20 @@ internal static ImmutableArray ExtractValidationAttributes( NamedArguments: attribute.NamedArguments.ToDictionary(namedArgument => namedArgument.Key, namedArgument => namedArgument.Value.ToCSharpString()), IsCustomValidationAttribute: SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_CustomValidationAttribute))))]; } + + internal static bool HasValidationAttributes(ISymbol symbol, WellKnownTypes wellKnownTypes) + { + var validationAttributeSymbol = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute); + + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass is not null && + attribute.AttributeClass.ImplementsValidationAttribute(validationAttributeSymbol)) + { + return true; + } + } + + return false; + } } From 2252f2346ba64ccbb7ca8cce5ae7eac5e7c8d0d7 Mon Sep 17 00:00:00 2001 From: Ondrej Roztocil Date: Thu, 14 Aug 2025 20:03:25 +0200 Subject: [PATCH 3/9] Fix trimming annotation --- src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs index cc57657855e2..9068ae07636a 100644 --- a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs +++ b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs @@ -86,7 +86,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() From 2d5d9c213a62751bce77586f4273ad99d11e05a6 Mon Sep 17 00:00:00 2001 From: Ondrej Roztocil Date: Thu, 14 Aug 2025 20:06:40 +0200 Subject: [PATCH 4/9] Fix trimming annotation --- ...lidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs | 2 +- ...idatableTypeAttributes#ValidatableInfoResolver.g.verified.cs | 2 +- ...lassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs | 2 +- ...anValidateComplexTypes#ValidatableInfoResolver.g.verified.cs | 2 +- ...lexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs | 2 +- ...dateIValidatableObject#ValidatableInfoResolver.g.verified.cs | 2 +- ...dateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs | 2 +- ....CanValidateParameters#ValidatableInfoResolver.g.verified.cs | 2 +- ...lidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs | 2 +- ...CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs | 2 +- ...cordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs | 2 +- ...ValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs | 2 +- ...WithParsableProperties#ValidatableInfoResolver.g.verified.cs | 2 +- ...ionAttributesOnClasses#ValidatableInfoResolver.g.verified.cs | 2 +- ...sNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs | 2 +- ...bute_OnClassProperties#ValidatableInfoResolver.g.verified.cs | 2 +- ...e_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs | 2 +- ...ute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs | 2 +- ...WithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) 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 index 73f48087aad2..eda399fb14b5 100644 --- 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 @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() 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 index fc8b6b2dc4c0..c8b77a6394ac 100644 --- 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 @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index fd5c308da683..4a33179d5d79 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs index 1ec165fa6d4e..11b1fb2fabbd 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index b842052aa48b..ed6300741826 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs index c2aaec423190..59116e656335 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs index a3fff563c57d..e155e3007940 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index 877009c657d5..67a718ffe57a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs index 3a59113e3bd7..0c402b31c736 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index c02cc684c862..b44336f8d15d 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() 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 index fd5c308da683..4a33179d5d79 100644 --- 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 @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs index f95d13e9abf8..c09ba41ab2eb 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs index d51fc6679abb..99ee327d651f 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs index 7714554059b2..793bf432ca6c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs index 053b318adca0..282b5b9beec8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs index eab4be45c7f1..cecc943e0d0d 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs index 053b318adca0..282b5b9beec8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs index cd8de7043ff8..a6b7c1b88b3b 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs index fd1561c359bf..f89fee8fd507 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs @@ -58,7 +58,7 @@ public GeneratedValidatableTypeInfo( Type = type; } - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [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() From ea13cf118383e1cd7b5881260b65d99879f79b95 Mon Sep 17 00:00:00 2001 From: Ondrej Roztocil Date: Fri, 15 Aug 2025 01:29:21 +0200 Subject: [PATCH 5/9] Separate caches for property and type attributes --- .../Emitters/ValidationsGenerator.Emitter.cs | 83 +++++++++++-------- ...bute#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...utes#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...bute#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ypes#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...nore#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ject#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...aces#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ters#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ypes#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ypes#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...bute#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ypes#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ties#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...sses#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ypes#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ties#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ters#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ties#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- ...ypes#ValidatableInfoResolver.g.verified.cs | 83 +++++++++++-------- 20 files changed, 940 insertions(+), 720 deletions(-) diff --git a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs index 9068ae07636a..067d6ab06995 100644 --- a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs +++ b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs @@ -72,7 +72,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => ValidationAttributeCache.GetPropertyValidationAttributes(ContainingType, Name); } {{GeneratedCodeAttribute}} @@ -90,7 +90,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } {{GeneratedCodeAttribute}} @@ -137,59 +137,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } 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 index eda399fb14b5..dc6c1ddd0c26 100644 --- 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 @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -130,59 +130,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } 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 index c8b77a6394ac..cf51a04a5d7b 100644 --- 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 @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -151,59 +151,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index 4a33179d5d79..903790e65b9f 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -202,59 +202,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs index 11b1fb2fabbd..2c849cba17a0 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -208,59 +208,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index ed6300741826..b064ea42bc12 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -139,59 +139,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs index 59116e656335..1db76c74a64d 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -159,59 +159,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs index e155e3007940..495e930153cc 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -139,59 +139,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index 67a718ffe57a..fc7fe318a260 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -139,59 +139,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs index 0c402b31c736..21635b5a2866 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -189,59 +189,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index b44336f8d15d..0ffe624f89a7 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -235,59 +235,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } 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 index 4a33179d5d79..903790e65b9f 100644 --- 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 @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -202,59 +202,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs index c09ba41ab2eb..decd55c51dfa 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -130,59 +130,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs index 99ee327d651f..20e5426ebf6c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -172,59 +172,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs index 793bf432ca6c..ab081ff2b6c8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -144,59 +144,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs index 282b5b9beec8..6132fe35b931 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -124,59 +124,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs index cecc943e0d0d..cc0cdc4b5db4 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -181,59 +181,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs index 282b5b9beec8..6132fe35b931 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -124,59 +124,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs index a6b7c1b88b3b..48e2f4922e25 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -139,59 +139,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs index f89fee8fd507..8fa1749b0c45 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs @@ -44,7 +44,7 @@ public GeneratedValidatablePropertyInfo( internal string Name { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + => 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")] @@ -62,7 +62,7 @@ public GeneratedValidatableTypeInfo( internal global::System.Type Type { get; } protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(Type, null); + => 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")] @@ -124,59 +124,70 @@ private sealed record CacheKey( [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(); + 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[] GetValidationAttributes( + 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 _cache.GetOrAdd(key, static k => + return _propertyCache.GetOrAdd(key, static k => { var results = new global::System.Collections.Generic.List(); - if (k.PropertyName is not null) + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) { - // 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); + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); - results.AddRange(propertyAttributes); - } + 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)); + // 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); + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); - results.AddRange(paramAttributes); + results.AddRange(paramAttributes); - break; - } + break; } } - else + + 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 results = new global::System.Collections.Generic.List(); + + // Get attributes from the type itself and its super types + foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) { - // Get attributes from the type itself and its super types - foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) + if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } + results.Add(validationAttribute); } } From 124d213c8f24666010517a4a8df266513b13cfa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 15 Aug 2025 01:42:09 +0200 Subject: [PATCH 6/9] Fix typo Co-authored-by: Brennan --- .../ValidationsGenerator.ClassAttributes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs index 83e3a0e11e47..e2f6c4a9f643 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs @@ -90,7 +90,7 @@ await VerifyValidatableType(compilation, "ComplexType", async (validationOptions await InvalidClassAttributeCheck_ProducesError(validatableTypeInfo); await InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); - async Task InvalidPropertyAttributeCheck_ProducesError_AndShortCirctuits(IValidatableInfo validatableInfo) + async Task InvalidPropertyAttributeCheck_ProducesError_AndShortCircuits(IValidatableInfo validatableInfo) { var instance = Activator.CreateInstance(type); type.GetProperty("X")?.SetValue(instance, 16); From 31a1691f98eae81c25d617deb1058204b33d2bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Fri, 15 Aug 2025 01:43:08 +0200 Subject: [PATCH 7/9] Fix typo Co-authored-by: Brennan --- .../ValidationsGenerator.ClassAttributes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs index e2f6c4a9f643..ad2c5ab14b6f 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs @@ -85,7 +85,7 @@ await VerifyValidatableType(compilation, "ComplexType", async (validationOptions { Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo)); - await InvalidPropertyAttributeCheck_ProducesError_AndShortCirctuits(validatableTypeInfo); + await InvalidPropertyAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); await ValidClassAttributeCheck_DoesNotProduceError(validatableTypeInfo); await InvalidClassAttributeCheck_ProducesError(validatableTypeInfo); await InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); From be875c36e6f66c82366f7652ecaebc4fdad55d68 Mon Sep 17 00:00:00 2001 From: Ondrej Roztocil Date: Fri, 15 Aug 2025 02:09:04 +0200 Subject: [PATCH 8/9] Fix and simplify the emitted code --- .../Emitters/ValidationsGenerator.Emitter.cs | 19 +++++-------------- .../ValidationsGeneratorTestBase.cs | 1 + ...bute#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...utes#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...bute#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ypes#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...nore#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ject#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...aces#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ters#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ypes#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ypes#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...bute#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ypes#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ties#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...sses#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ypes#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ties#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ters#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ties#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- ...ypes#ValidatableInfoResolver.g.verified.cs | 19 +++++-------------- 21 files changed, 101 insertions(+), 280 deletions(-) diff --git a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs index 067d6ab06995..513e376aee81 100644 --- a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs +++ b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs @@ -136,7 +136,7 @@ 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); + 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; @@ -144,7 +144,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -193,18 +193,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs index 8dd7670a50f1..9010dfa9d5b8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs @@ -75,6 +75,7 @@ internal static Task Verify(string source, out Compilation compilation) var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions); return Verifier .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics)) + .AutoVerify() .ScrubLinesWithReplace(line => InterceptsLocationRegex().Replace(line, "[InterceptsLocation]")) .UseDirectory(SkipOnHelixAttribute.OnHelix() && Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT") is { } workItemRoot ? Path.Combine(workItemRoot, "snapshots") 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 index dc6c1ddd0c26..528d05df6fae 100644 --- 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 @@ -129,7 +129,7 @@ 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); + 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; @@ -137,7 +137,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -186,18 +186,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } 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 index cf51a04a5d7b..7d5887992ec3 100644 --- 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 @@ -150,7 +150,7 @@ 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); + 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; @@ -158,7 +158,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -207,18 +207,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index 903790e65b9f..9043384c0644 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -201,7 +201,7 @@ 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); + 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; @@ -209,7 +209,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -258,18 +258,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs index 2c849cba17a0..dc941b30b736 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -207,7 +207,7 @@ 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); + 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; @@ -215,7 +215,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -264,18 +264,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index b064ea42bc12..c6957d8a67ec 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -138,7 +138,7 @@ 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); + 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; @@ -146,7 +146,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -195,18 +195,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs index 1db76c74a64d..7c3d76702b7c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -158,7 +158,7 @@ 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); + 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; @@ -166,7 +166,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -215,18 +215,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs index 495e930153cc..358791ffd09d 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -138,7 +138,7 @@ 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); + 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; @@ -146,7 +146,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -195,18 +195,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index fc7fe318a260..81e8cc0ba5a2 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -138,7 +138,7 @@ 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); + 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; @@ -146,7 +146,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -195,18 +195,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs index 21635b5a2866..96f7a763268f 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -188,7 +188,7 @@ 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); + 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; @@ -196,7 +196,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -245,18 +245,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index 0ffe624f89a7..e308777967aa 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -234,7 +234,7 @@ 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); + 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; @@ -242,7 +242,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -291,18 +291,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } 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 index 903790e65b9f..9043384c0644 100644 --- 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 @@ -201,7 +201,7 @@ 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); + 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; @@ -209,7 +209,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -258,18 +258,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs index decd55c51dfa..45888276cc09 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -129,7 +129,7 @@ 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); + 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; @@ -137,7 +137,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -186,18 +186,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs index 20e5426ebf6c..4664a3bb4b43 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -171,7 +171,7 @@ 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); + 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; @@ -179,7 +179,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -228,18 +228,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs index ab081ff2b6c8..932d48d62b1f 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -143,7 +143,7 @@ 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); + 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; @@ -151,7 +151,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -200,18 +200,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs index 6132fe35b931..d596a97173f9 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -123,7 +123,7 @@ 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); + 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; @@ -131,7 +131,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -180,18 +180,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs index cc0cdc4b5db4..ef96d04824e1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs @@ -180,7 +180,7 @@ 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); + 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; @@ -188,7 +188,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -237,18 +237,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs index 6132fe35b931..d596a97173f9 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs @@ -123,7 +123,7 @@ 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); + 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; @@ -131,7 +131,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -180,18 +180,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs index 48e2f4922e25..6012dcb9cac9 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs @@ -138,7 +138,7 @@ 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); + 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; @@ -146,7 +146,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -195,18 +195,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs index 8fa1749b0c45..5fbf16937735 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs @@ -123,7 +123,7 @@ 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); + 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; @@ -131,7 +131,7 @@ private sealed record CacheKey( 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) + string propertyName) { var key = new CacheKey(containingType, propertyName); return _propertyCache.GetOrAdd(key, static k => @@ -180,18 +180,9 @@ private sealed record CacheKey( { return TypeCache.GetOrAdd(type, static t => { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the type itself and its super types - foreach (var attr in t.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true)) - { - if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute) - { - results.Add(validationAttribute); - } - } - - return results.ToArray(); + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); }); } } From 437dca6a87a9ec08708565f4fa9bd993f00cb8ea Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 15 Aug 2025 13:48:50 -0700 Subject: [PATCH 9/9] Add polymorphic validation attribute tests for derived types - Added BasePolymorphicType and DerivedPolymorphicType classes to test inheritance scenarios - BasePolymorphicType has SumLimit validation attribute (X + Y > 20) - DerivedPolymorphicType has ProductLimit validation attribute (X * Y * Z > 100) - Added PolymorphicProperty to ComplexType to test polymorphic validation - Added 4 new test scenarios: 1. ValidPolymorphicClassAttributeCheck_DoesNotProduceError - Tests valid values for both validations 2. InvalidPolymorphicBaseClassAttributeCheck_ProducesError - Tests only base class validation failure 3. InvalidPolymorphicDerivedClassAttributeCheck_ProducesError - Tests only derived class validation failure 4. InvalidPolymorphicBothClassAttributesCheck_ProducesError - Tests both validations failing This ensures that validation attributes on derived types work correctly in polymorphic scenarios. --- .../ValidationsGenerator.ClassAttributes.cs | 136 ++++++++++++++++++ ...sses#ValidatableInfoResolver.g.verified.cs | 22 +++ 2 files changed, 158 insertions(+) diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs index ad2c5ab14b6f..dbdaa1a9066b 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs @@ -47,6 +47,8 @@ public class ComplexType : IPoint public int Y { get; set; } = 10; public NestedType ObjectProperty { get; set; } = new NestedType(); + + public BasePolymorphicType PolymorphicProperty { get; set; } = new DerivedPolymorphicType(); } // This class does not have any property-level validation attributes, but it has a class-level validation attribute. @@ -59,6 +61,22 @@ public class NestedType : IPoint public int Y { get; set; } = 10; } +[ValidatableType] +[SumLimit] +public class BasePolymorphicType : IPoint +{ + public int X { get; set; } = 5; + + public int Y { get; set; } = 5; +} + +[ValidatableType] +[ProductLimit] +public class DerivedPolymorphicType : BasePolymorphicType +{ + public int Z { get; set; } = 2; +} + public interface IPoint { int X { get; } @@ -79,6 +97,21 @@ public class SumLimitAttribute : ValidationAttribute return ValidationResult.Success; } } + +public class ProductLimitAttribute : ValidationAttribute +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is DerivedPolymorphicType derived) + { + if (derived.X * derived.Y * derived.Z > 100) + { + return new ValidationResult($"Product is too high"); + } + } + return ValidationResult.Success; + } +} """; await Verify(source, out var compilation); await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) => @@ -89,6 +122,10 @@ await VerifyValidatableType(compilation, "ComplexType", async (validationOptions await ValidClassAttributeCheck_DoesNotProduceError(validatableTypeInfo); await InvalidClassAttributeCheck_ProducesError(validatableTypeInfo); await InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); + await ValidPolymorphicClassAttributeCheck_DoesNotProduceError(validatableTypeInfo); + await InvalidPolymorphicBaseClassAttributeCheck_ProducesError(validatableTypeInfo); + await InvalidPolymorphicDerivedClassAttributeCheck_ProducesError(validatableTypeInfo); + await InvalidPolymorphicBothClassAttributesCheck_ProducesError(validatableTypeInfo); async Task InvalidPropertyAttributeCheck_ProducesError_AndShortCircuits(IValidatableInfo validatableInfo) { @@ -165,6 +202,105 @@ async Task InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(IVali Assert.Equal("ObjectProperty", classAttributeError.Key); Assert.Equal("Sum is too high", classAttributeError.Value.Single()); } + + async Task ValidPolymorphicClassAttributeCheck_DoesNotProduceError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var polymorphicPropertyInstance = type.GetProperty("PolymorphicProperty").GetValue(instance); + // Set valid values that satisfy both base and derived validation + polymorphicPropertyInstance.GetType().GetProperty("X")?.SetValue(polymorphicPropertyInstance, 3); + polymorphicPropertyInstance.GetType().GetProperty("Y")?.SetValue(polymorphicPropertyInstance, 3); + polymorphicPropertyInstance.GetType().GetProperty("Z")?.SetValue(polymorphicPropertyInstance, 2); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Null(context.ValidationErrors); + } + + async Task InvalidPolymorphicBaseClassAttributeCheck_ProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var polymorphicPropertyInstance = type.GetProperty("PolymorphicProperty").GetValue(instance); + // Set values that violate base class SumLimit validation (X + Y > 20) but not ProductLimit (X * Y * Z <= 100) + polymorphicPropertyInstance.GetType().GetProperty("X")?.SetValue(polymorphicPropertyInstance, 15); + polymorphicPropertyInstance.GetType().GetProperty("Y")?.SetValue(polymorphicPropertyInstance, 6); + polymorphicPropertyInstance.GetType().GetProperty("Z")?.SetValue(polymorphicPropertyInstance, 1); // Sum: 21 > 20, Product: 90 <= 100 + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("PolymorphicProperty", classAttributeError.Key); + Assert.Equal("Sum is too high", classAttributeError.Value.Single()); + } + + async Task InvalidPolymorphicDerivedClassAttributeCheck_ProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var polymorphicPropertyInstance = type.GetProperty("PolymorphicProperty").GetValue(instance); + // Set values that violate derived class ProductLimit validation (X * Y * Z > 100) but not SumLimit (X + Y <= 20) + polymorphicPropertyInstance.GetType().GetProperty("X")?.SetValue(polymorphicPropertyInstance, 5); + polymorphicPropertyInstance.GetType().GetProperty("Y")?.SetValue(polymorphicPropertyInstance, 5); + polymorphicPropertyInstance.GetType().GetProperty("Z")?.SetValue(polymorphicPropertyInstance, 5); // Sum: 10 <= 20, Product: 125 > 100 + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + var classAttributeError = Assert.Single(context.ValidationErrors); + Assert.Equal("PolymorphicProperty", classAttributeError.Key); + Assert.Equal("Product is too high", classAttributeError.Value.Single()); + } + + async Task InvalidPolymorphicBothClassAttributesCheck_ProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var polymorphicPropertyInstance = type.GetProperty("PolymorphicProperty").GetValue(instance); + // Set values that violate both base and derived class validations + polymorphicPropertyInstance.GetType().GetProperty("X")?.SetValue(polymorphicPropertyInstance, 11); + polymorphicPropertyInstance.GetType().GetProperty("Y")?.SetValue(polymorphicPropertyInstance, 12); + polymorphicPropertyInstance.GetType().GetProperty("Z")?.SetValue(polymorphicPropertyInstance, 5); // Sum: 23 > 20, Product: 660 > 100 + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("PolymorphicProperty", kvp.Key); + Assert.Collection(kvp.Value, + error => + { + Assert.Equal("Product is too high", error); + }, + error => + { + Assert.Equal("Sum is too high", error); + }); + }); + } }); } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs index 932d48d62b1f..0cb4c36c38df 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -79,6 +79,14 @@ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System. ); return true; } + if (type == typeof(global::BasePolymorphicType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::BasePolymorphicType), + members: [] + ); + return true; + } if (type == typeof(global::ComplexType)) { validatableInfo = new GeneratedValidatableTypeInfo( @@ -102,10 +110,24 @@ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System. name: "ObjectProperty", displayName: "ObjectProperty" ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::BasePolymorphicType), + name: "PolymorphicProperty", + displayName: "PolymorphicProperty" + ), ] ); return true; } + if (type == typeof(global::DerivedPolymorphicType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::DerivedPolymorphicType), + members: [] + ); + return true; + } return false; }