Skip to content

Commit 9a3d379

Browse files
authored
Add SkipValidationAttribute to Microsoft.Extensions.Validation (#63103)
1 parent 58c3b48 commit 9a3d379

15 files changed

+1193
-10
lines changed

src/Components/test/testassets/Components.TestServer/RazorComponents/ComplexValidationComponent.razor

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@
3838
<ValidationMessage For="@(() => order.CustomerDetails.Email)" />
3939
</div>
4040

41+
<h5>Payment Address</h5>
42+
<div class="card mb-3">
43+
<div class="card-body">
44+
<div class="row">
45+
<div class="mb-3 col-sm-8">
46+
<label for="paymentStreet" class="form-label">Street</label>
47+
<InputText id="paymentStreet" @bind-Value="order.CustomerDetails.PaymentAddress.Street" class="form-control" />
48+
<ValidationMessage For="@(() => order.CustomerDetails.PaymentAddress.Street)" />
49+
</div>
50+
<div class="mb-3 col-sm">
51+
<label for="paymentZipCode" class="form-label">Zip Code</label>
52+
<InputText id="paymentZipCode" @bind-Value="order.CustomerDetails.PaymentAddress.ZipCode" class="form-control" />
53+
<ValidationMessage For="@(() => order.CustomerDetails.PaymentAddress.ZipCode)" />
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
4159
<h5>Shipping Address</h5>
4260
<div class="card mb-3">
4361
<div class="card-body">

src/Components/test/testassets/Components.TestServer/RazorComponents/ValidationModels/CustomerModel.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel.DataAnnotations;
5+
using Microsoft.Extensions.Validation;
56

67
namespace BasicTestApp.ValidationModels;
78

@@ -14,5 +15,11 @@ public class CustomerModel
1415
[EmailAddress(ErrorMessage = "Invalid Email Address.")]
1516
public string Email { get; set; }
1617

18+
public AddressModel PaymentAddress { get; set; } = new AddressModel();
19+
20+
#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.
21+
[SkipValidation]
22+
#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
1723
public AddressModel ShippingAddress { get; set; } = new AddressModel();
24+
1825
}

src/Shared/RoslynUtils/SymbolExtensions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ public static bool HasAttribute(this ImmutableArray<AttributeData> attributes, I
6767
return attributes.TryGetAttribute(attributeType, out _);
6868
}
6969

70+
public static bool HasAttribute(this ITypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
71+
{
72+
var current = typeSymbol;
73+
74+
while (current is not null)
75+
{
76+
if (current.GetAttributes().Any(attr =>
77+
attr.AttributeClass is not null &&
78+
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol)))
79+
{
80+
return true;
81+
}
82+
83+
current = current.BaseType;
84+
}
85+
86+
return false;
87+
}
88+
7089
public static bool TryGetAttribute(this ImmutableArray<AttributeData> attributes, INamedTypeSymbol attributeType, [NotNullWhen(true)] out AttributeData? matchedAttribute)
7190
{
7291
foreach (var attributeData in attributes)

src/Shared/RoslynUtils/WellKnownTypeData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ public enum WellKnownType
124124
System_ComponentModel_DataAnnotations_ValidationAttribute,
125125
System_ComponentModel_DataAnnotations_RequiredAttribute,
126126
System_ComponentModel_DataAnnotations_CustomValidationAttribute,
127+
Microsoft_Extensions_Validation_SkipValidationAttribute,
127128
System_Type,
128129
}
129130

@@ -246,6 +247,7 @@ public enum WellKnownType
246247
"System.ComponentModel.DataAnnotations.ValidationAttribute",
247248
"System.ComponentModel.DataAnnotations.RequiredAttribute",
248249
"System.ComponentModel.DataAnnotations.CustomValidationAttribute",
250+
"Microsoft.Extensions.Validation.SkipValidationAttribute",
249251
"System.Type",
250252
];
251253
}

src/Validation/gen/Extensions/ITypeSymbolExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Immutable;
55
using System.Linq;
6+
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
67
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
78
using Microsoft.CodeAnalysis;
89

