diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/ValidationModels/CustomerModel.cs b/src/Components/test/testassets/Components.TestServer/RazorComponents/ValidationModels/CustomerModel.cs
index 408216cae8fd..94606771bff2 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/ValidationModels/CustomerModel.cs
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/ValidationModels/CustomerModel.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel.DataAnnotations;
+using Microsoft.Extensions.Validation;
namespace BasicTestApp.ValidationModels;
@@ -14,5 +15,11 @@ public class CustomerModel
[EmailAddress(ErrorMessage = "Invalid Email Address.")]
public string Email { get; set; }
+ public AddressModel PaymentAddress { get; set; } = new AddressModel();
+
+#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.
+ [SkipValidation]
+#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
public AddressModel ShippingAddress { get; set; } = new AddressModel();
+
}
diff --git a/src/Shared/RoslynUtils/SymbolExtensions.cs b/src/Shared/RoslynUtils/SymbolExtensions.cs
index cb41458638fc..d45d518212c8 100644
--- a/src/Shared/RoslynUtils/SymbolExtensions.cs
+++ b/src/Shared/RoslynUtils/SymbolExtensions.cs
@@ -67,6 +67,25 @@ public static bool HasAttribute(this ImmutableArray
attributes, I
return attributes.TryGetAttribute(attributeType, out _);
}
+ public static bool HasAttribute(this ITypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
+ {
+ var current = typeSymbol;
+
+ while (current is not null)
+ {
+ if (current.GetAttributes().Any(attr =>
+ attr.AttributeClass is not null &&
+ SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol)))
+ {
+ return true;
+ }
+
+ current = current.BaseType;
+ }
+
+ return false;
+ }
+
public static bool TryGetAttribute(this ImmutableArray attributes, INamedTypeSymbol attributeType, [NotNullWhen(true)] out AttributeData? matchedAttribute)
{
foreach (var attributeData in attributes)
diff --git a/src/Shared/RoslynUtils/WellKnownTypeData.cs b/src/Shared/RoslynUtils/WellKnownTypeData.cs
index 1549ad178eac..4311eed4c6ac 100644
--- a/src/Shared/RoslynUtils/WellKnownTypeData.cs
+++ b/src/Shared/RoslynUtils/WellKnownTypeData.cs
@@ -124,6 +124,7 @@ public enum WellKnownType
System_ComponentModel_DataAnnotations_ValidationAttribute,
System_ComponentModel_DataAnnotations_RequiredAttribute,
System_ComponentModel_DataAnnotations_CustomValidationAttribute,
+ Microsoft_Extensions_Validation_SkipValidationAttribute,
System_Type,
}
@@ -246,6 +247,7 @@ public enum WellKnownType
"System.ComponentModel.DataAnnotations.ValidationAttribute",
"System.ComponentModel.DataAnnotations.RequiredAttribute",
"System.ComponentModel.DataAnnotations.CustomValidationAttribute",
+ "Microsoft.Extensions.Validation.SkipValidationAttribute",
"System.Type",
];
}
diff --git a/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs
index 364ee29b0b40..4b34054571aa 100644
--- a/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs
+++ b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.Linq;
+using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
@@ -164,4 +165,14 @@ internal static bool IsJsonIgnoredProperty(this IPropertySymbol property, INamed
attr.AttributeClass is not null &&
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
}
+
+ internal static bool IsSkippedValidationProperty(this IPropertySymbol property, INamedTypeSymbol skipValidationAttributeSymbol)
+ {
+ return property.HasAttribute(skipValidationAttributeSymbol) || property.Type.HasAttribute(skipValidationAttributeSymbol);
+ }
+
+ internal static bool IsSkippedValidationParameter(this IParameterSymbol parameter, INamedTypeSymbol skipValidationAttributeSymbol)
+ {
+ return parameter.HasAttribute(skipValidationAttributeSymbol) || parameter.Type.HasAttribute(skipValidationAttributeSymbol);
+ }
}
diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs
index 2dcdaf10da27..f0f453c7fec0 100644
--- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs
+++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs
@@ -5,6 +5,7 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Analyzers.Infrastructure;
+using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
using Microsoft.CodeAnalysis;
@@ -30,6 +31,8 @@ internal ImmutableArray ExtractValidatableTypes(IInvocationOper
WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get(
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
+ var skipValidationAttributeSymbol = wellKnownTypes.Get(
+ WellKnownTypeData.WellKnownType.Microsoft_Extensions_Validation_SkipValidationAttribute);
var validatableTypes = new HashSet(ValidatableTypeComparer.Instance);
List visitedTypes = [];
@@ -42,6 +45,12 @@ internal ImmutableArray ExtractValidatableTypes(IInvocationOper
continue;
}
+ // Skip method parameter if it or its type are annotated with SkipValidationAttribute
+ if (parameter.IsSkippedValidationParameter(skipValidationAttributeSymbol))
+ {
+ continue;
+ }
+
_ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
}
return [.. validatableTypes];
@@ -122,6 +131,8 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
var jsonIgnoreAttributeSymbol = wellKnownTypes.Get(
WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonIgnoreAttribute);
+ var skipValidationAttributeSymbol = wellKnownTypes.Get(
+ WellKnownTypeData.WellKnownType.Microsoft_Extensions_Validation_SkipValidationAttribute);
// Special handling for record types to extract properties from
// the primary constructor.
@@ -156,6 +167,12 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb
continue;
}
+ // Skip primary constructor parameter if it or its type are annotated with SkipValidationAttribute
+ if (parameter.IsSkippedValidationParameter(skipValidationAttributeSymbol))
+ {
+ continue;
+ }
+
// Skip properties that are not accessible from generated code
if (correspondingProperty.DeclaredAccessibility is not Accessibility.Public)
{
@@ -218,6 +235,12 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb
continue;
}
+ // Skip property if it or its type are annotated with SkipValidationAttribute
+ if (member.IsSkippedValidationProperty(skipValidationAttributeSymbol))
+ {
+ continue;
+ }
+
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
diff --git a/src/Validation/src/PublicAPI.Unshipped.txt b/src/Validation/src/PublicAPI.Unshipped.txt
index d7f657e38875..e2e20423b5ea 100644
--- a/src/Validation/src/PublicAPI.Unshipped.txt
+++ b/src/Validation/src/PublicAPI.Unshipped.txt
@@ -5,6 +5,8 @@ Microsoft.Extensions.Validation.IValidatableInfo.ValidateAsync(object? value, Mi
Microsoft.Extensions.Validation.IValidatableInfoResolver
Microsoft.Extensions.Validation.IValidatableInfoResolver.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) -> bool
Microsoft.Extensions.Validation.IValidatableInfoResolver.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) -> bool
+Microsoft.Extensions.Validation.SkipValidationAttribute
+Microsoft.Extensions.Validation.SkipValidationAttribute.SkipValidationAttribute() -> void
Microsoft.Extensions.Validation.ValidatableParameterInfo
Microsoft.Extensions.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName) -> void
Microsoft.Extensions.Validation.ValidatablePropertyInfo
diff --git a/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs b/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs
index d8f0c3699dbf..e95490e98e2e 100644
--- a/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs
+++ b/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs
@@ -28,6 +28,14 @@ public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNull
throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name.");
}
+ // Skip method parameter if it or its type are annotated with SkipValidationAttribute.
+ if (parameterInfo.GetCustomAttribute() != null ||
+ parameterInfo.ParameterType.GetCustomAttribute() != null)
+ {
+ validatableInfo = null;
+ return false;
+ }
+
var validationAttributes = parameterInfo
.GetCustomAttributes()
.ToArray();
diff --git a/src/Validation/src/SkipValidationAttribute.cs b/src/Validation/src/SkipValidationAttribute.cs
new file mode 100644
index 000000000000..ddbb2f56531e
--- /dev/null
+++ b/src/Validation/src/SkipValidationAttribute.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Extensions.Validation;
+
+///
+/// Indicates that a property, parameter, or a type should not be validated.
+/// When applied to a property, validation is skipped for that property.
+/// When applied to a parameter, validation is skipped for that parameter.
+/// When applied to a type, validation is skipped for all properties and parameters of that type.
+/// This includes skipping validation of nested properties for complex types.
+///
+[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+public sealed class SkipValidationAttribute : Attribute
+{
+}
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.SkipValidation.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.SkipValidation.cs
new file mode 100644
index 000000000000..366b53b174d6
--- /dev/null
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.SkipValidation.cs
@@ -0,0 +1,515 @@
+#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 DoesNotEmit_ForSkipValidationAttribute_OnClassProperties()
+ {
+ var source = """
+#pragma warning disable ASP0029
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Validation;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddValidation();
+
+var app = builder.Build();
+
+app.Run();
+
+[ValidatableType]
+public class ComplexType
+{
+ [SkipValidation]
+ [Range(10, 100)]
+ public int IntegerWithRange { get; set; } = 10;
+
+ [SkipValidation]
+ public NestedType SkippedObjectProperty { get; set; } = new NestedType();
+
+ public NestedType ObjectProperty { get; set; } = new NestedType();
+
+ [SkipValidation]
+ public List SkippedListOfNestedTypes { get; set; } = [];
+
+ public List ListOfNestedTypes { get; set; } = [];
+
+ [SkipValidation]
+ public NonSkippedBaseType SkippedBaseTypeProperty { get; set; } = new NonSkippedBaseType();
+
+ public NonSkippedSubType NonSkippedSubTypeProperty { get; set; } = new NonSkippedSubType();
+
+ public AlwaysSkippedType AlwaysSkippedProperty { get; set; } = new AlwaysSkippedType();
+
+ public SubTypeOfSkippedBase SubTypeOfSkippedBaseProperty { get; set; } = new SubTypeOfSkippedBase();
+}
+
+public class NestedType
+{
+ [Range(10, 100)]
+ public int IntegerWithRange { get; set; } = 10;
+}
+
+public class NonSkippedBaseType
+{
+ [Range(10, 100)]
+ public int IntegerWithRange1 { get; set; } = 10;
+}
+
+public class NonSkippedSubType : NonSkippedBaseType
+{
+ [Range(10, 100)]
+ public int IntegerWithRange2 { get; set; } = 10;
+}
+
+[SkipValidation]
+public class AlwaysSkippedType
+{
+ public NestedType ObjectProperty { get; set; } = new NestedType();
+}
+
+[SkipValidation]
+public class SkippedBaseType
+{
+ [Range(10, 100)]
+ public int IntegerWithRange1 { get; set; } = 10;
+}
+
+public class SubTypeOfSkippedBase : SkippedBaseType
+{
+ [Range(10, 100)]
+ public int IntegerWithRange2 { get; set; } = 10;
+}
+""";
+ await Verify(source, out var compilation);
+ await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
+ {
+ Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo));
+
+ await InvalidSkippedInteger_DoesNotProduceError(validatableTypeInfo);
+ await InvalidNestedInteger_ProducesError(validatableTypeInfo);
+ await InvalidSkippedNestedInteger_DoesNotProduceError(validatableTypeInfo);
+ await InvalidList_ProducesError(validatableTypeInfo);
+ await InvalidSkippedList_DoesNotProduceError(validatableTypeInfo);
+ await InvalidSubTypeNestedIntegers_ProduceErrors(validatableTypeInfo);
+ await InvalidAlwaysSkippedType_DoesNotProduceError(validatableTypeInfo);
+
+ async Task InvalidSkippedInteger_DoesNotProduceError(IValidatableInfo validatableInfo)
+ {
+ var instance = Activator.CreateInstance(type);
+ var intProperty = type.GetProperty("IntegerWithRange");
+ intProperty?.SetValue(instance, 5); // Set invalid value
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ Assert.Null(context.ValidationErrors);
+ }
+
+ async Task InvalidNestedInteger_ProducesError(IValidatableInfo validatableInfo)
+ {
+ var instance = Activator.CreateInstance(type);
+ var objectPropertyInstance = type.GetProperty("ObjectProperty").GetValue(instance);
+ var nestedIntProperty = objectPropertyInstance.GetType().GetProperty("IntegerWithRange");
+ nestedIntProperty?.SetValue(objectPropertyInstance, 5); // Set invalid value
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ Assert.Collection(context.ValidationErrors, kvp =>
+ {
+ Assert.Equal("ObjectProperty.IntegerWithRange", kvp.Key);
+ Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
+ });
+ }
+
+ async Task InvalidSkippedNestedInteger_DoesNotProduceError(IValidatableInfo validatableInfo)
+ {
+ var instance = Activator.CreateInstance(type);
+ var objectPropertyInstance = type.GetProperty("SkippedObjectProperty").GetValue(instance);
+ var nestedIntProperty = objectPropertyInstance.GetType().GetProperty("IntegerWithRange");
+ nestedIntProperty?.SetValue(objectPropertyInstance, 5); // Set invalid value
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ Assert.Null(context.ValidationErrors);
+ }
+
+ async Task InvalidList_ProducesError(IValidatableInfo validatableInfo)
+ {
+ var rootInstance = Activator.CreateInstance(type);
+ var listInstance = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("NestedType")!));
+
+ // Create invalid item
+ var nestedTypeInstance = Activator.CreateInstance(type.Assembly.GetType("NestedType")!);
+ nestedTypeInstance.GetType().GetProperty("IntegerWithRange")?.SetValue(nestedTypeInstance, 5);
+
+ // Add to list
+ listInstance.GetType().GetMethod("Add")?.Invoke(listInstance, [nestedTypeInstance]);
+
+ type.GetProperty("ListOfNestedTypes")?.SetValue(rootInstance, listInstance);
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(rootInstance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(rootInstance, context, CancellationToken.None);
+
+ Assert.Collection(context.ValidationErrors, kvp =>
+ {
+ Assert.Equal("ListOfNestedTypes[0].IntegerWithRange", kvp.Key);
+ Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
+ });
+ }
+
+ async Task InvalidSkippedList_DoesNotProduceError(IValidatableInfo validatableInfo)
+ {
+ var rootInstance = Activator.CreateInstance(type);
+ var listInstance = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("NestedType")!));
+
+ // Create invalid item
+ var nestedTypeInstance = Activator.CreateInstance(type.Assembly.GetType("NestedType")!);
+ nestedTypeInstance.GetType().GetProperty("IntegerWithRange")?.SetValue(nestedTypeInstance, 5);
+
+ // Add to list
+ listInstance.GetType().GetMethod("Add")?.Invoke(listInstance, [nestedTypeInstance]);
+
+ type.GetProperty("SkippedListOfNestedTypes")?.SetValue(rootInstance, listInstance);
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(rootInstance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(rootInstance, context, CancellationToken.None);
+
+ Assert.Null(context.ValidationErrors);
+ }
+
+ async Task InvalidSubTypeNestedIntegers_ProduceErrors(IValidatableInfo validatableInfo)
+ {
+ var instance = Activator.CreateInstance(type);
+ var objectPropertyInstance = type.GetProperty("NonSkippedSubTypeProperty").GetValue(instance);
+ var nestedIntProperty1 = objectPropertyInstance.GetType().GetProperty("IntegerWithRange1");
+ nestedIntProperty1?.SetValue(objectPropertyInstance, 5); // Set invalid value
+ var nestedIntProperty2 = objectPropertyInstance.GetType().GetProperty("IntegerWithRange2");
+ nestedIntProperty2?.SetValue(objectPropertyInstance, 6); // Set invalid value
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ // Errors are (currently) reported in the order from derived to base type.
+ Assert.Collection(context.ValidationErrors,
+ kvp =>
+ {
+ Assert.Equal("NonSkippedSubTypeProperty.IntegerWithRange2", kvp.Key);
+ Assert.Equal("The field IntegerWithRange2 must be between 10 and 100.", kvp.Value.Single());
+ },
+ kvp =>
+ {
+ Assert.Equal("NonSkippedSubTypeProperty.IntegerWithRange1", kvp.Key);
+ Assert.Equal("The field IntegerWithRange1 must be between 10 and 100.", kvp.Value.Single());
+ });
+ }
+
+ async Task InvalidAlwaysSkippedType_DoesNotProduceError(IValidatableInfo validatableInfo)
+ {
+ var instance = Activator.CreateInstance(type);
+ var objectPropertyInstance = type.GetProperty("AlwaysSkippedProperty").GetValue(instance);
+ var nestedIntProperty = objectPropertyInstance.GetType().GetProperty("IntegerWithRange");
+ nestedIntProperty?.SetValue(objectPropertyInstance, 5); // Set invalid value
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ Assert.Null(context.ValidationErrors);
+ }
+ });
+ }
+
+ [Fact]
+ public async Task DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties()
+ {
+ 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;
+
+static class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddValidation();
+ var app = builder.Build();
+ app.Run();
+ }
+}
+
+[ValidatableType]
+public record ComplexType(
+ [Range(10, 100)][SkipValidation] int IntegerWithRange,
+ NestedType ObjectProperty,
+ [SkipValidation] NestedType SkippedObjectProperty
+);
+
+public record NestedType
+{
+ [Range(10, 100)]
+ public int IntegerWithRange { get; set; } = 10;
+}
+
+[SkipValidation]
+public record AlwaysSkippedType
+{
+ public NestedType ObjectProperty { get; set; } = new NestedType();
+}
+""";
+ await Verify(source, out var compilation);
+ await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
+ {
+ Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo));
+
+ await InvalidNestedIntegerWithRangeProducesError(validatableTypeInfo);
+ await InvalidSkippedNestedIntegerWithRangeDoesNotProduceProduceError(validatableTypeInfo);
+ await InvalidSkippedIntegerWithRangeDoesNotProduceError(validatableTypeInfo);
+
+ async Task InvalidNestedIntegerWithRangeProducesError(IValidatableInfo validatableInfo)
+ {
+ var objectProperty = type.GetProperty("ObjectProperty");
+ var nestedType = objectProperty.PropertyType;
+ var nestedTypeInstance = Activator.CreateInstance(nestedType);
+ var skippedNestedTypeInstance = Activator.CreateInstance(nestedType);
+ nestedTypeInstance.GetType().GetProperty("IntegerWithRange")?.SetValue(nestedTypeInstance, 5); // Set invalid value
+ var instance = Activator.CreateInstance(type, 10, nestedTypeInstance, skippedNestedTypeInstance);
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ Assert.Collection(context.ValidationErrors, kvp =>
+ {
+ Assert.Equal("ObjectProperty.IntegerWithRange", kvp.Key);
+ Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
+ });
+ }
+
+ async Task InvalidSkippedNestedIntegerWithRangeDoesNotProduceProduceError(IValidatableInfo validatableInfo)
+ {
+ var objectProperty = type.GetProperty("ObjectProperty");
+ var nestedType = objectProperty.PropertyType;
+ var nestedTypeInstance = Activator.CreateInstance(nestedType);
+ var skippedNestedTypeInstance = Activator.CreateInstance(nestedType);
+ skippedNestedTypeInstance.GetType().GetProperty("IntegerWithRange")?.SetValue(skippedNestedTypeInstance, 5); // Set invalid value
+ var instance = Activator.CreateInstance(type, 10, nestedTypeInstance, skippedNestedTypeInstance);
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ Assert.Null(context.ValidationErrors);
+ }
+
+ async Task InvalidSkippedIntegerWithRangeDoesNotProduceError(IValidatableInfo validatableInfo)
+ {
+ var objectProperty = type.GetProperty("ObjectProperty");
+ var nestedType = objectProperty.PropertyType;
+ var nestedTypeInstance = Activator.CreateInstance(nestedType);
+ var instance = Activator.CreateInstance(type, 5, nestedTypeInstance, nestedTypeInstance); // Create with invalid value
+
+ var context = new ValidateContext
+ {
+ ValidationOptions = validationOptions,
+ ValidationContext = new ValidationContext(instance)
+ };
+
+ await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
+
+ Assert.Null(context.ValidationErrors);
+ }
+ });
+ }
+
+ [Fact]
+ public async Task DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters()
+ {
+ 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.AspNetCore.Mvc;
+using Microsoft.Extensions.Validation;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+static class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddValidation();
+ var app = builder.Build();
+
+ app.MapPost("/simple-params", (
+ [Range(10, 100)] int intParam,
+ [SkipValidation][Range(10, 100)] int skippedIntParam) => "OK");
+
+ app.MapPost("/non-skipped-complex-type", (ComplexType objectParam) => "OK");
+
+ app.MapPost("/skipped-complex-type", ([SkipValidation] ComplexType objectParam) => "OK");
+
+ app.MapPost("/always-skipped-type", (AlwaysSkippedType objectParam) => "OK");
+
+ app.Run();
+ }
+}
+
+// This should have generated validation code
+public class ComplexType
+{
+ [Range(10, 100)]
+ public int IntegerWithRange { get; set; } = 10;
+}
+
+// This should have generated validation code
+[SkipValidation]
+public class AlwaysSkippedType
+{
+ public ComplexType ObjectProperty { get; set; } = new ComplexType();
+}
+""";
+ await Verify(source, out var compilation);
+
+ await VerifyEndpoint(compilation, "/simple-params", async (endpoint, serviceProvider) =>
+ {
+ var context = CreateHttpContext(serviceProvider);
+ context.Request.QueryString = new QueryString("?intParam=5&skippedIntParam=5");
+ await endpoint.RequestDelegate(context);
+ var problemDetails = await AssertBadRequest(context);
+
+ Assert.Collection(problemDetails.Errors,
+ error =>
+ {
+ Assert.Equal("intParam", error.Key);
+ Assert.Equal("The field intParam must be between 10 and 100.", error.Value.Single());
+ }
+ );
+ });
+
+ await VerifyEndpoint(compilation, "/non-skipped-complex-type", async (endpoint, serviceProvider) =>
+ {
+ var payload = """
+ {
+ "IntegerWithRange": 5
+ }
+ """;
+
+ var context = CreateHttpContextWithPayload(payload, serviceProvider);
+ await endpoint.RequestDelegate(context);
+ var problemDetails = await AssertBadRequest(context);
+
+ Assert.Collection(problemDetails.Errors,
+ error =>
+ {
+ Assert.Equal("IntegerWithRange", error.Key);
+ Assert.Equal("The field IntegerWithRange must be between 10 and 100.", error.Value.Single());
+ }
+ );
+ });
+
+ await VerifyEndpoint(compilation, "/skipped-complex-type", async (endpoint, serviceProvider) =>
+ {
+ var payload = """
+ {
+ "IntegerWithRange": 5
+ }
+ """;
+
+ var context = CreateHttpContextWithPayload(payload, serviceProvider);
+ await endpoint.RequestDelegate(context);
+
+ Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
+ });
+
+ await VerifyEndpoint(compilation, "/always-skipped-type", async (endpoint, serviceProvider) =>
+ {
+ var payload = """
+ {
+ "IntegerWithRange": 5
+ }
+ """;
+
+ var context = CreateHttpContextWithPayload(payload, serviceProvider);
+ await endpoint.RequestDelegate(context);
+
+ Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
+ });
+ }
+}
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs
index f4041f62b979..aff6b34be47a 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs
@@ -80,7 +80,7 @@ public class SubTypeWithInheritance : SubType
}
""";
await Verify(source, out var compilation);
- VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
+ await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
{
Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo));
@@ -449,7 +449,7 @@ public record SubTypeWithInheritance : SubType
}
""";
await Verify(source, out var compilation);
- VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
+ await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
{
Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo));
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs
index 81500f090c96..8dd7670a50f1 100644
--- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs
@@ -5,31 +5,26 @@
using System.Diagnostics;
using System.Globalization;
+using System.IO.Pipelines;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
-using System.IO.Pipelines;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.InternalTesting;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Validation;
-using Xunit;
namespace Microsoft.Extensions.Validation.GeneratorTests;
@@ -86,7 +81,7 @@ internal static Task Verify(string source, out Compilation compilation)
: "snapshots");
}
- internal static void VerifyValidatableType(Compilation compilation, string typeName, Action verifyFunc)
+ internal static async Task VerifyValidatableType(Compilation compilation, string typeName, Func verifyFunc)
{
if (TryResolveServicesFromCompilation(compilation, targetAssemblyName: "Microsoft.Extensions.Validation", typeName: "Microsoft.Extensions.Validation.ValidationOptions", out var services, out var serviceType, out var outputAssemblyName) is false)
{
@@ -103,7 +98,7 @@ internal static void VerifyValidatableType(Compilation compilation, string typeN
// Then access the Value property
var valueProperty = optionsType.GetProperty("Value");
var service = (ValidationOptions)valueProperty.GetValue(optionsInstance) ?? throw new InvalidOperationException("Could not resolve ValidationOptions.");
- verifyFunc(service, type);
+ await verifyFunc(service, type);
}
internal static async Task VerifyEndpoint(Compilation compilation, string routePattern, Func verifyFunc)
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
new file mode 100644
index 000000000000..d8135a12e14c
--- /dev/null
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs
@@ -0,0 +1,221 @@
+//HintName: ValidatableInfoResolver.g.cs
+#nullable enable annotations
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+#pragma warning disable ASP0029
+
+namespace System.Runtime.CompilerServices
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+namespace Microsoft.Extensions.Validation.Generated
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo
+ {
+ public GeneratedValidatablePropertyInfo(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type containingType,
+ global::System.Type propertyType,
+ string name,
+ string displayName) : base(containingType, propertyType, name, displayName)
+ {
+ ContainingType = containingType;
+ Name = name;
+ }
+
+ [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ internal global::System.Type ContainingType { get; }
+ internal string Name { get; }
+
+ protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
+ => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo
+ {
+ public GeneratedValidatableTypeInfo(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
+ global::System.Type type,
+ ValidatablePropertyInfo[] members) : base(type, members) { }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver
+ {
+ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ if (type == typeof(global::NestedType))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::NestedType),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::NestedType),
+ propertyType: typeof(int),
+ name: "IntegerWithRange",
+ displayName: "IntegerWithRange"
+ ),
+ ]
+ );
+ return true;
+ }
+ if (type == typeof(global::NonSkippedBaseType))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::NonSkippedBaseType),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::NonSkippedBaseType),
+ propertyType: typeof(int),
+ name: "IntegerWithRange1",
+ displayName: "IntegerWithRange1"
+ ),
+ ]
+ );
+ return true;
+ }
+ if (type == typeof(global::NonSkippedSubType))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::NonSkippedSubType),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::NonSkippedSubType),
+ propertyType: typeof(int),
+ name: "IntegerWithRange2",
+ displayName: "IntegerWithRange2"
+ ),
+ ]
+ );
+ return true;
+ }
+ if (type == typeof(global::ComplexType))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::ComplexType),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexType),
+ propertyType: typeof(global::NestedType),
+ name: "ObjectProperty",
+ displayName: "ObjectProperty"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexType),
+ propertyType: typeof(global::System.Collections.Generic.List),
+ name: "ListOfNestedTypes",
+ displayName: "ListOfNestedTypes"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexType),
+ propertyType: typeof(global::NonSkippedSubType),
+ name: "NonSkippedSubTypeProperty",
+ displayName: "NonSkippedSubTypeProperty"
+ ),
+ ]
+ );
+ return true;
+ }
+
+ return false;
+ }
+
+ // No-ops, rely on runtime code for ParameterInfo-based resolution
+ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ return false;
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class GeneratedServiceCollectionExtensions
+ {
+ [InterceptsLocation]
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null)
+ {
+ // Use non-extension method to avoid infinite recursion.
+ return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
+ {
+ options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
+ if (configureOptions is not null)
+ {
+ configureOptions(options);
+ }
+ });
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class ValidationAttributeCache
+ {
+ private sealed record CacheKey(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type ContainingType,
+ string PropertyName);
+ private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new();
+
+ public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
+ [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type containingType,
+ string propertyName)
+ {
+ var key = new CacheKey(containingType, propertyName);
+ return _cache.GetOrAdd(key, static k =>
+ {
+ var results = new global::System.Collections.Generic.List();
+
+ // Get attributes from the property
+ var property = k.ContainingType.GetProperty(k.PropertyName);
+ if (property != null)
+ {
+ var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
+ .GetCustomAttributes(property, inherit: true);
+
+ results.AddRange(propertyAttributes);
+ }
+
+ // Check constructors for parameters that match the property name
+ // to handle record scenarios
+ foreach (var constructor in k.ContainingType.GetConstructors())
+ {
+ // Look for parameter with matching name (case insensitive)
+ var parameter = global::System.Linq.Enumerable.FirstOrDefault(
+ constructor.GetParameters(),
+ p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
+
+ if (parameter != null)
+ {
+ var paramAttributes = global::System.Reflection.CustomAttributeExtensions
+ .GetCustomAttributes(parameter, inherit: true);
+
+ results.AddRange(paramAttributes);
+
+ break;
+ }
+ }
+
+ return results.ToArray();
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs
new file mode 100644
index 000000000000..9db7cb497042
--- /dev/null
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs
@@ -0,0 +1,164 @@
+//HintName: ValidatableInfoResolver.g.cs
+#nullable enable annotations
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+#pragma warning disable ASP0029
+
+namespace System.Runtime.CompilerServices
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+namespace Microsoft.Extensions.Validation.Generated
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo
+ {
+ public GeneratedValidatablePropertyInfo(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type containingType,
+ global::System.Type propertyType,
+ string name,
+ string displayName) : base(containingType, propertyType, name, displayName)
+ {
+ ContainingType = containingType;
+ Name = name;
+ }
+
+ [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ internal global::System.Type ContainingType { get; }
+ internal string Name { get; }
+
+ protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
+ => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo
+ {
+ public GeneratedValidatableTypeInfo(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
+ global::System.Type type,
+ ValidatablePropertyInfo[] members) : base(type, members) { }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver
+ {
+ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ if (type == typeof(global::ComplexType))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::ComplexType),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexType),
+ propertyType: typeof(int),
+ name: "IntegerWithRange",
+ displayName: "IntegerWithRange"
+ ),
+ ]
+ );
+ return true;
+ }
+
+ return false;
+ }
+
+ // No-ops, rely on runtime code for ParameterInfo-based resolution
+ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ return false;
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class GeneratedServiceCollectionExtensions
+ {
+ [InterceptsLocation]
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null)
+ {
+ // Use non-extension method to avoid infinite recursion.
+ return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
+ {
+ options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
+ if (configureOptions is not null)
+ {
+ configureOptions(options);
+ }
+ });
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class ValidationAttributeCache
+ {
+ private sealed record CacheKey(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type ContainingType,
+ string PropertyName);
+ private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new();
+
+ public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
+ [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type containingType,
+ string propertyName)
+ {
+ var key = new CacheKey(containingType, propertyName);
+ return _cache.GetOrAdd(key, static k =>
+ {
+ var results = new global::System.Collections.Generic.List();
+
+ // Get attributes from the property
+ var property = k.ContainingType.GetProperty(k.PropertyName);
+ if (property != null)
+ {
+ var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
+ .GetCustomAttributes(property, inherit: true);
+
+ results.AddRange(propertyAttributes);
+ }
+
+ // Check constructors for parameters that match the property name
+ // to handle record scenarios
+ foreach (var constructor in k.ContainingType.GetConstructors())
+ {
+ // Look for parameter with matching name (case insensitive)
+ var parameter = global::System.Linq.Enumerable.FirstOrDefault(
+ constructor.GetParameters(),
+ p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
+
+ if (parameter != null)
+ {
+ var paramAttributes = global::System.Reflection.CustomAttributeExtensions
+ .GetCustomAttributes(parameter, inherit: true);
+
+ results.AddRange(paramAttributes);
+
+ break;
+ }
+ }
+
+ return results.ToArray();
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs
new file mode 100644
index 000000000000..5fff86fac3f1
--- /dev/null
+++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs
@@ -0,0 +1,179 @@
+//HintName: ValidatableInfoResolver.g.cs
+#nullable enable annotations
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+#nullable enable
+#pragma warning disable ASP0029
+
+namespace System.Runtime.CompilerServices
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ file sealed class InterceptsLocationAttribute : System.Attribute
+ {
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ }
+ }
+}
+
+namespace Microsoft.Extensions.Validation.Generated
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo
+ {
+ public GeneratedValidatablePropertyInfo(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type containingType,
+ global::System.Type propertyType,
+ string name,
+ string displayName) : base(containingType, propertyType, name, displayName)
+ {
+ ContainingType = containingType;
+ Name = name;
+ }
+
+ [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ internal global::System.Type ContainingType { get; }
+ internal string Name { get; }
+
+ protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
+ => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo
+ {
+ public GeneratedValidatableTypeInfo(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
+ global::System.Type type,
+ ValidatablePropertyInfo[] members) : base(type, members) { }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver
+ {
+ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ if (type == typeof(global::NestedType))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::NestedType),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::NestedType),
+ propertyType: typeof(int),
+ name: "IntegerWithRange",
+ displayName: "IntegerWithRange"
+ ),
+ ]
+ );
+ return true;
+ }
+ if (type == typeof(global::ComplexType))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::ComplexType),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexType),
+ propertyType: typeof(global::NestedType),
+ name: "ObjectProperty",
+ displayName: "ObjectProperty"
+ ),
+ ]
+ );
+ return true;
+ }
+
+ return false;
+ }
+
+ // No-ops, rely on runtime code for ParameterInfo-based resolution
+ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ return false;
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class GeneratedServiceCollectionExtensions
+ {
+ [InterceptsLocation]
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null)
+ {
+ // Use non-extension method to avoid infinite recursion.
+ return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
+ {
+ options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
+ if (configureOptions is not null)
+ {
+ configureOptions(options);
+ }
+ });
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class ValidationAttributeCache
+ {
+ private sealed record CacheKey(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ [property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type ContainingType,
+ string PropertyName);
+ private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new();
+
+ public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
+ [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
+ global::System.Type containingType,
+ string propertyName)
+ {
+ var key = new CacheKey(containingType, propertyName);
+ return _cache.GetOrAdd(key, static k =>
+ {
+ var results = new global::System.Collections.Generic.List();
+
+ // Get attributes from the property
+ var property = k.ContainingType.GetProperty(k.PropertyName);
+ if (property != null)
+ {
+ var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
+ .GetCustomAttributes(property, inherit: true);
+
+ results.AddRange(propertyAttributes);
+ }
+
+ // Check constructors for parameters that match the property name
+ // to handle record scenarios
+ foreach (var constructor in k.ContainingType.GetConstructors())
+ {
+ // Look for parameter with matching name (case insensitive)
+ var parameter = global::System.Linq.Enumerable.FirstOrDefault(
+ constructor.GetParameters(),
+ p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
+
+ if (parameter != null)
+ {
+ var paramAttributes = global::System.Reflection.CustomAttributeExtensions
+ .GetCustomAttributes(parameter, inherit: true);
+
+ results.AddRange(paramAttributes);
+
+ break;
+ }
+ }
+
+ return results.ToArray();
+ });
+ }
+ }
+}
\ No newline at end of file