diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs
index 3158896d3e59..a5a580a33a53 100644
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs
+++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs
@@ -3,6 +3,7 @@
using System.Collections.Immutable;
using System.Linq;
+using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Http.ValidationsGenerator;
@@ -90,17 +91,17 @@ internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol inte
// Types exempted here have special binding rules in RDF and RDG and are not validatable
// types themselves so we short-circuit on them.
- internal static bool IsExemptType(this ITypeSymbol type, RequiredSymbols requiredSymbols)
+ internal static bool IsExemptType(this ITypeSymbol type, WellKnownTypes wellKnownTypes)
{
- return SymbolEqualityComparer.Default.Equals(type, requiredSymbols.HttpContext)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.HttpRequest)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.HttpResponse)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.CancellationToken)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.IFormCollection)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.IFormFileCollection)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.IFormFile)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.Stream)
- || SymbolEqualityComparer.Default.Equals(type, requiredSymbols.PipeReader);
+ return SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpContext))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpRequest))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpResponse))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_CancellationToken))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormCollection))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormFile))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_IO_Stream))
+ || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_IO_Pipelines_PipeReader));
}
internal static IPropertySymbol? FindPropertyIncludingBaseTypes(this INamedTypeSymbol typeSymbol, string propertyName)
diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj
index 710a337f2bea..a55218bb4185 100644
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj
+++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj
@@ -28,6 +28,8 @@
+
+
diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs
index 5bea9a8ad218..3ba763b1b342 100644
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs
+++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
+using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -20,8 +21,8 @@ internal ImmutableArray TransformValidatableTypeWithAttribute(G
{
var validatableTypes = new HashSet(ValidatableTypeComparer.Instance);
List visitedTypes = [];
- var requiredSymbols = ExtractRequiredSymbols(context.SemanticModel.Compilation, cancellationToken);
- if (TryExtractValidatableType((ITypeSymbol)context.TargetSymbol, requiredSymbols, ref validatableTypes, ref visitedTypes))
+ var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation);
+ if (TryExtractValidatableType((ITypeSymbol)context.TargetSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes))
{
return [..validatableTypes];
}
diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs
index da325a562d1f..2752368cb46a 100644
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs
+++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs
@@ -6,6 +6,7 @@
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Analyzers.Infrastructure;
+using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -38,10 +39,12 @@ internal bool FindEndpoints(SyntaxNode syntaxNode, CancellationToken cancellatio
: null;
}
- internal ImmutableArray ExtractValidatableEndpoint((IInvocationOperation? Operation, RequiredSymbols RequiredSymbols) input, CancellationToken cancellationToken)
+ internal ImmutableArray ExtractValidatableEndpoint(IInvocationOperation? operation, CancellationToken cancellationToken)
{
- AnalyzerDebug.Assert(input.Operation != null, "Operation should not be null.");
- var validatableTypes = ExtractValidatableTypes(input.Operation, input.RequiredSymbols);
+ AnalyzerDebug.Assert(operation != null, "Operation should not be null.");
+ AnalyzerDebug.Assert(operation.SemanticModel != null, "Operation should have a semantic model.");
+ var wellKnownTypes = WellKnownTypes.GetOrCreate(operation.SemanticModel.Compilation);
+ var validatableTypes = ExtractValidatableTypes(operation, wellKnownTypes);
return validatableTypes;
}
}
diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.RequiredSymbolsParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.RequiredSymbolsParser.cs
deleted file mode 100644
index ca1486a518aa..000000000000
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.RequiredSymbolsParser.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Threading;
-using Microsoft.CodeAnalysis;
-
-namespace Microsoft.AspNetCore.Http.ValidationsGenerator;
-
-public sealed partial class ValidationsGenerator : IIncrementalGenerator
-{
- internal RequiredSymbols ExtractRequiredSymbols(Compilation compilation, CancellationToken cancellationToken)
- {
- return new RequiredSymbols(
- compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute")!,
- compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!,
- compilation.GetTypeByMetadataName("System.Collections.IEnumerable")!,
- compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.IValidatableObject")!,
- compilation.GetTypeByMetadataName("System.Text.Json.Serialization.JsonDerivedTypeAttribute")!,
- compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.RequiredAttribute")!,
- compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.CustomValidationAttribute")!,
- compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpContext")!,
- compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpRequest")!,
- compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.HttpResponse")!,
- compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!,
- compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IFormCollection")!,
- compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IFormFileCollection")!,
- compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IFormFile")!,
- compilation.GetTypeByMetadataName("System.IO.Stream")!,
- compilation.GetTypeByMetadataName("System.IO.Pipelines.PipeReader")!
- );
- }
-}
diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs
index b887399d4c4a..79bbc82e45a7 100644
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs
+++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs
@@ -5,6 +5,7 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Analyzers.Infrastructure;
+using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -18,7 +19,7 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
- internal ImmutableArray ExtractValidatableTypes(IInvocationOperation operation, RequiredSymbols requiredSymbols)
+ internal ImmutableArray ExtractValidatableTypes(IInvocationOperation operation, WellKnownTypes wellKnownTypes)
{
AnalyzerDebug.Assert(operation.SemanticModel != null, "SemanticModel should not be null.");
var parameters = operation.TryGetRouteHandlerMethod(operation.SemanticModel, out var method)
@@ -28,12 +29,12 @@ internal ImmutableArray ExtractValidatableTypes(IInvocationOper
List visitedTypes = [];
foreach (var parameter in parameters)
{
- _ = TryExtractValidatableType(parameter.Type.UnwrapType(requiredSymbols.IEnumerable), requiredSymbols, ref validatableTypes, ref visitedTypes);
+ _ = TryExtractValidatableType(parameter.Type.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)), wellKnownTypes, ref validatableTypes, ref visitedTypes);
}
return [.. validatableTypes];
}
- internal bool TryExtractValidatableType(ITypeSymbol typeSymbol, RequiredSymbols requiredSymbols, ref HashSet validatableTypes, ref List visitedTypes)
+ internal bool TryExtractValidatableType(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, ref HashSet validatableTypes, ref List visitedTypes)
{
if (typeSymbol.SpecialType != SpecialType.None)
{
@@ -45,7 +46,7 @@ internal bool TryExtractValidatableType(ITypeSymbol typeSymbol, RequiredSymbols
return true;
}
- if (typeSymbol.IsExemptType(requiredSymbols))
+ if (typeSymbol.IsExemptType(wellKnownTypes))
{
return false;
}
@@ -57,19 +58,23 @@ internal bool TryExtractValidatableType(ITypeSymbol typeSymbol, RequiredSymbols
var hasValidatableBaseType = false;
while (current != null && current.SpecialType != SpecialType.System_Object)
{
- hasValidatableBaseType |= TryExtractValidatableType(current, requiredSymbols, ref validatableTypes, ref visitedTypes);
+ hasValidatableBaseType |= TryExtractValidatableType(current, wellKnownTypes, ref validatableTypes, ref visitedTypes);
current = current.BaseType;
}
// Extract validatable types discovered in members of this type and add them to the top-level list.
- var members = ExtractValidatableMembers(typeSymbol, requiredSymbols, ref validatableTypes, ref visitedTypes);
+ ImmutableArray members = [];
+ if (ParsabilityHelper.GetParsability(typeSymbol, wellKnownTypes) is Parsability.NotParsable)
+ {
+ members = ExtractValidatableMembers(typeSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes);
+ }
// Extract the validatable types discovered in the JsonDerivedTypeAttributes of this type and add them to the top-level list.
- var derivedTypes = typeSymbol.GetJsonDerivedTypes(requiredSymbols.JsonDerivedTypeAttribute);
+ var derivedTypes = typeSymbol.GetJsonDerivedTypes(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonDerivedTypeAttribute));
var hasValidatableDerivedTypes = false;
foreach (var derivedType in derivedTypes ?? [])
{
- hasValidatableDerivedTypes |= TryExtractValidatableType(derivedType, requiredSymbols, ref validatableTypes, ref visitedTypes);
+ hasValidatableDerivedTypes |= TryExtractValidatableType(derivedType, wellKnownTypes, ref validatableTypes, ref visitedTypes);
}
// No validatable members or derived types found, so we don't need to add this type.
@@ -86,7 +91,7 @@ internal bool TryExtractValidatableType(ITypeSymbol typeSymbol, RequiredSymbols
return true;
}
- internal ImmutableArray ExtractValidatableMembers(ITypeSymbol typeSymbol, RequiredSymbols requiredSymbols, ref HashSet validatableTypes, ref List visitedTypes)
+ internal ImmutableArray ExtractValidatableMembers(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, ref HashSet validatableTypes, ref List visitedTypes)
{
var members = new List();
var resolvedRecordProperty = new List();
@@ -121,8 +126,8 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb
// Check if the property's type is validatable, this resolves
// validatable types in the inheritance hierarchy
var hasValidatableType = TryExtractValidatableType(
- correspondingProperty.Type.UnwrapType(requiredSymbols.IEnumerable),
- requiredSymbols,
+ correspondingProperty.Type.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)),
+ wellKnownTypes,
ref validatableTypes,
ref visitedTypes);
@@ -130,8 +135,8 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb
ContainingType: correspondingProperty.ContainingType,
Type: correspondingProperty.Type,
Name: correspondingProperty.Name,
- DisplayName: parameter.GetDisplayName(requiredSymbols.DisplayAttribute) ??
- correspondingProperty.GetDisplayName(requiredSymbols.DisplayAttribute),
+ DisplayName: parameter.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)) ??
+ correspondingProperty.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)),
Attributes: []));
}
}
@@ -148,8 +153,8 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb
continue;
}
- var hasValidatableType = TryExtractValidatableType(member.Type.UnwrapType(requiredSymbols.IEnumerable), requiredSymbols, ref validatableTypes, ref visitedTypes);
- var attributes = ExtractValidationAttributes(member, requiredSymbols, out var isRequired);
+ var hasValidatableType = TryExtractValidatableType(member.Type.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)), wellKnownTypes, ref validatableTypes, ref visitedTypes);
+ var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
// If the member has no validation attributes or validatable types and is not required, skip it.
if (attributes.IsDefaultOrEmpty && !hasValidatableType && !isRequired)
@@ -161,14 +166,14 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb
ContainingType: member.ContainingType,
Type: member.Type,
Name: member.Name,
- DisplayName: member.GetDisplayName(requiredSymbols.DisplayAttribute),
+ DisplayName: member.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)),
Attributes: attributes));
}
return [.. members];
}
- internal static ImmutableArray ExtractValidationAttributes(ISymbol symbol, RequiredSymbols requiredSymbols, out bool isRequired)
+ internal static ImmutableArray ExtractValidationAttributes(ISymbol symbol, WellKnownTypes wellKnownTypes, out bool isRequired)
{
var attributes = symbol.GetAttributes();
if (attributes.Length == 0)
@@ -179,15 +184,15 @@ internal static ImmutableArray ExtractValidationAttributes(
var validationAttributes = attributes
.Where(attribute => attribute.AttributeClass != null)
- .Where(attribute => attribute.AttributeClass!.ImplementsValidationAttribute(requiredSymbols.ValidationAttribute));
- isRequired = validationAttributes.Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, requiredSymbols.RequiredAttribute));
+ .Where(attribute => attribute.AttributeClass!.ImplementsValidationAttribute(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute)));
+ isRequired = validationAttributes.Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_RequiredAttribute)));
return [.. validationAttributes
- .Where(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, requiredSymbols.ValidationAttribute))
+ .Where(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute)))
.Select(attribute => new ValidationAttribute(
Name: symbol.Name + attribute.AttributeClass!.Name,
ClassName: attribute.AttributeClass!.ToDisplayString(_symbolDisplayFormat),
Arguments: [.. attribute.ConstructorArguments.Select(a => a.ToCSharpString())],
NamedArguments: attribute.NamedArguments.ToDictionary(namedArgument => namedArgument.Key, namedArgument => namedArgument.Value.ToCSharpString()),
- IsCustomValidationAttribute: SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, requiredSymbols.CustomValidationAttribute)))];
+ IsCustomValidationAttribute: SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_CustomValidationAttribute))))];
}
}
diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs
index 4949ed71825f..a538dde7fcda 100644
--- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs
+++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs
@@ -11,10 +11,6 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
- // Resolve the symbols that will be required when making comparisons
- // in future steps.
- var requiredSymbols = context.CompilationProvider.Select(ExtractRequiredSymbols);
-
// Find the builder.Services.AddValidation() call in the application.
var addValidation = context.SyntaxProvider.CreateSyntaxProvider(
predicate: FindAddValidation,
@@ -34,7 +30,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Where(endpoint => endpoint is not null);
// Extract validatable types from all endpoints.
var validatableTypesFromEndpoints = endpoints
- .Combine(requiredSymbols)
.Select(ExtractValidatableEndpoint);
// Join all validatable types encountered in the type graph.
var validatableTypes = validatableTypesWithAttribute
diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs
new file mode 100644
index 000000000000..79bd2916ae12
--- /dev/null
+++ b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests;
+
+public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
+{
+ [Fact]
+ public async Task CanValidateTypeWithParsableProperties()
+ {
+ // Arrange
+ var source = """
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Validation;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder();
+
+builder.Services.AddValidation();
+
+var app = builder.Build();
+
+app.MapPost("/complex-type-with-parsable-properties", (ComplexTypeWithParsableProperties complexType) => Results.Ok("Passed"!));
+
+app.Run();
+
+public class ComplexTypeWithParsableProperties
+{
+ [RegularExpression("^((?!00000000-0000-0000-0000-000000000000).)*$", ErrorMessage = "Cannot use default Guid")]
+ public Guid? GuidWithRegularExpression { get; set; } = default;
+
+ [Required]
+ public TimeOnly? TimeOnlyWithRequiredValue { get; set; } = TimeOnly.FromDateTime(DateTime.UtcNow);
+
+ [Url(ErrorMessage = "The field Url must be a valid URL.")]
+ public Uri? Url { get; set; } = new Uri("https://example.com");
+
+ [Required]
+ [Range(typeof(DateOnly), "2023-01-01", "2025-12-31", ErrorMessage = "Date must be between 2023-01-01 and 2025-12-31")]
+ public DateOnly? DateOnlyWithRange { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow);
+
+ [Range(typeof(DateTime), "2023-01-01", "2025-12-31", ErrorMessage = "DateTime must be between 2023-01-01 and 2025-12-31")]
+ public DateTime? DateTimeWithRange { get; set; } = DateTime.UtcNow;
+
+ [Range(typeof(decimal), "0.1", "100.5", ErrorMessage = "Amount must be between 0.1 and 100.5")]
+ public decimal? DecimalWithRange { get; set; } = 50.5m;
+
+ [Range(0, 12, ErrorMessage = "Hours must be between 0 and 12")]
+ public TimeSpan? TimeSpanWithHourRange { get; set; } = TimeSpan.FromHours(12);
+
+ [Range(0, 1, ErrorMessage = "Boolean value must be 0 or 1")]
+ public bool BooleanWithRange { get; set; } = true;
+
+ [RegularExpression(@"^\d+\.\d+\.\d+$", ErrorMessage = "Must be a valid version number (e.g. 1.0.0)")]
+ public Version? VersionWithRegex { get; set; } = new Version(1, 0, 0);
+}
+""";
+ await Verify(source, out var compilation);
+ await VerifyEndpoint(compilation, "/complex-type-with-parsable-properties", async (endpoint, serviceProvider) =>
+ {
+ var payload = """
+ {
+ "TimeOnlyWithRequiredValue": null,
+ "IntWithRange": 150,
+ "StringWithLength": "AB",
+ "Email": "invalid-email",
+ "Url": "invalid-url",
+ "DateOnlyWithRange": "2026-05-01",
+ "DecimalWithRange": "150.75",
+ "TimeSpanWithHourRange": "22:00:00",
+ "VersionWithRegex": "1.0",
+ "EnumProperty": "Invalid"
+ }
+ """;
+ var context = CreateHttpContextWithPayload(payload, serviceProvider);
+
+ await endpoint.RequestDelegate(context);
+
+ var problemDetails = await AssertBadRequest(context);
+
+ // Assert on each error with Assert.Collection
+ Assert.Collection(problemDetails.Errors.OrderBy(kvp => kvp.Key),
+ error =>
+ {
+ Assert.Equal("DateOnlyWithRange", error.Key);
+ Assert.Contains("Date must be between 2023-01-01 and 2025-12-31", error.Value);
+ },
+ error =>
+ {
+ Assert.Equal("DecimalWithRange", error.Key);
+ Assert.Contains("Amount must be between 0.1 and 100.5", error.Value);
+ },
+ error =>
+ {
+ Assert.Equal("TimeOnlyWithRequiredValue", error.Key);
+ Assert.Contains("The TimeOnlyWithRequiredValue field is required.", error.Value);
+ },
+ error =>
+ {
+ Assert.Equal("TimeSpanWithHourRange", error.Key);
+ Assert.Contains("Hours must be between 0 and 12", error.Value);
+ },
+ error =>
+ {
+ Assert.Equal("Url", error.Key);
+ Assert.Contains("The field Url must be a valid URL.", error.Value);
+ },
+ error =>
+ {
+ Assert.Equal("VersionWithRegex", error.Key);
+ Assert.Contains("Must be a valid version number (e.g. 1.0.0)", error.Value);
+ }
+ );
+ });
+ }
+}
diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs
new file mode 100644
index 000000000000..ef6057388d2a
--- /dev/null
+++ b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs
@@ -0,0 +1,211 @@
+//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
+
+namespace System.Runtime.CompilerServices
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.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.AspNetCore.Http.Validation.Generated
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
+ {
+ public GeneratedValidatablePropertyInfo(
+ [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
+ global::System.Type containingType,
+ global::System.Type propertyType,
+ string name,
+ string displayName) : base(containingType, propertyType, name, displayName)
+ {
+ ContainingType = containingType;
+ Name = name;
+ }
+
+ 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.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.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.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver
+ {
+ public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ if (type == typeof(global::ComplexTypeWithParsableProperties))
+ {
+ validatableInfo = CreateComplexTypeWithParsableProperties();
+ 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.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo)
+ {
+ validatableInfo = null;
+ return false;
+ }
+
+ private ValidatableTypeInfo CreateComplexTypeWithParsableProperties()
+ {
+ return new GeneratedValidatableTypeInfo(
+ type: typeof(global::ComplexTypeWithParsableProperties),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(global::System.Guid?),
+ name: "GuidWithRegularExpression",
+ displayName: "GuidWithRegularExpression"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(global::System.TimeOnly?),
+ name: "TimeOnlyWithRequiredValue",
+ displayName: "TimeOnlyWithRequiredValue"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(global::System.Uri),
+ name: "Url",
+ displayName: "Url"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(global::System.DateOnly?),
+ name: "DateOnlyWithRange",
+ displayName: "DateOnlyWithRange"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(global::System.DateTime?),
+ name: "DateTimeWithRange",
+ displayName: "DateTimeWithRange"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(decimal?),
+ name: "DecimalWithRange",
+ displayName: "DecimalWithRange"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(global::System.TimeSpan?),
+ name: "TimeSpanWithHourRange",
+ displayName: "TimeSpanWithHourRange"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(bool),
+ name: "BooleanWithRange",
+ displayName: "BooleanWithRange"
+ ),
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::ComplexTypeWithParsableProperties),
+ propertyType: typeof(global::System.Version),
+ name: "VersionWithRegex",
+ displayName: "VersionWithRegex"
+ ),
+ ]
+ );
+ }
+
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.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.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
+ file static class ValidationAttributeCache
+ {
+ private sealed record CacheKey(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.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/Shared/RoslynUtils/WellKnownTypeData.cs b/src/Shared/RoslynUtils/WellKnownTypeData.cs
index 494357b17a3a..f60d17e9e0a8 100644
--- a/src/Shared/RoslynUtils/WellKnownTypeData.cs
+++ b/src/Shared/RoslynUtils/WellKnownTypeData.cs
@@ -118,6 +118,11 @@ public enum WellKnownType
Microsoft_AspNetCore_Authorization_IAllowAnonymous,
Microsoft_AspNetCore_Authorization_IAuthorizeData,
System_AttributeUsageAttribute,
+ System_Text_Json_Serialization_JsonDerivedTypeAttribute,
+ System_ComponentModel_DataAnnotations_DisplayAttribute,
+ System_ComponentModel_DataAnnotations_ValidationAttribute,
+ System_ComponentModel_DataAnnotations_RequiredAttribute,
+ System_ComponentModel_DataAnnotations_CustomValidationAttribute,
}
public static string[] WellKnownTypeNames =
@@ -233,5 +238,10 @@ public enum WellKnownType
"Microsoft.AspNetCore.Authorization.IAllowAnonymous",
"Microsoft.AspNetCore.Authorization.IAuthorizeData",
"System.AttributeUsageAttribute",
+ "System.Text.Json.Serialization.JsonDerivedTypeAttribute",
+ "System.ComponentModel.DataAnnotations.DisplayAttribute",
+ "System.ComponentModel.DataAnnotations.ValidationAttribute",
+ "System.ComponentModel.DataAnnotations.RequiredAttribute",
+ "System.ComponentModel.DataAnnotations.CustomValidationAttribute",
];
}