Skip to content

Commit 07e71a6

Browse files
Copilotcaptainsafia
andcommitted
Add JsonIgnore attribute support to validation generator
Co-authored-by: captainsafia <[email protected]>
1 parent 690e927 commit 07e71a6

File tree

6 files changed

+293
-0
lines changed

6 files changed

+293
-0
lines changed

src/Shared/RoslynUtils/WellKnownTypeData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public enum WellKnownType
119119
Microsoft_AspNetCore_Authorization_IAuthorizeData,
120120
System_AttributeUsageAttribute,
121121
System_Text_Json_Serialization_JsonDerivedTypeAttribute,
122+
System_Text_Json_Serialization_JsonIgnoreAttribute,
122123
System_ComponentModel_DataAnnotations_DisplayAttribute,
123124
System_ComponentModel_DataAnnotations_ValidationAttribute,
124125
System_ComponentModel_DataAnnotations_RequiredAttribute,
@@ -240,6 +241,7 @@ public enum WellKnownType
240241
"Microsoft.AspNetCore.Authorization.IAuthorizeData",
241242
"System.AttributeUsageAttribute",
242243
"System.Text.Json.Serialization.JsonDerivedTypeAttribute",
244+
"System.Text.Json.Serialization.JsonIgnoreAttribute",
243245
"System.ComponentModel.DataAnnotations.DisplayAttribute",
244246
"System.ComponentModel.DataAnnotations.ValidationAttribute",
245247
"System.ComponentModel.DataAnnotations.RequiredAttribute",

