Skip to content

Commit c1a86cc

Browse files
committed
Add support for type-level validation attributes (WIP)
1 parent b48d272 commit c1a86cc

File tree

25 files changed

+905
-516
lines changed

25 files changed

+905
-516
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ dotnet_diagnostic.IDE0005.severity = silent
466466

467467

468468
# Verify settings
469-
[*.{received,verified}.{txt,xml,json}]
469+
[*.{received,verified}.{txt,xml,json,cs}]
470470
charset = "utf-8-bom"
471471
end_of_line = lf
472472
indent_size = unset

src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,13 @@ public GeneratedValidatableTypeInfo(
8484
ValidatablePropertyInfo[] members) : base(type, members)
8585
{
8686
Type = type;
87-
Name = type.Name;
8887
}
8988
9089
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
9190
internal global::System.Type Type { get; }
92-
internal string Name { get; }
9391
9492
protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
95-
=> ValidationAttributeCache.GetValidationAttributes(Type, Name);
93+
=> ValidationAttributeCache.GetValidationAttributes(Type, null);
9694
}
9795
9896
{{GeneratedCodeAttribute}}
@@ -138,46 +136,60 @@ private sealed record CacheKey(
138136
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
139137
[property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
140138
global::System.Type ContainingType,
141-
string PropertyName);
139+
string? PropertyName);
142140
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();
143141
144142
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
145143
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
146144
global::System.Type containingType,
147-
string propertyName)
145+
string? propertyName)
148146
{
149147
var key = new CacheKey(containingType, propertyName);
150148
return _cache.GetOrAdd(key, static k =>
151149
{
152150
var results = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();
153151
154-
// Get attributes from the property
155-
var property = k.ContainingType.GetProperty(k.PropertyName);
156-
if (property != null)
152+
if (k.PropertyName is not null)
157153
{
158-
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
159-
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
160-
161-
results.AddRange(propertyAttributes);
162-
}
154+
// Get attributes from the property
155+
var property = k.ContainingType.GetProperty(k.PropertyName);
156+
if (property != null)
157+
{
158+
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
159+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
163160
164-
// Check constructors for parameters that match the property name
165-
// to handle record scenarios
166-
foreach (var constructor in k.ContainingType.GetConstructors())
167-
{
168-
// Look for parameter with matching name (case insensitive)
169-
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
170-
constructor.GetParameters(),
171-
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
161+
results.AddRange(propertyAttributes);
162+
}
172163
173-
if (parameter != null)
164+
// Check constructors for parameters that match the property name
165+
// to handle record scenarios
166+
foreach (var constructor in k.ContainingType.GetConstructors())
174167
{
175-
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
176-
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
168+
// Look for parameter with matching name (case insensitive)
169+
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
170+
constructor.GetParameters(),
171+
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
177172
178-
results.AddRange(paramAttributes);
173+
if (parameter != null)
174+
{
175+
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
176+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
179177
180-
break;
178+
results.AddRange(paramAttributes);
179+
180+
break;
181+
}
182+
}
183+
}
184+
else
185+
{
186+
// Get attributes from the type itself and its super types
187+
foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true))
188+
{
189+
if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute)
190+
{
191+
results.Add(validationAttribute);
192+
}
181193
}
182194
}
183195

src/Validation/gen/Models/ValidatableType.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ namespace Microsoft.Extensions.Validation;
88

99
internal sealed record class ValidatableType(
1010
ITypeSymbol Type,
11-
ImmutableArray<ValidatableProperty> Members,
12-
ImmutableArray<ValidationAttribute> Attributes
11+
ImmutableArray<ValidatableProperty> Members
1312
);

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,7 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow
115115
// Add the type itself as a validatable type itself.
116116
validatableTypes.Add(new ValidatableType(
117117
Type: typeSymbol,
118-
Members: members,
119-
Attributes: [])); // TODO: Extract validation attributes from the type itself.
118+
Members: members));
120119

121120
return true;
122121
}

src/Validation/src/ValidatableTypeInfo.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
7676
// Then validate inherited members
7777
foreach (var superTypeInfo in GetSuperTypeInfos(actualType, context))
7878
{
79-
await superTypeInfo.ValidateAsync(value, context, cancellationToken);
79+
await superTypeInfo.ValidateMembersAsync(value, context, cancellationToken);
8080
context.CurrentValidationPath = originalPrefix;
8181
}
8282

@@ -86,14 +86,13 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
8686
return;
8787
}
8888

89-
// Validate direct type-level attributes
89+
// Validate type-level attributes
9090
ValidateTypeAttributes(value, Type.Name, context.CurrentValidationPath, context);
9191

92-
// Validate inherited type-level attributes
93-
foreach (var superTypeInfo in GetSuperTypeInfos(actualType, context))
92+
// If any type-level attribute errors were found, return early
93+
if (context.ValidationErrors is not null && context.ValidationErrors.Count > 0)
9494
{
95-
superTypeInfo.ValidateTypeAttributes(value, Type.Name, context.CurrentValidationPath, context);
96-
context.CurrentValidationPath = originalPrefix;
95+
return;
9796
}
9897

