Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
96592d5
Add generic implementation for validations source generator
captainsafia Feb 27, 2025
1f4e615
Make validate methods async and fix AoT suppressions
captainsafia Feb 27, 2025
0b77a21
Update emitted code
captainsafia Feb 27, 2025
f928939
Remove deadcode
captainsafia Feb 28, 2025
75f3d1b
Clean up API shapes a bit
captainsafia Feb 28, 2025
3a7867a
Fix up RequiredAttribute handling
captainsafia Feb 28, 2025
5e25ec3
Add ValidatableContext and simplify API signature
captainsafia Feb 28, 2025
a4bf559
Support ValidationOptions and multiple resolvers
captainsafia Feb 28, 2025
c1970e5
Make ValidatableContext a setup entry
captainsafia Feb 28, 2025
c6f88cf
Move registration of filter to route handlers
captainsafia Mar 3, 2025
98cad73
Fix async for tests and IValidatableObject
captainsafia Mar 3, 2025
d69c466
Add doc comments
captainsafia Mar 3, 2025
ec8848f
Add more tests
captainsafia Mar 3, 2025
3196915
Enable PublicAPI analyzers and update public API
captainsafia Mar 3, 2025
18b4939
Add MaxDepth handling
captainsafia Mar 3, 2025
0d91665
Docs tweaks and package generator in shared framework
captainsafia Mar 3, 2025
bcc2529
Clean up tests
captainsafia Mar 3, 2025
c345a1b
Update for trimming
captainsafia Mar 3, 2025
889fd26
Harden parameter resolution check
captainsafia Mar 4, 2025
5bbd091
Switch to runtime-based resolution for ParameterInfo validations
captainsafia Mar 4, 2025
248e82a
Prune out uneeded types
captainsafia Mar 4, 2025
95b4d5d
Fix up ValidatableParameterInfo signature
captainsafia Mar 4, 2025
0df79fb
Make Validate methods virtual and support CustomValidationAttribute
captainsafia Mar 4, 2025
0195662
Fix up emitted code and use explicit namespaces
captainsafia Mar 5, 2025
0878c62
Fix up suppression for ValidationContext trimming
captainsafia Mar 5, 2025
8fc9d1b
Actually use attribute-based suppression
captainsafia Mar 5, 2025
03a3c06
Fix up suppression for trimming warnings
captainsafia Mar 5, 2025
fbe46cf
Benchmarks, more tests, some tweaks
captainsafia Mar 6, 2025
1ff9b04
More tests and add DisableValidationFilter
captainsafia Mar 6, 2025
669443e
Update API and add sample app
captainsafia Mar 9, 2025
4d776c0
Tweak more APIs
captainsafia Mar 9, 2025
7ee3e1a
Tweaks after API review
captainsafia Mar 10, 2025
cda8a39
Fix nullability handling in generator and add sample Http file
captainsafia Mar 10, 2025
bac0c95
Harden nullability and index checks in generator
captainsafia Mar 11, 2025
a976778
Don't generate TypeInfo for invalidatable types
captainsafia Mar 11, 2025
1490f4e
No-op code gen under more cases
captainsafia Mar 11, 2025
120c6ed
Address feedback
captainsafia Mar 11, 2025
b03f15a
Address feedback
captainsafia Mar 12, 2025
21c2418
Scrub out InterceptsLocationAttribute lines
captainsafia Mar 12, 2025
c063ce5
Exempt more types, add no-op tests
captainsafia Mar 12, 2025
144d56e
Use List<Type> directly for method
captainsafia Mar 12, 2025
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 @@ -76,6 +76,11 @@
Private="false"
OutputItemType="AspNetCoreAnalyzer"
ReferenceOutputAssembly="false" />

