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/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs index 45b1426a1955..513e376aee81 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}} @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } {{GeneratedCodeAttribute}} @@ -128,15 +137,17 @@ 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(); @@ -173,6 +184,20 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } """; diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index f0f453c7fec0..7544c9d72feb 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -82,6 +82,8 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow visitedTypes.Add(typeSymbol); + 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; var hasValidatableBaseType = false; @@ -107,7 +109,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; } @@ -283,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; + } } 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..dbdaa1a9066b --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ClassAttributes.cs @@ -0,0 +1,306 @@ +#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(); + + 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. +// 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; +} + +[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; } + 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; + } +} + +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) => + { + Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo)); + + await InvalidPropertyAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo); + 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) + { + 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()); + } + + 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/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/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 226fc69f7d3a..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -121,15 +130,17 @@ 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(); @@ -166,5 +177,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs index 20a3fe742bd2..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -142,15 +151,17 @@ 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(); @@ -187,5 +198,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -193,15 +202,17 @@ 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(); @@ -238,5 +249,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -199,15 +208,17 @@ 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(); @@ -244,5 +255,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ 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(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -150,15 +159,17 @@ 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(); @@ -195,5 +206,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ 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(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ 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(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -180,15 +189,17 @@ 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(); @@ -225,5 +236,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -226,15 +235,17 @@ 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(); @@ -271,5 +282,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -193,15 +202,17 @@ 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(); @@ -238,5 +249,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -121,15 +130,17 @@ 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(); @@ -166,5 +177,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -163,15 +172,17 @@ 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(); @@ -208,5 +219,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..0cb4c36c38df --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,231 @@ +//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.GetPropertyValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) + { + Type = type; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::NestedType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::NestedType), + members: [] + ); + 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( + 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" + ), + 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; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type ContainingType, + string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _propertyCache = new(); + private static readonly global::System.Lazy> _lazyTypeCache = new (() => new ()); + private static global::System.Collections.Concurrent.ConcurrentDictionary TypeCache => _lazyTypeCache.Value; + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetPropertyValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _propertyCache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } + } +} \ 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -115,15 +124,17 @@ 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(); @@ -160,5 +171,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -172,15 +181,17 @@ 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(); @@ -217,5 +228,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -115,15 +124,17 @@ 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(); @@ -160,5 +171,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -130,15 +139,17 @@ 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(); @@ -175,5 +186,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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..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 @@ -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")] @@ -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.Interfaces)] + internal global::System.Type Type { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetTypeValidationAttributes(Type); } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] @@ -115,15 +124,17 @@ 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(); @@ -160,5 +171,19 @@ private sealed record CacheKey( return results.ToArray(); }); } + + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetTypeValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type + ) + { + return TypeCache.GetOrAdd(type, static t => + { + var typeAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(t, inherit: true); + return global::System.Linq.Enumerable.ToArray(typeAttributes); + }); + } } } \ No newline at end of file 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)