9998
// Finally validate IValidatableObject if implemented

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

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,10 @@ public class TestService
104104
await Verify(source, out var compilation);
105105
await VerifyEndpoint(compilation, "/validatable-object", async (endpoint, serviceProvider) =>
106106
{
107-
await ValidateMethodCalledIfPropertyValidationsFail();
108-
await ValidateForSubtypeInvokedFirst();
107+
await ValidateMethodNotCalledIfPropertyValidationsFail();
109108
await ValidateForTopLevelInvoked();
110109

111-
async Task ValidateMethodCalledIfPropertyValidationsFail()
110+
async Task ValidateMethodNotCalledIfPropertyValidationsFail()
112111
{
113112
var httpContext = CreateHttpContextWithPayload("""
114113
{
@@ -136,46 +135,6 @@ async Task ValidateMethodCalledIfPropertyValidationsFail()
136135
{
137136
Assert.Equal("SubType.RequiredProperty", error.Key);
138137
Assert.Equal("The RequiredProperty field is required.", error.Value.Single());
139-
},
140-
error =>
141-
{
142-
Assert.Equal("SubType.Value3", error.Key);
143-
Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single());
144-
},
145-
error =>
146-
{
147-
Assert.Equal("Value1", error.Key);
148-
Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
149-
});
150-
}
151-
152-
async Task ValidateForSubtypeInvokedFirst()
153-
{
154-
var httpContext = CreateHttpContextWithPayload("""
155-
{
156-
"Value1": 5,
157-
"Value2": "[email protected]",
158-
"SubType": {
159-
"Value3": "foo",
160-
"RequiredProperty": "some-value-2",
161-
"StringWithLength": "element"
162-
}
163-
}
164-
""", serviceProvider);
165-
166-
await endpoint.RequestDelegate(httpContext);
167-
168-
var problemDetails = await AssertBadRequest(httpContext);
169-
Assert.Collection(problemDetails.Errors,
170-
error =>
171-
{
172-
Assert.Equal("SubType.Value3", error.Key);
173-
Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single());
174-
},
175-
error =>
176-
{
177-
Assert.Equal("Value1", error.Key);
178-
Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single());
179138
});
180139
}
181140

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,16 @@ public GeneratedValidatablePropertyInfo(
5353
public GeneratedValidatableTypeInfo(
5454
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5555
global::System.Type type,
56-
ValidatablePropertyInfo[] members) : base(type, members) { }
56+
ValidatablePropertyInfo[] members) : base(type, members)
57+
{
58+
Type = type;
59+
}
60+
61+
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
62+
internal global::System.Type Type { get; }
63+
64+
protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes()
65+
=> ValidationAttributeCache.GetValidationAttributes(Type, null);
5766
}
5867

5968
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
@@ -120,46 +129,60 @@ private sealed record CacheKey(
120129
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
121130
[property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
122131
global::System.Type ContainingType,
123-
string PropertyName);
132+
string? PropertyName);
124133
private static readonly global::System.Collections.Concurrent.ConcurrentDictionary<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _cache = new();
125134

126135
public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes(
127136
[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]
128137
global::System.Type containingType,
129-
string propertyName)
138+
string? propertyName)
130139
{
131140
var key = new CacheKey(containingType, propertyName);
132141
return _cache.GetOrAdd(key, static k =>
133142
{
134143
var results = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();
135144

136-
// Get attributes from the property
137-
var property = k.ContainingType.GetProperty(k.PropertyName);
138-
if (property != null)
145+
if (k.PropertyName is not null)
139146
{
140-
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
141-
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
142-
143-
results.AddRange(propertyAttributes);
144-
}
147+
// Get attributes from the property
148+
var property = k.ContainingType.GetProperty(k.PropertyName);
149+
if (property != null)
150+
{
151+
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
152+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(property, inherit: true);
145153

146-
// Check constructors for parameters that match the property name
147-
// to handle record scenarios
148-
foreach (var constructor in k.ContainingType.GetConstructors())
149-
{
150-
// Look for parameter with matching name (case insensitive)
151-
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
152-
constructor.GetParameters(),
153-
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
154+
results.AddRange(propertyAttributes);
155+
}
154156

155-
if (parameter != null)
157+
// Check constructors for parameters that match the property name
158+
// to handle record scenarios
159+
foreach (var constructor in k.ContainingType.GetConstructors())
156160
{
157-
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
158-
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
161+
// Look for parameter with matching name (case insensitive)
162+
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
163+
constructor.GetParameters(),
164+
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));
165+
166+
if (parameter != null)
167+
{
168+
var paramAttributes = global::System.Reflection.CustomAttributeExtensions
169+
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);
159170

160-
results.AddRange(paramAttributes);
171+
results.AddRange(paramAttributes);
161172

162-
break;
173+
break;
174+
}
175+
}
176+
}
177+
else
178+
{
179+
// Get attributes from the type itself and its super types
180+
foreach (var attr in k.ContainingType.GetCustomAttributes(typeof(global::System.ComponentModel.DataAnnotations.ValidationAttribute), true))
181+
{
182+
if (attr is global::System.ComponentModel.DataAnnotations.ValidationAttribute validationAttribute)
183+
{
184+
results.Add(validationAttribute);
185+
}
163186
}
164187
}
165188

0 commit comments

Comments
 (0)