@@ -164,4 +165,14 @@ internal static bool IsJsonIgnoredProperty(this IPropertySymbol property, INamed
164165
attr.AttributeClass is not null &&
165166
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
166167
}
168+
169+
internal static bool IsSkippedValidationProperty(this IPropertySymbol property, INamedTypeSymbol skipValidationAttributeSymbol)
170+
{
171+
return property.HasAttribute(skipValidationAttributeSymbol) || property.Type.HasAttribute(skipValidationAttributeSymbol);
172+
}
173+
174+
internal static bool IsSkippedValidationParameter(this IParameterSymbol parameter, INamedTypeSymbol skipValidationAttributeSymbol)
175+
{
176+
return parameter.HasAttribute(skipValidationAttributeSymbol) || parameter.Type.HasAttribute(skipValidationAttributeSymbol);
177+
}
167178
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Immutable;
66
using System.Linq;
77
using Microsoft.AspNetCore.Analyzers.Infrastructure;
8+
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
89
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
910
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
1011
using Microsoft.CodeAnalysis;
@@ -30,6 +31,8 @@ internal ImmutableArray<ValidatableType> ExtractValidatableTypes(IInvocationOper
3031
WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
3132
var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get(
3233
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
34+
var skipValidationAttributeSymbol = wellKnownTypes.Get(
35+
WellKnownTypeData.WellKnownType.Microsoft_Extensions_Validation_SkipValidationAttribute);
3336

3437
var validatableTypes = new HashSet<ValidatableType>(ValidatableTypeComparer.Instance);
3538
List<ITypeSymbol> visitedTypes = [];
@@ -42,6 +45,12 @@ internal ImmutableArray<ValidatableType> ExtractValidatableTypes(IInvocationOper
4245
continue;
4346
}
4447

48+
// Skip method parameter if it or its type are annotated with SkipValidationAttribute
49+
if (parameter.IsSkippedValidationParameter(skipValidationAttributeSymbol))
50+
{
51+
continue;
52+
}
53+
4554
_ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
4655
}
4756
return [.. validatableTypes];
@@ -122,6 +131,8 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
122131
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
123132
var jsonIgnoreAttributeSymbol = wellKnownTypes.Get(
124133
WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonIgnoreAttribute);
134+
var skipValidationAttributeSymbol = wellKnownTypes.Get(
135+
WellKnownTypeData.WellKnownType.Microsoft_Extensions_Validation_SkipValidationAttribute);
125136

126137
// Special handling for record types to extract properties from
127138
// the primary constructor.
@@ -156,6 +167,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
156167
continue;
157168
}
158169

170+
// Skip primary constructor parameter if it or its type are annotated with SkipValidationAttribute
171+
if (parameter.IsSkippedValidationParameter(skipValidationAttributeSymbol))
172+
{
173+
continue;
174+
}
175+
159176
// Skip properties that are not accessible from generated code
160177
if (correspondingProperty.DeclaredAccessibility is not Accessibility.Public)
161178
{
@@ -218,6 +235,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
218235
continue;
219236
}
220237

238+
// Skip property if it or its type are annotated with SkipValidationAttribute
239+
if (member.IsSkippedValidationProperty(skipValidationAttributeSymbol))
240+
{
241+
continue;
242+
}
243+
221244
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
222245
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
223246

src/Validation/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Microsoft.Extensions.Validation.IValidatableInfo.ValidateAsync(object? value, Mi
55
Microsoft.Extensions.Validation.IValidatableInfoResolver
66
Microsoft.Extensions.Validation.IValidatableInfoResolver.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) -> bool
77
Microsoft.Extensions.Validation.IValidatableInfoResolver.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) -> bool
8+
Microsoft.Extensions.Validation.SkipValidationAttribute
9+
Microsoft.Extensions.Validation.SkipValidationAttribute.SkipValidationAttribute() -> void
810
Microsoft.Extensions.Validation.ValidatableParameterInfo
911
Microsoft.Extensions.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName) -> void
1012
Microsoft.Extensions.Validation.ValidatablePropertyInfo

src/Validation/src/RuntimeValidatableParameterInfoResolver.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNull
2828
throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name.");
2929
}
3030

31+
// Skip method parameter if it or its type are annotated with SkipValidationAttribute.
32+
if (parameterInfo.GetCustomAttribute<SkipValidationAttribute>() != null ||
33+
parameterInfo.ParameterType.GetCustomAttribute<SkipValidationAttribute>() != null)
34+
{
35+
validatableInfo = null;
36+
return false;
37+
}
38+
3139
var validationAttributes = parameterInfo
3240
.GetCustomAttributes<ValidationAttribute>()
3341
.ToArray();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Microsoft.Extensions.Validation;
7+
8+
/// <summary>
9+
/// Indicates that a property, parameter, or a type should not be validated.
10+
/// When applied to a property, validation is skipped for that property.
11+
/// When applied to a parameter, validation is skipped for that parameter.
12+
/// When applied to a type, validation is skipped for all properties and parameters of that type.
13+
/// This includes skipping validation of nested properties for complex types.
14+
/// </summary>
15+
[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
16+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
17+
public sealed class SkipValidationAttribute : Attribute
18+
{
19+
}

0 commit comments

Comments
 (0)