Skip to content

Commit 2ebdeba

Browse files
Copilotcaptainsafia
andcommitted
Add tests for FromServices attribute filtering functionality
Co-authored-by: captainsafia <[email protected]>
1 parent 41f8afc commit 2ebdeba

5 files changed

+513
-6
lines changed

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,6 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
148148
{
149149
resolvedRecordProperty.Add(correspondingProperty);
150150

151-
// Skip properties that are injected as services
152-
if (correspondingProperty.IsServiceProperty(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol))
153-
{
154-
continue;
155-
}
156-
157151
// Check if the property's type is validatable, this resolves
158152
// validatable types in the inheritance hierarchy
159153
var hasValidatableType = TryExtractValidatableType(

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,94 @@ 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+
}
376466
}

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,86 @@ async Task ValidInputProducesNoWarnings(Endpoint endpoint)
370370
});
371371

372372
}
373+
374+
[Fact]
375+
public async Task DoesNotValidateRecordPropertiesWithFromServicesAttribute()
376+
{
377+
// Arrange
378+
var source = """
379+
using System;
380+
using System.ComponentModel.DataAnnotations;
381+
using System.Collections.Generic;
382+
using System.Threading.Tasks;
383+
using Microsoft.AspNetCore.Builder;
384+
using Microsoft.AspNetCore.Http;
385+
using Microsoft.Extensions.Validation;
386+
using Microsoft.AspNetCore.Routing;
387+
using Microsoft.Extensions.DependencyInjection;
388+
using Microsoft.AspNetCore.Mvc;
389+
390+
var builder = WebApplication.CreateBuilder();
391+
392+
builder.Services.AddValidation();
393+
builder.Services.AddSingleton<TestService>();
394+
395+
var app = builder.Build();
396+
397+
app.MapPost("/with-from-services-record", ([AsParameters] ComplexRecordWithFromServices complexType) => Results.Ok("Passed"!));
398+
399+
app.Run();
400+
401+
public record ComplexRecordWithFromServices(
402+
[Range(10, 100)] int ValidatableProperty,
403+
[FromServices] [Required] TestService ServiceProperty, // This should be ignored because of [FromServices]
404+
[FromKeyedServices("serviceKey")] [Range(10, 100)] int KeyedServiceProperty // This should be ignored because of [FromKeyedServices]
405+
);
406+
407+
public class TestService
408+
{
409+
[Range(10, 100)]
410+
public int Value { get; set; } = 4;
411+
}
412+
""";
413+
await Verify(source, out var compilation);
414+
await VerifyEndpoint(compilation, "/with-from-services-record", async (endpoint, serviceProvider) =>
415+
{
416+
await ValidInputWithFromServicesProducesNoWarnings(endpoint);
417+
await InvalidValidatablePropertyProducesError(endpoint);
418+
419+
async Task ValidInputWithFromServicesProducesNoWarnings(Endpoint endpoint)
420+
{
421+
var payload = """
422+
{
423+
"ValidatableProperty": 50,
424+
"ServiceProperty": null,
425+
"KeyedServiceProperty": 5
426+
}
427+
""";
428+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
429+
await endpoint.RequestDelegate(context);
430+
431+
Assert.Equal(200, context.Response.StatusCode);
432+
}
433+
434+
async Task InvalidValidatablePropertyProducesError(Endpoint endpoint)
435+
{
436+
var payload = """
437+
{
438+
"ValidatableProperty": 5,
439+
"ServiceProperty": null,
440+
"KeyedServiceProperty": 5
441+
}
442+
""";
443+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
444+
await endpoint.RequestDelegate(context);
445+
446+
var problemDetails = await AssertBadRequest(context);
447+
Assert.Collection(problemDetails.Errors, kvp =>
448+
{
449+
Assert.Equal("ValidatableProperty", kvp.Key);
450+
Assert.Equal("The field ValidatableProperty must be between 10 and 100.", kvp.Value.Single());
451+
});
452+
}
453+
});
454+
}
373455
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
//HintName: ValidatableInfoResolver.g.cs
2+
#nullable enable annotations
3+
//------------------------------------------------------------------------------
4+
// <auto-generated>
5+
// This code was generated by a tool.
6+
//
7+
// Changes to this file may cause incorrect behavior and will be lost if
8+
// the code is regenerated.
9+
// </auto-generated>
10+
//------------------------------------------------------------------------------
11+
#nullable enable
12+
#pragma warning disable ASP0029
13+
14+
namespace System.Runtime.CompilerServices
15+
{
16+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
17+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
18+
file sealed class InterceptsLocationAttribute : System.Attribute
19+
{
20+
public InterceptsLocationAttribute(int version, string data)
21+
{
22+
}
23+
}
24+
}
25+
26+
namespace Microsoft.Extensions.Validation.Generated
27+
{
28+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
29+
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo
30+
{
31+
public GeneratedValidatablePropertyInfo(
32+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
33+
global::System.Type containingType,
34+
global::System.Type propertyType,
35+
string name,
36+
string displayName) : base(containingType, propertyType, name, displayName)
37+
{
38+
ContainingType = containingType;
39+
Name = name;
40+
}
41+
42+
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
43+
internal global::System.Type ContainingType { get; }
44+
internal string Name { get; }
45+
46+
protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
47+
=> ValidationAttributeCache.GetValidationAttributes(ContainingType, Name);
48+
}
49+
50+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
51+
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo
52+
{
53+
public GeneratedValidatableTypeInfo(
54+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
55+
global::System.Type type,
56+
ValidatablePropertyInfo[] members) : base(type, members) { }
57+
}
58+
59+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
60+
file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver
61+
{
62+
public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
63+
{
64+
validatableInfo = null;
65+
if (type == typeof(global::ComplexTypeWithFromServices))
66+
{
67+
validatableInfo = new GeneratedValidatableTypeInfo(
68+
type: typeof(global::ComplexTypeWithFromServices),
69+
members: [
70+
new GeneratedValidatablePropertyInfo(
71+
containingType: typeof(global::ComplexTypeWithFromServices),
72+
propertyType: typeof(int),
73+
name: "ValidatableProperty",
74+
displayName: "ValidatableProperty"
75+
),
76+
]
77+
);
78+
return true;
79+
}
80+
81+
return false;
82+
}
83+
84+
// No-ops, rely on runtime code for ParameterInfo-based resolution
85+
public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
86+
{
87+
validatableInfo = null;
88+
return false;
89+
}
90+
}
91+
92+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
93+
file static class GeneratedServiceCollectionExtensions
94+
{
95+
[InterceptsLocation]
96+
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action<global::Microsoft.Extensions.Validation.ValidationOptions>? configureOptions = null)
97+
{
98+
// Use non-extension method to avoid infinite recursion.
99+
return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
100+
{
101+
options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
102+
if (configureOptions is not null)
103+
{
104+
configureOptions(options);
105+
}
106+
});
107+
}
108+
}
109+
110+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
111+
file static class ValidationAttributeCache
112+
{
113+
private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName);
114+
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();
115+
116+
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
117+
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
118+
global::System.Type containingType,
119+
string propertyName)
120+
{
121+
var key = new CacheKey(containingType, propertyName);
122+
return _cache.GetOrAdd(key, static k =>
123+
{
124+
var results = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();
125+
126+
// Get attributes from the property
127+
var property = k.ContainingType.GetProperty(k.PropertyName);
128+
if (property != null)
129+
{
130+
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
131+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
132+
133+
results.AddRange(propertyAttributes);
134+
}
135+
136+
// Check constructors for parameters that match the property name
137+
// to handle record scenarios
138+
foreach (var constructor in k.ContainingType.GetConstructors())
139+
{
140+
// Look for parameter with matching name (case insensitive)
141+
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
142+
constructor.GetParameters(),
143+
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
144+
145+
if (parameter != null)
146+
{
147+
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
148+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
149+
150+
results.AddRange(paramAttributes);
151+
152+
break;
153+
}
154+
}
155+
156+
return results.ToArray();
157+
});
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)