diff --git a/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs index 37186fee8a8a..408ec7defb89 100644 --- a/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs +++ b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs @@ -138,4 +138,18 @@ attr.AttributeClass is not null && (attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) || SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol))); } + + /// + /// Checks if the property is marked with [FromServices] or [FromKeyedServices] attributes. + /// + /// The property to check. + /// The symbol representing the [FromServices] attribute. + /// The symbol representing the [FromKeyedServices] attribute. + internal static bool IsServiceProperty(this IPropertySymbol property, INamedTypeSymbol fromServiceMetadataSymbol, INamedTypeSymbol fromKeyedServiceAttributeSymbol) + { + return property.GetAttributes().Any(attr => + attr.AttributeClass is not null && + (attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) || + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol))); + } } diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index c7155b8c049b..b0617d79b3a6 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -110,6 +110,11 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb var members = new List(); var resolvedRecordProperty = new List(); + var fromServiceMetadataSymbol = wellKnownTypes.Get( + WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata); + var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get( + WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute); + // Special handling for record types to extract properties from // the primary constructor. if (typeSymbol is INamedTypeSymbol { IsRecord: true } namedType) @@ -137,6 +142,12 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb { resolvedRecordProperty.Add(correspondingProperty); + // Skip parameters that are injected as services + if (parameter.IsServiceParameter(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol)) + { + continue; + } + // Check if the property's type is validatable, this resolves // validatable types in the inheritance hierarchy var hasValidatableType = TryExtractValidatableType( @@ -162,13 +173,19 @@ internal ImmutableArray ExtractValidatableMembers(ITypeSymb { // Skip compiler generated properties and properties already processed via // the record processing logic above. - if (member.IsImplicitlyDeclared - || member.IsEqualityContract(wellKnownTypes) + if (member.IsImplicitlyDeclared + || member.IsEqualityContract(wellKnownTypes) || resolvedRecordProperty.Contains(member, SymbolEqualityComparer.Default)) { continue; } + // Skip properties that are injected as services + if (member.IsServiceProperty(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol)) + { + continue; + } + var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs index 7bdf0bea29b2..46158b90632d 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs @@ -21,10 +21,12 @@ public async Task CanValidateComplexTypes() using Microsoft.Extensions.Validation; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(); builder.Services.AddValidation(); +builder.Services.AddSingleton(); var app = builder.Build(); @@ -58,6 +60,10 @@ public class ComplexType [DerivedValidation, Range(10, 100)] public int PropertyWithMultipleAttributes { get; set; } = 10; + + [FromServices] + [Required] // This should be ignored because of [FromServices] + public TestService ServiceProperty { get; set; } = null!; } public class DerivedValidationAttribute : ValidationAttribute @@ -96,6 +102,12 @@ public static ValidationResult Validate(int number, ValidationContext validation return ValidationResult.Success; } } + +public class TestService +{ + [Range(10, 100)] + public int Value { get; set; } = 4; +} """; await Verify(source, out var compilation); await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs index cfc1372b9a7a..a5a4b2e09ed0 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs @@ -21,10 +21,12 @@ public async Task CanValidateRecordTypes() using Microsoft.Extensions.Validation; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(); builder.Services.AddValidation(); +builder.Services.AddSingleton(); var app = builder.Build(); @@ -48,6 +50,10 @@ public record SubTypeWithoutConstructor [StringLength(10)] public string? StringWithLength { get; set; } + + [FromServices] + [Required] + public TestService ServiceProperty { get; set; } = null!; } public static class CustomValidators @@ -66,6 +72,12 @@ public static ValidationResult Validate(int number, ValidationContext validation } } +public class TestService +{ + [Range(10, 100)] + public int Value { get; set; } = 4; +} + public record ValidatableRecord( [Range(10, 100)] int IntegerWithRange = 10, @@ -81,7 +93,9 @@ public record ValidatableRecord( [CustomValidation(typeof(CustomValidators), nameof(CustomValidators.Validate))] int IntegerWithCustomValidation = 0, [DerivedValidation, Range(10, 100)] - int PropertyWithMultipleAttributes = 10 + int PropertyWithMultipleAttributes = 10, + [FromServices] [Required] TestService ServiceProperty = null!, // This should be ignored because of [FromServices] + [FromKeyedServices("serviceKey")] [Range(10, 100)] int KeyedServiceProperty = 5 // This should be ignored because of [FromKeyedServices] ); """; await Verify(source, out var compilation);