Skip to content

Commit 3b95995

Browse files
committed
Emit validation info for types with only type-level attributes
1 parent c1a86cc commit 3b95995

File tree

3 files changed

+383
-1
lines changed

3 files changed

+383
-1
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow
8282

8383
visitedTypes.Add(typeSymbol);
8484

85+
var hasValidationAttributes = typeSymbol.GetAttributes()
86+
.Any(attribute => attribute.AttributeClass != null &&
87+
attribute.AttributeClass.ImplementsValidationAttribute(
88+
wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute)));
89+
8590
// Extract validatable types discovered in base types of this type and add them to the top-level list.
8691
var current = typeSymbol.BaseType;
8792
var hasValidatableBaseType = false;
@@ -107,7 +112,7 @@ internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnow
107112
}
108113

109114
// No validatable members or derived types found, so we don't need to add this type.
110-
if (members.IsDefaultOrEmpty && !hasValidatableBaseType && !hasValidatableDerivedTypes)
115+
if (members.IsDefaultOrEmpty && !hasValidationAttributes && !hasValidatableBaseType && !hasValidatableDerivedTypes)
111116
{
112117
return false;
113118
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
2+
3+
// Licensed to the .NET Foundation under one or more agreements.
4+
// The .NET Foundation licenses this file to you under the MIT license.
5+
6+
using System;
7+
using System.ComponentModel.DataAnnotations;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.Validation;
10+
11+
namespace Microsoft.Extensions.Validation.GeneratorTests;
12+
13+
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
14+
{
15+
[Fact]
16+
public async Task CanValidateValidationAttributesOnClasses()
17+
{
18+
var source = """
19+
#pragma warning disable ASP0029
20+
21+
using System;
22+
using System.ComponentModel.DataAnnotations;
23+
using System.Collections.Generic;
24+
using System.Linq;
25+
using Microsoft.AspNetCore.Builder;
26+
using Microsoft.AspNetCore.Http;
27+
using Microsoft.Extensions.Validation;
28+
using Microsoft.AspNetCore.Routing;
29+
using Microsoft.Extensions.DependencyInjection;
30+
31+
var builder = WebApplication.CreateBuilder();
32+
33+
builder.Services.AddValidation();
34+
35+
var app = builder.Build();
36+
37+
app.Run();
38+
39+
[ValidatableType]
40+
[SumLimit]
41+
public class ComplexType : IPoint
42+
{
43+
[Range(0, 15)]
44+
public int X { get; set; } = 10;
45+
46+
[Range(0, 15)]
47+
public int Y { get; set; } = 10;
48+
49+
public NestedType ObjectProperty { get; set; } = new NestedType();
50+
}
51+
52+
// This class does not have any property-level validation attributes, but it has a class-level validation attribute.
53+
// Therefore, its type info should still be emitted in the generator output.
54+
[SumLimit]
55+
public class NestedType : IPoint
56+
{
57+
public int X { get; set; } = 10;
58+
59+
public int Y { get; set; } = 10;
60+
}
61+
62+
public interface IPoint
63+
{
64+
int X { get; }
65+
int Y { get; }
66+
}
67+
68+
public class SumLimitAttribute : ValidationAttribute
69+
{
70+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
71+
{
72+
if (value is IPoint point)
73+
{
74+
if (point.X + point.Y > 20)
75+
{
76+
return new ValidationResult($"Sum is too high");
77+
}
78+
}
79+
return ValidationResult.Success;
80+
}
81+
}
82+
""";
83+
await Verify(source, out var compilation);
84+
await VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
85+
{
86+
Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo));
87+
88+
await InvalidPropertyAttributeCheck_ProducesError_AndShortCirctuits(validatableTypeInfo);
89+
await ValidClassAttributeCheck_DoesNotProduceError(validatableTypeInfo);
90+
await InvalidClassAttributeCheck_ProducesError(validatableTypeInfo);
91+
await InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(validatableTypeInfo);
92+
93+
async Task InvalidPropertyAttributeCheck_ProducesError_AndShortCirctuits(IValidatableInfo validatableInfo)
94+
{
95+
var instance = Activator.CreateInstance(type);
96+
type.GetProperty("X")?.SetValue(instance, 16);
97+
type.GetProperty("Y")?.SetValue(instance, 0);
98+
99+
var context = new ValidateContext
100+
{
101+
ValidationOptions = validationOptions,
102+
ValidationContext = new ValidationContext(instance)
103+
};
104+
105+
await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
106+
107+
Assert.NotNull(context.ValidationErrors);
108+
var propertyAttributeError = Assert.Single(context.ValidationErrors);
109+
Assert.Equal("X", propertyAttributeError.Key);
110+
Assert.Equal("The field X must be between 0 and 15.", propertyAttributeError.Value.Single());
111+
}
112+
113+
async Task ValidClassAttributeCheck_DoesNotProduceError(IValidatableInfo validatableInfo)
114+
{
115+
var instance = Activator.CreateInstance(type);
116+
117+
var context = new ValidateContext
118+
{
119+
ValidationOptions = validationOptions,
120+
ValidationContext = new ValidationContext(instance)
121+
};
122+
123+
await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
124+
125+
Assert.Null(context.ValidationErrors);
126+
}
127+
128+
async Task InvalidClassAttributeCheck_ProducesError(IValidatableInfo validatableInfo)
129+
{
130+
var instance = Activator.CreateInstance(type);
131+
type.GetProperty("X")?.SetValue(instance, 11);
132+
type.GetProperty("Y")?.SetValue(instance, 12);
133+
134+
var context = new ValidateContext
135+
{
136+
ValidationOptions = validationOptions,
137+
ValidationContext = new ValidationContext(instance)
138+
};
139+
140+
await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
141+
142+
Assert.NotNull(context.ValidationErrors);
143+
var classAttributeError = Assert.Single(context.ValidationErrors);
144+
Assert.Equal(string.Empty, classAttributeError.Key);
145+
Assert.Equal("Sum is too high", classAttributeError.Value.Single());
146+
}
147+
148+
async Task InvalidNestedClassAttributeCheck_ProducesError_AndShortCircuits(IValidatableInfo validatableInfo)
149+
{
150+
var instance = Activator.CreateInstance(type);
151+
var objectPropertyInstance = type.GetProperty("ObjectProperty").GetValue(instance);
152+
objectPropertyInstance.GetType().GetProperty("X")?.SetValue(objectPropertyInstance, 11);
153+
objectPropertyInstance.GetType().GetProperty("Y")?.SetValue(objectPropertyInstance, 12);
154+
155+
var context = new ValidateContext
156+
{
157+
ValidationOptions = validationOptions,
158+
ValidationContext = new ValidationContext(instance)
159+
};
160+
161+
await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
162+
163+
Assert.NotNull(context.ValidationErrors);
164+
var classAttributeError = Assert.Single(context.ValidationErrors);
165+
Assert.Equal("ObjectProperty", classAttributeError.Key);
166+
Assert.Equal("Sum is too high", classAttributeError.Value.Single());
167+
}
168+
});
169+
}
170+
}

0 commit comments

Comments
 (0)