Skip to content

Commit 26b66d2

Browse files
Copilotcaptainsafia
andcommitted
Add IsServiceProperty method and update property filtering logic
Co-authored-by: captainsafia <[email protected]>
1 parent f9ab34d commit 26b66d2

7 files changed

+885
-0
lines changed

src/Validation/gen/Extensions/ITypeSymbolExtensions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,18 @@ attr.AttributeClass is not null &&
138138
(attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) ||
139139
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol)));
140140
}
141+
142+
/// <summary>
143+
/// Checks if the property is marked with [FromService] or [FromKeyedService] attributes.
144+
/// </summary>
145+
/// <param name="property">The property to check.</param>
146+
/// <param name="fromServiceMetadataSymbol">The symbol representing the [FromService] attribute.</param>
147+
/// <param name="fromKeyedServiceAttributeSymbol">The symbol representing the [FromKeyedService] attribute.</param>
148+
internal static bool IsServiceProperty(this IPropertySymbol property, INamedTypeSymbol fromServiceMetadataSymbol, INamedTypeSymbol fromKeyedServiceAttributeSymbol)
149+
{
150+
return property.GetAttributes().Any(attr =>
151+
attr.AttributeClass is not null &&
152+
(attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) ||
153+
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol)));
154+
}
141155
}

src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
110110
var members = new List<ValidatableProperty>();
111111
var resolvedRecordProperty = new List<IPropertySymbol>();
112112