src/Validation/gen/Extensions/ITypeSymbolExtensions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,28 @@ attr.AttributeClass is not null &&
152152
(attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) ||
153153
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol)));
154154
}
155+
156+
/// <summary>
157+
/// Checks if the property is marked with [JsonIgnore] attribute.
158+
/// </summary>
159+
/// <param name="property">The property to check.</param>
160+
/// <param name="jsonIgnoreAttributeSymbol">The symbol representing the [JsonIgnore] attribute.</param>
161+
internal static bool IsJsonIgnoredProperty(this IPropertySymbol property, INamedTypeSymbol jsonIgnoreAttributeSymbol)
162+
{
163+
return property.GetAttributes().Any(attr =>
164+
attr.AttributeClass is not null &&
165+
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
166+
}
167+
168+
/// <summary>
169+
/// Checks if the parameter is marked with [JsonIgnore] attribute.
170+
/// </summary>
171+
/// <param name="parameter">The parameter to check.</param>
172+
/// <param name="jsonIgnoreAttributeSymbol">The symbol representing the [JsonIgnore] attribute.</param>
173+
internal static bool IsJsonIgnoredParameter(this IParameterSymbol parameter, INamedTypeSymbol jsonIgnoreAttributeSymbol)
174+
{
175+
return parameter.GetAttributes().Any(attr =>
176+
attr.AttributeClass is not null &&
177+
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
178+
}
155179
}

src/Validation/gen/Models/RequiredSymbols.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal sealed record class RequiredSymbols(
1111
INamedTypeSymbol IEnumerable,
1212
INamedTypeSymbol IValidatableObject,
1313
INamedTypeSymbol JsonDerivedTypeAttribute,
14+
INamedTypeSymbol JsonIgnoreAttribute,
1415
INamedTypeSymbol RequiredAttribute,
1516
INamedTypeSymbol CustomValidationAttribute,
1617
INamedTypeSymbol HttpContext,

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
114114
WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
115115
var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get(
116116
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
117+
var jsonIgnoreAttributeSymbol = wellKnownTypes.Get(
118+
WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonIgnoreAttribute);
117119

118120
// Special handling for record types to extract properties from
119121
// the primary constructor.
@@ -148,6 +150,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
148150
continue;
149151
}
150152

153+
// Skip parameters that have JsonIgnore attribute
154+
if (parameter.IsJsonIgnoredParameter(jsonIgnoreAttributeSymbol))
155+
{
156+
continue;
157+
}
158+
151159
// Check if the property's type is validatable, this resolves
152160
// validatable types in the inheritance hierarchy
153161
var hasValidatableType = TryExtractValidatableType(
@@ -186,6 +194,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
186194
continue;
187195
}
188196

197+
// Skip properties that have JsonIgnore attribute
198+
if (member.IsJsonIgnoredProperty(jsonIgnoreAttributeSymbol))
199+
{
200+
continue;
201+
}
202+
189203
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
190204
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
191205

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,94 @@ namespace Microsoft.Extensions.Validation.GeneratorTests;
77

88
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
99
{
10+
[Fact]
11+
public async Task CanValidateComplexTypesWithJsonIgnore()
12+
{
13+
// Arrange
14+
var source = """
15+
using System;
16+
using System.ComponentModel.DataAnnotations;
17+
using System.Collections.Generic;
18+
using System.Threading.Tasks;
19+
using Microsoft.AspNetCore.Builder;
20+
using Microsoft.AspNetCore.Http;
21+
using Microsoft.Extensions.Validation;
22+
using Microsoft.AspNetCore.Routing;
23+
using Microsoft.Extensions.DependencyInjection;
24+
using Microsoft.AspNetCore.Mvc;
25+
using System.Text.Json.Serialization;
26+
27+
var builder = WebApplication.CreateBuilder();
28+
29+
builder.Services.AddValidation();
30+
31+
var app = builder.Build();
32+
33+
app.MapPost("/complex-type-with-json-ignore", (ComplexTypeWithJsonIgnore complexType) => Results.Ok("Passed"!));
34+
35+
app.Run();
36+
37+
public class ComplexTypeWithJsonIgnore
38+
{
39+
[Range(10, 100)]
40+
public int ValidatedProperty { get; set; } = 10;
41+
42+
[JsonIgnore]
43+
[Required] // This should be ignored because of [JsonIgnore]
44+
public string IgnoredProperty { get; set; } = null!;
45+
46+
[JsonIgnore]
47+
public CircularReferenceType CircularReference { get; set; } = new CircularReferenceType();
48+
}
49+
50+
public class CircularReferenceType
51+
{
52+
[JsonIgnore]
53+
public ComplexTypeWithJsonIgnore Parent { get; set; } = new ComplexTypeWithJsonIgnore();
54+
55+
public string Name { get; set; } = "test";
56+
}
57+
""";
58+
await Verify(source, out var compilation);
59+
await VerifyEndpoint(compilation, "/complex-type-with-json-ignore", async (endpoint, serviceProvider) =>
60+
{
61+
await ValidInputWithJsonIgnoreProducesNoWarnings(endpoint);
62+
await InvalidValidatedPropertyProducesError(endpoint);
63+
64+
async Task ValidInputWithJsonIgnoreProducesNoWarnings(Endpoint endpoint)
65+
{
66+
var payload = """
67+
{
68+
"ValidatedProperty": 50
69+
}
70+
""";
71+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
72+
await endpoint.RequestDelegate(context);
73+
74+
Assert.Equal(200, context.Response.StatusCode);
75+
}
76+
77+
async Task InvalidValidatedPropertyProducesError(Endpoint endpoint)
78+
{
79+
var payload = """
80+
{
81+
"ValidatedProperty": 5
82+
}
83+
""";
84+
var context = CreateHttpContextWithPayload(payload, serviceProvider);
85+
86+
await endpoint.RequestDelegate(context);
87+
88+
var problemDetails = await AssertBadRequest(context);
89+
Assert.Collection(problemDetails.Errors, kvp =>
90+
{
91+
Assert.Equal("ValidatedProperty", kvp.Key);
92+
Assert.Equal("The field ValidatedProperty must be between 10 and 100.", kvp.Value.Single());
93+
});
94+
}
95+
});
96+
}
97+
1098
[Fact]
1199
public async Task CanValidateComplexTypes()
12100
{
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
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(
114+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
115+
[property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
116+
global::System.Type ContainingType,
117+
string PropertyName);
118+
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();
119+
120+
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
121+
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
122+
global::System.Type containingType,
123+
string propertyName)
124+
{
125+
var key = new CacheKey(containingType, propertyName);
126+
return _cache.GetOrAdd(key, static k =>
127+
{
128+
var results = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();
129+
130+
// Get attributes from the property
131+
var property = k.ContainingType.GetProperty(k.PropertyName);
132+
if (property != null)
133+
{
134+
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
135+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
136+
137+
results.AddRange(propertyAttributes);
138+
}
139+
140+
// Check constructors for parameters that match the property name
141+
// to handle record scenarios
142+
foreach (var constructor in k.ContainingType.GetConstructors())
143+
{
144+
// Look for parameter with matching name (case insensitive)
145+
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
146+
constructor.GetParameters(),
147+
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
148+
149+
if (parameter != null)
150+
{
151+
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
152+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
153+
154+
results.AddRange(paramAttributes);
155+
156+
break;
157+
}
158+
}
159+
160+
return results.ToArray();
161+
});
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)