<ProjectReference Include="$(RepoRoot)src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.ValidationsGenerator\Microsoft.AspNetCore.Http.ValidationsGenerator.csproj"
Private="false"
OutputItemType="AspNetCoreAnalyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
Expand Down
55 changes: 55 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,59 @@
#nullable enable
abstract Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]!
abstract Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]!
Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string?
Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.set -> void
Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string?
Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver
Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.GetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo) -> Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo?
Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.GetValidatableTypeInfo(System.Type! type) -> Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo?
Microsoft.AspNetCore.Http.Validation.ValidatableContext
Microsoft.AspNetCore.Http.Validation.ValidatableContext.CurrentDepth.get -> int
Microsoft.AspNetCore.Http.Validation.ValidatableContext.CurrentDepth.set -> void
Microsoft.AspNetCore.Http.Validation.ValidatableContext.Prefix.get -> string!
Microsoft.AspNetCore.Http.Validation.ValidatableContext.Prefix.set -> void
Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidatableContext() -> void
Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext?
Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationContext.set -> void
Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationErrors.get -> System.Collections.Generic.Dictionary<string!, string![]!>?
Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationErrors.set -> void
Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationOptions.get -> Microsoft.AspNetCore.Http.Validation.ValidationOptions!
Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationOptions.set -> void
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.DisplayName.get -> string!
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.IsEnumerable.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.IsNullable.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.IsRequired.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.Name.get -> string!
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.ParameterType.get -> System.Type!
Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName, bool isNullable, bool isRequired, bool isEnumerable) -> void
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.DeclaringType.get -> System.Type!
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.DisplayName.get -> string!
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.HasValidatableType.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.IsEnumerable.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.IsNullable.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.IsRequired.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.Name.get -> string!
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.PropertyType.get -> System.Type!
Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.ValidatablePropertyInfo(System.Type! declaringType, System.Type! propertyType, string! name, string! displayName, bool isEnumerable, bool isNullable, bool isRequired, bool hasValidatableType) -> void
Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute
Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute.ValidatableTypeAttribute() -> void
Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.IsIValidatableObject.get -> bool
Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.Members.get -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo!>!
Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.Type.get -> System.Type!
Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidatableSubTypes.get -> System.Collections.Generic.IReadOnlyList<System.Type!>?
Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidatableTypeInfo(System.Type! type, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo!>! members, bool implementsIValidatableObject, System.Collections.Generic.IReadOnlyList<System.Type!>? validatableSubTypes = null) -> void
Microsoft.AspNetCore.Http.Validation.ValidationOptions
Microsoft.AspNetCore.Http.Validation.ValidationOptions.MaxDepth.get -> int
Microsoft.AspNetCore.Http.Validation.ValidationOptions.MaxDepth.set -> void
Microsoft.AspNetCore.Http.Validation.ValidationOptions.Resolvers.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver!>!
Microsoft.AspNetCore.Http.Validation.ValidationOptions.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo? validatableParameterInfo) -> bool
Microsoft.AspNetCore.Http.Validation.ValidationOptions.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo? validatableTypeInfo) -> bool
Microsoft.AspNetCore.Http.Validation.ValidationOptions.ValidationOptions() -> void
Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions
static Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Http.Validation.ValidationOptions!>? configureOptions = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
virtual Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.Validate(object? value, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.Validate(object! obj, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.Validate(object? value, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task!
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;

namespace Microsoft.AspNetCore.Http.Validation;

/// <summary>
/// Provides an interface for resolving the validation information associated
/// with a given <seealso cref="Type"/> or <seealso cref="ParameterInfo"/>.
/// </summary>
public interface IValidatableInfoResolver
{
/// <summary>
/// Gets validation type information for the specified type.
/// </summary>
/// <param name="type">The type to get validation information for.</param>
/// <returns>The validation type information, or null if the type is not validatable.</returns>
ValidatableTypeInfo? GetValidatableTypeInfo(Type type);

/// <summary>
/// Gets validation parameter information for the specified parameter.
/// </summary>
/// <param name="parameterInfo">The parameter information to get validation for.</param>
/// <returns>The validation parameter information, or null if the parameter is not validatable.</returns>
ValidatableParameterInfo? GetValidatableParameterInfo(ParameterInfo parameterInfo);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Reflection;

namespace Microsoft.AspNetCore.Http.Validation;

internal class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver
{
public ValidatableParameterInfo? GetValidatableParameterInfo(ParameterInfo parameterInfo)
{
Debug.Assert(parameterInfo.Name != null, "Parameter must have name");
var validationAttributes = parameterInfo
.GetCustomAttributes<ValidationAttribute>()
.ToArray();
return new RuntimeValidatableParameterInfo(
parameterType: parameterInfo.ParameterType,
name: parameterInfo.Name,
displayName: GetDisplayName(parameterInfo),
isNullable: IsNullable(parameterInfo),
isRequired: validationAttributes.Any(a => a is RequiredAttribute),
isEnumerable: IsEnumerable(parameterInfo),
validationAttributes: validationAttributes
);
}

private static bool IsNullable(ParameterInfo parameterInfo)
{
if (parameterInfo.ParameterType.IsValueType)
{
return false;
}

if (parameterInfo.ParameterType.IsGenericType &&
parameterInfo.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return true;
}

return false;
}

private static string GetDisplayName(ParameterInfo parameterInfo)
{
var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
{
return displayAttribute.Name ?? parameterInfo.Name!;
}

return parameterInfo.Name!;
}

private static bool IsEnumerable(ParameterInfo parameterInfo)
{
if (parameterInfo.ParameterType.IsGenericType &&
parameterInfo.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return true;
}

if (parameterInfo.ParameterType.IsArray)
{
return true;
}

return false;
}

public ValidatableTypeInfo? GetValidatableTypeInfo(Type type)
{
return null;
}

private class RuntimeValidatableParameterInfo(
Type parameterType,
string name,
string displayName,
bool isNullable,
bool isRequired,
bool isEnumerable,
ValidationAttribute[] validationAttributes) :
ValidatableParameterInfo(parameterType, name, displayName, isNullable, isRequired, isEnumerable)
{
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;

private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
}
}
80 changes: 80 additions & 0 deletions src/Http/Http.Abstractions/src/Validation/ValidatableContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;

namespace Microsoft.AspNetCore.Http.Validation;

/// <summary>
/// Represents the context for validating a validatable object.
/// </summary>
public sealed class ValidatableContext
{
/// <summary>
/// Gets or sets the validation context used for validating objects that implement <see cref="IValidatableObject"/> or have <see cref="ValidationAttribute"/>.
/// This context provides access to service provider and other validation metadata.
/// </summary>
public ValidationContext? ValidationContext { get; set; }

/// <summary>
/// Gets or sets the prefix used to identify the current object being validated in a complex object graph.
/// This is used to build property paths in validation error messages (e.g., "Customer.Address.Street").
/// </summary>
public string Prefix { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the validation options that control validation behavior,
/// including validation depth limits and resolver registration.
/// </summary>
public required ValidationOptions ValidationOptions { get; set; }

/// <summary>
/// Gets or sets the dictionary of validation errors collected during validation.
/// Keys are property names or paths, and values are arrays of error messages.
/// This dictionary is lazily initialized when the first validation error is added.
/// </summary>
public Dictionary<string, string[]>? ValidationErrors { get; set; }

/// <summary>
/// Gets or sets the current depth in the validation hierarchy.
/// This is used to prevent stack overflows from circular references.
/// </summary>
public int CurrentDepth { get; set; }

internal void AddValidationError(string key, string[] error)
{
ValidationErrors ??= [];

ValidationErrors[key] = error;
}

internal void AddOrExtendValidationErrors(string key, string[] errors)
{
ValidationErrors ??= [];

if (ValidationErrors.TryGetValue(key, out var existingErrors))
{
ValidationErrors[key] = new string[existingErrors.Length + errors.Length];
existingErrors.CopyTo(ValidationErrors[key], 0);
errors.CopyTo(ValidationErrors[key], existingErrors.Length);
}
else
{
ValidationErrors[key] = errors;
}
}

internal void AddOrExtendValidationError(string key, string error)
{
ValidationErrors ??= [];

if (ValidationErrors.TryGetValue(key, out var existingErrors) && !existingErrors.Contains(error))
{
ValidationErrors[key] = [.. existingErrors, error];
}
else
{
ValidationErrors[key] = [error];
}
}
}
Loading
Loading