113+
var fromServiceMetadataSymbol = wellKnownTypes.Get(
114+
WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
115+
var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get(
116+
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
117+
113118
// Special handling for record types to extract properties from
114119
// the primary constructor.
115120
if (typeSymbol is INamedTypeSymbol { IsRecord: true } namedType)
@@ -126,6 +131,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
126131
// Process all parameters in constructor order to maintain parameter ordering
127132
foreach (var parameter in primaryConstructor.Parameters)
128133
{
134+
// Skip parameters that are injected as services
135+
if (parameter.IsServiceParameter(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol))
136+
{
137+
continue;
138+
}
139+
129140
// Find the corresponding property in this type, we ignore
130141
// base types here since that will be handled by the inheritance
131142
// checks in the default ValidatableTypeInfo implementation.
@@ -137,6 +148,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
137148
{
138149
resolvedRecordProperty.Add(correspondingProperty);
139150

151+
// Skip properties that are injected as services
152+
if (correspondingProperty.IsServiceProperty(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol))
153+
{
154+
continue;
155+
}
156+
140157
// Check if the property's type is validatable, this resolves
141158
// validatable types in the inheritance hierarchy
142159
var hasValidatableType = TryExtractValidatableType(
@@ -169,6 +186,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
169186
continue;
170187
}
171188

189+
// Skip properties that are injected as services
190+
if (member.IsServiceProperty(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol))
191+
{
192+
continue;
193+
}
194+
172195
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
173196
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
174197

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,176 @@ async Task ValidInputProducesNoWarnings(Endpoint endpoint)
373373
}
374374
});
375375
}
376+
377+
[Fact]
378+
public async Task DoesNotValidatePropertiesWithFromServicesAttribute()
379+
{
380+
// Arrange
381+
var source = """
382+
using System;
383+
using System.ComponentModel.DataAnnotations;
384+
using System.Collections.Generic;
385+
using System.Threading.Tasks;
386+
using Microsoft.AspNetCore.Builder;
387+
using Microsoft.AspNetCore.Http;
388+
using Microsoft.Extensions.Validation;
389+
using Microsoft.AspNetCore.Routing;
390+
using Microsoft.Extensions.DependencyInjection;
391+
using Microsoft.AspNetCore.Mvc;
392+
393+
var builder = WebApplication.CreateBuilder();
394+
395+
builder.Services.AddValidation();
396+
builder.Services.AddSingleton<TestService>();
397+
398+
var app = builder.Build();
399+
400+
app.MapPost("/with-from-services", ([AsParameters] ComplexTypeWithFromServices complexType) => Results.Ok("Passed"!));
401+
402+
app.Run();
403+
404+
public class ComplexTypeWithFromServices
405+
{
406+
[Range(10, 100)]
407+
public int ValidatableProperty { get; set; } = 10;
408+
409+
[FromServices]
410+
[Required] // This should be ignored because of [FromServices]
411+
public TestService ServiceProperty { get; set; } = null!;
412+
413+
[FromKeyedServices("serviceKey")]
414+
[Range(10, 100)] // This should be ignored because of [FromKeyedServices]
415+
public int KeyedServiceProperty { get; set; } = 5;
416+
}
417+
418+
public class TestService
419+
{
420+
[Range(10, 100)]
421+
public int Value { get; set; } = 4;
422+
}
423+
""";
424+
await Verify(source, out var compilation);
425+
await VerifyEndpoint(compilation, "/with-from-services", async (endpoint, serviceProvider) =>
426+
{
427+
await ValidInputWithFromServicesProducesNoWarnings(endpoint);
428+
await InvalidValidatablePropertyProducesError(endpoint);
429+
430+
async Task ValidInputWithFromServicesProducesNoWarnings(Endpoint endpoint)
431+
{
432+
var payload = """
433+
{
434+
"ValidatableProperty": 50,
435+
"ServiceProperty": null,
436+
"KeyedServiceProperty": 5
437+
}
438+
""";
439+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
440+
await endpoint.RequestDelegate(context);
441+
442+
Assert.Equal(200, context.Response.StatusCode);
443+
}
444+
445+
async Task InvalidValidatablePropertyProducesError(Endpoint endpoint)
446+
{
447+
var payload = """
448+
{
449+
"ValidatableProperty": 5,
450+
"ServiceProperty": null,
451+
"KeyedServiceProperty": 5
452+
}
453+
""";
454+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
455+
await endpoint.RequestDelegate(context);
456+
457+
var problemDetails = await AssertBadRequest(context);
458+
Assert.Collection(problemDetails.Errors, kvp =>
459+
{
460+
Assert.Equal("ValidatableProperty", kvp.Key);
461+
Assert.Equal("The field ValidatableProperty must be between 10 and 100.", kvp.Value.Single());
462+
});
463+
}
464+
});
465+
}
466+
467+
[Fact]
468+
public async Task DoesNotValidateRecordPropertiesWithFromServicesAttribute()
469+
{
470+
// Arrange
471+
var source = """
472+
using System;
473+
using System.ComponentModel.DataAnnotations;
474+
using System.Collections.Generic;
475+
using System.Threading.Tasks;
476+
using Microsoft.AspNetCore.Builder;
477+
using Microsoft.AspNetCore.Http;
478+
using Microsoft.Extensions.Validation;
479+
using Microsoft.AspNetCore.Routing;
480+
using Microsoft.Extensions.DependencyInjection;
481+
using Microsoft.AspNetCore.Mvc;
482+
483+
var builder = WebApplication.CreateBuilder();
484+
485+
builder.Services.AddValidation();
486+
builder.Services.AddSingleton<TestService>();
487+
488+
var app = builder.Build();
489+
490+
app.MapPost("/with-from-services-record", ([AsParameters] ComplexRecordWithFromServices complexType) => Results.Ok("Passed"!));
491+
492+
app.Run();
493+
494+
public record ComplexRecordWithFromServices(
495+
[Range(10, 100)] int ValidatableProperty,
496+
[FromServices] [Required] TestService ServiceProperty, // This should be ignored because of [FromServices]
497+
[FromKeyedServices("serviceKey")] [Range(10, 100)] int KeyedServiceProperty // This should be ignored because of [FromKeyedServices]
498+
);
499+
500+
public class TestService
501+
{
502+
[Range(10, 100)]
503+
public int Value { get; set; } = 4;
504+
}
505+
""";
506+
await Verify(source, out var compilation);
507+
await VerifyEndpoint(compilation, "/with-from-services-record", async (endpoint, serviceProvider) =>
508+
{
509+
await ValidInputWithFromServicesProducesNoWarnings(endpoint);
510+
await InvalidValidatablePropertyProducesError(endpoint);
511+
512+
async Task ValidInputWithFromServicesProducesNoWarnings(Endpoint endpoint)
513+
{
514+
var payload = """
515+
{
516+
"ValidatableProperty": 50,
517+
"ServiceProperty": null,
518+
"KeyedServiceProperty": 5
519+
}
520+
""";
521+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
522+
await endpoint.RequestDelegate(context);
523+
524+
Assert.Equal(200, context.Response.StatusCode);
525+
}
526+
527+
async Task InvalidValidatablePropertyProducesError(Endpoint endpoint)
528+
{
529+
var payload = """
530+
{
531+
"ValidatableProperty": 5,
532+
"ServiceProperty": null,
533+
"KeyedServiceProperty": 5
534+
}
535+
""";
536+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
537+
await endpoint.RequestDelegate(context);
538+
539+
var problemDetails = await AssertBadRequest(context);
540+
Assert.Collection(problemDetails.Errors, kvp =>
541+
{
542+
Assert.Equal("ValidatableProperty", kvp.Key);
543+
Assert.Equal("The field ValidatableProperty must be between 10 and 100.", kvp.Value.Single());
544+
});
545+
}
546+
});
547+
}
376548
}

0 commit comments

Comments
 (0)