Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@
<ValidationMessage For="@(() => order.CustomerDetails.Email)" />
</div>

<h5>Payment Address</h5>
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="mb-3 col-sm-8">
<label for="paymentStreet" class="form-label">Street</label>
<InputText id="paymentStreet" @bind-Value="order.CustomerDetails.PaymentAddress.Street" class="form-control" />
<ValidationMessage For="@(() => order.CustomerDetails.PaymentAddress.Street)" />
</div>
<div class="mb-3 col-sm">
<label for="paymentZipCode" class="form-label">Zip Code</label>
<InputText id="paymentZipCode" @bind-Value="order.CustomerDetails.PaymentAddress.ZipCode" class="form-control" />
<ValidationMessage For="@(() => order.CustomerDetails.PaymentAddress.ZipCode)" />
</div>
</div>
</div>
</div>

<h5>Shipping Address</h5>
<div class="card mb-3">
<div class="card-body">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Validation;

namespace BasicTestApp.ValidationModels;

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

public AddressModel PaymentAddress { get; set; } = new AddressModel();

#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.
[SkipValidation]
#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.
public AddressModel ShippingAddress { get; set; } = new AddressModel();

}
19 changes: 19 additions & 0 deletions src/Shared/RoslynUtils/SymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ public static bool HasAttribute(this ImmutableArray<AttributeData> attributes, I
return attributes.TryGetAttribute(attributeType, out _);
}

public static bool HasAttribute(this ITypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
{
var current = typeSymbol;

while (current is not null)
{
if (current.GetAttributes().Any(attr =>
attr.AttributeClass is not null &&
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol)))
{
return true;
}

current = current.BaseType;
}

return false;
}

public static bool TryGetAttribute(this ImmutableArray<AttributeData> attributes, INamedTypeSymbol attributeType, [NotNullWhen(true)] out AttributeData? matchedAttribute)
{
foreach (var attributeData in attributes)
Expand Down
2 changes: 2 additions & 0 deletions src/Shared/RoslynUtils/WellKnownTypeData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public enum WellKnownType
System_ComponentModel_DataAnnotations_ValidationAttribute,
System_ComponentModel_DataAnnotations_RequiredAttribute,
System_ComponentModel_DataAnnotations_CustomValidationAttribute,
Microsoft_Extensions_Validation_SkipValidationAttribute,
System_Type,
}

Expand Down Expand Up @@ -246,6 +247,7 @@ public enum WellKnownType
"System.ComponentModel.DataAnnotations.ValidationAttribute",
"System.ComponentModel.DataAnnotations.RequiredAttribute",
"System.ComponentModel.DataAnnotations.CustomValidationAttribute",
"Microsoft.Extensions.Validation.SkipValidationAttribute",
"System.Type",
];
}
11 changes: 11 additions & 0 deletions src/Validation/gen/Extensions/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;

Expand Down Expand Up @@ -164,4 +165,14 @@ internal static bool IsJsonIgnoredProperty(this IPropertySymbol property, INamed
attr.AttributeClass is not null &&
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, jsonIgnoreAttributeSymbol));
}

internal static bool IsSkippedValidationProperty(this IPropertySymbol property, INamedTypeSymbol skipValidationAttributeSymbol)
{
return property.HasAttribute(skipValidationAttributeSymbol) || property.Type.HasAttribute(skipValidationAttributeSymbol);
}

internal static bool IsSkippedValidationParameter(this IParameterSymbol parameter, INamedTypeSymbol skipValidationAttributeSymbol)
{
return parameter.HasAttribute(skipValidationAttributeSymbol) || parameter.Type.HasAttribute(skipValidationAttributeSymbol);
}
}
23 changes: 23 additions & 0 deletions src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Analyzers.Infrastructure;
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
using Microsoft.CodeAnalysis;
Expand All @@ -30,6 +31,8 @@ internal ImmutableArray<ValidatableType> ExtractValidatableTypes(IInvocationOper
WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata);
var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get(
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
var skipValidationAttributeSymbol = wellKnownTypes.Get(
WellKnownTypeData.WellKnownType.Microsoft_Extensions_Validation_SkipValidationAttribute);

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

// Skip method parameter if it or its type are annotated with SkipValidationAttribute
if (parameter.IsSkippedValidationParameter(skipValidationAttributeSymbol))
{
continue;
}

_ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
}
return [.. validatableTypes];
Expand Down Expand Up @@ -122,6 +131,8 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute);
var jsonIgnoreAttributeSymbol = wellKnownTypes.Get(
WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonIgnoreAttribute);
var skipValidationAttributeSymbol = wellKnownTypes.Get(
WellKnownTypeData.WellKnownType.Microsoft_Extensions_Validation_SkipValidationAttribute);

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

// Skip primary constructor parameter if it or its type are annotated with SkipValidationAttribute
if (parameter.IsSkippedValidationParameter(skipValidationAttributeSymbol))
{
continue;
}

// Skip properties that are not accessible from generated code
if (correspondingProperty.DeclaredAccessibility is not Accessibility.Public)
{
Expand Down Expand Up @@ -218,6 +235,12 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
continue;
}

// Skip property if it or its type are annotated with SkipValidationAttribute
if (member.IsSkippedValidationProperty(skipValidationAttributeSymbol))
{
continue;
}

var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);

Expand Down
2 changes: 2 additions & 0 deletions src/Validation/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Microsoft.Extensions.Validation.IValidatableInfo.ValidateAsync(object? value, Mi
Microsoft.Extensions.Validation.IValidatableInfoResolver
Microsoft.Extensions.Validation.IValidatableInfoResolver.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) -> bool
Microsoft.Extensions.Validation.IValidatableInfoResolver.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) -> bool
Microsoft.Extensions.Validation.SkipValidationAttribute
Microsoft.Extensions.Validation.SkipValidationAttribute.SkipValidationAttribute() -> void
Microsoft.Extensions.Validation.ValidatableParameterInfo
Microsoft.Extensions.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName) -> void
Microsoft.Extensions.Validation.ValidatablePropertyInfo
Expand Down
8 changes: 8 additions & 0 deletions src/Validation/src/RuntimeValidatableParameterInfoResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNull
throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name.");
}

// Skip method parameter if it or its type are annotated with SkipValidationAttribute.
if (parameterInfo.GetCustomAttribute<SkipValidationAttribute>() != null ||
parameterInfo.ParameterType.GetCustomAttribute<SkipValidationAttribute>() != null)
{
validatableInfo = null;
return false;
}

var validationAttributes = parameterInfo
.GetCustomAttributes<ValidationAttribute>()
.ToArray();
Expand Down
19 changes: 19 additions & 0 deletions src/Validation/src/SkipValidationAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.Validation;

/// <summary>
/// Indicates that a property, parameter, or a type should not be validated.
/// When applied to a property, validation is skipped for that property.
/// When applied to a parameter, validation is skipped for that parameter.
/// When applied to a type, validation is skipped for all properties and parameters of that type.
/// This includes skipping validation of nested properties for complex types.
/// </summary>
[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class SkipValidationAttribute : Attribute
{
}
Loading
Loading