Skip to content

Commit d038ce7

Browse files
Copilotcaptainsafia
andcommitted
Update CanValidateComplexTypesWithJsonIgnore test with record type cases
Co-authored-by: captainsafia <[email protected]>
1 parent 2247d7f commit d038ce7

File tree

2 files changed

+239
-1
lines changed

2 files changed

+239
-1
lines changed

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public async Task CanValidateComplexTypesWithJsonIgnore()
3131
var app = builder.Build();
3232
3333
app.MapPost("/complex-type-with-json-ignore", (ComplexTypeWithJsonIgnore complexType) => Results.Ok("Passed"!));
34+
app.MapPost("/record-type-with-json-ignore", (RecordTypeWithJsonIgnore recordType) => Results.Ok("Passed"!));
3435
3536
app.Run();
3637
@@ -54,6 +55,27 @@ public class CircularReferenceType
5455
5556
public string Name { get; set; } = "test";
5657
}
58+
59+
public record RecordTypeWithJsonIgnore
60+
{
61+
[Range(10, 100)]
62+
public int ValidatedProperty { get; set; } = 10;
63+
64+
[JsonIgnore]
65+
[Required] // This should be ignored because of [JsonIgnore]
66+
public string IgnoredProperty { get; set; } = null!;
67+
68+
[JsonIgnore]
69+
public CircularReferenceRecord? CircularReference { get; set; }
70+
}
71+
72+
public record CircularReferenceRecord
73+
{
74+
[JsonIgnore]
75+
public RecordTypeWithJsonIgnore? Parent { get; set; }
76+
77+
public string Name { get; set; } = "test";
78+
}
5779
""";
5880
await Verify(source, out var compilation);
5981
await VerifyEndpoint(compilation, "/complex-type-with-json-ignore", async (endpoint, serviceProvider) =>
@@ -93,8 +115,45 @@ async Task InvalidValidatedPropertyProducesError(Endpoint endpoint)
93115
});
94116
}
95117
});
96-
}
118+
119+
await VerifyEndpoint(compilation, "/record-type-with-json-ignore", async (endpoint, serviceProvider) =>
120+
{
121+
await ValidInputWithJsonIgnoreProducesNoWarningsForRecord(endpoint);
122+
await InvalidValidatedPropertyProducesErrorForRecord(endpoint);
123+
124+
async Task ValidInputWithJsonIgnoreProducesNoWarningsForRecord(Endpoint endpoint)
125+
{
126+
var payload = """
127+
{
128+
"ValidatedProperty": 50
129+
}
130+
""";
131+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
132+
await endpoint.RequestDelegate(context);
97133

134+
Assert.Equal(200, context.Response.StatusCode);
135+
}
136+
137+
async Task InvalidValidatedPropertyProducesErrorForRecord(Endpoint endpoint)
138+
{
139+
var payload = """
140+
{
141+
"ValidatedProperty": 5
142+
}
143+
""";
144+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
145+
146+
await endpoint.RequestDelegate(context);
147+
148+
var problemDetails = await AssertBadRequest(context);
149+
Assert.Collection(problemDetails.Errors, kvp =>
150+
{
151+
Assert.Equal("ValidatedProperty", kvp.Key);
152+
Assert.Equal("The field ValidatedProperty must be between 10 and 100.", kvp.Value.Single());
153+
});
154+
}
155+
});
156+
}
98157
[Fact]
99158
public async Task CanValidateComplexTypes()
100159
{
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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 | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
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 | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
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::ComplexTypeWithJsonIgnore))
66+
{
67+
validatableInfo = new GeneratedValidatableTypeInfo(
68+
type: typeof(global::ComplexTypeWithJsonIgnore),
69+
members: [
70+
new GeneratedValidatablePropertyInfo(
71+
containingType: typeof(global::ComplexTypeWithJsonIgnore),
72+
propertyType: typeof(int),
73+
name: "ValidatedProperty",
74+
displayName: "ValidatedProperty"
75+
),
76+
]
77+
);
78+
return true;
79+
}
80+
if (type == typeof(global::RecordTypeWithJsonIgnore))
81+
{
82+
validatableInfo = new GeneratedValidatableTypeInfo(
83+
type: typeof(global::RecordTypeWithJsonIgnore),
84+
members: [
85+
new GeneratedValidatablePropertyInfo(
86+
containingType: typeof(global::RecordTypeWithJsonIgnore),
87+
propertyType: typeof(int),
88+
name: "ValidatedProperty",
89+
displayName: "ValidatedProperty"
90+
),
91+
]
92+
);
93+
return true;
94+
}
95+
96+
return false;
97+
}
98+
99+
// No-ops, rely on runtime code for ParameterInfo-based resolution
100+
public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo)
101+
{
102+
validatableInfo = null;
103+
return false;
104+
}
105+
}
106+
107+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
108+
file static class GeneratedServiceCollectionExtensions
109+
{
110+
[InterceptsLocation]
111+
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)
112+
{
113+
// Use non-extension method to avoid infinite recursion.
114+
return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options =>
115+
{
116+
options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver());
117+
if (configureOptions is not null)
118+
{
119+
configureOptions(options);
120+
}
121+
});
122+
}
123+
}
124+
125+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
126+
file static class ValidationAttributeCache
127+
{
128+
private sealed record CacheKey(
129+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
130+
[property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
131+
global::System.Type ContainingType,
132+
string PropertyName);
133+
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();
134+
135+
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
136+
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
137+
global::System.Type containingType,
138+
string propertyName)
139+
{
140+
var key = new CacheKey(containingType, propertyName);
141+
return _cache.GetOrAdd(key, static k =>
142+
{
143+
var results = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();
144+
145+
// Get attributes from the property
146+
var property = k.ContainingType.GetProperty(k.PropertyName);
147+
if (property != null)
148+
{
149+
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
150+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
151+
152+
results.AddRange(propertyAttributes);
153+
}
154+
155+
// Check constructors for parameters that match the property name
156+
// to handle record scenarios
157+
foreach (var constructor in k.ContainingType.GetConstructors())
158+
{
159+
// Look for parameter with matching name (case insensitive)
160+
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
161+
constructor.GetParameters(),
162+
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
163+
164+
if (parameter != null)
165+
{
166+
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
167+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
168+
169+
results.AddRange(paramAttributes);
170+
171+
break;
172+
}
173+
}
174+
175+
return results.ToArray();
176+
});
177+
}
178+
}
179+
}

0 commit comments

Comments
 (0)