From b65017b3c7eaa4e5e85cc722e13ea50fd6452f2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 15:56:12 +0000 Subject: [PATCH 1/7] Initial plan for issue From 0f681b4e11032ab339694517e2c67d06d607c2ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 16:12:42 +0000 Subject: [PATCH 2/7] Created project structure and moved source files Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com> --- eng/ProjectReferences.props | 1 + ...rosoft.AspNetCore.Http.Abstractions.csproj | 1 + .../ForwardingValidationDirectives.cs | 18 + ...spNetCore.Http.ValidationsGenerator.csproj | 19 +- .../ValidationsGeneratorForwarding.cs | 20 + ...icrosoft.AspNetCore.Http.Extensions.csproj | 1 + src/Validation/Validations.slnf | 11 + src/Validation/build.cmd | 4 + src/Validation/build.sh | 6 + .../Emitters/ValidationsGenerator.Emitter.cs | 222 +++++ .../gen/Extensions/ISymbolExtensions.cs | 35 + .../gen/Extensions/ITypeSymbolExtensions.cs | 141 ++++ .../IncrementalValuesProviderExtensions.cs | 112 +++ ...ons.Validation.ValidationsGenerator.csproj | 34 + src/Validation/gen/Models/RequiredSymbols.cs | 25 + .../gen/Models/ValidatableProperty.cs | 15 + src/Validation/gen/Models/ValidatableType.cs | 12 + .../gen/Models/ValidatableTypeComparer.cs | 30 + .../gen/Models/ValidationAttribute.cs | 14 + .../ValidationsGenerator.AddValidation.cs | 38 + .../ValidationsGenerator.AttributeParser.cs | 31 + .../ValidationsGenerator.EndpointsParser.cs | 50 ++ .../ValidationsGenerator.TypesParser.cs | 212 +++++ src/Validation/gen/ValidationsGenerator.cs | 47 ++ src/Validation/src/IValidatableInfo.cs | 22 + .../src/IValidatableInfoResolver.cs | 33 + .../Microsoft.Extensions.Validation.csproj | 22 + src/Validation/src/PublicAPI.Shipped.txt | 1 + src/Validation/src/PublicAPI.Unshipped.txt | 40 + ...RuntimeValidatableParameterInfoResolver.cs | 111 +++ src/Validation/src/TypeExtensions.cs | 134 +++ .../src/ValidatableParameterInfo.cs | 139 +++ src/Validation/src/ValidatablePropertyInfo.cs | 173 ++++ .../src/ValidatableTypeAttribute.cs | 16 + src/Validation/src/ValidatableTypeInfo.cs | 133 +++ src/Validation/src/ValidateContext.cs | 100 +++ src/Validation/src/ValidationOptions.cs | 75 ++ .../ValidationServiceCollectionExtensions.cs | 34 + ...crosoft.Extensions.Validation.Tests.csproj | 12 + ...meValidatableParameterInfoResolverTests.cs | 191 +++++ .../ValidatableInfoResolverTests.cs | 223 +++++ .../ValidatableParameterInfoTests.cs | 432 ++++++++++ .../ValidatableTypeInfoTests.cs | 790 ++++++++++++++++++ ...lidation.ValidationsGenerator.Tests.csproj | 22 + .../ValidationsGenerator.ComplexType.cs | 374 +++++++++ ...ValidationsGenerator.IValidatableObject.cs | 208 +++++ ...ValidationsGenerator.MultipleNamespaces.cs | 126 +++ .../ValidationsGenerator.NoOp.cs | 178 ++++ .../ValidationsGenerator.Parameters.cs | 100 +++ .../ValidationsGenerator.Parsable.cs | 122 +++ .../ValidationsGenerator.Polymorphism.cs | 202 +++++ .../ValidationsGenerator.RecordType.cs | 371 ++++++++ .../ValidationsGenerator.Recursion.cs | 158 ++++ .../ValidationsGenerator.ValidatableType.cs | 381 +++++++++ .../ValidationsGeneratorTestBase.cs | 586 +++++++++++++ ...ypes#ValidatableInfoResolver.g.verified.cs | 244 ++++++ ...ject#ValidatableInfoResolver.g.verified.cs | 195 +++++ ...aces#ValidatableInfoResolver.g.verified.cs | 175 ++++ ...ters#ValidatableInfoResolver.g.verified.cs | 193 +++++ ...ypes#ValidatableInfoResolver.g.verified.cs | 225 +++++ ...ypes#ValidatableInfoResolver.g.verified.cs | 271 ++++++ ...ypes#ValidatableInfoResolver.g.verified.cs | 166 ++++ ...ties#ValidatableInfoResolver.g.verified.cs | 208 +++++ ...bute#ValidatableInfoResolver.g.verified.cs | 238 ++++++ ...ypes#ValidatableInfoResolver.g.verified.cs | 160 ++++ 65 files changed, 8667 insertions(+), 16 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs create mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs create mode 100644 src/Validation/Validations.slnf create mode 100644 src/Validation/build.cmd create mode 100755 src/Validation/build.sh create mode 100644 src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs create mode 100644 src/Validation/gen/Extensions/ISymbolExtensions.cs create mode 100644 src/Validation/gen/Extensions/ITypeSymbolExtensions.cs create mode 100644 src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs create mode 100644 src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj create mode 100644 src/Validation/gen/Models/RequiredSymbols.cs create mode 100644 src/Validation/gen/Models/ValidatableProperty.cs create mode 100644 src/Validation/gen/Models/ValidatableType.cs create mode 100644 src/Validation/gen/Models/ValidatableTypeComparer.cs create mode 100644 src/Validation/gen/Models/ValidationAttribute.cs create mode 100644 src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs create mode 100644 src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs create mode 100644 src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs create mode 100644 src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs create mode 100644 src/Validation/gen/ValidationsGenerator.cs create mode 100644 src/Validation/src/IValidatableInfo.cs create mode 100644 src/Validation/src/IValidatableInfoResolver.cs create mode 100644 src/Validation/src/Microsoft.Extensions.Validation.csproj create mode 100644 src/Validation/src/PublicAPI.Shipped.txt create mode 100644 src/Validation/src/PublicAPI.Unshipped.txt create mode 100644 src/Validation/src/RuntimeValidatableParameterInfoResolver.cs create mode 100644 src/Validation/src/TypeExtensions.cs create mode 100644 src/Validation/src/ValidatableParameterInfo.cs create mode 100644 src/Validation/src/ValidatablePropertyInfo.cs create mode 100644 src/Validation/src/ValidatableTypeAttribute.cs create mode 100644 src/Validation/src/ValidatableTypeInfo.cs create mode 100644 src/Validation/src/ValidateContext.cs create mode 100644 src/Validation/src/ValidationOptions.cs create mode 100644 src/Validation/src/ValidationServiceCollectionExtensions.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ComplexType.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.IValidatableObject.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.MultipleNamespaces.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.NoOp.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parameters.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parsable.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Polymorphism.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.RecordType.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Recursion.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ValidatableType.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGeneratorTestBase.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 58eb2c5d2d61..6efcc28f2f2b 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -38,6 +38,7 @@ + diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index d383efa6a20e..3196824956cf 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -21,6 +21,7 @@ Microsoft.AspNetCore.Http.HttpResponse + diff --git a/src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs b/src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs new file mode 100644 index 000000000000..0d7c3c48d63d --- /dev/null +++ b/src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Forward validation-related types from Microsoft.Extensions.Validation +// to maintain backward compatibility + +using Microsoft.Extensions.DependencyInjection; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.IValidatableInfo))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.IValidatableInfoResolver))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatableParameterInfo))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatablePropertyInfo))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatableTypeAttribute))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatableTypeInfo))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidateContext))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidationOptions))] +[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions))] \ No newline at end of file diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj index a55218bb4185..d666ac8015e6 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj @@ -11,25 +11,12 @@ - - + - - - - - - - - - - - - - - + + diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs new file mode 100644 index 000000000000..c8a432c4a00a --- /dev/null +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Validation.ValidationsGenerator; + +// This class forwards to the new generator implementation +namespace Microsoft.AspNetCore.Http.ValidationsGenerator; + +public sealed partial class ValidationsGenerator : IIncrementalGenerator +{ + private static readonly Microsoft.Extensions.Validation.ValidationsGenerator.ValidationsGenerator _forwardingGenerator = + new Microsoft.Extensions.Validation.ValidationsGenerator.ValidationsGenerator(); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + _forwardingGenerator.Initialize(context); + } +} \ No newline at end of file diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index aa9645a5faa1..e1560b3d4428 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -43,6 +43,7 @@ + diff --git a/src/Validation/Validations.slnf b/src/Validation/Validations.slnf new file mode 100644 index 000000000000..51c95f704d46 --- /dev/null +++ b/src/Validation/Validations.slnf @@ -0,0 +1,11 @@ +{ + "solution": { + "path": "..\\..\\AspNetCore.sln", + "projects": [ + "src\\Validation\\src\\Microsoft.Extensions.Validation.csproj", + "src\\Validation\\test\\Microsoft.Extensions.Validation.Tests\\Microsoft.Extensions.Validation.Tests.csproj", + "src\\Validation\\gen\\Microsoft.Extensions.Validation.ValidationsGenerator.csproj", + "src\\Validation\\test\\Microsoft.Extensions.Validation.ValidationsGenerator.Tests\\Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj" + ] + } +} \ No newline at end of file diff --git a/src/Validation/build.cmd b/src/Validation/build.cmd new file mode 100644 index 000000000000..7be8285e8a42 --- /dev/null +++ b/src/Validation/build.cmd @@ -0,0 +1,4 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. + +call %RepoRoot%\eng\build.cmd -projects %RepoRoot%\src\Validation\**\*.csproj %* \ No newline at end of file diff --git a/src/Validation/build.sh b/src/Validation/build.sh new file mode 100755 index 000000000000..2663e52d431d --- /dev/null +++ b/src/Validation/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +"$DIR/../../eng/build.sh" --projects "$DIR/**/*.csproj" "$@" \ No newline at end of file diff --git a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs new file mode 100644 index 000000000000..729215931d93 --- /dev/null +++ b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System.Text; +using Microsoft.CodeAnalysis.CSharp; +using System.IO; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +public sealed partial class ValidationsGenerator : IIncrementalGenerator +{ + public static string GeneratedCodeConstructor => $@"global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(ValidationsGenerator).Assembly.FullName}"", ""{typeof(ValidationsGenerator).Assembly.GetName().Version}"")"; + public static string GeneratedCodeAttribute => $"[{GeneratedCodeConstructor}]"; + + internal static void Emit(SourceProductionContext context, (InterceptableLocation? AddValidation, ImmutableArray ValidatableTypes) emitInputs) + { + if (emitInputs.AddValidation is null) + { + // Avoid generating code if no AddValidation call was found. + return; + } + var source = Emit(emitInputs.AddValidation, emitInputs.ValidatableTypes); + context.AddSource("ValidatableInfoResolver.g.cs", SourceText.From(source, Encoding.UTF8)); + } + + private static string Emit(InterceptableLocation addValidation, ImmutableArray validatableTypes) => $$""" +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + {{GeneratedCodeAttribute}} + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + {{GeneratedCodeAttribute}} + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + {{GeneratedCodeAttribute}} + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + {{GeneratedCodeAttribute}} + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; +{{EmitTypeChecks(validatableTypes)}} + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + {{GeneratedCodeAttribute}} + file static class GeneratedServiceCollectionExtensions + { + {{addValidation.GetInterceptsLocationAttributeSyntax()}} + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + {{GeneratedCodeAttribute}} + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} +"""; + + private static string EmitTypeChecks(ImmutableArray validatableTypes) + { + var sw = new StringWriter(); + var cw = new CodeWriter(sw, baseIndent: 3); + foreach (var validatableType in validatableTypes) + { + var typeName = validatableType.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + cw.WriteLine($"if (type == typeof({typeName}))"); + cw.StartBlock(); + cw.WriteLine($"validatableInfo = new GeneratedValidatableTypeInfo("); + cw.Indent++; + cw.WriteLine($"type: typeof({typeName}),"); + if (validatableType.Members.IsDefaultOrEmpty) + { + cw.WriteLine("members: []"); + } + else + { + cw.WriteLine("members: ["); + cw.Indent++; + foreach (var member in validatableType.Members) + { + EmitValidatableMemberForCreate(member, cw); + } + cw.Indent--; + cw.WriteLine("]"); + } + cw.Indent--; + cw.WriteLine(");"); + cw.WriteLine("return true;"); + cw.EndBlock(); + } + return sw.ToString(); + } + + private static void EmitValidatableMemberForCreate(ValidatableProperty member, CodeWriter cw) + { + cw.WriteLine("new GeneratedValidatablePropertyInfo("); + cw.Indent++; + cw.WriteLine($"containingType: typeof({member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}),"); + cw.WriteLine($"propertyType: typeof({member.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}),"); + cw.WriteLine($"name: \"{member.Name}\","); + cw.WriteLine($"displayName: \"{member.DisplayName}\""); + cw.Indent--; + cw.WriteLine("),"); + } +} diff --git a/src/Validation/gen/Extensions/ISymbolExtensions.cs b/src/Validation/gen/Extensions/ISymbolExtensions.cs new file mode 100644 index 000000000000..2e7d72d852fc --- /dev/null +++ b/src/Validation/gen/Extensions/ISymbolExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal static class ISymbolExtensions +{ + public static string GetDisplayName(this ISymbol property, INamedTypeSymbol displayAttribute) + { + var displayNameAttribute = property.GetAttributes() + .FirstOrDefault(attribute => + attribute.AttributeClass is { } attributeClass && + SymbolEqualityComparer.Default.Equals(attributeClass, displayAttribute)); + + if (displayNameAttribute is not null) + { + if (!displayNameAttribute.NamedArguments.IsDefaultOrEmpty) + { + foreach (var namedArgument in displayNameAttribute.NamedArguments) + { + if (string.Equals(namedArgument.Key, "Name", StringComparison.Ordinal)) + { + return namedArgument.Value.Value?.ToString() ?? property.Name; + } + } + } + } + + return property.Name; + } +} diff --git a/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs new file mode 100644 index 000000000000..38a4105e67f0 --- /dev/null +++ b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal static class ITypeSymbolExtensions +{ + public static bool IsEnumerable(this ITypeSymbol type, INamedTypeSymbol enumerable) + { + if (type.SpecialType == SpecialType.System_String) + { + return false; + } + + return type.ImplementsInterface(enumerable) || SymbolEqualityComparer.Default.Equals(type, enumerable); + } + + public static bool ImplementsValidationAttribute(this ITypeSymbol typeSymbol, INamedTypeSymbol validationAttributeSymbol) + { + var baseType = typeSymbol.BaseType; + while (baseType != null) + { + if (SymbolEqualityComparer.Default.Equals(baseType, validationAttributeSymbol)) + { + return true; + } + baseType = baseType.BaseType; + } + + return false; + } + + public static ITypeSymbol UnwrapType(this ITypeSymbol type, INamedTypeSymbol enumerable) + { + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && + type is INamedTypeSymbol { TypeArguments.Length: 1 }) + { + // Extract the T from a Nullable + type = ((INamedTypeSymbol)type).TypeArguments[0]; + } + + if (type.NullableAnnotation == NullableAnnotation.Annotated) + { + // Remove the nullable annotation but keep any generic arguments, e.g. List? → List + // so we can retain them in future steps. + type = type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + } + + if (type is INamedTypeSymbol namedType && namedType.IsEnumerable(enumerable) && namedType.TypeArguments.Length == 1) + { + // Extract the T from an IEnumerable or List + type = namedType.TypeArguments[0]; + } + + return type; + } + + internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol interfaceType) + { + foreach (var iface in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(interfaceType, iface)) + { + return true; + } + } + return false; + } + + internal static ImmutableArray? GetJsonDerivedTypes(this ITypeSymbol type, INamedTypeSymbol jsonDerivedTypeAttribute) + { + var derivedTypes = ImmutableArray.CreateBuilder(); + foreach (var attribute in type.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, jsonDerivedTypeAttribute)) + { + var derivedType = (INamedTypeSymbol?)attribute.ConstructorArguments[0].Value; + if (derivedType is not null && !SymbolEqualityComparer.Default.Equals(derivedType, type)) + { + derivedTypes.Add(derivedType); + } + } + } + + return derivedTypes.Count == 0 ? null : derivedTypes.ToImmutable(); + } + + // Types exempted here have special binding rules in RDF and RDG and are not validatable + // types themselves so we short-circuit on them. + internal static bool IsExemptType(this ITypeSymbol type, WellKnownTypes wellKnownTypes) + { + return SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpContext)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpRequest)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpResponse)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_CancellationToken)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormFile)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_IO_Stream)) + || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_IO_Pipelines_PipeReader)); + } + + internal static IPropertySymbol? FindPropertyIncludingBaseTypes(this INamedTypeSymbol typeSymbol, string propertyName) + { + var property = typeSymbol.GetMembers() + .OfType() + .FirstOrDefault(p => string.Equals(p.Name, propertyName, System.StringComparison.OrdinalIgnoreCase)); + + if (property != null) + { + return property; + } + + // If not found, recursively search base types + if (typeSymbol.BaseType is INamedTypeSymbol baseType) + { + return FindPropertyIncludingBaseTypes(baseType, propertyName); + } + + return null; + } + + /// + /// Checks if the parameter is marked with [FromService] or [FromKeyedService] attributes. + /// + /// The parameter to check. + /// The symbol representing the [FromService] attribute. + /// The symbol representing the [FromKeyedService] attribute. + internal static bool IsServiceParameter(this IParameterSymbol parameter, INamedTypeSymbol fromServiceMetadataSymbol, INamedTypeSymbol fromKeyedServiceAttributeSymbol) + { + return parameter.GetAttributes().Any(attr => + attr.AttributeClass is not null && + (attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) || + SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol))); + } +} diff --git a/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs b/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs new file mode 100644 index 000000000000..5b7aa171fac8 --- /dev/null +++ b/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal static class IncrementalValuesProviderExtensions +{ + public static IncrementalValuesProvider Distinct(this IncrementalValuesProvider source, IEqualityComparer comparer) + { + return source + .Collect() + .WithComparer(ImmutableArrayEqualityComparer.Instance) + .SelectMany((values, cancellationToken) => + { + if (values.IsEmpty) + { + return values; + } + + var results = ImmutableArray.CreateBuilder(values.Length); + HashSet set = new(comparer); + + foreach (var value in values) + { + if (set.Add(value)) + { + results.Add(value); + } + } + + return results.DrainToImmutable(); + }); + } + + public static IncrementalValuesProvider Concat( + this IncrementalValuesProvider> first, + IncrementalValuesProvider> second) + { + return first.Collect() + .Combine(second.Collect()) + .SelectMany((tuple, _) => + { + if (tuple.Left.IsEmpty && tuple.Right.IsEmpty) + { + return []; + } + + var results = ImmutableArray.CreateBuilder(tuple.Left.Length + tuple.Right.Length); + for (var i = 0; i < tuple.Left.Length; i++) + { + results.AddRange(tuple.Left[i]); + } + for (var i = 0; i < tuple.Right.Length; i++) + { + results.AddRange(tuple.Right[i]); + } + return results.DrainToImmutable(); + }); + } + + private sealed class ImmutableArrayEqualityComparer : IEqualityComparer> + { + public static readonly ImmutableArrayEqualityComparer Instance = new(); + + public bool Equals(ImmutableArray x, ImmutableArray y) + { + if (x.IsDefault) + { + return y.IsDefault; + } + else if (y.IsDefault) + { + return false; + } + + if (x.Length != y.Length) + { + return false; + } + + for (var i = 0; i < x.Length; i++) + { + if (!EqualityComparer.Default.Equals(x[i], y[i])) + { + return false; + } + } + + return true; + } + + public int GetHashCode(ImmutableArray obj) + { + if (obj.IsDefault) + { + return 0; + } + var hashCode = -450793227; + foreach (var item in obj) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(item); + } + + return hashCode; + } + } +} diff --git a/src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj b/src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj new file mode 100644 index 000000000000..b05a37f970f6 --- /dev/null +++ b/src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj @@ -0,0 +1,34 @@ + + + + netstandard2.0 + true + false + true + false + enable + true + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Validation/gen/Models/RequiredSymbols.cs b/src/Validation/gen/Models/RequiredSymbols.cs new file mode 100644 index 000000000000..c67e1e63c38d --- /dev/null +++ b/src/Validation/gen/Models/RequiredSymbols.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal sealed record class RequiredSymbols( + INamedTypeSymbol DisplayAttribute, + INamedTypeSymbol ValidationAttribute, + INamedTypeSymbol IEnumerable, + INamedTypeSymbol IValidatableObject, + INamedTypeSymbol JsonDerivedTypeAttribute, + INamedTypeSymbol RequiredAttribute, + INamedTypeSymbol CustomValidationAttribute, + INamedTypeSymbol HttpContext, + INamedTypeSymbol HttpRequest, + INamedTypeSymbol HttpResponse, + INamedTypeSymbol CancellationToken, + INamedTypeSymbol IFormCollection, + INamedTypeSymbol IFormFileCollection, + INamedTypeSymbol IFormFile, + INamedTypeSymbol Stream, + INamedTypeSymbol PipeReader +); diff --git a/src/Validation/gen/Models/ValidatableProperty.cs b/src/Validation/gen/Models/ValidatableProperty.cs new file mode 100644 index 000000000000..8dee45af31cc --- /dev/null +++ b/src/Validation/gen/Models/ValidatableProperty.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal sealed record class ValidatableProperty( + ITypeSymbol ContainingType, + ITypeSymbol Type, + string Name, + string DisplayName, + ImmutableArray Attributes +); diff --git a/src/Validation/gen/Models/ValidatableType.cs b/src/Validation/gen/Models/ValidatableType.cs new file mode 100644 index 000000000000..784663750929 --- /dev/null +++ b/src/Validation/gen/Models/ValidatableType.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal sealed record class ValidatableType( + ITypeSymbol Type, + ImmutableArray Members +); diff --git a/src/Validation/gen/Models/ValidatableTypeComparer.cs b/src/Validation/gen/Models/ValidatableTypeComparer.cs new file mode 100644 index 000000000000..2338a1fa2cca --- /dev/null +++ b/src/Validation/gen/Models/ValidatableTypeComparer.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal sealed class ValidatableTypeComparer : IEqualityComparer +{ + public static ValidatableTypeComparer Instance { get; } = new(); + + public bool Equals(ValidatableType? x, ValidatableType? y) + { + if (x is null && y is null) + { + return true; + } + if (x is null || y is null) + { + return false; + } + return SymbolEqualityComparer.Default.Equals(x.Type, y.Type); + } + + public int GetHashCode(ValidatableType? obj) + { + return SymbolEqualityComparer.Default.GetHashCode(obj?.Type); + } +} diff --git a/src/Validation/gen/Models/ValidationAttribute.cs b/src/Validation/gen/Models/ValidationAttribute.cs new file mode 100644 index 000000000000..55adcd0835f9 --- /dev/null +++ b/src/Validation/gen/Models/ValidationAttribute.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +internal sealed record class ValidationAttribute( + string Name, + string ClassName, + List Arguments, + Dictionary NamedArguments, + bool IsCustomValidationAttribute +); diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs b/src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs new file mode 100644 index 000000000000..a3fb17b099c1 --- /dev/null +++ b/src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +public sealed partial class ValidationsGenerator : IIncrementalGenerator +{ + internal bool FindAddValidation(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + if (syntaxNode is InvocationExpressionSyntax + && syntaxNode.TryGetMapMethodName(out var method) + && method == "AddValidation") + { + return true; + } + return false; + } + + internal InterceptableLocation? TransformAddValidation(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + var node = (InvocationExpressionSyntax)context.Node; + var semanticModel = context.SemanticModel; + var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol; + if (symbol is not IMethodSymbol methodSymbol + || methodSymbol.ContainingType.Name != "ValidationServiceCollectionExtensions" + || methodSymbol.ContainingAssembly.Name != "Microsoft.AspNetCore.Http.Abstractions") + { + return null; + } + return semanticModel.GetInterceptableLocation(node, cancellationToken); + } +} diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs new file mode 100644 index 000000000000..959c9bbfb948 --- /dev/null +++ b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +public sealed partial class ValidationsGenerator : IIncrementalGenerator +{ + internal static bool ShouldTransformSymbolWithAttribute(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + return syntaxNode is ClassDeclarationSyntax; + } + + internal ImmutableArray TransformValidatableTypeWithAttribute(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + var validatableTypes = new HashSet(ValidatableTypeComparer.Instance); + List visitedTypes = []; + var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation); + if (TryExtractValidatableType((ITypeSymbol)context.TargetSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes)) + { + return [..validatableTypes]; + } + return []; + } +} diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs new file mode 100644 index 000000000000..f6c9b566b725 --- /dev/null +++ b/src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.AspNetCore.Analyzers.Infrastructure; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +public sealed partial class ValidationsGenerator : IIncrementalGenerator +{ + internal bool FindEndpoints(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + if (syntaxNode is InvocationExpressionSyntax + && syntaxNode.TryGetMapMethodName(out var method)) + { + return method == "MapMethods" || InvocationOperationExtensions.KnownMethods.Contains(method); + } + return false; + } + + internal IInvocationOperation? TransformEndpoints(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + if (context.Node is not InvocationExpressionSyntax node) + { + return null; + } + var operation = context.SemanticModel.GetOperation(node, cancellationToken); + AnalyzerDebug.Assert(operation != null, "Operation should not be null."); + return operation is IInvocationOperation invocationOperation + ? invocationOperation + : null; + } + + internal ImmutableArray ExtractValidatableEndpoint(IInvocationOperation? operation, CancellationToken cancellationToken) + { + AnalyzerDebug.Assert(operation != null, "Operation should not be null."); + AnalyzerDebug.Assert(operation.SemanticModel != null, "Operation should have a semantic model."); + var wellKnownTypes = WellKnownTypes.GetOrCreate(operation.SemanticModel.Compilation); + var validatableTypes = ExtractValidatableTypes(operation, wellKnownTypes); + return validatableTypes; + } +} diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs new file mode 100644 index 000000000000..30fbf38368fc --- /dev/null +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.AspNetCore.Analyzers.Infrastructure; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +public sealed partial class ValidationsGenerator : IIncrementalGenerator +{ + private static readonly SymbolDisplayFormat _symbolDisplayFormat = new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + + internal ImmutableArray ExtractValidatableTypes(IInvocationOperation operation, WellKnownTypes wellKnownTypes) + { + AnalyzerDebug.Assert(operation.SemanticModel != null, "SemanticModel should not be null."); + var parameters = operation.TryGetRouteHandlerMethod(operation.SemanticModel, out var method) + ? method.Parameters + : []; + + var fromServiceMetadataSymbol = wellKnownTypes.Get( + WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata); + var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get( + WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute); + + var validatableTypes = new HashSet(ValidatableTypeComparer.Instance); + List visitedTypes = []; + + foreach (var parameter in parameters) + { + // Skip parameters that are injected as services + if (parameter.IsServiceParameter(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol)) + { + continue; + } + + _ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); + } + return [.. validatableTypes]; + } + + internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnownTypes wellKnownTypes, ref HashSet validatableTypes, ref List visitedTypes) + { + var typeSymbol = incomingTypeSymbol.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)); + if (typeSymbol.SpecialType != SpecialType.None) + { + return false; + } + + if (visitedTypes.Contains(typeSymbol)) + { + return true; + } + + if (typeSymbol.IsExemptType(wellKnownTypes)) + { + return false; + } + + visitedTypes.Add(typeSymbol); + + // Extract validatable types discovered in base types of this type and add them to the top-level list. + var current = typeSymbol.BaseType; + var hasValidatableBaseType = false; + while (current != null && current.SpecialType != SpecialType.System_Object) + { + hasValidatableBaseType |= TryExtractValidatableType(current, wellKnownTypes, ref validatableTypes, ref visitedTypes); + current = current.BaseType; + } + + // Extract validatable types discovered in members of this type and add them to the top-level list. + ImmutableArray members = []; + if (ParsabilityHelper.GetParsability(typeSymbol, wellKnownTypes) is Parsability.NotParsable) + { + members = ExtractValidatableMembers(typeSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes); + } + + // Extract the validatable types discovered in the JsonDerivedTypeAttributes of this type and add them to the top-level list. + var derivedTypes = typeSymbol.GetJsonDerivedTypes(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonDerivedTypeAttribute)); + var hasValidatableDerivedTypes = false; + foreach (var derivedType in derivedTypes ?? []) + { + hasValidatableDerivedTypes |= TryExtractValidatableType(derivedType, wellKnownTypes, ref validatableTypes, ref visitedTypes); + } + + // No validatable members or derived types found, so we don't need to add this type. + if (members.IsDefaultOrEmpty && !hasValidatableBaseType && !hasValidatableDerivedTypes) + { + return false; + } + + // Add the type itself as a validatable type itself. + validatableTypes.Add(new ValidatableType( + Type: typeSymbol, + Members: members)); + + return true; + } + + internal ImmutableArray ExtractValidatableMembers(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, ref HashSet validatableTypes, ref List visitedTypes) + { + var members = new List(); + var resolvedRecordProperty = new List(); + + // Special handling for record types to extract properties from + // the primary constructor. + if (typeSymbol is INamedTypeSymbol { IsRecord: true } namedType) + { + // Find the primary constructor for the record, account + // for members that are in base types to account for + // record inheritance scenarios + var primaryConstructor = namedType.Constructors + .FirstOrDefault(c => c.Parameters.Length > 0 && c.Parameters.All(p => + namedType.FindPropertyIncludingBaseTypes(p.Name) != null)); + + if (primaryConstructor != null) + { + // Process all parameters in constructor order to maintain parameter ordering + foreach (var parameter in primaryConstructor.Parameters) + { + // Find the corresponding property in this type, we ignore + // base types here since that will be handled by the inheritance + // checks in the default ValidatableTypeInfo implementation. + var correspondingProperty = typeSymbol.GetMembers() + .OfType() + .FirstOrDefault(p => string.Equals(p.Name, parameter.Name, System.StringComparison.OrdinalIgnoreCase)); + + if (correspondingProperty != null) + { + resolvedRecordProperty.Add(correspondingProperty); + + // Check if the property's type is validatable, this resolves + // validatable types in the inheritance hierarchy + var hasValidatableType = TryExtractValidatableType( + correspondingProperty.Type, + wellKnownTypes, + ref validatableTypes, + ref visitedTypes); + + members.Add(new ValidatableProperty( + ContainingType: correspondingProperty.ContainingType, + Type: correspondingProperty.Type, + Name: correspondingProperty.Name, + DisplayName: parameter.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)) ?? + correspondingProperty.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)), + Attributes: [])); + } + } + } + } + + // Handle properties for classes and any properties not handled by the constructor + foreach (var member in typeSymbol.GetMembers().OfType()) + { + // Skip compiler generated properties and properties already processed via + // the record processing logic above. + if (member.IsImplicitlyDeclared || resolvedRecordProperty.Contains(member, SymbolEqualityComparer.Default)) + { + continue; + } + + var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); + var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired); + + // If the member has no validation attributes or validatable types and is not required, skip it. + if (attributes.IsDefaultOrEmpty && !hasValidatableType && !isRequired) + { + continue; + } + + members.Add(new ValidatableProperty( + ContainingType: member.ContainingType, + Type: member.Type, + Name: member.Name, + DisplayName: member.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)), + Attributes: attributes)); + } + + return [.. members]; + } + + internal static ImmutableArray ExtractValidationAttributes(ISymbol symbol, WellKnownTypes wellKnownTypes, out bool isRequired) + { + var attributes = symbol.GetAttributes(); + if (attributes.Length == 0) + { + isRequired = false; + return []; + } + + var validationAttributes = attributes + .Where(attribute => attribute.AttributeClass != null) + .Where(attribute => attribute.AttributeClass!.ImplementsValidationAttribute(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute))); + isRequired = validationAttributes.Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_RequiredAttribute))); + return [.. validationAttributes + .Where(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute))) + .Select(attribute => new ValidationAttribute( + Name: symbol.Name + attribute.AttributeClass!.Name, + ClassName: attribute.AttributeClass!.ToDisplayString(_symbolDisplayFormat), + Arguments: [.. attribute.ConstructorArguments.Select(a => a.ToCSharpString())], + NamedArguments: attribute.NamedArguments.ToDictionary(namedArgument => namedArgument.Key, namedArgument => namedArgument.Value.ToCSharpString()), + IsCustomValidationAttribute: SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_CustomValidationAttribute))))]; + } +} diff --git a/src/Validation/gen/ValidationsGenerator.cs b/src/Validation/gen/ValidationsGenerator.cs new file mode 100644 index 000000000000..f67e964ab76b --- /dev/null +++ b/src/Validation/gen/ValidationsGenerator.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator; + +[Generator] +public sealed partial class ValidationsGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Find the builder.Services.AddValidation() call in the application. + var addValidation = context.SyntaxProvider.CreateSyntaxProvider( + predicate: FindAddValidation, + transform: TransformAddValidation + ); + // Extract types that have been marked with [ValidatableType]. + var validatableTypesWithAttribute = context.SyntaxProvider.ForAttributeWithMetadataName( + "Microsoft.Extensions.Validation.ValidatableTypeAttribute", + predicate: ShouldTransformSymbolWithAttribute, + transform: TransformValidatableTypeWithAttribute + ); + // Extract all minimal API endpoints in the application. + var endpoints = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: FindEndpoints, + transform: TransformEndpoints) + .Where(endpoint => endpoint is not null); + // Extract validatable types from all endpoints. + var validatableTypesFromEndpoints = endpoints + .Select(ExtractValidatableEndpoint); + // Join all validatable types encountered in the type graph. + var validatableTypes = validatableTypesWithAttribute + .Concat(validatableTypesFromEndpoints) + .Distinct(ValidatableTypeComparer.Instance) + .Collect(); + + var emitInputs = addValidation + .Combine(validatableTypes); + + // Emit the IValidatableInfo resolver injection and + // ValidatableTypeInfo for all validatable types. + context.RegisterSourceOutput(emitInputs, Emit); + } +} diff --git a/src/Validation/src/IValidatableInfo.cs b/src/Validation/src/IValidatableInfo.cs new file mode 100644 index 000000000000..4b5ab3cd24b2 --- /dev/null +++ b/src/Validation/src/IValidatableInfo.cs @@ -0,0 +1,22 @@ +// 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; + +/// +/// Represents an interface for validating a value. +/// +[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] +public interface IValidatableInfo +{ + /// + /// Validates the specified . + /// + /// The value to validate. + /// The validation context. + /// A cancellation token to support cancellation of the validation. + /// A representing the asynchronous operation. + Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken); +} diff --git a/src/Validation/src/IValidatableInfoResolver.cs b/src/Validation/src/IValidatableInfoResolver.cs new file mode 100644 index 000000000000..706204ab6284 --- /dev/null +++ b/src/Validation/src/IValidatableInfoResolver.cs @@ -0,0 +1,33 @@ +// 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; +using System.Reflection; + +namespace Microsoft.Extensions.Validation; + +/// +/// Provides an interface for resolving the validation information associated +/// with a given or . +/// +[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] +public interface IValidatableInfoResolver +{ + /// + /// Gets validation information for the specified type. + /// + /// The type to get validation information for. + /// + /// The output parameter that will contain the validatable information if found. + /// + /// if the validatable type information was found; otherwise, false. + bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo); + + /// + /// Gets validation information for the specified parameter. + /// + /// The parameter to get validation information for. + /// The output parameter that will contain the validatable information if found. + /// if the validatable parameter information was found; otherwise, false. + bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo); +} diff --git a/src/Validation/src/Microsoft.Extensions.Validation.csproj b/src/Validation/src/Microsoft.Extensions.Validation.csproj new file mode 100644 index 000000000000..495114e86263 --- /dev/null +++ b/src/Validation/src/Microsoft.Extensions.Validation.csproj @@ -0,0 +1,22 @@ + + + + Common validation abstractions and validation infrastructure for .NET applications. + $(DefaultNetCoreTargetFramework) + true + true + validation + true + true + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Validation/src/PublicAPI.Shipped.txt b/src/Validation/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..815c92006af7 --- /dev/null +++ b/src/Validation/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/src/Validation/src/PublicAPI.Unshipped.txt b/src/Validation/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..ce3b07db31b0 --- /dev/null +++ b/src/Validation/src/PublicAPI.Unshipped.txt @@ -0,0 +1,40 @@ +#nullable enable +Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions +Microsoft.Extensions.Validation.IValidatableInfo +Microsoft.Extensions.Validation.IValidatableInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +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.ValidatableParameterInfo +Microsoft.Extensions.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName) -> void +Microsoft.Extensions.Validation.ValidatablePropertyInfo +Microsoft.Extensions.Validation.ValidatablePropertyInfo.ValidatablePropertyInfo(System.Type! declaringType, System.Type! propertyType, string! name, string! displayName) -> void +Microsoft.Extensions.Validation.ValidatableTypeAttribute +Microsoft.Extensions.Validation.ValidatableTypeAttribute.ValidatableTypeAttribute() -> void +Microsoft.Extensions.Validation.ValidatableTypeInfo +Microsoft.Extensions.Validation.ValidatableTypeInfo.ValidatableTypeInfo(System.Type! type, System.Collections.Generic.IReadOnlyList! members) -> void +Microsoft.Extensions.Validation.ValidateContext +Microsoft.Extensions.Validation.ValidateContext.CurrentDepth.get -> int +Microsoft.Extensions.Validation.ValidateContext.CurrentDepth.set -> void +Microsoft.Extensions.Validation.ValidateContext.CurrentValidationPath.get -> string! +Microsoft.Extensions.Validation.ValidateContext.CurrentValidationPath.set -> void +Microsoft.Extensions.Validation.ValidateContext.ValidateContext() -> void +Microsoft.Extensions.Validation.ValidateContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext! +Microsoft.Extensions.Validation.ValidateContext.ValidationContext.set -> void +Microsoft.Extensions.Validation.ValidateContext.ValidationErrors.get -> System.Collections.Generic.Dictionary? +Microsoft.Extensions.Validation.ValidateContext.ValidationErrors.set -> void +Microsoft.Extensions.Validation.ValidateContext.ValidationOptions.get -> Microsoft.Extensions.Validation.ValidationOptions! +Microsoft.Extensions.Validation.ValidateContext.ValidationOptions.set -> void +Microsoft.Extensions.Validation.ValidationOptions +Microsoft.Extensions.Validation.ValidationOptions.MaxDepth.get -> int +Microsoft.Extensions.Validation.ValidationOptions.MaxDepth.set -> void +Microsoft.Extensions.Validation.ValidationOptions.Resolvers.get -> System.Collections.Generic.IList! +Microsoft.Extensions.Validation.ValidationOptions.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) -> bool +Microsoft.Extensions.Validation.ValidationOptions.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.Extensions.Validation.IValidatableInfo? validatableTypeInfo) -> bool +Microsoft.Extensions.Validation.ValidationOptions.ValidationOptions() -> void +abstract Microsoft.Extensions.Validation.ValidatableParameterInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]! +abstract Microsoft.Extensions.Validation.ValidatablePropertyInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]! +static Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configureOptions = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +virtual Microsoft.Extensions.Validation.ValidatableParameterInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual Microsoft.Extensions.Validation.ValidatablePropertyInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual Microsoft.Extensions.Validation.ValidatableTypeInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! \ No newline at end of file diff --git a/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs b/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs new file mode 100644 index 000000000000..e6aa1e01836d --- /dev/null +++ b/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs @@ -0,0 +1,111 @@ +#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. + +// 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.CodeAnalysis; +using System.IO.Pipelines; +using System.Linq; +using System.Reflection; +using System.Security.Claims; + +namespace Microsoft.Extensions.Validation; + +internal sealed class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver +{ + // TODO: the implementation currently relies on static discovery of types. + public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + + public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) + { + if (parameterInfo.Name == null) + { + throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name."); + } + + var validationAttributes = parameterInfo + .GetCustomAttributes() + .ToArray(); + + // If there are no validation attributes and this type is not a complex type + // we don't need to validate it. Complex types without attributes are still + // validatable because we want to run the validations on the properties. + if (validationAttributes.Length == 0 && !IsClass(parameterInfo.ParameterType)) + { + validatableInfo = null; + return false; + } + validatableInfo = new RuntimeValidatableParameterInfo( + parameterType: parameterInfo.ParameterType, + name: parameterInfo.Name, + displayName: GetDisplayName(parameterInfo), + validationAttributes: validationAttributes + ); + return true; + } + + private static string GetDisplayName(ParameterInfo parameterInfo) + { + var displayAttribute = parameterInfo.GetCustomAttribute(); + if (displayAttribute != null) + { + return displayAttribute.Name ?? parameterInfo.Name!; + } + + return parameterInfo.Name!; + } + + internal sealed class RuntimeValidatableParameterInfo( + Type parameterType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) : + ValidatableParameterInfo(parameterType, name, displayName) + { + protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; + + private readonly ValidationAttribute[] _validationAttributes = validationAttributes; + } + + private static bool IsClass(Type type) + { + // Skip primitives, enums, common built-in types, and types that are specially + // handled by RDF/RDG that don't need validation if they don't have attributes + if (type.IsPrimitive || + type.IsEnum || + type == typeof(string) || + type == typeof(decimal) || + type == typeof(DateTime) || + type == typeof(DateTimeOffset) || + type == typeof(TimeOnly) || + type == typeof(DateOnly) || + type == typeof(TimeSpan) || + type == typeof(Guid) || + type == typeof(IFormFile) || + type == typeof(IFormFileCollection) || + type == typeof(IFormCollection) || + type == typeof(HttpContext) || + type == typeof(HttpRequest) || + type == typeof(HttpResponse) || + type == typeof(ClaimsPrincipal) || + type == typeof(CancellationToken) || + type == typeof(Stream) || + type == typeof(PipeReader)) + { + return false; + } + + // Check if the underlying type in a nullable is valid + if (Nullable.GetUnderlyingType(type) is { } nullableType) + { + return IsClass(nullableType); + } + + return type.IsClass; + } +} diff --git a/src/Validation/src/TypeExtensions.cs b/src/Validation/src/TypeExtensions.cs new file mode 100644 index 000000000000..0b11e6ec45e5 --- /dev/null +++ b/src/Validation/src/TypeExtensions.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Validation; + +internal static class TypeExtensions +{ + /// + /// Determines whether the specified type is an enumerable type. + /// + /// The type to check. + /// if the type is enumerable; otherwise, . + public static bool IsEnumerable(this Type type) + { + // Check if type itself is an IEnumerable + if (type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(IEnumerable<>) || + type.GetGenericTypeDefinition() == typeof(ICollection<>) || + type.GetGenericTypeDefinition() == typeof(List<>) || + type.GetGenericTypeDefinition() == typeof(IList<>))) + { + return true; + } + + // Or an array + if (type.IsArray) + { + return true; + } + + // Then evaluate if it implements IEnumerable and is not a string + if (typeof(IEnumerable).IsAssignableFrom(type) && + type != typeof(string)) + { + return true; + } + + return false; + } + + /// + /// Determines whether the specified type is a nullable type. + /// + /// The type to check. + /// if the type is nullable; otherwise, . + public static bool IsNullable(this Type type) + { + if (type.IsValueType) + { + return false; + } + + if (type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return true; + } + + return false; + } + + /// + /// Tries to get the from the specified array of validation attributes. + /// + /// The array of to search. + /// The found if available, otherwise null. + /// if a is found; otherwise, . + public static bool TryGetRequiredAttribute(this ValidationAttribute[] attributes, [NotNullWhen(true)] out RequiredAttribute? requiredAttribute) + { + foreach (var attribute in attributes) + { + if (attribute is RequiredAttribute requiredAttr) + { + requiredAttribute = requiredAttr; + return true; + } + } + + requiredAttribute = null; + return false; + } + + /// + /// Gets all types that the specified type implements or inherits from. + /// + /// The type to analyze. + /// A collection containing all implemented interfaces and all base types of the given type. + public static List GetAllImplementedTypes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type) + { + ArgumentNullException.ThrowIfNull(type); + + var implementedTypes = new List(); + + // Yield all interfaces directly and indirectly implemented by this type + foreach (var interfaceType in type.GetInterfaces()) + { + implementedTypes.Add(interfaceType); + } + + // Finally, walk up the inheritance chain + var baseType = type.BaseType; + while (baseType != null && baseType != typeof(object)) + { + implementedTypes.Add(baseType); + baseType = baseType.BaseType; + } + + return implementedTypes; + } + + /// + /// Determines whether the specified type implements the given interface. + /// + /// The type to check. + /// The interface type to check for. + /// True if the type implements the specified interface; otherwise, false. + public static bool ImplementsInterface(this Type type, Type interfaceType) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(interfaceType); + + // Check if interfaceType is actually an interface + if (!interfaceType.IsInterface) + { + throw new ArgumentException($"Type {interfaceType.FullName} is not an interface.", nameof(interfaceType)); + } + + return interfaceType.IsAssignableFrom(type); + } +} diff --git a/src/Validation/src/ValidatableParameterInfo.cs b/src/Validation/src/ValidatableParameterInfo.cs new file mode 100644 index 000000000000..8481887ff8ce --- /dev/null +++ b/src/Validation/src/ValidatableParameterInfo.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Validation; + +/// +/// Contains validation information for a parameter. +/// +[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] +public abstract class ValidatableParameterInfo : IValidatableInfo +{ + private RequiredAttribute? _requiredAttribute; + + /// + /// Creates a new instance of . + /// + /// The associated with the parameter. + /// The parameter name. + /// The display name for the parameter. + protected ValidatableParameterInfo( + Type parameterType, + string name, + string displayName) + { + ParameterType = parameterType; + Name = name; + DisplayName = displayName; + } + + /// + /// Gets the parameter type. + /// + internal Type ParameterType { get; } + + /// + /// Gets the parameter name. + /// + internal string Name { get; } + + /// + /// Gets the display name for the parameter. + /// + internal string DisplayName { get; } + + /// + /// Gets the validation attributes for this parameter. + /// + /// An array of validation attributes to apply to this parameter. + protected abstract ValidationAttribute[] GetValidationAttributes(); + + /// + /// + /// If the parameter is a collection, each item in the collection will be validated. + /// If the parameter is not a collection but has a validatable type, the single value will be validated. + /// + public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken) + { + // Skip validation if value is null and parameter is optional + if (value == null && ParameterType.IsNullable()) + { + return; + } + + context.ValidationContext.DisplayName = DisplayName; + context.ValidationContext.MemberName = Name; + + var validationAttributes = GetValidationAttributes(); + + if (_requiredAttribute is not null || validationAttributes.TryGetRequiredAttribute(out _requiredAttribute)) + { + var result = _requiredAttribute.GetValidationResult(value, context.ValidationContext); + + if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) + { + var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}"; + context.AddValidationError(key, [result.ErrorMessage]); + return; + } + } + + // Validate against validation attributes + for (var i = 0; i < validationAttributes.Length; i++) + { + var attribute = validationAttributes[i]; + try + { + var result = attribute.GetValidationResult(value, context.ValidationContext); + if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) + { + var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}"; + context.AddOrExtendValidationErrors(key, [result.ErrorMessage]); + } + } + catch (Exception ex) + { + var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}"; + context.AddValidationError(key, [ex.Message]); + } + } + + // If the parameter is a collection, validate each item + if (ParameterType.IsEnumerable() && value is IEnumerable enumerable) + { + var index = 0; + var currentPrefix = context.CurrentValidationPath; + + foreach (var item in enumerable) + { + if (item != null) + { + context.CurrentValidationPath = string.IsNullOrEmpty(currentPrefix) + ? $"{Name}[{index}]" + : $"{currentPrefix}.{Name}[{index}]"; + + if (context.ValidationOptions.TryGetValidatableTypeInfo(item.GetType(), out var validatableType)) + { + await validatableType.ValidateAsync(item, context, cancellationToken); + } + } + index++; + } + + context.CurrentValidationPath = currentPrefix; + } + // If not enumerable, validate the single value + else if (value != null) + { + var valueType = value.GetType(); + if (context.ValidationOptions.TryGetValidatableTypeInfo(valueType, out var validatableType)) + { + await validatableType.ValidateAsync(value, context, cancellationToken); + } + } + } +} diff --git a/src/Validation/src/ValidatablePropertyInfo.cs b/src/Validation/src/ValidatablePropertyInfo.cs new file mode 100644 index 000000000000..b73a8968fd62 --- /dev/null +++ b/src/Validation/src/ValidatablePropertyInfo.cs @@ -0,0 +1,173 @@ +// 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.CodeAnalysis; + +namespace Microsoft.Extensions.Validation; + +/// +/// Contains validation information for a member of a type. +/// +[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] +public abstract class ValidatablePropertyInfo : IValidatableInfo +{ + private RequiredAttribute? _requiredAttribute; + + /// + /// Creates a new instance of . + /// + protected ValidatablePropertyInfo( + [param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + Type declaringType, + Type propertyType, + string name, + string displayName) + { + DeclaringType = declaringType; + PropertyType = propertyType; + Name = name; + DisplayName = displayName; + } + + /// + /// Gets the member type. + /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + internal Type DeclaringType { get; } + + /// + /// Gets the member type. + /// + internal Type PropertyType { get; } + + /// + /// Gets the member name. + /// + internal string Name { get; } + + /// + /// Gets the display name for the member as designated by the . + /// + internal string DisplayName { get; } + + /// + /// Gets the validation attributes for this member. + /// + /// An array of validation attributes to apply to this member. + protected abstract ValidationAttribute[] GetValidationAttributes(); + + /// + public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken) + { + var property = DeclaringType.GetProperty(Name) ?? throw new InvalidOperationException($"Property '{Name}' not found on type '{DeclaringType.Name}'."); + var propertyValue = property.GetValue(value); + var validationAttributes = GetValidationAttributes(); + + // Calculate and save the current path + var originalPrefix = context.CurrentValidationPath; + if (string.IsNullOrEmpty(originalPrefix)) + { + context.CurrentValidationPath = Name; + } + else + { + context.CurrentValidationPath = $"{originalPrefix}.{Name}"; + } + + context.ValidationContext.DisplayName = DisplayName; + context.ValidationContext.MemberName = Name; + + // Check required attribute first + if (_requiredAttribute is not null || validationAttributes.TryGetRequiredAttribute(out _requiredAttribute)) + { + var result = _requiredAttribute.GetValidationResult(propertyValue, context.ValidationContext); + + if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) + { + context.AddValidationError(context.CurrentValidationPath, [result.ErrorMessage]); + context.CurrentValidationPath = originalPrefix; // Restore prefix + return; + } + } + + // Validate any other attributes + ValidateValue(propertyValue, context.CurrentValidationPath, validationAttributes); + + // Check if we've reached the maximum depth before validating complex properties + if (context.CurrentDepth >= context.ValidationOptions.MaxDepth) + { + throw new InvalidOperationException( + $"Maximum validation depth of {context.ValidationOptions.MaxDepth} exceeded at '{context.CurrentValidationPath}' in '{DeclaringType.Name}.{Name}'. " + + "This is likely caused by a circular reference in the object graph. " + + "Consider increasing the MaxDepth in ValidationOptions if deeper validation is required."); + } + + // Increment depth counter + context.CurrentDepth++; + + try + { + // Handle enumerable values + if (PropertyType.IsEnumerable() && propertyValue is System.Collections.IEnumerable enumerable) + { + var index = 0; + var currentPrefix = context.CurrentValidationPath; + + foreach (var item in enumerable) + { + context.CurrentValidationPath = $"{currentPrefix}[{index}]"; + + if (item != null) + { + var itemType = item.GetType(); + if (context.ValidationOptions.TryGetValidatableTypeInfo(itemType, out var validatableType)) + { + await validatableType.ValidateAsync(item, context, cancellationToken); + } + } + + index++; + } + + // Restore prefix to the property name before validating the next item + context.CurrentValidationPath = currentPrefix; + } + else if (propertyValue != null) + { + // Validate as a complex object + var valueType = propertyValue.GetType(); + if (context.ValidationOptions.TryGetValidatableTypeInfo(valueType, out var validatableType)) + { + await validatableType.ValidateAsync(propertyValue, context, cancellationToken); + } + } + } + finally + { + // Always decrement the depth counter and restore prefix + context.CurrentDepth--; + context.CurrentValidationPath = originalPrefix; + } + + void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] validationAttributes) + { + for (var i = 0; i < validationAttributes.Length; i++) + { + var attribute = validationAttributes[i]; + try + { + var result = attribute.GetValidationResult(val, context.ValidationContext); + if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) + { + context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [result.ErrorMessage]); + } + } + catch (Exception ex) + { + context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [ex.Message]); + } + } + } + } +} diff --git a/src/Validation/src/ValidatableTypeAttribute.cs b/src/Validation/src/ValidatableTypeAttribute.cs new file mode 100644 index 000000000000..c2af4cb2a7da --- /dev/null +++ b/src/Validation/src/ValidatableTypeAttribute.cs @@ -0,0 +1,16 @@ +// 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; + +/// +/// Indicates that a type is validatable to support discovery by the +/// validations generator. +/// +[AttributeUsage(AttributeTargets.Class)] +[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] +public sealed class ValidatableTypeAttribute : Attribute +{ +} diff --git a/src/Validation/src/ValidatableTypeInfo.cs b/src/Validation/src/ValidatableTypeInfo.cs new file mode 100644 index 000000000000..936a30d52d3b --- /dev/null +++ b/src/Validation/src/ValidatableTypeInfo.cs @@ -0,0 +1,133 @@ +// 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.CodeAnalysis; +using System.Linq; + +namespace Microsoft.Extensions.Validation; + +/// +/// Contains validation information for a type. +/// +[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] +public abstract class ValidatableTypeInfo : IValidatableInfo +{ + private readonly int _membersCount; + private readonly List _subTypes; + + /// + /// Creates a new instance of . + /// + /// The type being validated. + /// The members that can be validated. + protected ValidatableTypeInfo( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type, + IReadOnlyList members) + { + Type = type; + Members = members; + _membersCount = members.Count; + _subTypes = type.GetAllImplementedTypes(); + } + + /// + /// The type being validated. + /// + internal Type Type { get; } + + /// + /// The members that can be validated. + /// + internal IReadOnlyList Members { get; } + + /// + public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken) + { + if (value == null) + { + return; + } + + // Check if we've exceeded the maximum depth + if (context.CurrentDepth >= context.ValidationOptions.MaxDepth) + { + throw new InvalidOperationException( + $"Maximum validation depth of {context.ValidationOptions.MaxDepth} exceeded at '{context.CurrentValidationPath}' in '{Type.Name}'. " + + "This is likely caused by a circular reference in the object graph. " + + "Consider increasing the MaxDepth in ValidationOptions if deeper validation is required."); + } + + var originalPrefix = context.CurrentValidationPath; + + try + { + var actualType = value.GetType(); + + // First validate members + for (var i = 0; i < _membersCount; i++) + { + await Members[i].ValidateAsync(value, context, cancellationToken); + context.CurrentValidationPath = originalPrefix; + } + + // Then validate sub-types if any + foreach (var subType in _subTypes) + { + // Check if the actual type is assignable to the sub-type + // and validate it if it is + if (subType.IsAssignableFrom(actualType)) + { + if (context.ValidationOptions.TryGetValidatableTypeInfo(subType, out var subTypeInfo)) + { + await subTypeInfo.ValidateAsync(value, context, cancellationToken); + context.CurrentValidationPath = originalPrefix; + } + } + } + + // Finally validate IValidatableObject if implemented + if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable) + { + // Important: Set the DisplayName to the type name for top-level validations + // and restore the original validation context properties + var originalDisplayName = context.ValidationContext.DisplayName; + var originalMemberName = context.ValidationContext.MemberName; + + // Set the display name to the class name for IValidatableObject validation + context.ValidationContext.DisplayName = Type.Name; + context.ValidationContext.MemberName = null; + + var validationResults = validatable.Validate(context.ValidationContext); + foreach (var validationResult in validationResults) + { + if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null) + { + // Create a validation error for each member name that is provided + foreach (var memberName in validationResult.MemberNames) + { + var key = string.IsNullOrEmpty(originalPrefix) ? + memberName : + $"{originalPrefix}.{memberName}"; + context.AddOrExtendValidationError(key, validationResult.ErrorMessage); + } + + if (!validationResult.MemberNames.Any()) + { + // If no member names are specified, then treat this as a top-level error + context.AddOrExtendValidationError(string.Empty, validationResult.ErrorMessage); + } + } + } + + // Restore the original validation context properties + context.ValidationContext.DisplayName = originalDisplayName; + context.ValidationContext.MemberName = originalMemberName; + } + } + finally + { + context.CurrentValidationPath = originalPrefix; + } + } +} diff --git a/src/Validation/src/ValidateContext.cs b/src/Validation/src/ValidateContext.cs new file mode 100644 index 000000000000..b27408ce4844 --- /dev/null +++ b/src/Validation/src/ValidateContext.cs @@ -0,0 +1,100 @@ +// 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.CodeAnalysis; + +namespace Microsoft.Extensions.Validation; + +/// +/// Represents the context for validating a validatable object. +/// +[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] +public sealed class ValidateContext +{ + /// + /// Gets or sets the validation context used for validating objects that implement or have . + /// This context provides access to service provider and other validation metadata. + /// + /// + /// This property should be set by the consumer of the + /// interface to provide the necessary context for validation. The object should be initialized + /// with the current object being validated, the display name, and the service provider to support + /// the complete set of validation scenarios. + /// + /// + /// + /// var validationContext = new ValidationContext(objectToValidate, serviceProvider, items); + /// var validationOptions = serviceProvider.GetService<IOptions<ValidationOptions>>()?.Value; + /// var validateContext = new ValidateContext + /// { + /// ValidationContext = validationContext, + /// ValidationOptions = validationOptions + /// }; + /// + /// + public required ValidationContext ValidationContext { get; set; } + + /// + /// 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"). + /// + public string CurrentValidationPath { get; set; } = string.Empty; + + /// + /// Gets or sets the validation options that control validation behavior, + /// including validation depth limits and resolver registration. + /// + public required ValidationOptions ValidationOptions { get; set; } + + /// + /// Gets or sets the dictionary of validation errors collected during validation. + /// Keys are property names or paths, and values are arrays of error messages. + /// In the default implementation, this dictionary is initialized when the first error is added. + /// + public Dictionary? ValidationErrors { get; set; } + + /// + /// Gets or sets the current depth in the validation hierarchy. + /// This is used to prevent stack overflows from circular references. + /// + 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)) + { + var newErrors = new string[existingErrors.Length + errors.Length]; + existingErrors.CopyTo(newErrors, 0); + errors.CopyTo(newErrors, existingErrors.Length); + ValidationErrors[key] = newErrors; + } + 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]; + } + } +} diff --git a/src/Validation/src/ValidationOptions.cs b/src/Validation/src/ValidationOptions.cs new file mode 100644 index 000000000000..13729b5f124a --- /dev/null +++ b/src/Validation/src/ValidationOptions.cs @@ -0,0 +1,75 @@ +// 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; +using System.Reflection; + +namespace Microsoft.Extensions.Validation; + +/// +/// Provides configuration options for the validation system. +/// +public class ValidationOptions +{ + /// + /// Gets the list of resolvers that provide validation metadata for types and parameters. + /// Resolvers are processed in order, with the first resolver providing a non-null result being used. + /// + /// + /// Source-generated resolvers are typically inserted at the beginning of this list + /// to ensure they are checked before any runtime-based resolvers. + /// + [Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] + public IList Resolvers { get; } = []; + + /// + /// Gets or sets the maximum depth for validation of nested objects. + /// This prevents stack overflows from circular references or extremely deep object graphs. + /// Default value is 32. + /// + public int MaxDepth { get; set; } = 32; + + /// + /// Attempts to get validation information for the specified type. + /// + /// The type to get validation information for. + /// When this method returns, contains the validation information for the specified type, + /// if the type was found; otherwise, null. + /// true if validation information was found for the specified type; otherwise, false. + [Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] + public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableTypeInfo) + { + foreach (var resolver in Resolvers) + { + if (resolver.TryGetValidatableTypeInfo(type, out validatableTypeInfo)) + { + return true; + } + } + + validatableTypeInfo = null; + return false; + } + + /// + /// Attempts to get validation information for the specified parameter. + /// + /// The parameter to get validation information for. + /// When this method returns, contains the validation information for the specified parameter, + /// if validation information was found; otherwise, null. + /// true if validation information was found for the specified parameter; otherwise, false. + [Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] + public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) + { + foreach (var resolver in Resolvers) + { + if (resolver.TryGetValidatableParameterInfo(parameterInfo, out validatableInfo)) + { + return true; + } + } + + validatableInfo = null; + return false; + } +} diff --git a/src/Validation/src/ValidationServiceCollectionExtensions.cs b/src/Validation/src/ValidationServiceCollectionExtensions.cs new file mode 100644 index 000000000000..91d45a8d263e --- /dev/null +++ b/src/Validation/src/ValidationServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Validation; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for adding validation services. +/// +public static class ValidationServiceCollectionExtensions +{ + /// + /// Adds the validation services to the specified . + /// + /// The to add the services to. + /// An optional action to configure the . + /// The for chaining. + public static IServiceCollection AddValidation(this IServiceCollection services, Action? configureOptions = null) + { + services.Configure(options => + { + if (configureOptions is not null) + { + configureOptions(options); + } + // Support ParameterInfo resolution at runtime +#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. + options.Resolvers.Add(new RuntimeValidatableParameterInfoResolver()); +#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. + }); + return services; + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj b/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj new file mode 100644 index 000000000000..73b39892474c --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj @@ -0,0 +1,12 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + + \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs new file mode 100644 index 000000000000..ada596165dea --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs @@ -0,0 +1,191 @@ +// 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.IO.Pipelines; +using System.Reflection; +using System.Security.Claims; + +namespace Microsoft.Extensions.Validation.Tests; + +public class RuntimeValidatableParameterInfoResolverTests +{ + private readonly RuntimeValidatableParameterInfoResolver _resolver = new(); + + [Fact] + public void TryGetValidatableTypeInfo_AlwaysReturnsFalse() + { + var result = _resolver.TryGetValidatableTypeInfo(typeof(string), out var validatableInfo); + + Assert.False(result); + Assert.Null(validatableInfo); + } + + [Fact] + public void TryGetValidatableParameterInfo_WithNullName_ThrowsInvalidOperationException() + { + var parameterInfo = new NullNameParameterInfo(); + + var exception = Assert.Throws(() => + _resolver.TryGetValidatableParameterInfo(parameterInfo, out _)); + + Assert.Contains("without a name", exception.Message); + } + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(int))] + [InlineData(typeof(bool))] + [InlineData(typeof(DateTime))] + [InlineData(typeof(Guid))] + [InlineData(typeof(decimal))] + [InlineData(typeof(DayOfWeek))] // Enum + [InlineData(typeof(ClaimsPrincipal))] + [InlineData(typeof(PipeReader))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(TimeOnly))] + [InlineData(typeof(DateOnly))] + [InlineData(typeof(TimeSpan))] + [InlineData(typeof(IFormFile))] + [InlineData(typeof(IFormFileCollection))] + [InlineData(typeof(IFormCollection))] + [InlineData(typeof(HttpContext))] + [InlineData(typeof(HttpRequest))] + [InlineData(typeof(HttpResponse))] + [InlineData(typeof(CancellationToken))] + public void TryGetValidatableParameterInfo_WithSimpleTypesAndNoAttributes_ReturnsFalse(Type parameterType) + { + var parameterInfo = GetParameter(parameterType); + + var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); + + Assert.False(result); + Assert.Null(validatableInfo); + } + + [Fact] + public void TryGetValidatableParameterInfo_WithClassTypeAndNoAttributes_ReturnsTrue() + { + var parameterInfo = GetParameter(typeof(TestClass)); + + var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); + + Assert.True(result); + Assert.NotNull(validatableInfo); + var parameterValidatableInfo = Assert.IsType(validatableInfo); + Assert.Equal("testParam", parameterValidatableInfo.Name); + Assert.Equal("testParam", parameterValidatableInfo.DisplayName); + } + + [Fact] + public void TryGetValidatableParameterInfo_WithSimpleTypeAndAttributes_ReturnsTrue() + { + var parameterInfo = typeof(TestController) + .GetMethod(nameof(TestController.MethodWithAttributedParam))! + .GetParameters()[0]; + + var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); + + Assert.True(result); + Assert.NotNull(validatableInfo); + var parameterValidatableInfo = Assert.IsType(validatableInfo); + Assert.Equal("value", parameterValidatableInfo.Name); + Assert.Equal("value", parameterValidatableInfo.DisplayName); + } + + [Fact] + public void TryGetValidatableParameterInfo_WithDisplayAttribute_UsesDisplayNameFromAttribute() + { + var parameterInfo = typeof(TestController) + .GetMethod(nameof(TestController.MethodWithDisplayAttribute))! + .GetParameters()[0]; + + var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); + + Assert.True(result); + Assert.NotNull(validatableInfo); + var parameterValidatableInfo = Assert.IsType(validatableInfo); + Assert.Equal("value", parameterValidatableInfo.Name); + Assert.Equal("Custom Display Name", parameterValidatableInfo.DisplayName); + } + + [Fact] + public void TryGetValidatableParameterInfo_WithDisplayAttributeWithNullName_UsesParameterName() + { + var parameterInfo = typeof(TestController) + .GetMethod(nameof(TestController.MethodWithNullDisplayName))! + .GetParameters()[0]; + + var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); + + Assert.True(result); + Assert.NotNull(validatableInfo); + var parameterValidatableInfo = Assert.IsType(validatableInfo); + Assert.Equal("value", parameterValidatableInfo.Name); + Assert.Equal("value", parameterValidatableInfo.DisplayName); + } + + [Fact] + public void TryGetValidatableParameterInfo_WithNullableValueType_ReturnsFalse() + { + var parameterInfo = GetParameter(typeof(int?)); + + var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); + + Assert.False(result); + Assert.Null(validatableInfo); + } + + [Fact] + public void TryGetValidatableParameterInfo_WithNullableReferenceType_ReturnsTrue() + { + var parameterInfo = GetNullableParameter(typeof(TestClass)); + + var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); + + Assert.True(result); + Assert.NotNull(validatableInfo); + var parameterValidatableInfo = Assert.IsType(validatableInfo); + Assert.Equal("testParam", parameterValidatableInfo.Name); + Assert.Equal("testParam", parameterValidatableInfo.DisplayName); + } + + private static ParameterInfo GetParameter(Type parameterType) + { + return typeof(TestParameterHolder) + .GetMethod(nameof(TestParameterHolder.Method))! + .MakeGenericMethod(parameterType) + .GetParameters()[0]; + } + + private static ParameterInfo GetNullableParameter(Type parameterType) + { + return typeof(TestParameterHolder) + .GetMethod(nameof(TestParameterHolder.MethodWithNullable))! + .MakeGenericMethod(parameterType) + .GetParameters()[0]; + } + + private class TestClass { } + + private class TestParameterHolder + { + public void Method(T testParam) { } + public void MethodWithNullable(T? testParam) { } + } + + private class TestController + { + public void MethodWithAttributedParam([Required] string value) { } + + public void MethodWithDisplayAttribute([Display(Name = "Custom Display Name")][Required] string value) { } + + public void MethodWithNullDisplayName([Display(Name = null)][Required] string value) { } + } + + private class NullNameParameterInfo : ParameterInfo + { + public override string? Name => null; + public override Type ParameterType => typeof(string); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs new file mode 100644 index 000000000000..0a745a62a209 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs @@ -0,0 +1,223 @@ +#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. + +// 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.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.Extensions.Validation.Tests; + +public class ValidatableInfoResolverTests +{ + public delegate void TryGetValidatableTypeInfoCallback(Type type, out IValidatableInfo? validatableInfo); + public delegate void TryGetValidatableParameterInfoCallback(ParameterInfo parameter, out IValidatableInfo? validatableInfo); + + [Fact] + public void GetValidatableTypeInfo_ReturnsNull_ForNonValidatableType() + { + // Arrange + var resolver = new Mock(); + IValidatableInfo? validatableInfo = null; + resolver.Setup(r => r.TryGetValidatableTypeInfo(It.IsAny(), out validatableInfo)).Returns(false); + + // Act + var result = resolver.Object.TryGetValidatableTypeInfo(typeof(NonValidatableType), out validatableInfo); + + // Assert + Assert.False(result); + Assert.Null(validatableInfo); + } + + [Fact] + public void GetValidatableTypeInfo_ReturnsTypeInfo_ForValidatableType() + { + // Arrange + var mockTypeInfo = new Mock( + typeof(ValidatableType), + Array.Empty()).Object; + + var resolver = new Mock(); + IValidatableInfo? validatableInfo = null; + resolver + .Setup(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out validatableInfo)) + .Callback(new TryGetValidatableTypeInfoCallback((t, out info) => + { + info = mockTypeInfo; // Set the out parameter to our mock + })) + .Returns(true); + + // Act + var result = resolver.Object.TryGetValidatableTypeInfo(typeof(ValidatableType), out validatableInfo); + + // Assert + Assert.True(result); + var validatableTypeInfo = Assert.IsAssignableFrom(validatableInfo); + Assert.Equal(typeof(ValidatableType), validatableTypeInfo.Type); + } + + [Fact] + public void GetValidatableParameterInfo_ReturnsNull_ForNonValidatableParameter() + { + // Arrange + var method = typeof(TestMethods).GetMethod(nameof(TestMethods.MethodWithNonValidatableParam))!; + var parameter = method.GetParameters()[0]; + + var resolver = new Mock(); + IValidatableInfo? validatableInfo = null; + resolver.Setup(r => r.TryGetValidatableParameterInfo(It.IsAny(), out validatableInfo)).Returns(false); + + // Act + var result = resolver.Object.TryGetValidatableParameterInfo(parameter, out validatableInfo); + + // Assert + Assert.False(result); + } + + [Fact] + public void GetValidatableParameterInfo_ReturnsParameterInfo_ForValidatableParameter() + { + // Arrange + var method = typeof(TestMethods).GetMethod(nameof(TestMethods.MethodWithValidatableParam))!; + var parameter = method.GetParameters()[0]; + + var mockParamInfo = new Mock( + typeof(string), + "model", + "model").Object; + + var resolver = new Mock(); + + // Setup using the same pattern as in the type info test + resolver.Setup(r => r.TryGetValidatableParameterInfo(parameter, out It.Ref.IsAny)) + .Callback(new TryGetValidatableParameterInfoCallback((ParameterInfo p, out IValidatableInfo? info) => + { + info = mockParamInfo; // Set the out parameter to our mock + })) + .Returns(true); + + // Act + var result = resolver.Object.TryGetValidatableParameterInfo(parameter, out var validatableInfo); + + // Assert + Assert.True(result); + var validatableParamInfo = Assert.IsAssignableFrom(validatableInfo); + Assert.Equal("model", validatableParamInfo.Name); + } + + [Fact] + public void ResolversChain_ProcessesInCorrectOrder() + { + // Arrange + var services = new ServiceCollection(); + + var resolver1 = new Mock(); + var resolver2 = new Mock(); + var resolver3 = new Mock(); + + // Create the object that will be returned by resolver2 + var mockTypeInfo = new Mock(typeof(ValidatableType), Array.Empty()).Object; + + // Setup resolver1 to return false (doesn't handle this type) + resolver1 + .Setup(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny)) + .Callback(new TryGetValidatableTypeInfoCallback((Type t, out IValidatableInfo? info) => + { + info = null; + })) + .Returns(false); + + // Setup resolver2 to return true and set the mock type info + resolver2 + .Setup(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny)) + .Callback(new TryGetValidatableTypeInfoCallback((Type t, out IValidatableInfo? info) => + { + info = mockTypeInfo; + })) + .Returns(true); + + services.AddValidation(Options => + { + Options.Resolvers.Add(resolver1.Object); + Options.Resolvers.Add(resolver2.Object); + Options.Resolvers.Add(resolver3.Object); + }); + + var serviceProvider = services.BuildServiceProvider(); + var validationOptions = serviceProvider.GetRequiredService>().Value; + + // Act + var result = validationOptions.TryGetValidatableTypeInfo(typeof(ValidatableType), out var validatableInfo); + + // Assert + Assert.True(result); + Assert.NotNull(validatableInfo); + Assert.Equal(typeof(ValidatableType), ((ValidatableTypeInfo)validatableInfo).Type); + + // Verify that resolvers were called in the expected order + resolver1.Verify(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny), Times.Once); + resolver2.Verify(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny), Times.Once); + resolver3.Verify(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny), Times.Never); + } + + // Test types + private class NonValidatableType { } + + [ValidatableType] + private class ValidatableType + { + [Required] + public string Name { get; set; } = ""; + } + + private static class TestMethods + { + public static void MethodWithNonValidatableParam(NonValidatableType param) { } + public static void MethodWithValidatableParam(ValidatableType model) { } + } + + // Test implementations + private class TestValidatablePropertyInfo : ValidatablePropertyInfo + { + private readonly ValidationAttribute[] _validationAttributes; + + public TestValidatablePropertyInfo( + Type containingType, + Type propertyType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) + : base(containingType, propertyType, name, displayName) + { + _validationAttributes = validationAttributes; + } + + protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; + } + + private class TestValidatableParameterInfo : ValidatableParameterInfo + { + private readonly ValidationAttribute[] _validationAttributes; + + public TestValidatableParameterInfo( + Type parameterType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) + : base(parameterType, name, displayName) + { + _validationAttributes = validationAttributes; + } + + protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; + } + + private class TestValidatableTypeInfo( + Type type, + ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) + { + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs new file mode 100644 index 000000000000..3ede11071d3d --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableParameterInfoTests.cs @@ -0,0 +1,432 @@ +#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. + +// 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.CodeAnalysis; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Validation.Tests; + +public class ValidatableParameterInfoTests +{ + [Fact] + public async Task Validate_RequiredParameter_AddsErrorWhenNull() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(string), + name: "testParam", + displayName: "Test Parameter", + validationAttributes: [new RequiredAttribute()]); + + var context = CreateValidatableContext(); + + // Act + await paramInfo.ValidateAsync(null, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("testParam", error.Key); + Assert.Equal("The Test Parameter field is required.", error.Value.Single()); + } + + [Fact] + public async Task Validate_RequiredParameter_ShortCircuitsOtherValidations() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(string), + name: "testParam", + displayName: "Test Parameter", + // Most ValidationAttributes skip validation if the value is null + // so we use a custom one that always fails to assert on the behavior here + validationAttributes: [new RequiredAttribute(), new CustomTestValidationAttribute()]); + + var context = CreateValidatableContext(); + + // Act + await paramInfo.ValidateAsync(null, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("testParam", error.Key); + Assert.Equal("The Test Parameter field is required.", error.Value.Single()); + } + + [Fact] + public async Task Validate_SkipsValidation_WhenNullAndNotRequired() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(string), + name: "testParam", + displayName: "Test Parameter", + validationAttributes: [new StringLengthAttribute(10)]); + + var context = CreateValidatableContext(); + + // Act + await paramInfo.ValidateAsync(null, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.Null(errors); // No errors added + } + + [Fact] + public async Task Validate_WithRangeAttribute_ValidatesCorrectly() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(int), + name: "testParam", + displayName: "Test Parameter", + validationAttributes: [new RangeAttribute(10, 100)]); + + var context = CreateValidatableContext(); + + // Act + await paramInfo.ValidateAsync(5, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("testParam", error.Key); + Assert.Equal("The field Test Parameter must be between 10 and 100.", error.Value.First()); + } + + [Fact] + public async Task Validate_WithDisplayNameAttribute_UsesDisplayNameInErrorMessage() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(string), + name: "testParam", + displayName: "Custom Display Name", + validationAttributes: [new RequiredAttribute()]); + + var context = CreateValidatableContext(); + + // Act + await paramInfo.ValidateAsync(null, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("testParam", error.Key); + // The error message should use the display name + Assert.Equal("The Custom Display Name field is required.", error.Value.First()); + } + + [Fact] + public async Task Validate_WhenValidatableTypeHasErrors_AddsNestedErrors() + { + // Arrange + var personTypeInfo = new TestValidatableTypeInfo( + typeof(Person), + [ + new TestValidatablePropertyInfo( + typeof(Person), + typeof(string), + "Name", + "Name", + [new RequiredAttribute()]) + ]); + + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(Person), + name: "person", + displayName: "Person", + validationAttributes: []); + + var typeMapping = new Dictionary + { + { typeof(Person), personTypeInfo } + }; + + var context = CreateValidatableContext(typeMapping); + var person = new Person(); // Name is null, so should fail validation + + // Act + await paramInfo.ValidateAsync(person, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("Name", error.Key); + Assert.Equal("The Name field is required.", error.Value[0]); + } + + [Fact] + public async Task Validate_WithEnumerableOfValidatableType_ValidatesEachItem() + { + // Arrange + var personTypeInfo = new TestValidatableTypeInfo( + typeof(Person), + [ + new TestValidatablePropertyInfo( + typeof(Person), + typeof(string), + "Name", + "Name", + [new RequiredAttribute()]) + ]); + + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(IEnumerable), + name: "people", + displayName: "People", + validationAttributes: []); + + var typeMapping = new Dictionary + { + { typeof(Person), personTypeInfo } + }; + + var context = CreateValidatableContext(typeMapping); + var people = new List + { + new() { Name = "Valid" }, + new() // Name is null, should fail + }; + + // Act + await paramInfo.ValidateAsync(people, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("people[1].Name", error.Key); + Assert.Equal("The Name field is required.", error.Value[0]); + } + + [Fact] + public async Task Validate_MultipleErrorsOnSameParameter_CollectsAllErrors() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(int), + name: "testParam", + displayName: "Test Parameter", + validationAttributes: + [ + new RangeAttribute(10, 100) { ErrorMessage = "Range error" }, + new CustomTestValidationAttribute { ErrorMessage = "Custom error" } + ]); + + var context = CreateValidatableContext(); + + // Act + await paramInfo.ValidateAsync(5, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("testParam", error.Key); + Assert.Collection(error.Value, + e => Assert.Equal("Range error", e), + e => Assert.Equal("Custom error", e)); + } + + [Fact] + public async Task Validate_WithContextPrefix_AddsErrorsWithCorrectPrefix() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(int), + name: "testParam", + displayName: "Test Parameter", + validationAttributes: [new RangeAttribute(10, 100)]); + + var context = CreateValidatableContext(); + context.CurrentValidationPath = "parent"; + + // Act + await paramInfo.ValidateAsync(5, context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("parent.testParam", error.Key); + Assert.Equal("The field Test Parameter must be between 10 and 100.", error.Value.First()); + } + + [Fact] + public async Task Validate_ExceptionDuringValidation_CapturesExceptionAsError() + { + // Arrange + var paramInfo = CreateTestParameterInfo( + parameterType: typeof(string), + name: "testParam", + displayName: "Test Parameter", + validationAttributes: [new ThrowingValidationAttribute()]); + + var context = CreateValidatableContext(); + + // Act + await paramInfo.ValidateAsync("test", context, default); + + // Assert + var errors = context.ValidationErrors; + Assert.NotNull(errors); + var error = Assert.Single(errors); + Assert.Equal("testParam", error.Key); + Assert.Equal("Test exception", error.Value.First()); + } + + private TestValidatableParameterInfo CreateTestParameterInfo( + Type parameterType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) + { + return new TestValidatableParameterInfo( + parameterType, + name, + displayName, + validationAttributes); + } + + private ValidateContext CreateValidatableContext( + Dictionary? typeMapping = null) + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var validationContext = new ValidationContext(new object(), serviceProvider, null); + + return new ValidateContext + { + ValidationContext = validationContext, + ValidationOptions = new TestValidationOptions(typeMapping ?? new Dictionary()) + }; + } + + private class TestValidatableParameterInfo : ValidatableParameterInfo + { + private readonly ValidationAttribute[] _validationAttributes; + + public TestValidatableParameterInfo( + Type parameterType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) + : base(parameterType, name, displayName) + { + _validationAttributes = validationAttributes; + } + + protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; + } + + private class TestValidatablePropertyInfo : ValidatablePropertyInfo + { + private readonly ValidationAttribute[] _validationAttributes; + + public TestValidatablePropertyInfo( + Type containingType, + Type propertyType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) + : base(containingType, propertyType, name, displayName) + { + _validationAttributes = validationAttributes; + } + + protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; + } + + private class TestValidatableTypeInfo( + Type type, + ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) + { + } + + private class TestValidationOptions : ValidationOptions + { + public TestValidationOptions(Dictionary typeInfoMappings) + { + // Create a custom resolver that uses the dictionary + var resolver = new DictionaryBasedResolver(typeInfoMappings); + + // Add it to the resolvers collection + Resolvers.Add(resolver); + } + + // Private resolver implementation that uses a dictionary lookup + private class DictionaryBasedResolver : IValidatableInfoResolver + { + private readonly Dictionary _typeInfoMappings; + + public DictionaryBasedResolver(Dictionary typeInfoMappings) + { + _typeInfoMappings = typeInfoMappings; + } + + public ValidatableTypeInfo? TryGetValidatableTypeInfo(Type type) + { + _typeInfoMappings.TryGetValue(type, out var info); + return info; + } + + public ValidatableParameterInfo? GetValidatableParameterInfo(ParameterInfo parameterInfo) + { + // Not implemented in the test + return null; + } + + public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) + { + if (_typeInfoMappings.TryGetValue(type, out var validatableTypeInfo)) + { + validatableInfo = validatableTypeInfo; + return true; + } + validatableInfo = null; + return false; + } + + public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + } + + // Test data classes and validation attributes + + private class Person + { + public string? Name { get; set; } + } + + private class CustomTestValidationAttribute : ValidationAttribute + { + public override bool IsValid(object? value) + { + // Always fail for testing + return false; + } + } + + private class ThrowingValidationAttribute : ValidationAttribute + { + public override bool IsValid(object? value) + { + throw new InvalidOperationException("Test exception"); + } + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs new file mode 100644 index 000000000000..775ad0e947ee --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableTypeInfoTests.cs @@ -0,0 +1,790 @@ +#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. + +// 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.CodeAnalysis; +using System.Reflection; + +namespace Microsoft.Extensions.Validation.Tests; + +public class ValidatableTypeInfoTests +{ + [Fact] + public async Task Validate_ValidatesComplexType_WithNestedProperties() + { + // Arrange + var personType = new TestValidatableTypeInfo( + typeof(Person), + [ + CreatePropertyInfo(typeof(Person), typeof(string), "Name", "Name", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Person), typeof(int), "Age", "Age", + [new RangeAttribute(0, 120)]), + CreatePropertyInfo(typeof(Person), typeof(Address), "Address", "Address", + []) + ]); + + var addressType = new TestValidatableTypeInfo( + typeof(Address), + [ + CreatePropertyInfo(typeof(Address), typeof(string), "Street", "Street", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Address), typeof(string), "City", "City", + [new RequiredAttribute()]) + ]); + + var validationOptions = new TestValidationOptions(new Dictionary + { + { typeof(Person), personType }, + { typeof(Address), addressType } + }); + + var personWithMissingRequiredFields = new Person + { + Age = 150, // Invalid age + Address = new Address() // Missing required City and Street + }; + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(personWithMissingRequiredFields) + }; + + // Act + await personType.ValidateAsync(personWithMissingRequiredFields, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("Name", kvp.Key); + Assert.Equal("The Name field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("Age", kvp.Key); + Assert.Equal("The field Age must be between 0 and 120.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("Address.Street", kvp.Key); + Assert.Equal("The Street field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("Address.City", kvp.Key); + Assert.Equal("The City field is required.", kvp.Value.First()); + }); + } + + [Fact] + public async Task Validate_HandlesIValidatableObject_Implementation() + { + // Arrange + var employeeType = new TestValidatableTypeInfo( + typeof(Employee), + [ + CreatePropertyInfo(typeof(Employee), typeof(string), "Name", "Name", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Employee), typeof(string), "Department", "Department", + []), + CreatePropertyInfo(typeof(Employee), typeof(decimal), "Salary", "Salary", + []) + ]); + + var employee = new Employee + { + Name = "John Doe", + Department = "IT", + Salary = -5000 // Negative salary will trigger IValidatableObject validation + }; + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(Employee), employeeType } + }), + ValidationContext = new ValidationContext(employee) + }; + + // Act + await employeeType.ValidateAsync(employee, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + var error = Assert.Single(context.ValidationErrors); + Assert.Equal("Salary", error.Key); + Assert.Equal("Salary must be a positive value.", error.Value.First()); + } + + [Fact] + public async Task Validate_HandlesPolymorphicTypes_WithSubtypes() + { + // Arrange + var baseType = new TestValidatableTypeInfo( + typeof(Vehicle), + [ + CreatePropertyInfo(typeof(Vehicle), typeof(string), "Make", "Make", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Vehicle), typeof(string), "Model", "Model", + [new RequiredAttribute()]) + ]); + + var derivedType = new TestValidatableTypeInfo( + typeof(Car), + [ + CreatePropertyInfo(typeof(Car), typeof(int), "Doors", "Doors", + [new RangeAttribute(2, 5)]) + ]); + + var car = new Car + { + // Missing Make and Model (required in base type) + Doors = 7 // Invalid number of doors + }; + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(Vehicle), baseType }, + { typeof(Car), derivedType } + }), + ValidationContext = new ValidationContext(car) + }; + + // Act + await derivedType.ValidateAsync(car, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("Doors", kvp.Key); + Assert.Equal("The field Doors must be between 2 and 5.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("Make", kvp.Key); + Assert.Equal("The Make field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("Model", kvp.Key); + Assert.Equal("The Model field is required.", kvp.Value.First()); + }); + } + + [Fact] + public async Task Validate_HandlesCollections_OfValidatableTypes() + { + // Arrange + var itemType = new TestValidatableTypeInfo( + typeof(OrderItem), + [ + CreatePropertyInfo(typeof(OrderItem), typeof(string), "ProductName", "ProductName", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(OrderItem), typeof(int), "Quantity", "Quantity", + [new RangeAttribute(1, 100)]) + ]); + + var orderType = new TestValidatableTypeInfo( + typeof(Order), + [ + CreatePropertyInfo(typeof(Order), typeof(string), "OrderNumber", "OrderNumber", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(Order), typeof(List), "Items", "Items", + []) + ]); + + var order = new Order + { + OrderNumber = "ORD-12345", + Items = + [ + new OrderItem { ProductName = "Valid Product", Quantity = 5 }, + new OrderItem { /* Missing ProductName (required) */ Quantity = 0 /* Invalid quantity */ }, + new OrderItem { ProductName = "Another Product", Quantity = 200 /* Invalid quantity */ } + ] + }; + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(OrderItem), itemType }, + { typeof(Order), orderType } + }), + ValidationContext = new ValidationContext(order) + }; + + // Act + await orderType.ValidateAsync(order, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("Items[1].ProductName", kvp.Key); + Assert.Equal("The ProductName field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("Items[1].Quantity", kvp.Key); + Assert.Equal("The field Quantity must be between 1 and 100.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("Items[2].Quantity", kvp.Key); + Assert.Equal("The field Quantity must be between 1 and 100.", kvp.Value.First()); + }); + } + + [Fact] + public async Task Validate_HandlesNullValues_Appropriately() + { + // Arrange + var personType = new TestValidatableTypeInfo( + typeof(Person), + [ + CreatePropertyInfo(typeof(Person), typeof(string), "Name", "Name", + []), + CreatePropertyInfo(typeof(Person), typeof(Address), "Address", "Address", + []) + ]); + + var person = new Person + { + Name = null, + Address = null + }; + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(Person), personType } + }), + ValidationContext = new ValidationContext(person) + }; + + // Act + await personType.ValidateAsync(person, context, default); + + // Assert + Assert.Null(context.ValidationErrors); // No validation errors for nullable properties with null values + } + + [Fact] + public async Task Validate_RespectsMaxDepthOption_ForCircularReferences() + { + // Arrange + // Create a type that can contain itself (circular reference) + var nodeType = new TestValidatableTypeInfo( + typeof(TreeNode), + [ + CreatePropertyInfo(typeof(TreeNode), typeof(string), "Name", "Name", + [new RequiredAttribute()]), + CreatePropertyInfo(typeof(TreeNode), typeof(TreeNode), "Parent", "Parent", + []), + CreatePropertyInfo(typeof(TreeNode), typeof(List), "Children", "Children", + []) + ]); + + // Create a validation options with a small max depth + var validationOptions = new TestValidationOptions(new Dictionary + { + { typeof(TreeNode), nodeType } + }); + validationOptions.MaxDepth = 3; // Set a small max depth to trigger the limit + + // Create a deep tree with circular references + var rootNode = new TreeNode { Name = "Root" }; + var level1 = new TreeNode { Name = "Level1", Parent = rootNode }; + var level2 = new TreeNode { Name = "Level2", Parent = level1 }; + var level3 = new TreeNode { Name = "Level3", Parent = level2 }; + var level4 = new TreeNode { Name = "" }; // Invalid: missing required name + var level5 = new TreeNode { Name = "" }; // Invalid but beyond max depth, should not be validated + + rootNode.Children.Add(level1); + level1.Children.Add(level2); + level2.Children.Add(level3); + level3.Children.Add(level4); + level4.Children.Add(level5); + + // Add a circular reference + level5.Children.Add(rootNode); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationErrors = [], + ValidationContext = new ValidationContext(rootNode) + }; + + // Act + Assert + var exception = await Assert.ThrowsAsync( + async () => await nodeType.ValidateAsync(rootNode, context, default)); + + Assert.NotNull(exception); + Assert.Equal("Maximum validation depth of 3 exceeded at 'Children[0].Parent.Children[0]' in 'TreeNode'. This is likely caused by a circular reference in the object graph. Consider increasing the MaxDepth in ValidationOptions if deeper validation is required.", exception.Message); + Assert.Equal(0, context.CurrentDepth); + } + + [Fact] + public async Task Validate_HandlesCustomValidationAttributes() + { + // Arrange + var productType = new TestValidatableTypeInfo( + typeof(Product), + [ + CreatePropertyInfo(typeof(Product), typeof(string), "SKU", "SKU", [new RequiredAttribute(), new CustomSkuValidationAttribute()]), + ]); + + var product = new Product { SKU = "INVALID-SKU" }; + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(Product), productType } + }), + ValidationContext = new ValidationContext(product) + }; + + // Act + await productType.ValidateAsync(product, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + var error = Assert.Single(context.ValidationErrors); + Assert.Equal("SKU", error.Key); + Assert.Equal("SKU must start with 'PROD-'.", error.Value.First()); + } + + [Fact] + public async Task Validate_HandlesMultipleErrorsOnSameProperty() + { + // Arrange + var userType = new TestValidatableTypeInfo( + typeof(User), + [ + CreatePropertyInfo(typeof(User), typeof(string), "Password", "Password", + [ + new RequiredAttribute(), + new MinLengthAttribute(8) { ErrorMessage = "Password must be at least 8 characters." }, + new PasswordComplexityAttribute() + ]) + ]); + + var user = new User { Password = "abc" }; // Too short and not complex enough + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(User), userType } + }), + ValidationContext = new ValidationContext(user) + }; + + // Act + await userType.ValidateAsync(user, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Single(context.ValidationErrors.Keys); // Only the "Password" key + Assert.Equal(2, context.ValidationErrors["Password"].Length); // But with 2 errors + Assert.Contains("Password must be at least 8 characters.", context.ValidationErrors["Password"]); + Assert.Contains("Password must contain at least one number and one special character.", context.ValidationErrors["Password"]); + } + + [Fact] + public async Task Validate_HandlesMultiLevelInheritance() + { + // Arrange + var baseType = new TestValidatableTypeInfo( + typeof(BaseEntity), + [ + CreatePropertyInfo(typeof(BaseEntity), typeof(Guid), "Id", "Id", []) + ]); + + var intermediateType = new TestValidatableTypeInfo( + typeof(IntermediateEntity), + [ + CreatePropertyInfo(typeof(IntermediateEntity), typeof(DateTime), "CreatedAt", "CreatedAt", [new PastDateAttribute()]) + ]); + + var derivedType = new TestValidatableTypeInfo( + typeof(DerivedEntity), + [ + CreatePropertyInfo(typeof(DerivedEntity), typeof(string), "Name", "Name", [new RequiredAttribute()]) + ]); + + var entity = new DerivedEntity + { + Name = "", // Invalid: required + CreatedAt = DateTime.Now.AddDays(1) // Invalid: future date + }; + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(BaseEntity), baseType }, + { typeof(IntermediateEntity), intermediateType }, + { typeof(DerivedEntity), derivedType } + }), + ValidationContext = new ValidationContext(entity) + }; + + // Act + await derivedType.ValidateAsync(entity, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("Name", kvp.Key); + Assert.Equal("The Name field is required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("CreatedAt", kvp.Key); + Assert.Equal("Date must be in the past.", kvp.Value.First()); + }); + } + + [Fact] + public async Task Validate_RequiredOnPropertyShortCircuitsOtherValidations() + { + // Arrange + var userType = new TestValidatableTypeInfo( + typeof(User), + [ + CreatePropertyInfo(typeof(User), typeof(string), "Password", "Password", + [new RequiredAttribute(), new PasswordComplexityAttribute()]) + ]); + + var user = new User { Password = null }; // Invalid: required + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(User), userType } + }), + ValidationContext = new ValidationContext(user) // Invalid: required + }; + + // Act + await userType.ValidateAsync(user, context, default); + + // Assert + Assert.NotNull(context.ValidationErrors); + Assert.Single(context.ValidationErrors.Keys); + var error = Assert.Single(context.ValidationErrors); + Assert.Equal("Password", error.Key); + Assert.Equal("The Password field is required.", error.Value.Single()); + } + + [Fact] + public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_BehavesAsExpected() + { + var globalType = new TestValidatableTypeInfo( + typeof(GlobalErrorObject), + []); // no properties – nothing sets MemberName + var globalErrorInstance = new GlobalErrorObject { Data = -1 }; + + var context = new ValidateContext + { + ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(GlobalErrorObject), globalType } + }), + ValidationContext = new ValidationContext(globalErrorInstance) + }; + + await globalType.ValidateAsync(globalErrorInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + var globalError = Assert.Single(context.ValidationErrors); + Assert.Equal(string.Empty, globalError.Key); + Assert.Equal("Data must be positive.", globalError.Value.Single()); + + var multiType = new TestValidatableTypeInfo( + typeof(MultiMemberErrorObject), + [ + CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "FirstName", "FirstName", []), + CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "LastName", "LastName", []) + ]); + + context.ValidationErrors = []; + context.ValidationOptions = new TestValidationOptions(new Dictionary + { + { typeof(MultiMemberErrorObject), multiType } + }); + + var multiErrorInstance = new MultiMemberErrorObject { FirstName = "", LastName = "" }; + context.ValidationContext = new ValidationContext(multiErrorInstance); + + await multiType.ValidateAsync(multiErrorInstance, context, default); + + Assert.NotNull(context.ValidationErrors); + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("FirstName", kvp.Key); + Assert.Equal("FirstName and LastName are required.", kvp.Value.First()); + }, + kvp => + { + Assert.Equal("LastName", kvp.Key); + Assert.Equal("FirstName and LastName are required.", kvp.Value.First()); + }); + } + + // Returns no member names to validate https://github.com/dotnet/aspnetcore/issues/61739 + private class GlobalErrorObject : IValidatableObject + { + public int Data { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Data <= 0) + { + yield return new ValidationResult("Data must be positive."); + } + } + } + + // Returns multiple member names to validate https://github.com/dotnet/aspnetcore/issues/61739 + private class MultiMemberErrorObject : IValidatableObject + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrEmpty(FirstName) || string.IsNullOrEmpty(LastName)) + { + // MULTIPLE member names + yield return new ValidationResult( + "FirstName and LastName are required.", + [nameof(FirstName), nameof(LastName)]); + } + } + } + + private ValidatablePropertyInfo CreatePropertyInfo( + Type containingType, + Type propertyType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) + { + return new TestValidatablePropertyInfo( + containingType, + propertyType, + name, + displayName, + validationAttributes); + } + + // Test model classes + private class Person + { + public string? Name { get; set; } + public int Age { get; set; } + public Address? Address { get; set; } + } + + private class Address + { + public string? Street { get; set; } + public string? City { get; set; } + } + + private class Employee : IValidatableObject + { + public string? Name { get; set; } + public string? Department { get; set; } + public decimal Salary { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Salary < 0) + { + yield return new ValidationResult("Salary must be a positive value.", ["Salary"]); + } + } + } + + private class Vehicle + { + public string? Make { get; set; } + public string? Model { get; set; } + } + + private class Car : Vehicle + { + public int Doors { get; set; } + } + + private class Order + { + public string? OrderNumber { get; set; } + public List Items { get; set; } = []; + } + + private class OrderItem + { + public string? ProductName { get; set; } + public int Quantity { get; set; } + } + + private class TreeNode + { + public string Name { get; set; } = string.Empty; + public TreeNode? Parent { get; set; } + public List Children { get; set; } = []; + } + + private class Product + { + public string SKU { get; set; } = string.Empty; + } + + private class User + { + public string? Password { get; set; } = string.Empty; + } + + private class BaseEntity + { + public Guid Id { get; set; } = Guid.NewGuid(); + } + + private class IntermediateEntity : BaseEntity + { + public DateTime CreatedAt { get; set; } + } + + private class DerivedEntity : IntermediateEntity + { + public string Name { get; set; } = string.Empty; + } + + private class PastDateAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is DateTime date && date > DateTime.Now) + { + return new ValidationResult("Date must be in the past."); + } + + return ValidationResult.Success; + } + } + + private class CustomSkuValidationAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is string sku && !sku.StartsWith("PROD-", StringComparison.Ordinal)) + { + return new ValidationResult("SKU must start with 'PROD-'."); + } + + return ValidationResult.Success; + } + } + + private class PasswordComplexityAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is string password) + { + var hasDigit = password.Any(c => char.IsDigit(c)); + var hasSpecial = password.Any(c => !char.IsLetterOrDigit(c)); + + if (!hasDigit || !hasSpecial) + { + return new ValidationResult("Password must contain at least one number and one special character."); + } + } + + return ValidationResult.Success; + } + } + + // Test implementations + private class TestValidatablePropertyInfo : ValidatablePropertyInfo + { + private readonly ValidationAttribute[] _validationAttributes; + + public TestValidatablePropertyInfo( + Type containingType, + Type propertyType, + string name, + string displayName, + ValidationAttribute[] validationAttributes) + : base(containingType, propertyType, name, displayName) + { + _validationAttributes = validationAttributes; + } + + protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; + } + + private class TestValidatableTypeInfo : ValidatableTypeInfo + { + public TestValidatableTypeInfo( + Type type, + ValidatablePropertyInfo[] members) + : base(type, members) + { + } + } + + private class TestValidationOptions : ValidationOptions + { + public TestValidationOptions(Dictionary typeInfoMappings) + { + // Create a custom resolver that uses the dictionary + var resolver = new DictionaryBasedResolver(typeInfoMappings); + + // Add it to the resolvers collection + Resolvers.Add(resolver); + } + + // Private resolver implementation that uses a dictionary lookup + private class DictionaryBasedResolver : IValidatableInfoResolver + { + private readonly Dictionary _typeInfoMappings; + + public DictionaryBasedResolver(Dictionary typeInfoMappings) + { + _typeInfoMappings = typeInfoMappings; + } + + public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) + { + if (_typeInfoMappings.TryGetValue(type, out var info)) + { + validatableInfo = info; + return true; + } + validatableInfo = null; + return false; + } + + public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj new file mode 100644 index 000000000000..43c387d71140 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj @@ -0,0 +1,22 @@ + + + + $(DefaultNetCoreTargetFramework) + true + enable + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ComplexType.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ComplexType.cs new file mode 100644 index 000000000000..b694f809812c --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ComplexType.cs @@ -0,0 +1,374 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateComplexTypes() + { + // Arrange + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed"!)); + +app.Run(); + +public class ComplexType +{ + [Range(10, 100)] + public int IntegerWithRange { get; set; } = 10; + + [Range(10, 100), Display(Name = "Valid identifier")] + public int IntegerWithRangeAndDisplayName { get; set; } = 50; + + [Required] + public SubType PropertyWithMemberAttributes { get; set; } = new SubType("some-value", default); + + public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType("some-value", default); + + public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance("some-value", default); + + // Nullable to validate https://github.com/dotnet/aspnetcore/issues/61737 + public List? ListOfSubTypes { get; set; } = []; + + [DerivedValidation(ErrorMessage = "Value must be an even number")] + public int IntegerWithDerivedValidationAttribute { get; set; } + + [CustomValidation(typeof(CustomValidators), nameof(CustomValidators.Validate))] + public int IntegerWithCustomValidation { get; set; } = 0; + + [DerivedValidation, Range(10, 100)] + public int PropertyWithMultipleAttributes { get; set; } = 10; +} + +public class DerivedValidationAttribute : ValidationAttribute +{ + public override bool IsValid(object? value) => value is int number && number % 2 == 0; +} + +public class SubType(string? requiredProperty, string? stringWithLength) +{ + [Required] + public string RequiredProperty { get; } = requiredProperty; + + [StringLength(10)] + public string? StringWithLength { get; } = stringWithLength; +} + +public class SubTypeWithInheritance(string? requiredProperty, string? stringWithLength) : SubType(requiredProperty, stringWithLength) +{ + [EmailAddress] + public string? EmailString { get; set; } +} + +public static class CustomValidators +{ + public static ValidationResult Validate(int number, ValidationContext validationContext) + { + var parent = (ComplexType)validationContext.ObjectInstance; + + if (parent.IntegerWithRange == number) + { + return new ValidationResult( + "Can't use the same number value in two properties on the same class.", + new[] { validationContext.MemberName }); + } + + return ValidationResult.Success; + } +} +"""; + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => + { + await InvalidIntegerWithRangeProducesError(endpoint); + await InvalidIntegerWithRangeAndDisplayNameProducesError(endpoint); + await MissingRequiredSubtypePropertyProducesError(endpoint); + await InvalidRequiredSubtypePropertyProducesError(endpoint); + await InvalidSubTypeWithInheritancePropertyProducesError(endpoint); + await InvalidListOfSubTypesProducesError(endpoint); + await InvalidPropertyWithDerivedValidationAttributeProducesError(endpoint); + await InvalidPropertyWithMultipleAttributesProducesError(endpoint); + await InvalidPropertyWithCustomValidationProducesError(endpoint); + await ValidInputProducesNoWarnings(endpoint); + + async Task InvalidIntegerWithRangeProducesError(Endpoint endpoint) + { + + var payload = """ + { + "IntegerWithRange": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithRange", kvp.Key); + Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task InvalidIntegerWithRangeAndDisplayNameProducesError(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithRangeAndDisplayName": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key); + Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task MissingRequiredSubtypePropertyProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyWithMemberAttributes": null + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("PropertyWithMemberAttributes", kvp.Key); + Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single()); + }); + } + + async Task InvalidRequiredSubtypePropertyProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyWithMemberAttributes": { + "RequiredProperty": "", + "StringWithLength": "way-too-long" + } + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyWithInheritance": { + "RequiredProperty": "", + "StringWithLength": "way-too-long", + "EmailString": "not-an-email" + } + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + kvp => + { + Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key); + Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidListOfSubTypesProducesError(Endpoint endpoint) + { + var payload = """ + { + "ListOfSubTypes": [ + { + "RequiredProperty": "", + "StringWithLength": "way-too-long" + }, + { + "RequiredProperty": "valid", + "StringWithLength": "way-too-long" + }, + { + "RequiredProperty": "valid", + "StringWithLength": "valid" + } + ] + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + kvp => + { + Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithDerivedValidationAttribute": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithDerivedValidationAttribute", kvp.Key); + Assert.Equal("Value must be an even number", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyWithMultipleAttributes": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("PropertyWithMultipleAttributes", kvp.Key); + Assert.Collection(kvp.Value, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error); + }, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error); + }); + }); + } + + async Task InvalidPropertyWithCustomValidationProducesError(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithRange": 42, + "IntegerWithCustomValidation": 42 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithCustomValidation", kvp.Key); + var error = Assert.Single(kvp.Value); + Assert.Equal("Can't use the same number value in two properties on the same class.", error); + }); + } + + async Task ValidInputProducesNoWarnings(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithRange": 50, + "IntegerWithRangeAndDisplayName": 50, + "PropertyWithMemberAttributes": { + "RequiredProperty": "valid", + "StringWithLength": "valid" + }, + "PropertyWithoutMemberAttributes": { + "RequiredProperty": "valid", + "StringWithLength": "valid" + }, + "PropertyWithInheritance": { + "RequiredProperty": "valid", + "StringWithLength": "valid", + "EmailString": "test@example.com" + }, + "ListOfSubTypes": [], + "IntegerWithDerivedValidationAttribute": 2, + "IntegerWithCustomValidation": 0, + "PropertyWithMultipleAttributes": 12 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + await endpoint.RequestDelegate(context); + + Assert.Equal(200, context.Response.StatusCode); + } + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.IValidatableObject.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.IValidatableObject.cs new file mode 100644 index 000000000000..944aea69b187 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.IValidatableObject.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateIValidatableObject() + { + var source = """ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); +builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("serviceKey"); +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapPost("/validatable-object", ( + ComplexValidatableType model, + // Demonstrates that parameters that are annotated with [FromService] are not processed + // by the source generator and not emitted as ValidatableTypes in the generated code. + [FromServices] IRangeService rangeService, + [FromKeyedServices("serviceKey")] TestService testService) => Results.Ok(rangeService.GetMinimum())); + +app.Run(); + +public class ComplexValidatableType: IValidatableObject +{ + [Display(Name = "Value 1")] + public int Value1 { get; set; } + + [EmailAddress] + [Required] + public required string Value2 { get; set; } = "test@example.com"; + + public ValidatableSubType SubType { get; set; } = new ValidatableSubType(); + + public IEnumerable Validate(ValidationContext validationContext) + { + var rangeService = (IRangeService?)validationContext.GetService(typeof(IRangeService)); + var minimum = rangeService?.GetMinimum(); + var maximum = rangeService?.GetMaximum(); + if (Value1 < minimum || Value1 > maximum) + { + yield return new ValidationResult($"The field {nameof(Value1)} must be between {minimum} and {maximum}.", [nameof(Value1)]); + } + } +} + +public class SubType +{ + [Required] + // This gets ignored since it has an unsupported constructor name + [Display(ShortName = "SubType")] + public string RequiredProperty { get; set; } = "some-value"; + + [StringLength(10)] + public string? StringWithLength { get; set; } +} + +public class ValidatableSubType : SubType, IValidatableObject +{ + public string Value3 { get; set; } = "some-value"; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Value3 != "some-value") + { + yield return new ValidationResult($"The field {validationContext.DisplayName} must be 'some-value'.", [nameof(Value3)]); + } + } +} + +public interface IRangeService +{ + int GetMinimum(); + int GetMaximum(); +} + +public class RangeService : IRangeService +{ + public int GetMinimum() => 10; + public int GetMaximum() => 100; +} + +public class TestService +{ + [Range(10, 100)] + public int Value { get; set; } = 4; +} +"""; + + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/validatable-object", async (endpoint, serviceProvider) => + { + await ValidateMethodCalledIfPropertyValidationsFail(); + await ValidateForSubtypeInvokedFirst(); + await ValidateForTopLevelInvoked(); + + async Task ValidateMethodCalledIfPropertyValidationsFail() + { + var httpContext = CreateHttpContextWithPayload(""" + { + "Value1": 5, + "Value2": "", + "SubType": { + "Value3": "foo", + "RequiredProperty": "", + "StringWithLength": "" + } + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("Value2", error.Key); + Assert.Collection(error.Value, + msg => Assert.Equal("The Value2 field is required.", msg)); + }, + error => + { + Assert.Equal("SubType.RequiredProperty", error.Key); + Assert.Equal("The RequiredProperty field is required.", error.Value.Single()); + }, + error => + { + Assert.Equal("SubType.Value3", error.Key); + Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); + }, + error => + { + Assert.Equal("Value1", error.Key); + Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); + }); + } + + async Task ValidateForSubtypeInvokedFirst() + { + var httpContext = CreateHttpContextWithPayload(""" + { + "Value1": 5, + "Value2": "test@test.com", + "SubType": { + "Value3": "foo", + "RequiredProperty": "some-value-2", + "StringWithLength": "element" + } + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("SubType.Value3", error.Key); + Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); + }, + error => + { + Assert.Equal("Value1", error.Key); + Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); + }); + } + + async Task ValidateForTopLevelInvoked() + { + var httpContext = CreateHttpContextWithPayload(""" + { + "Value1": 5, + "Value2": "test@test.com", + "SubType": { + "Value3": "some-value", + "RequiredProperty": "some-value-2", + "StringWithLength": "element" + } + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("Value1", error.Key); + Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); + }); + } + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.MultipleNamespaces.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.MultipleNamespaces.cs new file mode 100644 index 000000000000..7272bc84ab92 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.MultipleNamespaces.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateMultipleNamespaces() + { + // Arrange + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapPost("/namespace-one", (NamespaceOne.Type obj) => Results.Ok("Passed")); +app.MapPost("/namespace-two", (NamespaceTwo.Type obj) => Results.Ok("Passed")); + +app.Run(); + +namespace NamespaceOne { + public class Type + { + [StringLength(10)] + public string StringWithLength { get; set; } = string.Empty; + } +} + +namespace NamespaceTwo { + public class Type + { + [StringLength(20)] + public string StringWithLength { get; set; } = string.Empty; + } +} +"""; + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/namespace-one", async (endpoint, serviceProvider) => + { + await InvalidStringWithLengthProducesError(endpoint); + await ValidInputProducesNoWarnings(endpoint); + + async Task InvalidStringWithLengthProducesError(Endpoint endpoint) + { + var payload = """ + { + "StringWithLength": "abcdefghijk" + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task ValidInputProducesNoWarnings(Endpoint endpoint) + { + var payload = """ + { + "StringWithLength": "abc" + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + await endpoint.RequestDelegate(context); + + Assert.Equal(200, context.Response.StatusCode); + } + }); + await VerifyEndpoint(compilation, "/namespace-two", async (endpoint, serviceProvider) => + { + await InvalidStringWithLengthProducesError(endpoint); + await ValidInputProducesNoWarnings(endpoint); + + async Task InvalidStringWithLengthProducesError(Endpoint endpoint) + { + var payload = """ + { + "StringWithLength": "abcdefghijklmnopqrstu" + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 20.", kvp.Value.Single()); + }); + } + + async Task ValidInputProducesNoWarnings(Endpoint endpoint) + { + var payload = """ + { + "StringWithLength": "abcdefghijk" + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + await endpoint.RequestDelegate(context); + + Assert.Equal(200, context.Response.StatusCode); + } + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.NoOp.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.NoOp.cs new file mode 100644 index 000000000000..843b6a7c2106 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.NoOp.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task DoesNotEmitIfNoAddValidationCallExists() + { + // Arrange + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +var app = builder.Build(); + +app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed")); + +app.Run(); + +public class ComplexType +{ + [Range(10, 100)] + public int IntegerWithRange { get; set; } = 10; +} +"""; + await Verify(source, out var compilation); + // Verify that we don't validate types if no AddValidation call exists + await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => + { + var payload = """ + { + "IntegerWithRange": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + }); + } + + [Fact] + public async Task DoesNotEmitIfNotCorrectAddValidationCallExists() + { + // Arrange + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation("example"); +SomeExtensions.AddValidation(builder.Services); + +var app = builder.Build(); + +app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed")); + +app.Run(); + +public class ComplexType +{ + [Range(10, 100)] + public int IntegerWithRange { get; set; } = 10; +} + +public static class SomeExtensions +{ + public static IServiceCollection AddValidation(this IServiceCollection services, string someString) + { + // This is not the correct AddValidation method + return services; + } + + public static IServiceCollection AddValidation(this IServiceCollection services, Action? configureOptions = null) + { + // This is not the correct AddValidation method + return services; + } +} +"""; + await Verify(source, out var compilation); + // Verify that we don't validate types if no AddValidation call exists + await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => + { + var payload = """ + { + "IntegerWithRange": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + }); + } + + [Fact] + public async Task DoesNotEmitForExemptTypes() + { + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapGet("/exempt-1", (HttpContext context) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-2", (HttpRequest request) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-3", (HttpResponse response) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-4", (IFormCollection formCollection) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-5", (IFormFileCollection formFileCollection) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-6", (IFormFile formFile) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-7", (Stream stream) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-8", (PipeReader pipeReader) => Results.Ok("Exempt Passed!")); +app.MapGet("/exempt-9", (CancellationToken cancellationToken) => Results.Ok("Exempt Passed!")); +app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed")); + +app.Run(); + +public class ComplexType +{ + [Range(10, 100)] + public int IntegerWithRange { get; set; } = 10; +} +"""; + await Verify(source, out var compilation); + // Verify that we can validate non-exempt types + await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => + { + var payload = """ + { + "IntegerWithRange": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithRange", kvp.Key); + Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); + }); + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parameters.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parameters.cs new file mode 100644 index 000000000000..56eb8016cd4a --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parameters.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateParameters() + { + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); +builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("serviceKey"); + +var app = builder.Build(); + +app.MapPost("/params", ( + // Skipped from validation because it is resolved as a service by IServiceProviderIsService + TestService testService, + // Skipped from validation because it is marked as a [FromKeyedService] parameter + [FromKeyedServices("serviceKey")] TestService testService2, + [Range(10, 100)] int value1, + [Range(10, 100), Display(Name = "Valid identifier")] int value2, + [Required] string value3 = "some-value", + [CustomValidation(ErrorMessage = "Value must be an even number")] int value4 = 4, + [CustomValidation, Range(10, 100)] int value5 = 10, + // Skipped from validation because it is marked as a [FromService] parameter + [FromServices] [Range(10, 100)] int? value6 = 4, + Dictionary? testDict = null) => "OK"); + +app.Run(); + +public class CustomValidationAttribute : ValidationAttribute +{ + public override bool IsValid(object? value) => value is int number && number % 2 == 0; +} + +public class TestService +{ + [Range(10, 100)] + public int Value { get; set; } = 4; +} +"""; + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/params", async (endpoint, serviceProvider) => + { + var context = CreateHttpContext(serviceProvider); + context.Request.QueryString = new QueryString("?value1=5&value2=5&value3=&value4=3&value5=5"); + await endpoint.RequestDelegate(context); + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("value1", error.Key); + Assert.Equal("The field value1 must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("value2", error.Key); + Assert.Equal("The field Valid identifier must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("value3", error.Key); + Assert.Equal("The value3 field is required.", error.Value.Single()); + }, + error => + { + Assert.Equal("value4", error.Key); + Assert.Equal("Value must be an even number", error.Value.Single()); + }, + error => + { + Assert.Equal("value5", error.Key); + Assert.Collection(error.Value, error => + { + Assert.Equal("The field value5 is invalid.", error); + }, + error => + { + Assert.Equal("The field value5 must be between 10 and 100.", error); + }); + }); + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parsable.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parsable.cs new file mode 100644 index 000000000000..3c3ae69658ff --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parsable.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateTypeWithParsableProperties() + { + // Arrange + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapPost("/complex-type-with-parsable-properties", (ComplexTypeWithParsableProperties complexType) => Results.Ok("Passed"!)); + +app.Run(); + +public class ComplexTypeWithParsableProperties +{ + [RegularExpression("^((?!00000000-0000-0000-0000-000000000000).)*$", ErrorMessage = "Cannot use default Guid")] + public Guid? GuidWithRegularExpression { get; set; } = default; + + [Required] + public TimeOnly? TimeOnlyWithRequiredValue { get; set; } = TimeOnly.FromDateTime(DateTime.UtcNow); + + [Url(ErrorMessage = "The field Url must be a valid URL.")] + public string? Url { get; set; } = "https://example.com"; + + [Required] + [Range(typeof(DateOnly), "2023-01-01", "2025-12-31", ErrorMessage = "Date must be between 2023-01-01 and 2025-12-31")] + public DateOnly? DateOnlyWithRange { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow); + + [Range(typeof(DateTime), "2023-01-01", "2025-12-31", ErrorMessage = "DateTime must be between 2023-01-01 and 2025-12-31")] + public DateTime? DateTimeWithRange { get; set; } = DateTime.UtcNow; + + [Range(typeof(decimal), "0.1", "100.5", ErrorMessage = "Amount must be between 0.1 and 100.5")] + public decimal? DecimalWithRange { get; set; } = 50.5m; + + [Range(0, 12, ErrorMessage = "Hours must be between 0 and 12")] + public TimeSpan? TimeSpanWithHourRange { get; set; } = TimeSpan.FromHours(12); + + [Range(0, 1, ErrorMessage = "Boolean value must be 0 or 1")] + public bool BooleanWithRange { get; set; } = true; + + [RegularExpression(@"^\d+\.\d+\.\d+$", ErrorMessage = "Must be a valid version number (e.g. 1.0.0)")] + public Version? VersionWithRegex { get; set; } = new Version(1, 0, 0); +} +"""; + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/complex-type-with-parsable-properties", async (endpoint, serviceProvider) => + { + var payload = """ + { + "TimeOnlyWithRequiredValue": null, + "IntWithRange": 150, + "StringWithLength": "AB", + "Email": "invalid-email", + "Url": "invalid-url", + "DateOnlyWithRange": "2026-05-01", + "DecimalWithRange": "150.75", + "TimeSpanWithHourRange": "22:00:00", + "VersionWithRegex": "1.0", + "EnumProperty": "Invalid" + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + + // Assert on each error with Assert.Collection + Assert.Collection(problemDetails.Errors.OrderBy(kvp => kvp.Key), + error => + { + Assert.Equal("DateOnlyWithRange", error.Key); + Assert.Contains("Date must be between 2023-01-01 and 2025-12-31", error.Value); + }, + error => + { + Assert.Equal("DecimalWithRange", error.Key); + Assert.Contains("Amount must be between 0.1 and 100.5", error.Value); + }, + error => + { + Assert.Equal("TimeOnlyWithRequiredValue", error.Key); + Assert.Contains("The TimeOnlyWithRequiredValue field is required.", error.Value); + }, + error => + { + Assert.Equal("TimeSpanWithHourRange", error.Key); + Assert.Contains("Hours must be between 0 and 12", error.Value); + }, + error => + { + Assert.Equal("Url", error.Key); + Assert.Contains("The field Url must be a valid URL.", error.Value); + }, + error => + { + Assert.Equal("VersionWithRegex", error.Key); + Assert.Contains("Must be a valid version number (e.g. 1.0.0)", error.Value); + } + ); + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Polymorphism.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Polymorphism.cs new file mode 100644 index 000000000000..f511aa8dad5f --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Polymorphism.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidatePolymorphicTypes() + { + var source = """ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapPost("/basic-polymorphism", (BaseType model) => Results.Ok()); +app.MapPost("/validatable-polymorphism", (BaseValidatableType model) => Results.Ok()); +app.MapPost("/polymorphism-container", (ContainerType model) => Results.Ok()); + +app.Run(); + +public class ContainerType +{ + public BaseType BaseType { get; set; } = new BaseType(); + public BaseValidatableType BaseValidatableType { get; set; } = new BaseValidatableType(); +} + +[JsonDerivedType(typeof(BaseType), typeDiscriminator: "base")] +[JsonDerivedType(typeof(DerivedType), typeDiscriminator: "derived")] +public class BaseType +{ + [Display(Name = "Value 1")] + [Range(10, 100)] + public int Value1 { get; set; } + + [EmailAddress] + [Required] + public string Value2 { get; set; } = "test@example.com"; +} + +public class DerivedType : BaseType +{ + [Base64String] + public string? Value3 { get; set; } +} + +[JsonDerivedType(typeof(BaseValidatableType), typeDiscriminator: "base")] +[JsonDerivedType(typeof(DerivedValidatableType), typeDiscriminator: "derived")] +public class BaseValidatableType : IValidatableObject +{ + [Display(Name = "Value 1")] + public int Value1 { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Value1 < 10 || Value1 > 100) + { + yield return new ValidationResult("The field Value 1 must be between 10 and 100.", new[] { nameof(Value1) }); + } + } +} + +public class DerivedValidatableType : BaseValidatableType +{ + [EmailAddress] + public required string Value3 { get; set; } +} +"""; + await Verify(source, out var compilation); + + await VerifyEndpoint(compilation, "/basic-polymorphism", async (endpoint, serviceProvider) => + { + var httpContext = CreateHttpContextWithPayload(""" + { + "$type": "derived", + "Value1": 5, + "Value2": "invalid-email", + "Value3": "invalid-base64" + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("Value3", error.Key); + Assert.Equal("The Value3 field is not a valid Base64 encoding.", error.Value.Single()); + }, + error => + { + Assert.Equal("Value1", error.Key); + Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Value2", error.Key); + Assert.Equal("The Value2 field is not a valid e-mail address.", error.Value.Single()); + }); + }); + + await VerifyEndpoint(compilation, "/validatable-polymorphism", async (endpoint, serviceProvider) => + { + var httpContext = CreateHttpContextWithPayload(""" + { + "$type": "derived", + "Value1": 5, + "Value3": "invalid-email" + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("Value3", error.Key); + Assert.Equal("The Value3 field is not a valid e-mail address.", error.Value.Single()); + }, + error => + { + Assert.Equal("Value1", error.Key); + Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); + }); + + httpContext = CreateHttpContextWithPayload(""" + { + "$type": "derived", + "Value1": 5, + "Value3": "test@example.com" + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails1 = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails1.Errors, + error => + { + Assert.Equal("Value1", error.Key); + Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); + }); + }); + + await VerifyEndpoint(compilation, "/polymorphism-container", async (endpoint, serviceProvider) => + { + var httpContext = CreateHttpContextWithPayload(""" + { + "BaseType": { + "$type": "derived", + "Value1": 5, + "Value2": "invalid-email", + "Value3": "invalid-base64" + }, + "BaseValidatableType": { + "$type": "derived", + "Value1": 5, + "Value3": "test@example.com" + } + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("BaseType.Value3", error.Key); + Assert.Equal("The Value3 field is not a valid Base64 encoding.", error.Value.Single()); + }, + error => + { + Assert.Equal("BaseType.Value1", error.Key); + Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("BaseType.Value2", error.Key); + Assert.Equal("The Value2 field is not a valid e-mail address.", error.Value.Single()); + }, + error => + { + Assert.Equal("BaseValidatableType.Value1", error.Key); + Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); + }); + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.RecordType.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.RecordType.cs new file mode 100644 index 000000000000..f8254fd72bc3 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.RecordType.cs @@ -0,0 +1,371 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateRecordTypes() + { + // Arrange + var source = """ +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.MapPost("/validatable-record", (ValidatableRecord validatableRecord) => Results.Ok("Passed"!)); + +app.Run(); + +public class DerivedValidationAttribute : ValidationAttribute +{ + public override bool IsValid(object? value) => value is int number && number % 2 == 0; +} + +public record SubType([Required] string RequiredProperty = "some-value", [StringLength(10)] string? StringWithLength = default); + +public record SubTypeWithInheritance([EmailAddress] string? EmailString, string RequiredProperty, string? StringWithLength) : SubType(RequiredProperty, StringWithLength); + +public record SubTypeWithoutConstructor +{ + [Required] + public string RequiredProperty { get; set; } = "some-value"; + + [StringLength(10)] + public string? StringWithLength { get; set; } +} + +public static class CustomValidators +{ + public static ValidationResult Validate(int number, ValidationContext validationContext) + { + var parent = (ValidatableRecord)validationContext.ObjectInstance; + if (number == parent.IntegerWithRange) + { + return new ValidationResult( + "Can't use the same number value in two properties on the same class.", + new[] { validationContext.MemberName }); + } + + return ValidationResult.Success; + } +} + +public record ValidatableRecord( + [Range(10, 100)] + int IntegerWithRange = 10, + [Range(10, 100), Display(Name = "Valid identifier")] + int IntegerWithRangeAndDisplayName = 50, + SubType PropertyWithMemberAttributes = default, + SubType PropertyWithoutMemberAttributes = default, + SubTypeWithInheritance PropertyWithInheritance = default, + SubTypeWithoutConstructor PropertyOfSubtypeWithoutConstructor = default, + List ListOfSubTypes = default, + [DerivedValidation(ErrorMessage = "Value must be an even number")] + int IntegerWithDerivedValidationAttribute = 0, + [CustomValidation(typeof(CustomValidators), nameof(CustomValidators.Validate))] + int IntegerWithCustomValidation = 0, + [DerivedValidation, Range(10, 100)] + int PropertyWithMultipleAttributes = 10 +); +"""; + await Verify(source, out var compilation); + await VerifyEndpoint(compilation, "/validatable-record", async (endpoint, serviceProvider) => + { + await InvalidIntegerWithRangeProducesError(endpoint); + await InvalidIntegerWithRangeAndDisplayNameProducesError(endpoint); + await InvalidRequiredSubtypePropertyProducesError(endpoint); + await InvalidSubTypeWithInheritancePropertyProducesError(endpoint); + await InvalidListOfSubTypesProducesError(endpoint); + await InvalidPropertyWithDerivedValidationAttributeProducesError(endpoint); + await InvalidPropertyWithMultipleAttributesProducesError(endpoint); + await InvalidPropertyWithCustomValidationProducesError(endpoint); + await InvalidPropertyOfSubtypeWithoutConstructorProducesError(endpoint); + await ValidInputProducesNoWarnings(endpoint); + + async Task InvalidIntegerWithRangeProducesError(Endpoint endpoint) + { + + var payload = """ + { + "IntegerWithRange": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithRange", kvp.Key); + Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task InvalidIntegerWithRangeAndDisplayNameProducesError(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithRangeAndDisplayName": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key); + Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task InvalidRequiredSubtypePropertyProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyWithMemberAttributes": { + "RequiredProperty": "", + "StringWithLength": "way-too-long" + } + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyWithInheritance": { + "RequiredProperty": "", + "StringWithLength": "way-too-long", + "EmailString": "not-an-email" + } + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + kvp => + { + Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key); + Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidListOfSubTypesProducesError(Endpoint endpoint) + { + var payload = """ + { + "ListOfSubTypes": [ + { + "RequiredProperty": "", + "StringWithLength": "way-too-long" + }, + { + "RequiredProperty": "valid", + "StringWithLength": "way-too-long" + }, + { + "RequiredProperty": "valid", + "StringWithLength": "valid" + } + ] + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + kvp => + { + Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithDerivedValidationAttribute": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithDerivedValidationAttribute", kvp.Key); + Assert.Equal("Value must be an even number", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyWithMultipleAttributes": 5 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("PropertyWithMultipleAttributes", kvp.Key); + Assert.Collection(kvp.Value, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error); + }, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error); + }); + }); + } + + async Task InvalidPropertyWithCustomValidationProducesError(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithRange": 42, + "IntegerWithCustomValidation": 42 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, kvp => + { + Assert.Equal("IntegerWithCustomValidation", kvp.Key); + var error = Assert.Single(kvp.Value); + Assert.Equal("Can't use the same number value in two properties on the same class.", error); + }); + } + + async Task InvalidPropertyOfSubtypeWithoutConstructorProducesError(Endpoint endpoint) + { + var payload = """ + { + "PropertyOfSubtypeWithoutConstructor": { + "RequiredProperty": "", + "StringWithLength": "way-too-long" + } + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + + await endpoint.RequestDelegate(context); + + var problemDetails = await AssertBadRequest(context); + Assert.Collection(problemDetails.Errors, + kvp => + { + Assert.Equal("PropertyOfSubtypeWithoutConstructor.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyOfSubtypeWithoutConstructor.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task ValidInputProducesNoWarnings(Endpoint endpoint) + { + var payload = """ + { + "IntegerWithRange": 50, + "IntegerWithRangeAndDisplayName": 50, + "PropertyWithMemberAttributes": { + "RequiredProperty": "valid", + "StringWithLength": "valid" + }, + "PropertyWithoutMemberAttributes": { + "RequiredProperty": "valid", + "StringWithLength": "valid" + }, + "PropertyWithInheritance": { + "RequiredProperty": "valid", + "StringWithLength": "valid", + "EmailString": "test@example.com" + }, + "ListOfSubTypes": [], + "IntegerWithDerivedValidationAttribute": 2, + "IntegerWithCustomValidation": 0, + "PropertyWithMultipleAttributes": 12 + } + """; + var context = CreateHttpContextWithPayload(payload, serviceProvider); + await endpoint.RequestDelegate(context); + + Assert.Equal(200, context.Response.StatusCode); + } + }); + + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Recursion.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Recursion.cs new file mode 100644 index 000000000000..464a91113085 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Recursion.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateRecursiveTypes() + { + var source = """ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); +builder.Services.AddValidation(options => +{ + options.MaxDepth = 8; +}); + +var app = builder.Build(); + +app.MapPost("/recursive-type", (RecursiveType model) => Results.Ok()); + +app.Run(); + +public class RecursiveType +{ + [Range(10, 100)] + public int Value { get; set; } + public RecursiveType? Next { get; set; } +} +"""; + await Verify(source, out var compilation); + + await VerifyEndpoint(compilation, "/recursive-type", async (endpoint, serviceProvider) => + { + await ThrowsExceptionForDeeplyNestedType(endpoint); + await ValidatesTypeWithLimitedNesting(endpoint); + + async Task ThrowsExceptionForDeeplyNestedType(Endpoint endpoint) + { + var httpContext = CreateHttpContextWithPayload(""" + { + "value": 1, + "next": { + "value": 2, + "next": { + "value": 3, + "next": { + "value": 4, + "next": { + "value": 5, + "next": { + "value": 6, + "next": { + "value": 7, + "next": { + "value": 8, + "next": { + "value": 9, + "next": { + "value": 10 + } + } + } + } + } + } + } + } + } + } + """, serviceProvider); + + var exception = await Assert.ThrowsAsync(async () => await endpoint.RequestDelegate(httpContext)); + } + + async Task ValidatesTypeWithLimitedNesting(Endpoint endpoint) + { + var httpContext = CreateHttpContextWithPayload(""" + { + "value": 1, + "next": { + "value": 2, + "next": { + "value": 3, + "next": { + "value": 4, + "next": { + "value": 5, + "next": { + "value": 6, + "next": { + "value": 7, + "next": { + "value": 8 + } + } + } + } + } + } + } + } + """, serviceProvider); + + await endpoint.RequestDelegate(httpContext); + + var problemDetails = await AssertBadRequest(httpContext); + Assert.Collection(problemDetails.Errors, + error => + { + Assert.Equal("Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Next.Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Next.Next.Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Next.Next.Next.Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Next.Next.Next.Next.Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Next.Next.Next.Next.Next.Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Next.Next.Next.Next.Next.Next.Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }, + error => + { + Assert.Equal("Next.Next.Next.Next.Next.Next.Next.Value", error.Key); + Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); + }); + } + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ValidatableType.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ValidatableType.cs new file mode 100644 index 000000000000..12cb575ee150 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ValidatableType.cs @@ -0,0 +1,381 @@ +#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. + +// 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 Microsoft.Extensions.Validation; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase +{ + [Fact] + public async Task CanValidateTypesWithAttribute() + { + var source = """ +#pragma warning disable ASP0029 + +using System; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddValidation(); + +var app = builder.Build(); + +app.Run(); + +[ValidatableType] +public class ComplexType +{ + [Range(10, 100)] + public int IntegerWithRange { get; set; } = 10; + + [Range(10, 100), Display(Name = "Valid identifier")] + public int IntegerWithRangeAndDisplayName { get; set; } = 50; + + [Required] + public SubType PropertyWithMemberAttributes { get; set; } = new SubType(); + + public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType(); + + public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance(); + + public List ListOfSubTypes { get; set; } = []; + + [CustomValidation(ErrorMessage = "Value must be an even number")] + public int IntegerWithCustomValidationAttribute { get; set; } + + [CustomValidation, Range(10, 100)] + public int PropertyWithMultipleAttributes { get; set; } = 10; +} + +public class CustomValidationAttribute : ValidationAttribute +{ + public override bool IsValid(object? value) => value is int number && number % 2 == 0; +} + +public class SubType +{ + [Required] + public string RequiredProperty { get; set; } = "some-value"; + + [StringLength(10)] + public string? StringWithLength { get; set; } +} + +public class SubTypeWithInheritance : SubType +{ + [EmailAddress] + public string? EmailString { get; set; } +} +"""; + await Verify(source, out var compilation); + VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) => + { + Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo)); + + await InvalidIntegerWithRangeProducesError(validatableTypeInfo); + await InvalidIntegerWithRangeAndDisplayNameProducesError(validatableTypeInfo); + await MissingRequiredSubtypePropertyProducesError(validatableTypeInfo); + await InvalidRequiredSubtypePropertyProducesError(validatableTypeInfo); + await InvalidSubTypeWithInheritancePropertyProducesError(validatableTypeInfo); + await InvalidListOfSubTypesProducesError(validatableTypeInfo); + await InvalidPropertyWithDerivedValidationAttributeProducesError(validatableTypeInfo); + await InvalidPropertyWithMultipleAttributesProducesError(validatableTypeInfo); + await InvalidPropertyWithCustomValidationProducesError(validatableTypeInfo); + await ValidInputProducesNoWarnings(validatableTypeInfo); + + async Task InvalidIntegerWithRangeProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithRange")?.SetValue(instance, 5); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithRange", kvp.Key); + Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task InvalidIntegerWithRangeAndDisplayNameProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 5); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key); + Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single()); + }); + } + + async Task MissingRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, null); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("PropertyWithMemberAttributes", kvp.Key); + Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single()); + }); + } + + async Task InvalidRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var subType = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType.GetType().GetProperty("RequiredProperty")?.SetValue(subType, ""); + subType.GetType().GetProperty("StringWithLength")?.SetValue(subType, "way-too-long"); + type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidSubTypeWithInheritancePropertyProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!); + inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, ""); + inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "way-too-long"); + inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "not-an-email"); + type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key); + Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidListOfSubTypesProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + var subTypeList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!)); + + // Create first invalid item + var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, ""); + subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "way-too-long"); + + // Create second invalid item + var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid"); + subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "way-too-long"); + + // Create valid item + var subType3 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType3.GetType().GetProperty("RequiredProperty")?.SetValue(subType3, "valid"); + subType3.GetType().GetProperty("StringWithLength")?.SetValue(subType3, "valid"); + + // Add to list + subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType1]); + subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType2]); + subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType3]); + + type.GetProperty("ListOfSubTypes")?.SetValue(instance, subTypeList); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, + kvp => + { + Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key); + Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }, + kvp => + { + Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key); + Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithDerivedValidationAttributeProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 5); // Odd number, should fail + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key); + Assert.Equal("Value must be an even number", kvp.Value.Single()); + }); + } + + async Task InvalidPropertyWithMultipleAttributesProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 5); + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("PropertyWithMultipleAttributes", kvp.Key); + Assert.Collection(kvp.Value, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error); + }, + error => + { + Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error); + }); + }); + } + + async Task InvalidPropertyWithCustomValidationProducesError(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 3); // Odd number should fail + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Collection(context.ValidationErrors, kvp => + { + Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key); + Assert.Equal("Value must be an even number", kvp.Value.Single()); + }); + } + + async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo) + { + var instance = Activator.CreateInstance(type); + + // Set all properties with valid values + type.GetProperty("IntegerWithRange")?.SetValue(instance, 50); + type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 50); + + // Create and set PropertyWithMemberAttributes + var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, "valid"); + subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "valid"); + type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType1); + + // Create and set PropertyWithoutMemberAttributes + var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); + subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid"); + subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "valid"); + type.GetProperty("PropertyWithoutMemberAttributes")?.SetValue(instance, subType2); + + // Create and set PropertyWithInheritance + var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!); + inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, "valid"); + inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "valid"); + inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "test@example.com"); + type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType); + + // Create empty list for ListOfSubTypes + var emptyList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!)); + type.GetProperty("ListOfSubTypes")?.SetValue(instance, emptyList); + + // Set custom validation attributes + type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 2); // Even number should pass + type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 12); + + var context = new ValidateContext + { + ValidationOptions = validationOptions, + ValidationContext = new ValidationContext(instance) + }; + + await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); + + Assert.Null(context.ValidationErrors); + } + }); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGeneratorTestBase.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGeneratorTestBase.cs new file mode 100644 index 000000000000..cd9affc4a8a4 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGeneratorTestBase.cs @@ -0,0 +1,586 @@ +#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. + +// 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; +using System.Globalization; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Validation; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Routing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using static Microsoft.AspNetCore.Http.Generators.Tests.RequestDelegateCreationTestBase; + +namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; + +[UsesVerify] +public partial class ValidationsGeneratorTestBase : LoggedTestBase +{ + [GeneratedRegex(@"\[global::System\.Runtime\.CompilerServices\.InterceptsLocationAttribute\([^)]*\)\]")] + private static partial Regex InterceptsLocationRegex(); + + private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview) + .WithFeatures([new KeyValuePair("InterceptorsNamespaces", "Microsoft.Extensions.Validation.Generated")]); + + internal static Task Verify(string source, out Compilation compilation) + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) + .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) + .Concat( + [ + MetadataReference.CreateFromFile(typeof(WebApplicationBuilder).Assembly.Location), + MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.AspNetCore.Mvc.ApiExplorer.IApiDescriptionProvider).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.AspNetCore.Mvc.ControllerBase).Assembly.Location), + MetadataReference.CreateFromFile(typeof(MvcCoreMvcBuilderExtensions).Assembly.Location), + MetadataReference.CreateFromFile(typeof(TypedResults).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Text.Json.Nodes.JsonArray).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.ValidationAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(RouteData).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IFeatureCollection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ValidateOptionsResult).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IHttpMethodMetadata).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IResult).Assembly.Location), + MetadataReference.CreateFromFile(typeof(HttpJsonServiceExtensions).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IValidatableInfoResolver).Assembly.Location), + MetadataReference.CreateFromFile(typeof(EndpointFilterFactoryContext).Assembly.Location), + ]); + var inputCompilation = CSharpCompilation.Create("ValidationsGeneratorSample", + [CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: "Program.cs")], + references, + new CSharpCompilationOptions(OutputKind.ConsoleApplication)); + var generator = new ValidationsGenerator(); + var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions); + return Verifier + .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics)) + .ScrubLinesWithReplace(line => InterceptsLocationRegex().Replace(line, "[InterceptsLocation]")) + .UseDirectory(SkipOnHelixAttribute.OnHelix() + ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "ValidationsGenerator", "snapshots") + : "snapshots"); + } + + internal static void VerifyValidatableType(Compilation compilation, string typeName, Action verifyFunc) + { + if (TryResolveServicesFromCompilation(compilation, targetAssemblyName: "Microsoft.AspNetCore.Http.Abstractions", typeName: "Microsoft.Extensions.Validation.ValidationOptions", out var services, out var serviceType, out var outputAssemblyName) is false) + { + throw new InvalidOperationException("Could not resolve services from compilation."); + } + var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == outputAssemblyName); + var type = targetAssembly.GetType(typeName, throwOnError: false); + + // Get IOptions first + var optionsType = typeof(IOptions<>).MakeGenericType(serviceType); + var optionsInstance = services.GetService(optionsType) ?? throw new InvalidOperationException("Could not resolve IOptions."); + + // Then access the Value property + var valueProperty = optionsType.GetProperty("Value"); + var service = (ValidationOptions)valueProperty.GetValue(optionsInstance) ?? throw new InvalidOperationException("Could not resolve ValidationOptions."); + verifyFunc(service, type); + } + + internal static async Task VerifyEndpoint(Compilation compilation, string routePattern, Func verifyFunc) + { + if (TryResolveServicesFromCompilation(compilation, targetAssemblyName: "Microsoft.AspNetCore.Routing", typeName: "Microsoft.AspNetCore.Routing.EndpointDataSource", out var services, out var serviceType, out var outputAssemblyName) is false) + { + throw new InvalidOperationException("Could not resolve services from compilation."); + } + var service = services.GetService(serviceType) ?? throw new InvalidOperationException("Could not resolve EndpointDataSource."); + var endpoints = (IReadOnlyList)service.GetType().GetProperty("Endpoints", BindingFlags.Instance | BindingFlags.Public).GetValue(service); + var endpoint = endpoints.FirstOrDefault(endpoint => endpoint is RouteEndpoint routeEndpoint && routeEndpoint.RoutePattern.RawText == routePattern); + await verifyFunc(endpoint, services); + } + + private static bool TryResolveServicesFromCompilation(Compilation compilation, string targetAssemblyName, string typeName, out IServiceProvider serviceProvider, out Type serviceType, out string outputAssemblyName) + { + serviceProvider = null; + serviceType = null; + outputAssemblyName = $"TestProject-{Guid.NewGuid()}"; + var assemblyName = compilation.AssemblyName; + var symbolsName = Path.ChangeExtension(assemblyName, "pdb"); + + var output = new MemoryStream(); + var pdb = new MemoryStream(); + + var emitOptions = new EmitOptions( + debugInformationFormat: DebugInformationFormat.PortablePdb, + pdbFilePath: symbolsName, + outputNameOverride: outputAssemblyName); + + var embeddedTexts = new List(); + + foreach (var syntaxTree in compilation.SyntaxTrees) + { + var text = syntaxTree.GetText(); + var encoding = text.Encoding ?? Encoding.UTF8; + var buffer = encoding.GetBytes(text.ToString()); + var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true); + + var syntaxRootNode = (CSharpSyntaxNode)syntaxTree.GetRoot(); + var newSyntaxTree = CSharpSyntaxTree.Create(syntaxRootNode, options: ParseOptions, encoding: encoding, path: syntaxTree.FilePath); + + compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree); + + embeddedTexts.Add(EmbeddedText.FromSource(syntaxTree.FilePath, sourceText)); + } + + var result = compilation.Emit(output, pdb, options: emitOptions, embeddedTexts: embeddedTexts); + + Assert.Empty(result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); + Assert.True(result.Success); + + output.Position = 0; + pdb.Position = 0; + + var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb); + + void ConfigureHostBuilder(object hostBuilder) + { + ((IHostBuilder)hostBuilder).ConfigureServices((context, services) => + { + services.AddSingleton(); + services.AddSingleton(); + }); + } + + var waitForStartTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + void OnEntryPointExit(Exception exception) + { + // If the entry point exited, we'll try to complete the wait + if (exception != null) + { + waitForStartTcs.TrySetException(exception); + } + else + { + waitForStartTcs.TrySetResult(0); + } + } + + var factory = HostFactoryResolver.ResolveHostFactory(assembly, + stopApplication: false, + configureHostBuilder: ConfigureHostBuilder, + entrypointCompleted: OnEntryPointExit); + + if (factory == null) + { + return false; + } + + var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; + + var applicationLifetime = services.GetRequiredService(); + using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(0)); + waitForStartTcs.Task.Wait(); + var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == targetAssemblyName); + serviceType = targetAssembly.GetType(typeName, throwOnError: false); + + if (serviceType == null) + { + return false; + } + + serviceProvider = services; + return true; + } + + private sealed class NoopHostLifetime : IHostLifetime + { + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + private sealed class NoopServer : IServer + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + public void Dispose() { } + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + private sealed class HostFactoryResolver + { + private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + + public const string BuildWebHost = nameof(BuildWebHost); + public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder); + public const string CreateHostBuilder = nameof(CreateHostBuilder); + private const string TimeoutEnvironmentKey = "DOTNET_HOST_FACTORY_RESOLVER_DEFAULT_TIMEOUT_IN_SECONDS"; + + // The amount of time we wait for the diagnostic source events to fire + private static readonly TimeSpan s_defaultWaitTimeout = SetupDefaultTimeout(); + + private static TimeSpan SetupDefaultTimeout() + { + if (Debugger.IsAttached) + { + return Timeout.InfiniteTimeSpan; + } + + if (uint.TryParse(Environment.GetEnvironmentVariable(TimeoutEnvironmentKey), out uint timeoutInSeconds)) + { + return TimeSpan.FromSeconds((int)timeoutInSeconds); + } + + return TimeSpan.FromMinutes(5); + } + + public static Func ResolveWebHostFactory(Assembly assembly) + { + return ResolveFactory(assembly, BuildWebHost); + } + + public static Func ResolveWebHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateWebHostBuilder); + } + + public static Func ResolveHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateHostBuilder); + } + + // This helpers encapsulates all of the complex logic required to: + // 1. Execute the entry point of the specified assembly in a different thread. + // 2. Wait for the diagnostic source events to fire + // 3. Give the caller a chance to execute logic to mutate the IHostBuilder + // 4. Resolve the instance of the applications's IHost + // 5. Allow the caller to determine if the entry point has completed + public static Func ResolveHostFactory(Assembly assembly, + TimeSpan waitTimeout = default, + bool stopApplication = true, + Action configureHostBuilder = null, + Action entrypointCompleted = null) + { + if (assembly.EntryPoint is null) + { + return null; + } + + return args => new HostingListener(args, assembly.EntryPoint, waitTimeout == default ? s_defaultWaitTimeout : waitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost(); + } + + private static Func ResolveFactory(Assembly assembly, string name) + { + var programType = assembly.EntryPoint.DeclaringType; + if (programType == null) + { + return null; + } + + var factory = programType.GetMethod(name, DeclaredOnlyLookup); + if (!IsFactory(factory)) + { + return null; + } + + return args => (T)factory!.Invoke(null, [args])!; + } + + // TReturn Factory(string[] args); + private static bool IsFactory(MethodInfo factory) + { + return factory != null + && typeof(TReturn).IsAssignableFrom(factory.ReturnType) + && factory.GetParameters().Length == 1 + && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); + } + + // Used by EF tooling without any Hosting references. Looses some return type safety checks. + public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan waitTimeout = default) + { + // Prefer the older patterns by default for back compat. + var webHostFactory = ResolveWebHostFactory(assembly); + if (webHostFactory != null) + { + return args => + { + var webHost = webHostFactory(args); + return GetServiceProvider(webHost); + }; + } + + var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly); + if (webHostBuilderFactory != null) + { + return args => + { + var webHostBuilder = webHostBuilderFactory(args); + var webHost = Build(webHostBuilder); + return GetServiceProvider(webHost); + }; + } + + var hostBuilderFactory = ResolveHostBuilderFactory(assembly); + if (hostBuilderFactory != null) + { + return args => + { + var hostBuilder = hostBuilderFactory(args); + var host = Build(hostBuilder); + return GetServiceProvider(host); + }; + } + + var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout); + if (hostFactory != null) + { + return args => + { + static bool IsApplicationNameArg(string arg) + => arg.Equals("--applicationName", StringComparison.OrdinalIgnoreCase) || + arg.Equals("/applicationName", StringComparison.OrdinalIgnoreCase); + + if (!args.Any(arg => IsApplicationNameArg(arg)) && assembly.GetName().Name is string assemblyName) + { + args = [.. args, .. new[] { "--applicationName", assemblyName }]; + } + + var host = hostFactory(args); + return GetServiceProvider(host); + }; + } + + return null; + } + + private static object Build(object builder) + { + var buildMethod = builder.GetType().GetMethod("Build"); + return buildMethod.Invoke(builder, []); + } + + private static IServiceProvider GetServiceProvider(object host) + { + if (host == null) + { + return null; + } + var hostType = host.GetType(); + var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup); + return (IServiceProvider)servicesProperty.GetValue(host); + } + + private sealed class HostingListener : IObserver, IObserver> + { + private readonly string[] _args; + private readonly MethodInfo _entryPoint; + private readonly TimeSpan _waitTimeout; + private readonly bool _stopApplication; + + private readonly TaskCompletionSource _hostTcs = new(); + private IDisposable _disposable; + private readonly Action _configure; + private readonly Action _entrypointCompleted; + private static readonly AsyncLocal _currentListener = new(); + + public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entrypointCompleted) + { + _args = args; + _entryPoint = entryPoint; + _waitTimeout = waitTimeout; + _stopApplication = stopApplication; + _configure = configure; + _entrypointCompleted = entrypointCompleted; + } + + public object CreateHost() + { + using var subscription = DiagnosticListener.AllListeners.Subscribe(this); + + // Kick off the entry point on a new thread so we don't block the current one + // in case we need to timeout the execution + var thread = new Thread(() => + { + Exception exception = null; + + try + { + // Set the async local to the instance of the HostingListener so we can filter events that + // aren't scoped to this execution of the entry point. + _currentListener.Value = this; + + var parameters = _entryPoint.GetParameters(); + if (parameters.Length == 0) + { + _entryPoint.Invoke(null, []); + } + else + { + _entryPoint.Invoke(null, new object[] { _args }); + } + + // Try to set an exception if the entry point returns gracefully, this will force + // build to throw + _hostTcs.TrySetException(new InvalidOperationException("The entry point exited without ever building an IHost.")); + } + catch (TargetInvocationException tie) when (tie.InnerException.GetType().Name == "HostAbortedException") + { + // The host was stopped by our own logic + } + catch (TargetInvocationException tie) + { + exception = tie.InnerException ?? tie; + + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(exception); + } + catch (Exception ex) + { + exception = ex; + + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(ex); + } + finally + { + // Signal that the entry point is completed + _entrypointCompleted.Invoke(exception); + } + }) + { + // Make sure this doesn't hang the process + IsBackground = true + }; + + // Start the thread + thread.Start(); + + try + { + // Wait before throwing an exception + if (!_hostTcs.Task.Wait(_waitTimeout)) + { + throw new InvalidOperationException($"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable."); + } + } + catch (AggregateException) when (_hostTcs.Task.IsCompleted) + { + // Lets this propagate out of the call to GetAwaiter().GetResult() + } + + Debug.Assert(_hostTcs.Task.IsCompleted); + + return _hostTcs.Task.GetAwaiter().GetResult(); + } + + public void OnCompleted() + { + _disposable.Dispose(); + } + + public void OnError(Exception error) + { + + } + + public void OnNext(DiagnosticListener value) + { + if (_currentListener.Value != this) + { + // Ignore events that aren't for this listener + return; + } + + if (value.Name == "Microsoft.Extensions.Hosting") + { + _disposable = value.Subscribe(this); + } + } + + public void OnNext(KeyValuePair value) + { + if (_currentListener.Value != this) + { + // Ignore events that aren't for this listener + return; + } + + if (value.Key == "HostBuilding") + { + _configure.Invoke(value.Value!); + } + + if (value.Key == "HostBuilt") + { + _hostTcs.TrySetResult(value.Value!); + + if (_stopApplication) + { + // Stop the host from running further + ThrowHostAborted(); + } + } + } + + // HostFactoryResolver is used by tools that explicitly don't want to reference Microsoft.Extensions.Hosting assemblies. + // So don't depend on the public HostAbortedException directly. Instead, load the exception type dynamically if it can + // be found. If it can't (possibly because the app is using an older version), throw a private exception with the same name. + private static void ThrowHostAborted() + { + var publicHostAbortedExceptionType = Type.GetType("Microsoft.Extensions.Hosting.HostAbortedException, Microsoft.Extensions.Hosting.Abstractions", throwOnError: false); + if (publicHostAbortedExceptionType != null) + { + throw (Exception)Activator.CreateInstance(publicHostAbortedExceptionType)!; + } + else + { + throw new HostAbortedException(); + } + } + + private sealed class HostAbortedException : Exception + { + } + } + } + + internal HttpContext CreateHttpContext(IServiceProvider serviceProvider) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = serviceProvider; + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + return httpContext; + } + + internal HttpContext CreateHttpContextWithPayload(string requestData, IServiceProvider serviceProvider = null) + { + var httpContext = CreateHttpContext(serviceProvider); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + httpContext.Request.Headers["Content-Type"] = "application/json"; + + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(requestData)); + httpContext.Request.Body = stream; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + return httpContext; + } + + internal static async Task AssertBadRequest(HttpContext context) + { + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + context.Response.Body.Position = 0; + using var reader = new StreamReader(context.Response.Body); + var responseBody = await reader.ReadToEndAsync(); + return JsonSerializer.Deserialize(responseBody); + } +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..2fe89720bc38 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,244 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::SubType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "RequiredProperty", + displayName: "RequiredProperty" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + if (type == typeof(global::SubTypeWithInheritance)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubTypeWithInheritance), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubTypeWithInheritance), + propertyType: typeof(string), + name: "EmailString", + displayName: "EmailString" + ), + ] + ); + return true; + } + if (type == typeof(global::ComplexType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithRange", + displayName: "IntegerWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithRangeAndDisplayName", + displayName: "Valid identifier" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubType), + name: "PropertyWithMemberAttributes", + displayName: "PropertyWithMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubType), + name: "PropertyWithoutMemberAttributes", + displayName: "PropertyWithoutMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubTypeWithInheritance), + name: "PropertyWithInheritance", + displayName: "PropertyWithInheritance" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::System.Collections.Generic.List), + name: "ListOfSubTypes", + displayName: "ListOfSubTypes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithDerivedValidationAttribute", + displayName: "IntegerWithDerivedValidationAttribute" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithCustomValidation", + displayName: "IntegerWithCustomValidation" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "PropertyWithMultipleAttributes", + displayName: "PropertyWithMultipleAttributes" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..b7d1a9c61ed3 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,195 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::SubType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "RequiredProperty", + displayName: "RequiredProperty" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + if (type == typeof(global::ValidatableSubType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ValidatableSubType), + members: [] + ); + return true; + } + if (type == typeof(global::ComplexValidatableType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexValidatableType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexValidatableType), + propertyType: typeof(string), + name: "Value2", + displayName: "Value2" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexValidatableType), + propertyType: typeof(global::ValidatableSubType), + name: "SubType", + displayName: "SubType" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..eb8ca791d504 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,175 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::NamespaceOne.Type)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::NamespaceOne.Type), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::NamespaceOne.Type), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + if (type == typeof(global::NamespaceTwo.Type)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::NamespaceTwo.Type), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::NamespaceTwo.Type), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..fb9570f39fb1 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,193 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::TestService)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::TestService), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::TestService), + propertyType: typeof(int), + name: "Value", + displayName: "Value" + ), + ] + ); + return true; + } + if (type == typeof(global::System.Collections.Generic.Dictionary)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::System.Collections.Generic.Dictionary), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::System.Collections.Generic.Dictionary), + propertyType: typeof(global::System.Collections.Generic.ICollection), + name: "System.Collections.Generic.IDictionary.Values", + displayName: "System.Collections.Generic.IDictionary.Values" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::System.Collections.Generic.Dictionary), + propertyType: typeof(global::System.Collections.Generic.IEnumerable), + name: "System.Collections.Generic.IReadOnlyDictionary.Values", + displayName: "System.Collections.Generic.IReadOnlyDictionary.Values" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::System.Collections.Generic.Dictionary), + propertyType: typeof(global::TestService), + name: "this[]", + displayName: "this[]" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::System.Collections.Generic.Dictionary), + propertyType: typeof(global::System.Collections.ICollection), + name: "System.Collections.IDictionary.Values", + displayName: "System.Collections.IDictionary.Values" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..86845c4ad153 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,225 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::DerivedType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::DerivedType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::DerivedType), + propertyType: typeof(string), + name: "Value3", + displayName: "Value3" + ), + ] + ); + return true; + } + if (type == typeof(global::BaseType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::BaseType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::BaseType), + propertyType: typeof(int), + name: "Value1", + displayName: "Value 1" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::BaseType), + propertyType: typeof(string), + name: "Value2", + displayName: "Value2" + ), + ] + ); + return true; + } + if (type == typeof(global::DerivedValidatableType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::DerivedValidatableType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::DerivedValidatableType), + propertyType: typeof(string), + name: "Value3", + displayName: "Value3" + ), + ] + ); + return true; + } + if (type == typeof(global::BaseValidatableType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::BaseValidatableType), + members: [] + ); + return true; + } + if (type == typeof(global::ContainerType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ContainerType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ContainerType), + propertyType: typeof(global::BaseType), + name: "BaseType", + displayName: "BaseType" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ContainerType), + propertyType: typeof(global::BaseValidatableType), + name: "BaseValidatableType", + displayName: "BaseValidatableType" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..a5c86c3b4b4e --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,271 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::SubType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "RequiredProperty", + displayName: "RequiredProperty" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + if (type == typeof(global::SubTypeWithInheritance)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubTypeWithInheritance), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubTypeWithInheritance), + propertyType: typeof(string), + name: "EmailString", + displayName: "EmailString" + ), + ] + ); + return true; + } + if (type == typeof(global::SubTypeWithoutConstructor)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubTypeWithoutConstructor), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubTypeWithoutConstructor), + propertyType: typeof(string), + name: "RequiredProperty", + displayName: "RequiredProperty" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubTypeWithoutConstructor), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + if (type == typeof(global::ValidatableRecord)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ValidatableRecord), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(int), + name: "IntegerWithRange", + displayName: "IntegerWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(int), + name: "IntegerWithRangeAndDisplayName", + displayName: "Valid identifier" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(global::SubType), + name: "PropertyWithMemberAttributes", + displayName: "PropertyWithMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(global::SubType), + name: "PropertyWithoutMemberAttributes", + displayName: "PropertyWithoutMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(global::SubTypeWithInheritance), + name: "PropertyWithInheritance", + displayName: "PropertyWithInheritance" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(global::SubTypeWithoutConstructor), + name: "PropertyOfSubtypeWithoutConstructor", + displayName: "PropertyOfSubtypeWithoutConstructor" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(global::System.Collections.Generic.List), + name: "ListOfSubTypes", + displayName: "ListOfSubTypes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(int), + name: "IntegerWithDerivedValidationAttribute", + displayName: "IntegerWithDerivedValidationAttribute" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(int), + name: "IntegerWithCustomValidation", + displayName: "IntegerWithCustomValidation" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ValidatableRecord), + propertyType: typeof(int), + name: "PropertyWithMultipleAttributes", + displayName: "PropertyWithMultipleAttributes" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..e4958777e0d3 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,166 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::RecursiveType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::RecursiveType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::RecursiveType), + propertyType: typeof(int), + name: "Value", + displayName: "Value" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::RecursiveType), + propertyType: typeof(global::RecursiveType), + name: "Next", + displayName: "Next" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..c7b0cba7ed13 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,208 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::ComplexTypeWithParsableProperties)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexTypeWithParsableProperties), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(global::System.Guid?), + name: "GuidWithRegularExpression", + displayName: "GuidWithRegularExpression" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(global::System.TimeOnly?), + name: "TimeOnlyWithRequiredValue", + displayName: "TimeOnlyWithRequiredValue" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(string), + name: "Url", + displayName: "Url" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(global::System.DateOnly?), + name: "DateOnlyWithRange", + displayName: "DateOnlyWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(global::System.DateTime?), + name: "DateTimeWithRange", + displayName: "DateTimeWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(decimal?), + name: "DecimalWithRange", + displayName: "DecimalWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(global::System.TimeSpan?), + name: "TimeSpanWithHourRange", + displayName: "TimeSpanWithHourRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(bool), + name: "BooleanWithRange", + displayName: "BooleanWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexTypeWithParsableProperties), + propertyType: typeof(global::System.Version), + name: "VersionWithRegex", + displayName: "VersionWithRegex" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..8d4bb3df2285 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,238 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::SubType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "RequiredProperty", + displayName: "RequiredProperty" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubType), + propertyType: typeof(string), + name: "StringWithLength", + displayName: "StringWithLength" + ), + ] + ); + return true; + } + if (type == typeof(global::SubTypeWithInheritance)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::SubTypeWithInheritance), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::SubTypeWithInheritance), + propertyType: typeof(string), + name: "EmailString", + displayName: "EmailString" + ), + ] + ); + return true; + } + if (type == typeof(global::ComplexType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithRange", + displayName: "IntegerWithRange" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithRangeAndDisplayName", + displayName: "Valid identifier" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubType), + name: "PropertyWithMemberAttributes", + displayName: "PropertyWithMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubType), + name: "PropertyWithoutMemberAttributes", + displayName: "PropertyWithoutMemberAttributes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::SubTypeWithInheritance), + name: "PropertyWithInheritance", + displayName: "PropertyWithInheritance" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(global::System.Collections.Generic.List), + name: "ListOfSubTypes", + displayName: "ListOfSubTypes" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithCustomValidationAttribute", + displayName: "IntegerWithCustomValidationAttribute" + ), + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "PropertyWithMultipleAttributes", + displayName: "PropertyWithMultipleAttributes" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs new file mode 100644 index 000000000000..34d61eb409d1 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -0,0 +1,160 @@ +//HintName: ValidatableInfoResolver.g.cs +#nullable enable annotations +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable ASP0029 + +namespace System.Runtime.CompilerServices +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + } + } +} + +namespace Microsoft.Extensions.Validation.Generated +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo + { + public GeneratedValidatablePropertyInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + global::System.Type propertyType, + string name, + string displayName) : base(containingType, propertyType, name, displayName) + { + ContainingType = containingType; + Name = name; + } + + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + internal global::System.Type ContainingType { get; } + internal string Name { get; } + + protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() + => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo + { + public GeneratedValidatableTypeInfo( + [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] + global::System.Type type, + ValidatablePropertyInfo[] members) : base(type, members) { } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver + { + public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + if (type == typeof(global::ComplexType)) + { + validatableInfo = new GeneratedValidatableTypeInfo( + type: typeof(global::ComplexType), + members: [ + new GeneratedValidatablePropertyInfo( + containingType: typeof(global::ComplexType), + propertyType: typeof(int), + name: "IntegerWithRange", + displayName: "IntegerWithRange" + ), + ] + ); + return true; + } + + return false; + } + + // No-ops, rely on runtime code for ParameterInfo-based resolution + public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) + { + validatableInfo = null; + return false; + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class GeneratedServiceCollectionExtensions + { + [InterceptsLocation] + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) + { + // Use non-extension method to avoid infinite recursion. + return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => + { + options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); + if (configureOptions is not null) + { + configureOptions(options); + } + }); + } + } + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + file static class ValidationAttributeCache + { + private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); + private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); + + public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( + [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] + global::System.Type containingType, + string propertyName) + { + var key = new CacheKey(containingType, propertyName); + return _cache.GetOrAdd(key, static k => + { + var results = new global::System.Collections.Generic.List(); + + // Get attributes from the property + var property = k.ContainingType.GetProperty(k.PropertyName); + if (property != null) + { + var propertyAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(property, inherit: true); + + results.AddRange(propertyAttributes); + } + + // Check constructors for parameters that match the property name + // to handle record scenarios + foreach (var constructor in k.ContainingType.GetConstructors()) + { + // Look for parameter with matching name (case insensitive) + var parameter = global::System.Linq.Enumerable.FirstOrDefault( + constructor.GetParameters(), + p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); + + if (parameter != null) + { + var paramAttributes = global::System.Reflection.CustomAttributeExtensions + .GetCustomAttributes(parameter, inherit: true); + + results.AddRange(paramAttributes); + + break; + } + } + + return results.ToArray(); + }); + } + } +} \ No newline at end of file From 47a71f187696079a48a03853dad33cf684cf28a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 16:21:00 +0000 Subject: [PATCH 3/7] Complete implementation of validation APIs in separate package Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com> --- .../src/Microsoft.AspNetCore.Http.Abstractions.csproj | 1 - .../Microsoft.AspNetCore.Http.ValidationsGenerator.csproj | 2 +- .../src/Microsoft.AspNetCore.Http.Extensions.csproj | 1 - src/Validation/src/Microsoft.Extensions.Validation.csproj | 3 ++- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 3196824956cf..d383efa6a20e 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -21,7 +21,6 @@ Microsoft.AspNetCore.Http.HttpResponse - diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj index d666ac8015e6..4a069fa0ce65 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index e1560b3d4428..aa9645a5faa1 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -43,7 +43,6 @@ - diff --git a/src/Validation/src/Microsoft.Extensions.Validation.csproj b/src/Validation/src/Microsoft.Extensions.Validation.csproj index 495114e86263..977d1518a04e 100644 --- a/src/Validation/src/Microsoft.Extensions.Validation.csproj +++ b/src/Validation/src/Microsoft.Extensions.Validation.csproj @@ -3,11 +3,12 @@ Common validation abstractions and validation infrastructure for .NET applications. $(DefaultNetCoreTargetFramework) - true + false true validation true true + true From 93c627ccc1c06e2b113c9df71d7a7327933fba51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 16:46:51 +0000 Subject: [PATCH 4/7] Remove type forwards and original validation code files Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com> --- .../src/PublicAPI.Unshipped.txt | 39 --- .../ForwardingValidationDirectives.cs | 18 -- .../src/Validation/IValidatableInfo.cs | 22 -- .../Validation/IValidatableInfoResolver.cs | 33 --- ...RuntimeValidatableParameterInfoResolver.cs | 111 --------- .../src/Validation/TypeExtensions.cs | 134 ----------- .../Validation/ValidatableParameterInfo.cs | 139 ----------- .../src/Validation/ValidatablePropertyInfo.cs | 173 -------------- .../Validation/ValidatableTypeAttribute.cs | 16 -- .../src/Validation/ValidatableTypeInfo.cs | 133 ----------- .../src/Validation/ValidateContext.cs | 100 -------- .../src/Validation/ValidationOptions.cs | 75 ------ .../ValidationServiceCollectionExtensions.cs | 34 --- .../Emitters/ValidationsGenerator.Emitter.cs | 222 ------------------ .../Extensions/ISymbolExtensions.cs | 35 --- .../Extensions/ITypeSymbolExtensions.cs | 141 ----------- .../IncrementalValuesProviderExtensions.cs | 112 --------- ...spNetCore.Http.ValidationsGenerator.csproj | 4 - .../Models/RequiredSymbols.cs | 25 -- .../Models/ValidatableProperty.cs | 15 -- .../Models/ValidatableType.cs | 12 - .../Models/ValidatableTypeComparer.cs | 30 --- .../Models/ValidationAttribute.cs | 14 -- .../ValidationsGenerator.AddValidation.cs | 38 --- .../ValidationsGenerator.AttributeParser.cs | 31 --- .../ValidationsGenerator.EndpointsParser.cs | 50 ---- .../ValidationsGenerator.TypesParser.cs | 212 ----------------- .../ValidationsGenerator.cs | 47 ---- .../ValidationsGeneratorForwarding.cs | 20 -- 29 files changed, 2035 deletions(-) delete mode 100644 src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/IValidatableInfo.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/IValidatableInfoResolver.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/RuntimeValidatableParameterInfoResolver.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/TypeExtensions.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/ValidatableTypeAttribute.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/ValidateContext.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/ValidationOptions.cs delete mode 100644 src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Emitters/ValidationsGenerator.Emitter.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ISymbolExtensions.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/IncrementalValuesProviderExtensions.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/RequiredSymbols.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableProperty.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableType.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableTypeComparer.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidationAttribute.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AddValidation.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 8cb332fa9f20..3a2461306d54 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,44 +1,5 @@ #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.Metadata.IDisableValidationMetadata 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.IValidatableInfo -Microsoft.AspNetCore.Http.Validation.IValidatableInfo.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver -Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) -> bool -Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) -> bool -Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo -Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName) -> void -Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo -Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.ValidatablePropertyInfo(System.Type! declaringType, System.Type! propertyType, string! name, string! displayName) -> void -Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute -Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute.ValidatableTypeAttribute() -> void -Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo -Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidatableTypeInfo(System.Type! type, System.Collections.Generic.IReadOnlyList! members) -> void -Microsoft.AspNetCore.Http.Validation.ValidateContext -Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.get -> int -Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.set -> void -Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.get -> string! -Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.set -> void -Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidateContext() -> void -Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext! -Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.set -> void -Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.get -> System.Collections.Generic.Dictionary? -Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.set -> void -Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.get -> Microsoft.AspNetCore.Http.Validation.ValidationOptions! -Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.set -> 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.ValidationOptions.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) -> bool -Microsoft.AspNetCore.Http.Validation.ValidationOptions.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.AspNetCore.Http.Validation.IValidatableInfo? 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? configureOptions = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -virtual Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -virtual Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -virtual Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! diff --git a/src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs b/src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs deleted file mode 100644 index 0d7c3c48d63d..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ForwardingValidationDirectives.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Forward validation-related types from Microsoft.Extensions.Validation -// to maintain backward compatibility - -using Microsoft.Extensions.DependencyInjection; -using System.Runtime.CompilerServices; - -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.IValidatableInfo))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.IValidatableInfoResolver))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatableParameterInfo))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatablePropertyInfo))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatableTypeAttribute))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidatableTypeInfo))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidateContext))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.Validation.ValidationOptions))] -[assembly: TypeForwardedTo(typeof(Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions))] \ No newline at end of file diff --git a/src/Http/Http.Abstractions/src/Validation/IValidatableInfo.cs b/src/Http/Http.Abstractions/src/Validation/IValidatableInfo.cs deleted file mode 100644 index d2a7ccd1fbcf..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/IValidatableInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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.AspNetCore.Http.Validation; - -/// -/// Represents an interface for validating a value. -/// -[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] -public interface IValidatableInfo -{ - /// - /// Validates the specified . - /// - /// The value to validate. - /// The validation context. - /// A cancellation token to support cancellation of the validation. - /// A representing the asynchronous operation. - Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken); -} diff --git a/src/Http/Http.Abstractions/src/Validation/IValidatableInfoResolver.cs b/src/Http/Http.Abstractions/src/Validation/IValidatableInfoResolver.cs deleted file mode 100644 index 80a2f0d0d748..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/IValidatableInfoResolver.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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; -using System.Reflection; - -namespace Microsoft.AspNetCore.Http.Validation; - -/// -/// Provides an interface for resolving the validation information associated -/// with a given or . -/// -[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] -public interface IValidatableInfoResolver -{ - /// - /// Gets validation information for the specified type. - /// - /// The type to get validation information for. - /// - /// The output parameter that will contain the validatable information if found. - /// - /// if the validatable type information was found; otherwise, false. - bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo); - - /// - /// Gets validation information for the specified parameter. - /// - /// The parameter to get validation information for. - /// The output parameter that will contain the validatable information if found. - /// if the validatable parameter information was found; otherwise, false. - bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo); -} diff --git a/src/Http/Http.Abstractions/src/Validation/RuntimeValidatableParameterInfoResolver.cs b/src/Http/Http.Abstractions/src/Validation/RuntimeValidatableParameterInfoResolver.cs deleted file mode 100644 index 0728c3ac8b8d..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/RuntimeValidatableParameterInfoResolver.cs +++ /dev/null @@ -1,111 +0,0 @@ -#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. - -// 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.CodeAnalysis; -using System.IO.Pipelines; -using System.Linq; -using System.Reflection; -using System.Security.Claims; - -namespace Microsoft.AspNetCore.Http.Validation; - -internal sealed class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver -{ - // TODO: the implementation currently relies on static discovery of types. - public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - - public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) - { - if (parameterInfo.Name == null) - { - throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name."); - } - - var validationAttributes = parameterInfo - .GetCustomAttributes() - .ToArray(); - - // If there are no validation attributes and this type is not a complex type - // we don't need to validate it. Complex types without attributes are still - // validatable because we want to run the validations on the properties. - if (validationAttributes.Length == 0 && !IsClass(parameterInfo.ParameterType)) - { - validatableInfo = null; - return false; - } - validatableInfo = new RuntimeValidatableParameterInfo( - parameterType: parameterInfo.ParameterType, - name: parameterInfo.Name, - displayName: GetDisplayName(parameterInfo), - validationAttributes: validationAttributes - ); - return true; - } - - private static string GetDisplayName(ParameterInfo parameterInfo) - { - var displayAttribute = parameterInfo.GetCustomAttribute(); - if (displayAttribute != null) - { - return displayAttribute.Name ?? parameterInfo.Name!; - } - - return parameterInfo.Name!; - } - - internal sealed class RuntimeValidatableParameterInfo( - Type parameterType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) : - ValidatableParameterInfo(parameterType, name, displayName) - { - protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; - - private readonly ValidationAttribute[] _validationAttributes = validationAttributes; - } - - private static bool IsClass(Type type) - { - // Skip primitives, enums, common built-in types, and types that are specially - // handled by RDF/RDG that don't need validation if they don't have attributes - if (type.IsPrimitive || - type.IsEnum || - type == typeof(string) || - type == typeof(decimal) || - type == typeof(DateTime) || - type == typeof(DateTimeOffset) || - type == typeof(TimeOnly) || - type == typeof(DateOnly) || - type == typeof(TimeSpan) || - type == typeof(Guid) || - type == typeof(IFormFile) || - type == typeof(IFormFileCollection) || - type == typeof(IFormCollection) || - type == typeof(HttpContext) || - type == typeof(HttpRequest) || - type == typeof(HttpResponse) || - type == typeof(ClaimsPrincipal) || - type == typeof(CancellationToken) || - type == typeof(Stream) || - type == typeof(PipeReader)) - { - return false; - } - - // Check if the underlying type in a nullable is valid - if (Nullable.GetUnderlyingType(type) is { } nullableType) - { - return IsClass(nullableType); - } - - return type.IsClass; - } -} diff --git a/src/Http/Http.Abstractions/src/Validation/TypeExtensions.cs b/src/Http/Http.Abstractions/src/Validation/TypeExtensions.cs deleted file mode 100644 index 244f0b3fe888..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/TypeExtensions.cs +++ /dev/null @@ -1,134 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.Validation; - -internal static class TypeExtensions -{ - /// - /// Determines whether the specified type is an enumerable type. - /// - /// The type to check. - /// if the type is enumerable; otherwise, . - public static bool IsEnumerable(this Type type) - { - // Check if type itself is an IEnumerable - if (type.IsGenericType && - (type.GetGenericTypeDefinition() == typeof(IEnumerable<>) || - type.GetGenericTypeDefinition() == typeof(ICollection<>) || - type.GetGenericTypeDefinition() == typeof(List<>) || - type.GetGenericTypeDefinition() == typeof(IList<>))) - { - return true; - } - - // Or an array - if (type.IsArray) - { - return true; - } - - // Then evaluate if it implements IEnumerable and is not a string - if (typeof(IEnumerable).IsAssignableFrom(type) && - type != typeof(string)) - { - return true; - } - - return false; - } - - /// - /// Determines whether the specified type is a nullable type. - /// - /// The type to check. - /// if the type is nullable; otherwise, . - public static bool IsNullable(this Type type) - { - if (type.IsValueType) - { - return false; - } - - if (type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - return true; - } - - return false; - } - - /// - /// Tries to get the from the specified array of validation attributes. - /// - /// The array of to search. - /// The found if available, otherwise null. - /// if a is found; otherwise, . - public static bool TryGetRequiredAttribute(this ValidationAttribute[] attributes, [NotNullWhen(true)] out RequiredAttribute? requiredAttribute) - { - foreach (var attribute in attributes) - { - if (attribute is RequiredAttribute requiredAttr) - { - requiredAttribute = requiredAttr; - return true; - } - } - - requiredAttribute = null; - return false; - } - - /// - /// Gets all types that the specified type implements or inherits from. - /// - /// The type to analyze. - /// A collection containing all implemented interfaces and all base types of the given type. - public static List GetAllImplementedTypes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type) - { - ArgumentNullException.ThrowIfNull(type); - - var implementedTypes = new List(); - - // Yield all interfaces directly and indirectly implemented by this type - foreach (var interfaceType in type.GetInterfaces()) - { - implementedTypes.Add(interfaceType); - } - - // Finally, walk up the inheritance chain - var baseType = type.BaseType; - while (baseType != null && baseType != typeof(object)) - { - implementedTypes.Add(baseType); - baseType = baseType.BaseType; - } - - return implementedTypes; - } - - /// - /// Determines whether the specified type implements the given interface. - /// - /// The type to check. - /// The interface type to check for. - /// True if the type implements the specified interface; otherwise, false. - public static bool ImplementsInterface(this Type type, Type interfaceType) - { - ArgumentNullException.ThrowIfNull(type); - ArgumentNullException.ThrowIfNull(interfaceType); - - // Check if interfaceType is actually an interface - if (!interfaceType.IsInterface) - { - throw new ArgumentException($"Type {interfaceType.FullName} is not an interface.", nameof(interfaceType)); - } - - return interfaceType.IsAssignableFrom(type); - } -} diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs deleted file mode 100644 index 48de32c0daff..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.Validation; - -/// -/// Contains validation information for a parameter. -/// -[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] -public abstract class ValidatableParameterInfo : IValidatableInfo -{ - private RequiredAttribute? _requiredAttribute; - - /// - /// Creates a new instance of . - /// - /// The associated with the parameter. - /// The parameter name. - /// The display name for the parameter. - protected ValidatableParameterInfo( - Type parameterType, - string name, - string displayName) - { - ParameterType = parameterType; - Name = name; - DisplayName = displayName; - } - - /// - /// Gets the parameter type. - /// - internal Type ParameterType { get; } - - /// - /// Gets the parameter name. - /// - internal string Name { get; } - - /// - /// Gets the display name for the parameter. - /// - internal string DisplayName { get; } - - /// - /// Gets the validation attributes for this parameter. - /// - /// An array of validation attributes to apply to this parameter. - protected abstract ValidationAttribute[] GetValidationAttributes(); - - /// - /// - /// If the parameter is a collection, each item in the collection will be validated. - /// If the parameter is not a collection but has a validatable type, the single value will be validated. - /// - public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken) - { - // Skip validation if value is null and parameter is optional - if (value == null && ParameterType.IsNullable()) - { - return; - } - - context.ValidationContext.DisplayName = DisplayName; - context.ValidationContext.MemberName = Name; - - var validationAttributes = GetValidationAttributes(); - - if (_requiredAttribute is not null || validationAttributes.TryGetRequiredAttribute(out _requiredAttribute)) - { - var result = _requiredAttribute.GetValidationResult(value, context.ValidationContext); - - if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) - { - var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}"; - context.AddValidationError(key, [result.ErrorMessage]); - return; - } - } - - // Validate against validation attributes - for (var i = 0; i < validationAttributes.Length; i++) - { - var attribute = validationAttributes[i]; - try - { - var result = attribute.GetValidationResult(value, context.ValidationContext); - if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) - { - var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}"; - context.AddOrExtendValidationErrors(key, [result.ErrorMessage]); - } - } - catch (Exception ex) - { - var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}"; - context.AddValidationError(key, [ex.Message]); - } - } - - // If the parameter is a collection, validate each item - if (ParameterType.IsEnumerable() && value is IEnumerable enumerable) - { - var index = 0; - var currentPrefix = context.CurrentValidationPath; - - foreach (var item in enumerable) - { - if (item != null) - { - context.CurrentValidationPath = string.IsNullOrEmpty(currentPrefix) - ? $"{Name}[{index}]" - : $"{currentPrefix}.{Name}[{index}]"; - - if (context.ValidationOptions.TryGetValidatableTypeInfo(item.GetType(), out var validatableType)) - { - await validatableType.ValidateAsync(item, context, cancellationToken); - } - } - index++; - } - - context.CurrentValidationPath = currentPrefix; - } - // If not enumerable, validate the single value - else if (value != null) - { - var valueType = value.GetType(); - if (context.ValidationOptions.TryGetValidatableTypeInfo(valueType, out var validatableType)) - { - await validatableType.ValidateAsync(value, context, cancellationToken); - } - } - } -} diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs deleted file mode 100644 index 0b16e34d1dc9..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs +++ /dev/null @@ -1,173 +0,0 @@ -// 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.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.Validation; - -/// -/// Contains validation information for a member of a type. -/// -[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] -public abstract class ValidatablePropertyInfo : IValidatableInfo -{ - private RequiredAttribute? _requiredAttribute; - - /// - /// Creates a new instance of . - /// - protected ValidatablePropertyInfo( - [param: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] - Type declaringType, - Type propertyType, - string name, - string displayName) - { - DeclaringType = declaringType; - PropertyType = propertyType; - Name = name; - DisplayName = displayName; - } - - /// - /// Gets the member type. - /// - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] - internal Type DeclaringType { get; } - - /// - /// Gets the member type. - /// - internal Type PropertyType { get; } - - /// - /// Gets the member name. - /// - internal string Name { get; } - - /// - /// Gets the display name for the member as designated by the . - /// - internal string DisplayName { get; } - - /// - /// Gets the validation attributes for this member. - /// - /// An array of validation attributes to apply to this member. - protected abstract ValidationAttribute[] GetValidationAttributes(); - - /// - public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken) - { - var property = DeclaringType.GetProperty(Name) ?? throw new InvalidOperationException($"Property '{Name}' not found on type '{DeclaringType.Name}'."); - var propertyValue = property.GetValue(value); - var validationAttributes = GetValidationAttributes(); - - // Calculate and save the current path - var originalPrefix = context.CurrentValidationPath; - if (string.IsNullOrEmpty(originalPrefix)) - { - context.CurrentValidationPath = Name; - } - else - { - context.CurrentValidationPath = $"{originalPrefix}.{Name}"; - } - - context.ValidationContext.DisplayName = DisplayName; - context.ValidationContext.MemberName = Name; - - // Check required attribute first - if (_requiredAttribute is not null || validationAttributes.TryGetRequiredAttribute(out _requiredAttribute)) - { - var result = _requiredAttribute.GetValidationResult(propertyValue, context.ValidationContext); - - if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) - { - context.AddValidationError(context.CurrentValidationPath, [result.ErrorMessage]); - context.CurrentValidationPath = originalPrefix; // Restore prefix - return; - } - } - - // Validate any other attributes - ValidateValue(propertyValue, context.CurrentValidationPath, validationAttributes); - - // Check if we've reached the maximum depth before validating complex properties - if (context.CurrentDepth >= context.ValidationOptions.MaxDepth) - { - throw new InvalidOperationException( - $"Maximum validation depth of {context.ValidationOptions.MaxDepth} exceeded at '{context.CurrentValidationPath}' in '{DeclaringType.Name}.{Name}'. " + - "This is likely caused by a circular reference in the object graph. " + - "Consider increasing the MaxDepth in ValidationOptions if deeper validation is required."); - } - - // Increment depth counter - context.CurrentDepth++; - - try - { - // Handle enumerable values - if (PropertyType.IsEnumerable() && propertyValue is System.Collections.IEnumerable enumerable) - { - var index = 0; - var currentPrefix = context.CurrentValidationPath; - - foreach (var item in enumerable) - { - context.CurrentValidationPath = $"{currentPrefix}[{index}]"; - - if (item != null) - { - var itemType = item.GetType(); - if (context.ValidationOptions.TryGetValidatableTypeInfo(itemType, out var validatableType)) - { - await validatableType.ValidateAsync(item, context, cancellationToken); - } - } - - index++; - } - - // Restore prefix to the property name before validating the next item - context.CurrentValidationPath = currentPrefix; - } - else if (propertyValue != null) - { - // Validate as a complex object - var valueType = propertyValue.GetType(); - if (context.ValidationOptions.TryGetValidatableTypeInfo(valueType, out var validatableType)) - { - await validatableType.ValidateAsync(propertyValue, context, cancellationToken); - } - } - } - finally - { - // Always decrement the depth counter and restore prefix - context.CurrentDepth--; - context.CurrentValidationPath = originalPrefix; - } - - void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] validationAttributes) - { - for (var i = 0; i < validationAttributes.Length; i++) - { - var attribute = validationAttributes[i]; - try - { - var result = attribute.GetValidationResult(val, context.ValidationContext); - if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null) - { - context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [result.ErrorMessage]); - } - } - catch (Exception ex) - { - context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [ex.Message]); - } - } - } - } -} diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatableTypeAttribute.cs b/src/Http/Http.Abstractions/src/Validation/ValidatableTypeAttribute.cs deleted file mode 100644 index a36402ecc408..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ValidatableTypeAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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.AspNetCore.Http.Validation; - -/// -/// Indicates that a type is validatable to support discovery by the -/// validations generator. -/// -[AttributeUsage(AttributeTargets.Class)] -[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] -public sealed class ValidatableTypeAttribute : Attribute -{ -} diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs deleted file mode 100644 index 6245c43c1b69..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs +++ /dev/null @@ -1,133 +0,0 @@ -// 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.CodeAnalysis; -using System.Linq; - -namespace Microsoft.AspNetCore.Http.Validation; - -/// -/// Contains validation information for a type. -/// -[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] -public abstract class ValidatableTypeInfo : IValidatableInfo -{ - private readonly int _membersCount; - private readonly List _subTypes; - - /// - /// Creates a new instance of . - /// - /// The type being validated. - /// The members that can be validated. - protected ValidatableTypeInfo( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type, - IReadOnlyList members) - { - Type = type; - Members = members; - _membersCount = members.Count; - _subTypes = type.GetAllImplementedTypes(); - } - - /// - /// The type being validated. - /// - internal Type Type { get; } - - /// - /// The members that can be validated. - /// - internal IReadOnlyList Members { get; } - - /// - public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken) - { - if (value == null) - { - return; - } - - // Check if we've exceeded the maximum depth - if (context.CurrentDepth >= context.ValidationOptions.MaxDepth) - { - throw new InvalidOperationException( - $"Maximum validation depth of {context.ValidationOptions.MaxDepth} exceeded at '{context.CurrentValidationPath}' in '{Type.Name}'. " + - "This is likely caused by a circular reference in the object graph. " + - "Consider increasing the MaxDepth in ValidationOptions if deeper validation is required."); - } - - var originalPrefix = context.CurrentValidationPath; - - try - { - var actualType = value.GetType(); - - // First validate members - for (var i = 0; i < _membersCount; i++) - { - await Members[i].ValidateAsync(value, context, cancellationToken); - context.CurrentValidationPath = originalPrefix; - } - - // Then validate sub-types if any - foreach (var subType in _subTypes) - { - // Check if the actual type is assignable to the sub-type - // and validate it if it is - if (subType.IsAssignableFrom(actualType)) - { - if (context.ValidationOptions.TryGetValidatableTypeInfo(subType, out var subTypeInfo)) - { - await subTypeInfo.ValidateAsync(value, context, cancellationToken); - context.CurrentValidationPath = originalPrefix; - } - } - } - - // Finally validate IValidatableObject if implemented - if (Type.ImplementsInterface(typeof(IValidatableObject)) && value is IValidatableObject validatable) - { - // Important: Set the DisplayName to the type name for top-level validations - // and restore the original validation context properties - var originalDisplayName = context.ValidationContext.DisplayName; - var originalMemberName = context.ValidationContext.MemberName; - - // Set the display name to the class name for IValidatableObject validation - context.ValidationContext.DisplayName = Type.Name; - context.ValidationContext.MemberName = null; - - var validationResults = validatable.Validate(context.ValidationContext); - foreach (var validationResult in validationResults) - { - if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null) - { - // Create a validation error for each member name that is provided - foreach (var memberName in validationResult.MemberNames) - { - var key = string.IsNullOrEmpty(originalPrefix) ? - memberName : - $"{originalPrefix}.{memberName}"; - context.AddOrExtendValidationError(key, validationResult.ErrorMessage); - } - - if (!validationResult.MemberNames.Any()) - { - // If no member names are specified, then treat this as a top-level error - context.AddOrExtendValidationError(string.Empty, validationResult.ErrorMessage); - } - } - } - - // Restore the original validation context properties - context.ValidationContext.DisplayName = originalDisplayName; - context.ValidationContext.MemberName = originalMemberName; - } - } - finally - { - context.CurrentValidationPath = originalPrefix; - } - } -} diff --git a/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs b/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs deleted file mode 100644 index d38ada2ddeb1..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs +++ /dev/null @@ -1,100 +0,0 @@ -// 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.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.Validation; - -/// -/// Represents the context for validating a validatable object. -/// -[Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] -public sealed class ValidateContext -{ - /// - /// Gets or sets the validation context used for validating objects that implement or have . - /// This context provides access to service provider and other validation metadata. - /// - /// - /// This property should be set by the consumer of the - /// interface to provide the necessary context for validation. The object should be initialized - /// with the current object being validated, the display name, and the service provider to support - /// the complete set of validation scenarios. - /// - /// - /// - /// var validationContext = new ValidationContext(objectToValidate, serviceProvider, items); - /// var validationOptions = serviceProvider.GetService<IOptions<ValidationOptions>>()?.Value; - /// var validateContext = new ValidateContext - /// { - /// ValidationContext = validationContext, - /// ValidationOptions = validationOptions - /// }; - /// - /// - public required ValidationContext ValidationContext { get; set; } - - /// - /// 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"). - /// - public string CurrentValidationPath { get; set; } = string.Empty; - - /// - /// Gets or sets the validation options that control validation behavior, - /// including validation depth limits and resolver registration. - /// - public required ValidationOptions ValidationOptions { get; set; } - - /// - /// Gets or sets the dictionary of validation errors collected during validation. - /// Keys are property names or paths, and values are arrays of error messages. - /// In the default implementation, this dictionary is initialized when the first error is added. - /// - public Dictionary? ValidationErrors { get; set; } - - /// - /// Gets or sets the current depth in the validation hierarchy. - /// This is used to prevent stack overflows from circular references. - /// - 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)) - { - var newErrors = new string[existingErrors.Length + errors.Length]; - existingErrors.CopyTo(newErrors, 0); - errors.CopyTo(newErrors, existingErrors.Length); - ValidationErrors[key] = newErrors; - } - 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]; - } - } -} diff --git a/src/Http/Http.Abstractions/src/Validation/ValidationOptions.cs b/src/Http/Http.Abstractions/src/Validation/ValidationOptions.cs deleted file mode 100644 index dc83ce9282a7..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ValidationOptions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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; -using System.Reflection; - -namespace Microsoft.AspNetCore.Http.Validation; - -/// -/// Provides configuration options for the validation system. -/// -public class ValidationOptions -{ - /// - /// Gets the list of resolvers that provide validation metadata for types and parameters. - /// Resolvers are processed in order, with the first resolver providing a non-null result being used. - /// - /// - /// Source-generated resolvers are typically inserted at the beginning of this list - /// to ensure they are checked before any runtime-based resolvers. - /// - [Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] - public IList Resolvers { get; } = []; - - /// - /// Gets or sets the maximum depth for validation of nested objects. - /// This prevents stack overflows from circular references or extremely deep object graphs. - /// Default value is 32. - /// - public int MaxDepth { get; set; } = 32; - - /// - /// Attempts to get validation information for the specified type. - /// - /// The type to get validation information for. - /// When this method returns, contains the validation information for the specified type, - /// if the type was found; otherwise, null. - /// true if validation information was found for the specified type; otherwise, false. - [Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] - public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableTypeInfo) - { - foreach (var resolver in Resolvers) - { - if (resolver.TryGetValidatableTypeInfo(type, out validatableTypeInfo)) - { - return true; - } - } - - validatableTypeInfo = null; - return false; - } - - /// - /// Attempts to get validation information for the specified parameter. - /// - /// The parameter to get validation information for. - /// When this method returns, contains the validation information for the specified parameter, - /// if validation information was found; otherwise, null. - /// true if validation information was found for the specified parameter; otherwise, false. - [Experimental("ASP0029", UrlFormat = "https://aka.ms/aspnet/analyzer/{0}")] - public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) - { - foreach (var resolver in Resolvers) - { - if (resolver.TryGetValidatableParameterInfo(parameterInfo, out validatableInfo)) - { - return true; - } - } - - validatableInfo = null; - return false; - } -} diff --git a/src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs b/src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs deleted file mode 100644 index bc4f6a77abaf..000000000000 --- a/src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http.Validation; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Extension methods for adding validation services. -/// -public static class ValidationServiceCollectionExtensions -{ - /// - /// Adds the validation services to the specified . - /// - /// The to add the services to. - /// An optional action to configure the . - /// The for chaining. - public static IServiceCollection AddValidation(this IServiceCollection services, Action? configureOptions = null) - { - services.Configure(options => - { - if (configureOptions is not null) - { - configureOptions(options); - } - // Support ParameterInfo resolution at runtime -#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. - options.Resolvers.Add(new RuntimeValidatableParameterInfoResolver()); -#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. - }); - return services; - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Emitters/ValidationsGenerator.Emitter.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Emitters/ValidationsGenerator.Emitter.cs deleted file mode 100644 index 9ff704d8fcf2..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Emitters/ValidationsGenerator.Emitter.cs +++ /dev/null @@ -1,222 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using System.Text; -using Microsoft.CodeAnalysis.CSharp; -using System.IO; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -public sealed partial class ValidationsGenerator : IIncrementalGenerator -{ - public static string GeneratedCodeConstructor => $@"global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(ValidationsGenerator).Assembly.FullName}"", ""{typeof(ValidationsGenerator).Assembly.GetName().Version}"")"; - public static string GeneratedCodeAttribute => $"[{GeneratedCodeConstructor}]"; - - internal static void Emit(SourceProductionContext context, (InterceptableLocation? AddValidation, ImmutableArray ValidatableTypes) emitInputs) - { - if (emitInputs.AddValidation is null) - { - // Avoid generating code if no AddValidation call was found. - return; - } - var source = Emit(emitInputs.AddValidation, emitInputs.ValidatableTypes); - context.AddSource("ValidatableInfoResolver.g.cs", SourceText.From(source, Encoding.UTF8)); - } - - private static string Emit(InterceptableLocation addValidation, ImmutableArray validatableTypes) => $$""" -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - {{GeneratedCodeAttribute}} - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - {{GeneratedCodeAttribute}} - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - {{GeneratedCodeAttribute}} - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - {{GeneratedCodeAttribute}} - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; -{{EmitTypeChecks(validatableTypes)}} - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - {{GeneratedCodeAttribute}} - file static class GeneratedServiceCollectionExtensions - { - {{addValidation.GetInterceptsLocationAttributeSyntax()}} - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - {{GeneratedCodeAttribute}} - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} -"""; - - private static string EmitTypeChecks(ImmutableArray validatableTypes) - { - var sw = new StringWriter(); - var cw = new CodeWriter(sw, baseIndent: 3); - foreach (var validatableType in validatableTypes) - { - var typeName = validatableType.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - cw.WriteLine($"if (type == typeof({typeName}))"); - cw.StartBlock(); - cw.WriteLine($"validatableInfo = new GeneratedValidatableTypeInfo("); - cw.Indent++; - cw.WriteLine($"type: typeof({typeName}),"); - if (validatableType.Members.IsDefaultOrEmpty) - { - cw.WriteLine("members: []"); - } - else - { - cw.WriteLine("members: ["); - cw.Indent++; - foreach (var member in validatableType.Members) - { - EmitValidatableMemberForCreate(member, cw); - } - cw.Indent--; - cw.WriteLine("]"); - } - cw.Indent--; - cw.WriteLine(");"); - cw.WriteLine("return true;"); - cw.EndBlock(); - } - return sw.ToString(); - } - - private static void EmitValidatableMemberForCreate(ValidatableProperty member, CodeWriter cw) - { - cw.WriteLine("new GeneratedValidatablePropertyInfo("); - cw.Indent++; - cw.WriteLine($"containingType: typeof({member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}),"); - cw.WriteLine($"propertyType: typeof({member.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}),"); - cw.WriteLine($"name: \"{member.Name}\","); - cw.WriteLine($"displayName: \"{member.DisplayName}\""); - cw.Indent--; - cw.WriteLine("),"); - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ISymbolExtensions.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ISymbolExtensions.cs deleted file mode 100644 index 86eb4e50a43a..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ISymbolExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Linq; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal static class ISymbolExtensions -{ - public static string GetDisplayName(this ISymbol property, INamedTypeSymbol displayAttribute) - { - var displayNameAttribute = property.GetAttributes() - .FirstOrDefault(attribute => - attribute.AttributeClass is { } attributeClass && - SymbolEqualityComparer.Default.Equals(attributeClass, displayAttribute)); - - if (displayNameAttribute is not null) - { - if (!displayNameAttribute.NamedArguments.IsDefaultOrEmpty) - { - foreach (var namedArgument in displayNameAttribute.NamedArguments) - { - if (string.Equals(namedArgument.Key, "Name", StringComparison.Ordinal)) - { - return namedArgument.Value.Value?.ToString() ?? property.Name; - } - } - } - } - - return property.Name; - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs deleted file mode 100644 index 0cd85d9df756..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using System.Linq; -using Microsoft.AspNetCore.App.Analyzers.Infrastructure; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal static class ITypeSymbolExtensions -{ - public static bool IsEnumerable(this ITypeSymbol type, INamedTypeSymbol enumerable) - { - if (type.SpecialType == SpecialType.System_String) - { - return false; - } - - return type.ImplementsInterface(enumerable) || SymbolEqualityComparer.Default.Equals(type, enumerable); - } - - public static bool ImplementsValidationAttribute(this ITypeSymbol typeSymbol, INamedTypeSymbol validationAttributeSymbol) - { - var baseType = typeSymbol.BaseType; - while (baseType != null) - { - if (SymbolEqualityComparer.Default.Equals(baseType, validationAttributeSymbol)) - { - return true; - } - baseType = baseType.BaseType; - } - - return false; - } - - public static ITypeSymbol UnwrapType(this ITypeSymbol type, INamedTypeSymbol enumerable) - { - if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && - type is INamedTypeSymbol { TypeArguments.Length: 1 }) - { - // Extract the T from a Nullable - type = ((INamedTypeSymbol)type).TypeArguments[0]; - } - - if (type.NullableAnnotation == NullableAnnotation.Annotated) - { - // Remove the nullable annotation but keep any generic arguments, e.g. List? → List - // so we can retain them in future steps. - type = type.WithNullableAnnotation(NullableAnnotation.NotAnnotated); - } - - if (type is INamedTypeSymbol namedType && namedType.IsEnumerable(enumerable) && namedType.TypeArguments.Length == 1) - { - // Extract the T from an IEnumerable or List - type = namedType.TypeArguments[0]; - } - - return type; - } - - internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol interfaceType) - { - foreach (var iface in type.AllInterfaces) - { - if (SymbolEqualityComparer.Default.Equals(interfaceType, iface)) - { - return true; - } - } - return false; - } - - internal static ImmutableArray? GetJsonDerivedTypes(this ITypeSymbol type, INamedTypeSymbol jsonDerivedTypeAttribute) - { - var derivedTypes = ImmutableArray.CreateBuilder(); - foreach (var attribute in type.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, jsonDerivedTypeAttribute)) - { - var derivedType = (INamedTypeSymbol?)attribute.ConstructorArguments[0].Value; - if (derivedType is not null && !SymbolEqualityComparer.Default.Equals(derivedType, type)) - { - derivedTypes.Add(derivedType); - } - } - } - - return derivedTypes.Count == 0 ? null : derivedTypes.ToImmutable(); - } - - // Types exempted here have special binding rules in RDF and RDG and are not validatable - // types themselves so we short-circuit on them. - internal static bool IsExemptType(this ITypeSymbol type, WellKnownTypes wellKnownTypes) - { - return SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpContext)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpRequest)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_HttpResponse)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_CancellationToken)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_IFormFile)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_IO_Stream)) - || SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_IO_Pipelines_PipeReader)); - } - - internal static IPropertySymbol? FindPropertyIncludingBaseTypes(this INamedTypeSymbol typeSymbol, string propertyName) - { - var property = typeSymbol.GetMembers() - .OfType() - .FirstOrDefault(p => string.Equals(p.Name, propertyName, System.StringComparison.OrdinalIgnoreCase)); - - if (property != null) - { - return property; - } - - // If not found, recursively search base types - if (typeSymbol.BaseType is INamedTypeSymbol baseType) - { - return FindPropertyIncludingBaseTypes(baseType, propertyName); - } - - return null; - } - - /// - /// Checks if the parameter is marked with [FromService] or [FromKeyedService] attributes. - /// - /// The parameter to check. - /// The symbol representing the [FromService] attribute. - /// The symbol representing the [FromKeyedService] attribute. - internal static bool IsServiceParameter(this IParameterSymbol parameter, INamedTypeSymbol fromServiceMetadataSymbol, INamedTypeSymbol fromKeyedServiceAttributeSymbol) - { - return parameter.GetAttributes().Any(attr => - attr.AttributeClass is not null && - (attr.AttributeClass.ImplementsInterface(fromServiceMetadataSymbol) || - SymbolEqualityComparer.Default.Equals(attr.AttributeClass, fromKeyedServiceAttributeSymbol))); - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/IncrementalValuesProviderExtensions.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/IncrementalValuesProviderExtensions.cs deleted file mode 100644 index bfbbd9369b62..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/IncrementalValuesProviderExtensions.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal static class IncrementalValuesProviderExtensions -{ - public static IncrementalValuesProvider Distinct(this IncrementalValuesProvider source, IEqualityComparer comparer) - { - return source - .Collect() - .WithComparer(ImmutableArrayEqualityComparer.Instance) - .SelectMany((values, cancellationToken) => - { - if (values.IsEmpty) - { - return values; - } - - var results = ImmutableArray.CreateBuilder(values.Length); - HashSet set = new(comparer); - - foreach (var value in values) - { - if (set.Add(value)) - { - results.Add(value); - } - } - - return results.DrainToImmutable(); - }); - } - - public static IncrementalValuesProvider Concat( - this IncrementalValuesProvider> first, - IncrementalValuesProvider> second) - { - return first.Collect() - .Combine(second.Collect()) - .SelectMany((tuple, _) => - { - if (tuple.Left.IsEmpty && tuple.Right.IsEmpty) - { - return []; - } - - var results = ImmutableArray.CreateBuilder(tuple.Left.Length + tuple.Right.Length); - for (var i = 0; i < tuple.Left.Length; i++) - { - results.AddRange(tuple.Left[i]); - } - for (var i = 0; i < tuple.Right.Length; i++) - { - results.AddRange(tuple.Right[i]); - } - return results.DrainToImmutable(); - }); - } - - private sealed class ImmutableArrayEqualityComparer : IEqualityComparer> - { - public static readonly ImmutableArrayEqualityComparer Instance = new(); - - public bool Equals(ImmutableArray x, ImmutableArray y) - { - if (x.IsDefault) - { - return y.IsDefault; - } - else if (y.IsDefault) - { - return false; - } - - if (x.Length != y.Length) - { - return false; - } - - for (var i = 0; i < x.Length; i++) - { - if (!EqualityComparer.Default.Equals(x[i], y[i])) - { - return false; - } - } - - return true; - } - - public int GetHashCode(ImmutableArray obj) - { - if (obj.IsDefault) - { - return 0; - } - var hashCode = -450793227; - foreach (var item in obj) - { - hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(item); - } - - return hashCode; - } - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj index 4a069fa0ce65..560b51e35691 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj @@ -10,10 +10,6 @@ true - - - - diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/RequiredSymbols.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/RequiredSymbols.cs deleted file mode 100644 index ea16b1de1490..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/RequiredSymbols.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal sealed record class RequiredSymbols( - INamedTypeSymbol DisplayAttribute, - INamedTypeSymbol ValidationAttribute, - INamedTypeSymbol IEnumerable, - INamedTypeSymbol IValidatableObject, - INamedTypeSymbol JsonDerivedTypeAttribute, - INamedTypeSymbol RequiredAttribute, - INamedTypeSymbol CustomValidationAttribute, - INamedTypeSymbol HttpContext, - INamedTypeSymbol HttpRequest, - INamedTypeSymbol HttpResponse, - INamedTypeSymbol CancellationToken, - INamedTypeSymbol IFormCollection, - INamedTypeSymbol IFormFileCollection, - INamedTypeSymbol IFormFile, - INamedTypeSymbol Stream, - INamedTypeSymbol PipeReader -); diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableProperty.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableProperty.cs deleted file mode 100644 index 658f27a82e6b..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableProperty.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal sealed record class ValidatableProperty( - ITypeSymbol ContainingType, - ITypeSymbol Type, - string Name, - string DisplayName, - ImmutableArray Attributes -); diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableType.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableType.cs deleted file mode 100644 index c6d7e36f36a9..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableType.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal sealed record class ValidatableType( - ITypeSymbol Type, - ImmutableArray Members -); diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableTypeComparer.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableTypeComparer.cs deleted file mode 100644 index fcd99f51dc0b..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidatableTypeComparer.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal sealed class ValidatableTypeComparer : IEqualityComparer -{ - public static ValidatableTypeComparer Instance { get; } = new(); - - public bool Equals(ValidatableType? x, ValidatableType? y) - { - if (x is null && y is null) - { - return true; - } - if (x is null || y is null) - { - return false; - } - return SymbolEqualityComparer.Default.Equals(x.Type, y.Type); - } - - public int GetHashCode(ValidatableType? obj) - { - return SymbolEqualityComparer.Default.GetHashCode(obj?.Type); - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidationAttribute.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidationAttribute.cs deleted file mode 100644 index c29e12a99c0d..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Models/ValidationAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -internal sealed record class ValidationAttribute( - string Name, - string ClassName, - List Arguments, - Dictionary NamedArguments, - bool IsCustomValidationAttribute -); diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AddValidation.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AddValidation.cs deleted file mode 100644 index 1cb8061204ee..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AddValidation.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading; -using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -public sealed partial class ValidationsGenerator : IIncrementalGenerator -{ - internal bool FindAddValidation(SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - if (syntaxNode is InvocationExpressionSyntax - && syntaxNode.TryGetMapMethodName(out var method) - && method == "AddValidation") - { - return true; - } - return false; - } - - internal InterceptableLocation? TransformAddValidation(GeneratorSyntaxContext context, CancellationToken cancellationToken) - { - var node = (InvocationExpressionSyntax)context.Node; - var semanticModel = context.SemanticModel; - var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol; - if (symbol is not IMethodSymbol methodSymbol - || methodSymbol.ContainingType.Name != "ValidationServiceCollectionExtensions" - || methodSymbol.ContainingAssembly.Name != "Microsoft.AspNetCore.Http.Abstractions") - { - return null; - } - return semanticModel.GetInterceptableLocation(node, cancellationToken); - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs deleted file mode 100644 index 3ba763b1b342..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.AttributeParser.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; -using Microsoft.AspNetCore.App.Analyzers.Infrastructure; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -public sealed partial class ValidationsGenerator : IIncrementalGenerator -{ - internal static bool ShouldTransformSymbolWithAttribute(SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - return syntaxNode is ClassDeclarationSyntax; - } - - internal ImmutableArray TransformValidatableTypeWithAttribute(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) - { - var validatableTypes = new HashSet(ValidatableTypeComparer.Instance); - List visitedTypes = []; - var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation); - if (TryExtractValidatableType((ITypeSymbol)context.TargetSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes)) - { - return [..validatableTypes]; - } - return []; - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs deleted file mode 100644 index 2752368cb46a..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.EndpointsParser.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Analyzers.Infrastructure; -using Microsoft.AspNetCore.App.Analyzers.Infrastructure; -using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Operations; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -public sealed partial class ValidationsGenerator : IIncrementalGenerator -{ - internal bool FindEndpoints(SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - if (syntaxNode is InvocationExpressionSyntax - && syntaxNode.TryGetMapMethodName(out var method)) - { - return method == "MapMethods" || InvocationOperationExtensions.KnownMethods.Contains(method); - } - return false; - } - - internal IInvocationOperation? TransformEndpoints(GeneratorSyntaxContext context, CancellationToken cancellationToken) - { - if (context.Node is not InvocationExpressionSyntax node) - { - return null; - } - var operation = context.SemanticModel.GetOperation(node, cancellationToken); - AnalyzerDebug.Assert(operation != null, "Operation should not be null."); - return operation is IInvocationOperation invocationOperation - ? invocationOperation - : null; - } - - internal ImmutableArray ExtractValidatableEndpoint(IInvocationOperation? operation, CancellationToken cancellationToken) - { - AnalyzerDebug.Assert(operation != null, "Operation should not be null."); - AnalyzerDebug.Assert(operation.SemanticModel != null, "Operation should have a semantic model."); - var wellKnownTypes = WellKnownTypes.GetOrCreate(operation.SemanticModel.Compilation); - var validatableTypes = ExtractValidatableTypes(operation, wellKnownTypes); - return validatableTypes; - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs deleted file mode 100644 index 482a90c334b0..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs +++ /dev/null @@ -1,212 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.AspNetCore.Analyzers.Infrastructure; -using Microsoft.AspNetCore.App.Analyzers.Infrastructure; -using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Operations; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -public sealed partial class ValidationsGenerator : IIncrementalGenerator -{ - private static readonly SymbolDisplayFormat _symbolDisplayFormat = new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); - - internal ImmutableArray ExtractValidatableTypes(IInvocationOperation operation, WellKnownTypes wellKnownTypes) - { - AnalyzerDebug.Assert(operation.SemanticModel != null, "SemanticModel should not be null."); - var parameters = operation.TryGetRouteHandlerMethod(operation.SemanticModel, out var method) - ? method.Parameters - : []; - - var fromServiceMetadataSymbol = wellKnownTypes.Get( - WellKnownTypeData.WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata); - var fromKeyedServiceAttributeSymbol = wellKnownTypes.Get( - WellKnownTypeData.WellKnownType.Microsoft_Extensions_DependencyInjection_FromKeyedServicesAttribute); - - var validatableTypes = new HashSet(ValidatableTypeComparer.Instance); - List visitedTypes = []; - - foreach (var parameter in parameters) - { - // Skip parameters that are injected as services - if (parameter.IsServiceParameter(fromServiceMetadataSymbol, fromKeyedServiceAttributeSymbol)) - { - continue; - } - - _ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); - } - return [.. validatableTypes]; - } - - internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnownTypes wellKnownTypes, ref HashSet validatableTypes, ref List visitedTypes) - { - var typeSymbol = incomingTypeSymbol.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)); - if (typeSymbol.SpecialType != SpecialType.None) - { - return false; - } - - if (visitedTypes.Contains(typeSymbol)) - { - return true; - } - - if (typeSymbol.IsExemptType(wellKnownTypes)) - { - return false; - } - - visitedTypes.Add(typeSymbol); - - // Extract validatable types discovered in base types of this type and add them to the top-level list. - var current = typeSymbol.BaseType; - var hasValidatableBaseType = false; - while (current != null && current.SpecialType != SpecialType.System_Object) - { - hasValidatableBaseType |= TryExtractValidatableType(current, wellKnownTypes, ref validatableTypes, ref visitedTypes); - current = current.BaseType; - } - - // Extract validatable types discovered in members of this type and add them to the top-level list. - ImmutableArray members = []; - if (ParsabilityHelper.GetParsability(typeSymbol, wellKnownTypes) is Parsability.NotParsable) - { - members = ExtractValidatableMembers(typeSymbol, wellKnownTypes, ref validatableTypes, ref visitedTypes); - } - - // Extract the validatable types discovered in the JsonDerivedTypeAttributes of this type and add them to the top-level list. - var derivedTypes = typeSymbol.GetJsonDerivedTypes(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Text_Json_Serialization_JsonDerivedTypeAttribute)); - var hasValidatableDerivedTypes = false; - foreach (var derivedType in derivedTypes ?? []) - { - hasValidatableDerivedTypes |= TryExtractValidatableType(derivedType, wellKnownTypes, ref validatableTypes, ref visitedTypes); - } - - // No validatable members or derived types found, so we don't need to add this type. - if (members.IsDefaultOrEmpty && !hasValidatableBaseType && !hasValidatableDerivedTypes) - { - return false; - } - - // Add the type itself as a validatable type itself. - validatableTypes.Add(new ValidatableType( - Type: typeSymbol, - Members: members)); - - return true; - } - - internal ImmutableArray ExtractValidatableMembers(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, ref HashSet validatableTypes, ref List visitedTypes) - { - var members = new List(); - var resolvedRecordProperty = new List(); - - // Special handling for record types to extract properties from - // the primary constructor. - if (typeSymbol is INamedTypeSymbol { IsRecord: true } namedType) - { - // Find the primary constructor for the record, account - // for members that are in base types to account for - // record inheritance scenarios - var primaryConstructor = namedType.Constructors - .FirstOrDefault(c => c.Parameters.Length > 0 && c.Parameters.All(p => - namedType.FindPropertyIncludingBaseTypes(p.Name) != null)); - - if (primaryConstructor != null) - { - // Process all parameters in constructor order to maintain parameter ordering - foreach (var parameter in primaryConstructor.Parameters) - { - // Find the corresponding property in this type, we ignore - // base types here since that will be handled by the inheritance - // checks in the default ValidatableTypeInfo implementation. - var correspondingProperty = typeSymbol.GetMembers() - .OfType() - .FirstOrDefault(p => string.Equals(p.Name, parameter.Name, System.StringComparison.OrdinalIgnoreCase)); - - if (correspondingProperty != null) - { - resolvedRecordProperty.Add(correspondingProperty); - - // Check if the property's type is validatable, this resolves - // validatable types in the inheritance hierarchy - var hasValidatableType = TryExtractValidatableType( - correspondingProperty.Type, - wellKnownTypes, - ref validatableTypes, - ref visitedTypes); - - members.Add(new ValidatableProperty( - ContainingType: correspondingProperty.ContainingType, - Type: correspondingProperty.Type, - Name: correspondingProperty.Name, - DisplayName: parameter.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)) ?? - correspondingProperty.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)), - Attributes: [])); - } - } - } - } - - // Handle properties for classes and any properties not handled by the constructor - foreach (var member in typeSymbol.GetMembers().OfType()) - { - // Skip compiler generated properties and properties already processed via - // the record processing logic above. - if (member.IsImplicitlyDeclared || resolvedRecordProperty.Contains(member, SymbolEqualityComparer.Default)) - { - continue; - } - - var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes); - var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired); - - // If the member has no validation attributes or validatable types and is not required, skip it. - if (attributes.IsDefaultOrEmpty && !hasValidatableType && !isRequired) - { - continue; - } - - members.Add(new ValidatableProperty( - ContainingType: member.ContainingType, - Type: member.Type, - Name: member.Name, - DisplayName: member.GetDisplayName(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_DisplayAttribute)), - Attributes: attributes)); - } - - return [.. members]; - } - - internal static ImmutableArray ExtractValidationAttributes(ISymbol symbol, WellKnownTypes wellKnownTypes, out bool isRequired) - { - var attributes = symbol.GetAttributes(); - if (attributes.Length == 0) - { - isRequired = false; - return []; - } - - var validationAttributes = attributes - .Where(attribute => attribute.AttributeClass != null) - .Where(attribute => attribute.AttributeClass!.ImplementsValidationAttribute(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute))); - isRequired = validationAttributes.Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_RequiredAttribute))); - return [.. validationAttributes - .Where(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_ValidationAttribute))) - .Select(attribute => new ValidationAttribute( - Name: symbol.Name + attribute.AttributeClass!.Name, - ClassName: attribute.AttributeClass!.ToDisplayString(_symbolDisplayFormat), - Arguments: [.. attribute.ConstructorArguments.Select(a => a.ToCSharpString())], - NamedArguments: attribute.NamedArguments.ToDictionary(namedArgument => namedArgument.Key, namedArgument => namedArgument.Value.ToCSharpString()), - IsCustomValidationAttribute: SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_ComponentModel_DataAnnotations_CustomValidationAttribute))))]; - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs deleted file mode 100644 index a538dde7fcda..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Linq; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -[Generator] -public sealed partial class ValidationsGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - // Find the builder.Services.AddValidation() call in the application. - var addValidation = context.SyntaxProvider.CreateSyntaxProvider( - predicate: FindAddValidation, - transform: TransformAddValidation - ); - // Extract types that have been marked with [ValidatableType]. - var validatableTypesWithAttribute = context.SyntaxProvider.ForAttributeWithMetadataName( - "Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute", - predicate: ShouldTransformSymbolWithAttribute, - transform: TransformValidatableTypeWithAttribute - ); - // Extract all minimal API endpoints in the application. - var endpoints = context.SyntaxProvider - .CreateSyntaxProvider( - predicate: FindEndpoints, - transform: TransformEndpoints) - .Where(endpoint => endpoint is not null); - // Extract validatable types from all endpoints. - var validatableTypesFromEndpoints = endpoints - .Select(ExtractValidatableEndpoint); - // Join all validatable types encountered in the type graph. - var validatableTypes = validatableTypesWithAttribute - .Concat(validatableTypesFromEndpoints) - .Distinct(ValidatableTypeComparer.Instance) - .Collect(); - - var emitInputs = addValidation - .Combine(validatableTypes); - - // Emit the IValidatableInfo resolver injection and - // ValidatableTypeInfo for all validatable types. - context.RegisterSourceOutput(emitInputs, Emit); - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs deleted file mode 100644 index c8a432c4a00a..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGeneratorForwarding.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Extensions.Validation.ValidationsGenerator; - -// This class forwards to the new generator implementation -namespace Microsoft.AspNetCore.Http.ValidationsGenerator; - -public sealed partial class ValidationsGenerator : IIncrementalGenerator -{ - private static readonly Microsoft.Extensions.Validation.ValidationsGenerator.ValidationsGenerator _forwardingGenerator = - new Microsoft.Extensions.Validation.ValidationsGenerator.ValidationsGenerator(); - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - _forwardingGenerator.Initialize(context); - } -} \ No newline at end of file From 1b094e524583f0612470fae980d8b35a8d81c5e3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 22 May 2025 22:20:12 +0000 Subject: [PATCH 5/7] Clean up some references --- AspNetCore.slnx | 596 ++++++------- eng/SharedFramework.Local.props | 1 + .../src/Microsoft.AspNetCore.App.Ref.sfxproj | 2 +- ...meValidatableParameterInfoResolverTests.cs | 191 ----- .../ValidatableInfoResolverTests.cs | 223 ----- .../ValidatableParameterInfoTests.cs | 432 ---------- .../Validation/ValidatableTypeInfoTests.cs | 790 ------------------ ...spNetCore.Http.ValidationsGenerator.csproj | 18 - ...ft.AspNetCore.Http.Extensions.Tests.csproj | 2 - .../ValidationsGenerator.ComplexType.cs | 374 --------- ...ValidationsGenerator.IValidatableObject.cs | 208 ----- ...ValidationsGenerator.MultipleNamespaces.cs | 126 --- .../ValidationsGenerator.NoOp.cs | 178 ---- .../ValidationsGenerator.Parameters.cs | 100 --- .../ValidationsGenerator.Parsable.cs | 122 --- .../ValidationsGenerator.Polymorphism.cs | 202 ----- .../ValidationsGenerator.RecordType.cs | 371 -------- .../ValidationsGenerator.Recursion.cs | 158 ---- .../ValidationsGenerator.ValidatableType.cs | 381 --------- .../ValidationsGeneratorTestBase.cs | 586 ------------- ...ypes#ValidatableInfoResolver.g.verified.cs | 244 ------ ...ject#ValidatableInfoResolver.g.verified.cs | 195 ----- ...aces#ValidatableInfoResolver.g.verified.cs | 175 ---- ...ters#ValidatableInfoResolver.g.verified.cs | 193 ----- ...ypes#ValidatableInfoResolver.g.verified.cs | 225 ----- ...ypes#ValidatableInfoResolver.g.verified.cs | 271 ------ ...ypes#ValidatableInfoResolver.g.verified.cs | 166 ---- ...ties#ValidatableInfoResolver.g.verified.cs | 208 ----- ...bute#ValidatableInfoResolver.g.verified.cs | 238 ------ ...ypes#ValidatableInfoResolver.g.verified.cs | 160 ---- ...oft.AspNetCore.Http.Microbenchmarks.csproj | 1 + .../ValidatableTypesBenchmark.cs | 2 +- src/Http/HttpAbstractions.slnf | 3 +- .../src/Microsoft.AspNetCore.Routing.csproj | 1 + .../Routing/src/RouteEndpointDataSource.cs | 1 + .../src/ValidationEndpointFilterFactory.cs | 1 + .../MinimalValidationSample.csproj | 5 +- .../MinimalValidationSample/Program.cs | 2 +- .../{Validations.slnf => Validation.slnf} | 6 +- .../Emitters/ValidationsGenerator.Emitter.cs | 2 +- .../gen/Extensions/ISymbolExtensions.cs | 2 +- .../gen/Extensions/ITypeSymbolExtensions.cs | 2 +- .../IncrementalValuesProviderExtensions.cs | 2 +- ...ons.Validation.ValidationsGenerator.csproj | 5 +- src/Validation/gen/Models/RequiredSymbols.cs | 2 +- .../gen/Models/ValidatableProperty.cs | 2 +- src/Validation/gen/Models/ValidatableType.cs | 2 +- .../gen/Models/ValidatableTypeComparer.cs | 2 +- .../gen/Models/ValidationAttribute.cs | 2 +- .../ValidationsGenerator.AddValidation.cs | 4 +- .../ValidationsGenerator.AttributeParser.cs | 2 +- .../ValidationsGenerator.EndpointsParser.cs | 2 +- .../ValidationsGenerator.TypesParser.cs | 2 +- src/Validation/gen/ValidationsGenerator.cs | 4 +- .../Microsoft.Extensions.Validation.csproj | 4 +- ...RuntimeValidatableParameterInfoResolver.cs | 6 - ...tensions.Validation.GeneratorTests.csproj} | 15 +- .../ModuleInitializer.cs | 11 + .../ValidationsGenerator.ComplexType.cs | 4 +- ...ValidationsGenerator.IValidatableObject.cs | 2 +- ...ValidationsGenerator.MultipleNamespaces.cs | 4 +- .../ValidationsGenerator.NoOp.cs | 5 +- .../ValidationsGenerator.Parameters.cs | 4 +- .../ValidationsGenerator.Parsable.cs | 2 +- .../ValidationsGenerator.Polymorphism.cs | 2 +- .../ValidationsGenerator.RecordType.cs | 4 +- .../ValidationsGenerator.Recursion.cs | 4 +- .../ValidationsGenerator.ValidatableType.cs | 2 +- .../ValidationsGeneratorTestBase.cs | 34 +- ...pes#ValidatableInfoResolver.g.received.cs} | 12 +- ...ect#ValidatableInfoResolver.g.received.cs} | 12 +- ...ces#ValidatableInfoResolver.g.received.cs} | 12 +- ...ers#ValidatableInfoResolver.g.received.cs} | 12 +- ...pes#ValidatableInfoResolver.g.received.cs} | 12 +- ...pes#ValidatableInfoResolver.g.received.cs} | 12 +- ...pes#ValidatableInfoResolver.g.received.cs} | 12 +- ...ies#ValidatableInfoResolver.g.received.cs} | 12 +- ...ute#ValidatableInfoResolver.g.received.cs} | 12 +- ...pes#ValidatableInfoResolver.g.received.cs} | 12 +- ...crosoft.Extensions.Validation.Tests.csproj | 3 +- ...meValidatableParameterInfoResolverTests.cs | 6 - 81 files changed, 475 insertions(+), 6952 deletions(-) delete mode 100644 src/Http/Http.Abstractions/test/Validation/RuntimeValidatableParameterInfoResolverTests.cs delete mode 100644 src/Http/Http.Abstractions/test/Validation/ValidatableInfoResolverTests.cs delete mode 100644 src/Http/Http.Abstractions/test/Validation/ValidatableParameterInfoTests.cs delete mode 100644 src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs delete mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Polymorphism.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ValidatableType.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGeneratorTestBase.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs delete mode 100644 src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs rename src/Validation/{Validations.slnf => Validation.slnf} (62%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj => Microsoft.Extensions.Validation.GeneratorTests/Microsoft.Extensions.Validation.GeneratorTests.csproj} (56%) create mode 100644 src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ModuleInitializer.cs rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.ComplexType.cs (99%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.IValidatableObject.cs (99%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.MultipleNamespaces.cs (97%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.NoOp.cs (98%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.Parameters.cs (97%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.Parsable.cs (98%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.Polymorphism.cs (99%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.RecordType.cs (99%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.Recursion.cs (98%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGenerator.ValidatableType.cs (99%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests => Microsoft.Extensions.Validation.GeneratorTests}/ValidationsGeneratorTestBase.cs (94%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.received.cs} (93%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.received.cs} (91%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.received.cs} (91%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.received.cs} (92%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.received.cs} (92%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.received.cs} (94%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.received.cs} (90%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.received.cs} (92%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.received.cs} (93%) rename src/Validation/test/{Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs => Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.received.cs} (90%) diff --git a/AspNetCore.slnx b/AspNetCore.slnx index 78284eb90b2c..63083e4fceb9 100644 --- a/AspNetCore.slnx +++ b/AspNetCore.slnx @@ -6,95 +6,95 @@ - + - + - + - - + + - + - - + + - + - + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + @@ -106,31 +106,31 @@ - + - - + + - + - + - - + + - + - + @@ -141,102 +141,102 @@ - + - + - + - - + + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - - + + @@ -244,171 +244,170 @@ - - + + - + - + - - + + - + - + - + - + - + - + - + - - + + - - + + - + - + - + - + - + - + - - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - - + + @@ -434,57 +433,57 @@ - - + + - + - + - - + + - + - + - + - + - + - + - + - + - + - + @@ -492,224 +491,224 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -736,120 +735,120 @@ - + - - + + - + - + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -859,29 +858,29 @@ - + - - + + - + - + - + - - + + @@ -921,7 +920,7 @@ - + @@ -931,7 +930,7 @@ - + @@ -940,7 +939,7 @@ - + @@ -949,7 +948,7 @@ - + @@ -978,7 +977,7 @@ - + @@ -988,33 +987,33 @@ - + - + - + - - + + - + - + - + @@ -1024,84 +1023,84 @@ - + - + - + - + - + - - + + - + - - - - + + + + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + @@ -1109,71 +1108,82 @@ - - + + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 533709fb9bf4..28efd974e7f0 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -37,6 +37,7 @@ + diff --git a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.sfxproj b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.sfxproj index 44cd40b611d7..33d1bcd4d9c9 100644 --- a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.sfxproj +++ b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.sfxproj @@ -77,7 +77,7 @@ OutputItemType="AspNetCoreAnalyzer" ReferenceOutputAssembly="false" /> - diff --git a/src/Http/Http.Abstractions/test/Validation/RuntimeValidatableParameterInfoResolverTests.cs b/src/Http/Http.Abstractions/test/Validation/RuntimeValidatableParameterInfoResolverTests.cs deleted file mode 100644 index e5f268ab4c32..000000000000 --- a/src/Http/Http.Abstractions/test/Validation/RuntimeValidatableParameterInfoResolverTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -// 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.IO.Pipelines; -using System.Reflection; -using System.Security.Claims; - -namespace Microsoft.AspNetCore.Http.Validation.Tests; - -public class RuntimeValidatableParameterInfoResolverTests -{ - private readonly RuntimeValidatableParameterInfoResolver _resolver = new(); - - [Fact] - public void TryGetValidatableTypeInfo_AlwaysReturnsFalse() - { - var result = _resolver.TryGetValidatableTypeInfo(typeof(string), out var validatableInfo); - - Assert.False(result); - Assert.Null(validatableInfo); - } - - [Fact] - public void TryGetValidatableParameterInfo_WithNullName_ThrowsInvalidOperationException() - { - var parameterInfo = new NullNameParameterInfo(); - - var exception = Assert.Throws(() => - _resolver.TryGetValidatableParameterInfo(parameterInfo, out _)); - - Assert.Contains("without a name", exception.Message); - } - - [Theory] - [InlineData(typeof(string))] - [InlineData(typeof(int))] - [InlineData(typeof(bool))] - [InlineData(typeof(DateTime))] - [InlineData(typeof(Guid))] - [InlineData(typeof(decimal))] - [InlineData(typeof(DayOfWeek))] // Enum - [InlineData(typeof(ClaimsPrincipal))] - [InlineData(typeof(PipeReader))] - [InlineData(typeof(DateTimeOffset))] - [InlineData(typeof(TimeOnly))] - [InlineData(typeof(DateOnly))] - [InlineData(typeof(TimeSpan))] - [InlineData(typeof(IFormFile))] - [InlineData(typeof(IFormFileCollection))] - [InlineData(typeof(IFormCollection))] - [InlineData(typeof(HttpContext))] - [InlineData(typeof(HttpRequest))] - [InlineData(typeof(HttpResponse))] - [InlineData(typeof(CancellationToken))] - public void TryGetValidatableParameterInfo_WithSimpleTypesAndNoAttributes_ReturnsFalse(Type parameterType) - { - var parameterInfo = GetParameter(parameterType); - - var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); - - Assert.False(result); - Assert.Null(validatableInfo); - } - - [Fact] - public void TryGetValidatableParameterInfo_WithClassTypeAndNoAttributes_ReturnsTrue() - { - var parameterInfo = GetParameter(typeof(TestClass)); - - var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); - - Assert.True(result); - Assert.NotNull(validatableInfo); - var parameterValidatableInfo = Assert.IsType(validatableInfo); - Assert.Equal("testParam", parameterValidatableInfo.Name); - Assert.Equal("testParam", parameterValidatableInfo.DisplayName); - } - - [Fact] - public void TryGetValidatableParameterInfo_WithSimpleTypeAndAttributes_ReturnsTrue() - { - var parameterInfo = typeof(TestController) - .GetMethod(nameof(TestController.MethodWithAttributedParam))! - .GetParameters()[0]; - - var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); - - Assert.True(result); - Assert.NotNull(validatableInfo); - var parameterValidatableInfo = Assert.IsType(validatableInfo); - Assert.Equal("value", parameterValidatableInfo.Name); - Assert.Equal("value", parameterValidatableInfo.DisplayName); - } - - [Fact] - public void TryGetValidatableParameterInfo_WithDisplayAttribute_UsesDisplayNameFromAttribute() - { - var parameterInfo = typeof(TestController) - .GetMethod(nameof(TestController.MethodWithDisplayAttribute))! - .GetParameters()[0]; - - var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); - - Assert.True(result); - Assert.NotNull(validatableInfo); - var parameterValidatableInfo = Assert.IsType(validatableInfo); - Assert.Equal("value", parameterValidatableInfo.Name); - Assert.Equal("Custom Display Name", parameterValidatableInfo.DisplayName); - } - - [Fact] - public void TryGetValidatableParameterInfo_WithDisplayAttributeWithNullName_UsesParameterName() - { - var parameterInfo = typeof(TestController) - .GetMethod(nameof(TestController.MethodWithNullDisplayName))! - .GetParameters()[0]; - - var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); - - Assert.True(result); - Assert.NotNull(validatableInfo); - var parameterValidatableInfo = Assert.IsType(validatableInfo); - Assert.Equal("value", parameterValidatableInfo.Name); - Assert.Equal("value", parameterValidatableInfo.DisplayName); - } - - [Fact] - public void TryGetValidatableParameterInfo_WithNullableValueType_ReturnsFalse() - { - var parameterInfo = GetParameter(typeof(int?)); - - var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); - - Assert.False(result); - Assert.Null(validatableInfo); - } - - [Fact] - public void TryGetValidatableParameterInfo_WithNullableReferenceType_ReturnsTrue() - { - var parameterInfo = GetNullableParameter(typeof(TestClass)); - - var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo); - - Assert.True(result); - Assert.NotNull(validatableInfo); - var parameterValidatableInfo = Assert.IsType(validatableInfo); - Assert.Equal("testParam", parameterValidatableInfo.Name); - Assert.Equal("testParam", parameterValidatableInfo.DisplayName); - } - - private static ParameterInfo GetParameter(Type parameterType) - { - return typeof(TestParameterHolder) - .GetMethod(nameof(TestParameterHolder.Method))! - .MakeGenericMethod(parameterType) - .GetParameters()[0]; - } - - private static ParameterInfo GetNullableParameter(Type parameterType) - { - return typeof(TestParameterHolder) - .GetMethod(nameof(TestParameterHolder.MethodWithNullable))! - .MakeGenericMethod(parameterType) - .GetParameters()[0]; - } - - private class TestClass { } - - private class TestParameterHolder - { - public void Method(T testParam) { } - public void MethodWithNullable(T? testParam) { } - } - - private class TestController - { - public void MethodWithAttributedParam([Required] string value) { } - - public void MethodWithDisplayAttribute([Display(Name = "Custom Display Name")][Required] string value) { } - - public void MethodWithNullDisplayName([Display(Name = null)][Required] string value) { } - } - - private class NullNameParameterInfo : ParameterInfo - { - public override string? Name => null; - public override Type ParameterType => typeof(string); - } -} diff --git a/src/Http/Http.Abstractions/test/Validation/ValidatableInfoResolverTests.cs b/src/Http/Http.Abstractions/test/Validation/ValidatableInfoResolverTests.cs deleted file mode 100644 index ab197cf6fde8..000000000000 --- a/src/Http/Http.Abstractions/test/Validation/ValidatableInfoResolverTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -#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. - -// 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.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Moq; - -namespace Microsoft.AspNetCore.Http.Validation.Tests; - -public class ValidatableInfoResolverTests -{ - public delegate void TryGetValidatableTypeInfoCallback(Type type, out IValidatableInfo? validatableInfo); - public delegate void TryGetValidatableParameterInfoCallback(ParameterInfo parameter, out IValidatableInfo? validatableInfo); - - [Fact] - public void GetValidatableTypeInfo_ReturnsNull_ForNonValidatableType() - { - // Arrange - var resolver = new Mock(); - IValidatableInfo? validatableInfo = null; - resolver.Setup(r => r.TryGetValidatableTypeInfo(It.IsAny(), out validatableInfo)).Returns(false); - - // Act - var result = resolver.Object.TryGetValidatableTypeInfo(typeof(NonValidatableType), out validatableInfo); - - // Assert - Assert.False(result); - Assert.Null(validatableInfo); - } - - [Fact] - public void GetValidatableTypeInfo_ReturnsTypeInfo_ForValidatableType() - { - // Arrange - var mockTypeInfo = new Mock( - typeof(ValidatableType), - Array.Empty()).Object; - - var resolver = new Mock(); - IValidatableInfo? validatableInfo = null; - resolver - .Setup(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out validatableInfo)) - .Callback(new TryGetValidatableTypeInfoCallback((t, out info) => - { - info = mockTypeInfo; // Set the out parameter to our mock - })) - .Returns(true); - - // Act - var result = resolver.Object.TryGetValidatableTypeInfo(typeof(ValidatableType), out validatableInfo); - - // Assert - Assert.True(result); - var validatableTypeInfo = Assert.IsAssignableFrom(validatableInfo); - Assert.Equal(typeof(ValidatableType), validatableTypeInfo.Type); - } - - [Fact] - public void GetValidatableParameterInfo_ReturnsNull_ForNonValidatableParameter() - { - // Arrange - var method = typeof(TestMethods).GetMethod(nameof(TestMethods.MethodWithNonValidatableParam))!; - var parameter = method.GetParameters()[0]; - - var resolver = new Mock(); - IValidatableInfo? validatableInfo = null; - resolver.Setup(r => r.TryGetValidatableParameterInfo(It.IsAny(), out validatableInfo)).Returns(false); - - // Act - var result = resolver.Object.TryGetValidatableParameterInfo(parameter, out validatableInfo); - - // Assert - Assert.False(result); - } - - [Fact] - public void GetValidatableParameterInfo_ReturnsParameterInfo_ForValidatableParameter() - { - // Arrange - var method = typeof(TestMethods).GetMethod(nameof(TestMethods.MethodWithValidatableParam))!; - var parameter = method.GetParameters()[0]; - - var mockParamInfo = new Mock( - typeof(string), - "model", - "model").Object; - - var resolver = new Mock(); - - // Setup using the same pattern as in the type info test - resolver.Setup(r => r.TryGetValidatableParameterInfo(parameter, out It.Ref.IsAny)) - .Callback(new TryGetValidatableParameterInfoCallback((ParameterInfo p, out IValidatableInfo? info) => - { - info = mockParamInfo; // Set the out parameter to our mock - })) - .Returns(true); - - // Act - var result = resolver.Object.TryGetValidatableParameterInfo(parameter, out var validatableInfo); - - // Assert - Assert.True(result); - var validatableParamInfo = Assert.IsAssignableFrom(validatableInfo); - Assert.Equal("model", validatableParamInfo.Name); - } - - [Fact] - public void ResolversChain_ProcessesInCorrectOrder() - { - // Arrange - var services = new ServiceCollection(); - - var resolver1 = new Mock(); - var resolver2 = new Mock(); - var resolver3 = new Mock(); - - // Create the object that will be returned by resolver2 - var mockTypeInfo = new Mock(typeof(ValidatableType), Array.Empty()).Object; - - // Setup resolver1 to return false (doesn't handle this type) - resolver1 - .Setup(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny)) - .Callback(new TryGetValidatableTypeInfoCallback((Type t, out IValidatableInfo? info) => - { - info = null; - })) - .Returns(false); - - // Setup resolver2 to return true and set the mock type info - resolver2 - .Setup(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny)) - .Callback(new TryGetValidatableTypeInfoCallback((Type t, out IValidatableInfo? info) => - { - info = mockTypeInfo; - })) - .Returns(true); - - services.AddValidation(Options => - { - Options.Resolvers.Add(resolver1.Object); - Options.Resolvers.Add(resolver2.Object); - Options.Resolvers.Add(resolver3.Object); - }); - - var serviceProvider = services.BuildServiceProvider(); - var validationOptions = serviceProvider.GetRequiredService>().Value; - - // Act - var result = validationOptions.TryGetValidatableTypeInfo(typeof(ValidatableType), out var validatableInfo); - - // Assert - Assert.True(result); - Assert.NotNull(validatableInfo); - Assert.Equal(typeof(ValidatableType), ((ValidatableTypeInfo)validatableInfo).Type); - - // Verify that resolvers were called in the expected order - resolver1.Verify(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny), Times.Once); - resolver2.Verify(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny), Times.Once); - resolver3.Verify(r => r.TryGetValidatableTypeInfo(typeof(ValidatableType), out It.Ref.IsAny), Times.Never); - } - - // Test types - private class NonValidatableType { } - - [ValidatableType] - private class ValidatableType - { - [Required] - public string Name { get; set; } = ""; - } - - private static class TestMethods - { - public static void MethodWithNonValidatableParam(NonValidatableType param) { } - public static void MethodWithValidatableParam(ValidatableType model) { } - } - - // Test implementations - private class TestValidatablePropertyInfo : ValidatablePropertyInfo - { - private readonly ValidationAttribute[] _validationAttributes; - - public TestValidatablePropertyInfo( - Type containingType, - Type propertyType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) - : base(containingType, propertyType, name, displayName) - { - _validationAttributes = validationAttributes; - } - - protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; - } - - private class TestValidatableParameterInfo : ValidatableParameterInfo - { - private readonly ValidationAttribute[] _validationAttributes; - - public TestValidatableParameterInfo( - Type parameterType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) - : base(parameterType, name, displayName) - { - _validationAttributes = validationAttributes; - } - - protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; - } - - private class TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) - { - } -} diff --git a/src/Http/Http.Abstractions/test/Validation/ValidatableParameterInfoTests.cs b/src/Http/Http.Abstractions/test/Validation/ValidatableParameterInfoTests.cs deleted file mode 100644 index e10614b2b19e..000000000000 --- a/src/Http/Http.Abstractions/test/Validation/ValidatableParameterInfoTests.cs +++ /dev/null @@ -1,432 +0,0 @@ -#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. - -// 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.CodeAnalysis; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Http.Validation.Tests; - -public class ValidatableParameterInfoTests -{ - [Fact] - public async Task Validate_RequiredParameter_AddsErrorWhenNull() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(string), - name: "testParam", - displayName: "Test Parameter", - validationAttributes: [new RequiredAttribute()]); - - var context = CreateValidatableContext(); - - // Act - await paramInfo.ValidateAsync(null, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("testParam", error.Key); - Assert.Equal("The Test Parameter field is required.", error.Value.Single()); - } - - [Fact] - public async Task Validate_RequiredParameter_ShortCircuitsOtherValidations() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(string), - name: "testParam", - displayName: "Test Parameter", - // Most ValidationAttributes skip validation if the value is null - // so we use a custom one that always fails to assert on the behavior here - validationAttributes: [new RequiredAttribute(), new CustomTestValidationAttribute()]); - - var context = CreateValidatableContext(); - - // Act - await paramInfo.ValidateAsync(null, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("testParam", error.Key); - Assert.Equal("The Test Parameter field is required.", error.Value.Single()); - } - - [Fact] - public async Task Validate_SkipsValidation_WhenNullAndNotRequired() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(string), - name: "testParam", - displayName: "Test Parameter", - validationAttributes: [new StringLengthAttribute(10)]); - - var context = CreateValidatableContext(); - - // Act - await paramInfo.ValidateAsync(null, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.Null(errors); // No errors added - } - - [Fact] - public async Task Validate_WithRangeAttribute_ValidatesCorrectly() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(int), - name: "testParam", - displayName: "Test Parameter", - validationAttributes: [new RangeAttribute(10, 100)]); - - var context = CreateValidatableContext(); - - // Act - await paramInfo.ValidateAsync(5, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("testParam", error.Key); - Assert.Equal("The field Test Parameter must be between 10 and 100.", error.Value.First()); - } - - [Fact] - public async Task Validate_WithDisplayNameAttribute_UsesDisplayNameInErrorMessage() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(string), - name: "testParam", - displayName: "Custom Display Name", - validationAttributes: [new RequiredAttribute()]); - - var context = CreateValidatableContext(); - - // Act - await paramInfo.ValidateAsync(null, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("testParam", error.Key); - // The error message should use the display name - Assert.Equal("The Custom Display Name field is required.", error.Value.First()); - } - - [Fact] - public async Task Validate_WhenValidatableTypeHasErrors_AddsNestedErrors() - { - // Arrange - var personTypeInfo = new TestValidatableTypeInfo( - typeof(Person), - [ - new TestValidatablePropertyInfo( - typeof(Person), - typeof(string), - "Name", - "Name", - [new RequiredAttribute()]) - ]); - - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(Person), - name: "person", - displayName: "Person", - validationAttributes: []); - - var typeMapping = new Dictionary - { - { typeof(Person), personTypeInfo } - }; - - var context = CreateValidatableContext(typeMapping); - var person = new Person(); // Name is null, so should fail validation - - // Act - await paramInfo.ValidateAsync(person, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("Name", error.Key); - Assert.Equal("The Name field is required.", error.Value[0]); - } - - [Fact] - public async Task Validate_WithEnumerableOfValidatableType_ValidatesEachItem() - { - // Arrange - var personTypeInfo = new TestValidatableTypeInfo( - typeof(Person), - [ - new TestValidatablePropertyInfo( - typeof(Person), - typeof(string), - "Name", - "Name", - [new RequiredAttribute()]) - ]); - - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(IEnumerable), - name: "people", - displayName: "People", - validationAttributes: []); - - var typeMapping = new Dictionary - { - { typeof(Person), personTypeInfo } - }; - - var context = CreateValidatableContext(typeMapping); - var people = new List - { - new() { Name = "Valid" }, - new() // Name is null, should fail - }; - - // Act - await paramInfo.ValidateAsync(people, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("people[1].Name", error.Key); - Assert.Equal("The Name field is required.", error.Value[0]); - } - - [Fact] - public async Task Validate_MultipleErrorsOnSameParameter_CollectsAllErrors() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(int), - name: "testParam", - displayName: "Test Parameter", - validationAttributes: - [ - new RangeAttribute(10, 100) { ErrorMessage = "Range error" }, - new CustomTestValidationAttribute { ErrorMessage = "Custom error" } - ]); - - var context = CreateValidatableContext(); - - // Act - await paramInfo.ValidateAsync(5, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("testParam", error.Key); - Assert.Collection(error.Value, - e => Assert.Equal("Range error", e), - e => Assert.Equal("Custom error", e)); - } - - [Fact] - public async Task Validate_WithContextPrefix_AddsErrorsWithCorrectPrefix() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(int), - name: "testParam", - displayName: "Test Parameter", - validationAttributes: [new RangeAttribute(10, 100)]); - - var context = CreateValidatableContext(); - context.CurrentValidationPath = "parent"; - - // Act - await paramInfo.ValidateAsync(5, context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("parent.testParam", error.Key); - Assert.Equal("The field Test Parameter must be between 10 and 100.", error.Value.First()); - } - - [Fact] - public async Task Validate_ExceptionDuringValidation_CapturesExceptionAsError() - { - // Arrange - var paramInfo = CreateTestParameterInfo( - parameterType: typeof(string), - name: "testParam", - displayName: "Test Parameter", - validationAttributes: [new ThrowingValidationAttribute()]); - - var context = CreateValidatableContext(); - - // Act - await paramInfo.ValidateAsync("test", context, default); - - // Assert - var errors = context.ValidationErrors; - Assert.NotNull(errors); - var error = Assert.Single(errors); - Assert.Equal("testParam", error.Key); - Assert.Equal("Test exception", error.Value.First()); - } - - private TestValidatableParameterInfo CreateTestParameterInfo( - Type parameterType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) - { - return new TestValidatableParameterInfo( - parameterType, - name, - displayName, - validationAttributes); - } - - private ValidateContext CreateValidatableContext( - Dictionary? typeMapping = null) - { - var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var validationContext = new ValidationContext(new object(), serviceProvider, null); - - return new ValidateContext - { - ValidationContext = validationContext, - ValidationOptions = new TestValidationOptions(typeMapping ?? new Dictionary()) - }; - } - - private class TestValidatableParameterInfo : ValidatableParameterInfo - { - private readonly ValidationAttribute[] _validationAttributes; - - public TestValidatableParameterInfo( - Type parameterType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) - : base(parameterType, name, displayName) - { - _validationAttributes = validationAttributes; - } - - protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; - } - - private class TestValidatablePropertyInfo : ValidatablePropertyInfo - { - private readonly ValidationAttribute[] _validationAttributes; - - public TestValidatablePropertyInfo( - Type containingType, - Type propertyType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) - : base(containingType, propertyType, name, displayName) - { - _validationAttributes = validationAttributes; - } - - protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; - } - - private class TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) : ValidatableTypeInfo(type, members) - { - } - - private class TestValidationOptions : ValidationOptions - { - public TestValidationOptions(Dictionary typeInfoMappings) - { - // Create a custom resolver that uses the dictionary - var resolver = new DictionaryBasedResolver(typeInfoMappings); - - // Add it to the resolvers collection - Resolvers.Add(resolver); - } - - // Private resolver implementation that uses a dictionary lookup - private class DictionaryBasedResolver : IValidatableInfoResolver - { - private readonly Dictionary _typeInfoMappings; - - public DictionaryBasedResolver(Dictionary typeInfoMappings) - { - _typeInfoMappings = typeInfoMappings; - } - - public ValidatableTypeInfo? TryGetValidatableTypeInfo(Type type) - { - _typeInfoMappings.TryGetValue(type, out var info); - return info; - } - - public ValidatableParameterInfo? GetValidatableParameterInfo(ParameterInfo parameterInfo) - { - // Not implemented in the test - return null; - } - - public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) - { - if (_typeInfoMappings.TryGetValue(type, out var validatableTypeInfo)) - { - validatableInfo = validatableTypeInfo; - return true; - } - validatableInfo = null; - return false; - } - - public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - } - - // Test data classes and validation attributes - - private class Person - { - public string? Name { get; set; } - } - - private class CustomTestValidationAttribute : ValidationAttribute - { - public override bool IsValid(object? value) - { - // Always fail for testing - return false; - } - } - - private class ThrowingValidationAttribute : ValidationAttribute - { - public override bool IsValid(object? value) - { - throw new InvalidOperationException("Test exception"); - } - } -} diff --git a/src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs b/src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs deleted file mode 100644 index a6123bb11c67..000000000000 --- a/src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs +++ /dev/null @@ -1,790 +0,0 @@ -#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. - -// 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.CodeAnalysis; -using System.Reflection; - -namespace Microsoft.AspNetCore.Http.Validation.Tests; - -public class ValidatableTypeInfoTests -{ - [Fact] - public async Task Validate_ValidatesComplexType_WithNestedProperties() - { - // Arrange - var personType = new TestValidatableTypeInfo( - typeof(Person), - [ - CreatePropertyInfo(typeof(Person), typeof(string), "Name", "Name", - [new RequiredAttribute()]), - CreatePropertyInfo(typeof(Person), typeof(int), "Age", "Age", - [new RangeAttribute(0, 120)]), - CreatePropertyInfo(typeof(Person), typeof(Address), "Address", "Address", - []) - ]); - - var addressType = new TestValidatableTypeInfo( - typeof(Address), - [ - CreatePropertyInfo(typeof(Address), typeof(string), "Street", "Street", - [new RequiredAttribute()]), - CreatePropertyInfo(typeof(Address), typeof(string), "City", "City", - [new RequiredAttribute()]) - ]); - - var validationOptions = new TestValidationOptions(new Dictionary - { - { typeof(Person), personType }, - { typeof(Address), addressType } - }); - - var personWithMissingRequiredFields = new Person - { - Age = 150, // Invalid age - Address = new Address() // Missing required City and Street - }; - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(personWithMissingRequiredFields) - }; - - // Act - await personType.ValidateAsync(personWithMissingRequiredFields, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("Name", kvp.Key); - Assert.Equal("The Name field is required.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("Age", kvp.Key); - Assert.Equal("The field Age must be between 0 and 120.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("Address.Street", kvp.Key); - Assert.Equal("The Street field is required.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("Address.City", kvp.Key); - Assert.Equal("The City field is required.", kvp.Value.First()); - }); - } - - [Fact] - public async Task Validate_HandlesIValidatableObject_Implementation() - { - // Arrange - var employeeType = new TestValidatableTypeInfo( - typeof(Employee), - [ - CreatePropertyInfo(typeof(Employee), typeof(string), "Name", "Name", - [new RequiredAttribute()]), - CreatePropertyInfo(typeof(Employee), typeof(string), "Department", "Department", - []), - CreatePropertyInfo(typeof(Employee), typeof(decimal), "Salary", "Salary", - []) - ]); - - var employee = new Employee - { - Name = "John Doe", - Department = "IT", - Salary = -5000 // Negative salary will trigger IValidatableObject validation - }; - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(Employee), employeeType } - }), - ValidationContext = new ValidationContext(employee) - }; - - // Act - await employeeType.ValidateAsync(employee, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - var error = Assert.Single(context.ValidationErrors); - Assert.Equal("Salary", error.Key); - Assert.Equal("Salary must be a positive value.", error.Value.First()); - } - - [Fact] - public async Task Validate_HandlesPolymorphicTypes_WithSubtypes() - { - // Arrange - var baseType = new TestValidatableTypeInfo( - typeof(Vehicle), - [ - CreatePropertyInfo(typeof(Vehicle), typeof(string), "Make", "Make", - [new RequiredAttribute()]), - CreatePropertyInfo(typeof(Vehicle), typeof(string), "Model", "Model", - [new RequiredAttribute()]) - ]); - - var derivedType = new TestValidatableTypeInfo( - typeof(Car), - [ - CreatePropertyInfo(typeof(Car), typeof(int), "Doors", "Doors", - [new RangeAttribute(2, 5)]) - ]); - - var car = new Car - { - // Missing Make and Model (required in base type) - Doors = 7 // Invalid number of doors - }; - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(Vehicle), baseType }, - { typeof(Car), derivedType } - }), - ValidationContext = new ValidationContext(car) - }; - - // Act - await derivedType.ValidateAsync(car, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("Doors", kvp.Key); - Assert.Equal("The field Doors must be between 2 and 5.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("Make", kvp.Key); - Assert.Equal("The Make field is required.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("Model", kvp.Key); - Assert.Equal("The Model field is required.", kvp.Value.First()); - }); - } - - [Fact] - public async Task Validate_HandlesCollections_OfValidatableTypes() - { - // Arrange - var itemType = new TestValidatableTypeInfo( - typeof(OrderItem), - [ - CreatePropertyInfo(typeof(OrderItem), typeof(string), "ProductName", "ProductName", - [new RequiredAttribute()]), - CreatePropertyInfo(typeof(OrderItem), typeof(int), "Quantity", "Quantity", - [new RangeAttribute(1, 100)]) - ]); - - var orderType = new TestValidatableTypeInfo( - typeof(Order), - [ - CreatePropertyInfo(typeof(Order), typeof(string), "OrderNumber", "OrderNumber", - [new RequiredAttribute()]), - CreatePropertyInfo(typeof(Order), typeof(List), "Items", "Items", - []) - ]); - - var order = new Order - { - OrderNumber = "ORD-12345", - Items = - [ - new OrderItem { ProductName = "Valid Product", Quantity = 5 }, - new OrderItem { /* Missing ProductName (required) */ Quantity = 0 /* Invalid quantity */ }, - new OrderItem { ProductName = "Another Product", Quantity = 200 /* Invalid quantity */ } - ] - }; - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(OrderItem), itemType }, - { typeof(Order), orderType } - }), - ValidationContext = new ValidationContext(order) - }; - - // Act - await orderType.ValidateAsync(order, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("Items[1].ProductName", kvp.Key); - Assert.Equal("The ProductName field is required.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("Items[1].Quantity", kvp.Key); - Assert.Equal("The field Quantity must be between 1 and 100.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("Items[2].Quantity", kvp.Key); - Assert.Equal("The field Quantity must be between 1 and 100.", kvp.Value.First()); - }); - } - - [Fact] - public async Task Validate_HandlesNullValues_Appropriately() - { - // Arrange - var personType = new TestValidatableTypeInfo( - typeof(Person), - [ - CreatePropertyInfo(typeof(Person), typeof(string), "Name", "Name", - []), - CreatePropertyInfo(typeof(Person), typeof(Address), "Address", "Address", - []) - ]); - - var person = new Person - { - Name = null, - Address = null - }; - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(Person), personType } - }), - ValidationContext = new ValidationContext(person) - }; - - // Act - await personType.ValidateAsync(person, context, default); - - // Assert - Assert.Null(context.ValidationErrors); // No validation errors for nullable properties with null values - } - - [Fact] - public async Task Validate_RespectsMaxDepthOption_ForCircularReferences() - { - // Arrange - // Create a type that can contain itself (circular reference) - var nodeType = new TestValidatableTypeInfo( - typeof(TreeNode), - [ - CreatePropertyInfo(typeof(TreeNode), typeof(string), "Name", "Name", - [new RequiredAttribute()]), - CreatePropertyInfo(typeof(TreeNode), typeof(TreeNode), "Parent", "Parent", - []), - CreatePropertyInfo(typeof(TreeNode), typeof(List), "Children", "Children", - []) - ]); - - // Create a validation options with a small max depth - var validationOptions = new TestValidationOptions(new Dictionary - { - { typeof(TreeNode), nodeType } - }); - validationOptions.MaxDepth = 3; // Set a small max depth to trigger the limit - - // Create a deep tree with circular references - var rootNode = new TreeNode { Name = "Root" }; - var level1 = new TreeNode { Name = "Level1", Parent = rootNode }; - var level2 = new TreeNode { Name = "Level2", Parent = level1 }; - var level3 = new TreeNode { Name = "Level3", Parent = level2 }; - var level4 = new TreeNode { Name = "" }; // Invalid: missing required name - var level5 = new TreeNode { Name = "" }; // Invalid but beyond max depth, should not be validated - - rootNode.Children.Add(level1); - level1.Children.Add(level2); - level2.Children.Add(level3); - level3.Children.Add(level4); - level4.Children.Add(level5); - - // Add a circular reference - level5.Children.Add(rootNode); - - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationErrors = [], - ValidationContext = new ValidationContext(rootNode) - }; - - // Act + Assert - var exception = await Assert.ThrowsAsync( - async () => await nodeType.ValidateAsync(rootNode, context, default)); - - Assert.NotNull(exception); - Assert.Equal("Maximum validation depth of 3 exceeded at 'Children[0].Parent.Children[0]' in 'TreeNode'. This is likely caused by a circular reference in the object graph. Consider increasing the MaxDepth in ValidationOptions if deeper validation is required.", exception.Message); - Assert.Equal(0, context.CurrentDepth); - } - - [Fact] - public async Task Validate_HandlesCustomValidationAttributes() - { - // Arrange - var productType = new TestValidatableTypeInfo( - typeof(Product), - [ - CreatePropertyInfo(typeof(Product), typeof(string), "SKU", "SKU", [new RequiredAttribute(), new CustomSkuValidationAttribute()]), - ]); - - var product = new Product { SKU = "INVALID-SKU" }; - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(Product), productType } - }), - ValidationContext = new ValidationContext(product) - }; - - // Act - await productType.ValidateAsync(product, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - var error = Assert.Single(context.ValidationErrors); - Assert.Equal("SKU", error.Key); - Assert.Equal("SKU must start with 'PROD-'.", error.Value.First()); - } - - [Fact] - public async Task Validate_HandlesMultipleErrorsOnSameProperty() - { - // Arrange - var userType = new TestValidatableTypeInfo( - typeof(User), - [ - CreatePropertyInfo(typeof(User), typeof(string), "Password", "Password", - [ - new RequiredAttribute(), - new MinLengthAttribute(8) { ErrorMessage = "Password must be at least 8 characters." }, - new PasswordComplexityAttribute() - ]) - ]); - - var user = new User { Password = "abc" }; // Too short and not complex enough - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(User), userType } - }), - ValidationContext = new ValidationContext(user) - }; - - // Act - await userType.ValidateAsync(user, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - Assert.Single(context.ValidationErrors.Keys); // Only the "Password" key - Assert.Equal(2, context.ValidationErrors["Password"].Length); // But with 2 errors - Assert.Contains("Password must be at least 8 characters.", context.ValidationErrors["Password"]); - Assert.Contains("Password must contain at least one number and one special character.", context.ValidationErrors["Password"]); - } - - [Fact] - public async Task Validate_HandlesMultiLevelInheritance() - { - // Arrange - var baseType = new TestValidatableTypeInfo( - typeof(BaseEntity), - [ - CreatePropertyInfo(typeof(BaseEntity), typeof(Guid), "Id", "Id", []) - ]); - - var intermediateType = new TestValidatableTypeInfo( - typeof(IntermediateEntity), - [ - CreatePropertyInfo(typeof(IntermediateEntity), typeof(DateTime), "CreatedAt", "CreatedAt", [new PastDateAttribute()]) - ]); - - var derivedType = new TestValidatableTypeInfo( - typeof(DerivedEntity), - [ - CreatePropertyInfo(typeof(DerivedEntity), typeof(string), "Name", "Name", [new RequiredAttribute()]) - ]); - - var entity = new DerivedEntity - { - Name = "", // Invalid: required - CreatedAt = DateTime.Now.AddDays(1) // Invalid: future date - }; - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(BaseEntity), baseType }, - { typeof(IntermediateEntity), intermediateType }, - { typeof(DerivedEntity), derivedType } - }), - ValidationContext = new ValidationContext(entity) - }; - - // Act - await derivedType.ValidateAsync(entity, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("Name", kvp.Key); - Assert.Equal("The Name field is required.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("CreatedAt", kvp.Key); - Assert.Equal("Date must be in the past.", kvp.Value.First()); - }); - } - - [Fact] - public async Task Validate_RequiredOnPropertyShortCircuitsOtherValidations() - { - // Arrange - var userType = new TestValidatableTypeInfo( - typeof(User), - [ - CreatePropertyInfo(typeof(User), typeof(string), "Password", "Password", - [new RequiredAttribute(), new PasswordComplexityAttribute()]) - ]); - - var user = new User { Password = null }; // Invalid: required - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(User), userType } - }), - ValidationContext = new ValidationContext(user) // Invalid: required - }; - - // Act - await userType.ValidateAsync(user, context, default); - - // Assert - Assert.NotNull(context.ValidationErrors); - Assert.Single(context.ValidationErrors.Keys); - var error = Assert.Single(context.ValidationErrors); - Assert.Equal("Password", error.Key); - Assert.Equal("The Password field is required.", error.Value.Single()); - } - - [Fact] - public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_BehavesAsExpected() - { - var globalType = new TestValidatableTypeInfo( - typeof(GlobalErrorObject), - []); // no properties – nothing sets MemberName - var globalErrorInstance = new GlobalErrorObject { Data = -1 }; - - var context = new ValidateContext - { - ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(GlobalErrorObject), globalType } - }), - ValidationContext = new ValidationContext(globalErrorInstance) - }; - - await globalType.ValidateAsync(globalErrorInstance, context, default); - - Assert.NotNull(context.ValidationErrors); - var globalError = Assert.Single(context.ValidationErrors); - Assert.Equal(string.Empty, globalError.Key); - Assert.Equal("Data must be positive.", globalError.Value.Single()); - - var multiType = new TestValidatableTypeInfo( - typeof(MultiMemberErrorObject), - [ - CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "FirstName", "FirstName", []), - CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "LastName", "LastName", []) - ]); - - context.ValidationErrors = []; - context.ValidationOptions = new TestValidationOptions(new Dictionary - { - { typeof(MultiMemberErrorObject), multiType } - }); - - var multiErrorInstance = new MultiMemberErrorObject { FirstName = "", LastName = "" }; - context.ValidationContext = new ValidationContext(multiErrorInstance); - - await multiType.ValidateAsync(multiErrorInstance, context, default); - - Assert.NotNull(context.ValidationErrors); - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("FirstName", kvp.Key); - Assert.Equal("FirstName and LastName are required.", kvp.Value.First()); - }, - kvp => - { - Assert.Equal("LastName", kvp.Key); - Assert.Equal("FirstName and LastName are required.", kvp.Value.First()); - }); - } - - // Returns no member names to validate https://github.com/dotnet/aspnetcore/issues/61739 - private class GlobalErrorObject : IValidatableObject - { - public int Data { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (Data <= 0) - { - yield return new ValidationResult("Data must be positive."); - } - } - } - - // Returns multiple member names to validate https://github.com/dotnet/aspnetcore/issues/61739 - private class MultiMemberErrorObject : IValidatableObject - { - public string? FirstName { get; set; } - public string? LastName { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (string.IsNullOrEmpty(FirstName) || string.IsNullOrEmpty(LastName)) - { - // MULTIPLE member names - yield return new ValidationResult( - "FirstName and LastName are required.", - [nameof(FirstName), nameof(LastName)]); - } - } - } - - private ValidatablePropertyInfo CreatePropertyInfo( - Type containingType, - Type propertyType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) - { - return new TestValidatablePropertyInfo( - containingType, - propertyType, - name, - displayName, - validationAttributes); - } - - // Test model classes - private class Person - { - public string? Name { get; set; } - public int Age { get; set; } - public Address? Address { get; set; } - } - - private class Address - { - public string? Street { get; set; } - public string? City { get; set; } - } - - private class Employee : IValidatableObject - { - public string? Name { get; set; } - public string? Department { get; set; } - public decimal Salary { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (Salary < 0) - { - yield return new ValidationResult("Salary must be a positive value.", ["Salary"]); - } - } - } - - private class Vehicle - { - public string? Make { get; set; } - public string? Model { get; set; } - } - - private class Car : Vehicle - { - public int Doors { get; set; } - } - - private class Order - { - public string? OrderNumber { get; set; } - public List Items { get; set; } = []; - } - - private class OrderItem - { - public string? ProductName { get; set; } - public int Quantity { get; set; } - } - - private class TreeNode - { - public string Name { get; set; } = string.Empty; - public TreeNode? Parent { get; set; } - public List Children { get; set; } = []; - } - - private class Product - { - public string SKU { get; set; } = string.Empty; - } - - private class User - { - public string? Password { get; set; } = string.Empty; - } - - private class BaseEntity - { - public Guid Id { get; set; } = Guid.NewGuid(); - } - - private class IntermediateEntity : BaseEntity - { - public DateTime CreatedAt { get; set; } - } - - private class DerivedEntity : IntermediateEntity - { - public string Name { get; set; } = string.Empty; - } - - private class PastDateAttribute : ValidationAttribute - { - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) - { - if (value is DateTime date && date > DateTime.Now) - { - return new ValidationResult("Date must be in the past."); - } - - return ValidationResult.Success; - } - } - - private class CustomSkuValidationAttribute : ValidationAttribute - { - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) - { - if (value is string sku && !sku.StartsWith("PROD-", StringComparison.Ordinal)) - { - return new ValidationResult("SKU must start with 'PROD-'."); - } - - return ValidationResult.Success; - } - } - - private class PasswordComplexityAttribute : ValidationAttribute - { - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) - { - if (value is string password) - { - var hasDigit = password.Any(c => char.IsDigit(c)); - var hasSpecial = password.Any(c => !char.IsLetterOrDigit(c)); - - if (!hasDigit || !hasSpecial) - { - return new ValidationResult("Password must contain at least one number and one special character."); - } - } - - return ValidationResult.Success; - } - } - - // Test implementations - private class TestValidatablePropertyInfo : ValidatablePropertyInfo - { - private readonly ValidationAttribute[] _validationAttributes; - - public TestValidatablePropertyInfo( - Type containingType, - Type propertyType, - string name, - string displayName, - ValidationAttribute[] validationAttributes) - : base(containingType, propertyType, name, displayName) - { - _validationAttributes = validationAttributes; - } - - protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; - } - - private class TestValidatableTypeInfo : ValidatableTypeInfo - { - public TestValidatableTypeInfo( - Type type, - ValidatablePropertyInfo[] members) - : base(type, members) - { - } - } - - private class TestValidationOptions : ValidationOptions - { - public TestValidationOptions(Dictionary typeInfoMappings) - { - // Create a custom resolver that uses the dictionary - var resolver = new DictionaryBasedResolver(typeInfoMappings); - - // Add it to the resolvers collection - Resolvers.Add(resolver); - } - - // Private resolver implementation that uses a dictionary lookup - private class DictionaryBasedResolver : IValidatableInfoResolver - { - private readonly Dictionary _typeInfoMappings; - - public DictionaryBasedResolver(Dictionary typeInfoMappings) - { - _typeInfoMappings = typeInfoMappings; - } - - public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) - { - if (_typeInfoMappings.TryGetValue(type, out var info)) - { - validatableInfo = info; - return true; - } - validatableInfo = null; - return false; - } - - public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - } -} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj deleted file mode 100644 index 560b51e35691..000000000000 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netstandard2.0 - true - false - true - false - enable - true - - - - - - - - diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index b45c15cbe792..1900e5ea3ebb 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -32,7 +32,6 @@ - @@ -45,6 +44,5 @@ - diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs deleted file mode 100644 index 50cd7eca1769..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs +++ /dev/null @@ -1,374 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateComplexTypes() - { - // Arrange - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed"!)); - -app.Run(); - -public class ComplexType -{ - [Range(10, 100)] - public int IntegerWithRange { get; set; } = 10; - - [Range(10, 100), Display(Name = "Valid identifier")] - public int IntegerWithRangeAndDisplayName { get; set; } = 50; - - [Required] - public SubType PropertyWithMemberAttributes { get; set; } = new SubType("some-value", default); - - public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType("some-value", default); - - public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance("some-value", default); - - // Nullable to validate https://github.com/dotnet/aspnetcore/issues/61737 - public List? ListOfSubTypes { get; set; } = []; - - [DerivedValidation(ErrorMessage = "Value must be an even number")] - public int IntegerWithDerivedValidationAttribute { get; set; } - - [CustomValidation(typeof(CustomValidators), nameof(CustomValidators.Validate))] - public int IntegerWithCustomValidation { get; set; } = 0; - - [DerivedValidation, Range(10, 100)] - public int PropertyWithMultipleAttributes { get; set; } = 10; -} - -public class DerivedValidationAttribute : ValidationAttribute -{ - public override bool IsValid(object? value) => value is int number && number % 2 == 0; -} - -public class SubType(string? requiredProperty, string? stringWithLength) -{ - [Required] - public string RequiredProperty { get; } = requiredProperty; - - [StringLength(10)] - public string? StringWithLength { get; } = stringWithLength; -} - -public class SubTypeWithInheritance(string? requiredProperty, string? stringWithLength) : SubType(requiredProperty, stringWithLength) -{ - [EmailAddress] - public string? EmailString { get; set; } -} - -public static class CustomValidators -{ - public static ValidationResult Validate(int number, ValidationContext validationContext) - { - var parent = (ComplexType)validationContext.ObjectInstance; - - if (parent.IntegerWithRange == number) - { - return new ValidationResult( - "Can't use the same number value in two properties on the same class.", - new[] { validationContext.MemberName }); - } - - return ValidationResult.Success; - } -} -"""; - await Verify(source, out var compilation); - await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => - { - await InvalidIntegerWithRangeProducesError(endpoint); - await InvalidIntegerWithRangeAndDisplayNameProducesError(endpoint); - await MissingRequiredSubtypePropertyProducesError(endpoint); - await InvalidRequiredSubtypePropertyProducesError(endpoint); - await InvalidSubTypeWithInheritancePropertyProducesError(endpoint); - await InvalidListOfSubTypesProducesError(endpoint); - await InvalidPropertyWithDerivedValidationAttributeProducesError(endpoint); - await InvalidPropertyWithMultipleAttributesProducesError(endpoint); - await InvalidPropertyWithCustomValidationProducesError(endpoint); - await ValidInputProducesNoWarnings(endpoint); - - async Task InvalidIntegerWithRangeProducesError(Endpoint endpoint) - { - - var payload = """ - { - "IntegerWithRange": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithRange", kvp.Key); - Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); - }); - } - - async Task InvalidIntegerWithRangeAndDisplayNameProducesError(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithRangeAndDisplayName": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key); - Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single()); - }); - } - - async Task MissingRequiredSubtypePropertyProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyWithMemberAttributes": null - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("PropertyWithMemberAttributes", kvp.Key); - Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single()); - }); - } - - async Task InvalidRequiredSubtypePropertyProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyWithMemberAttributes": { - "RequiredProperty": "", - "StringWithLength": "way-too-long" - } - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - kvp => - { - Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyWithInheritance": { - "RequiredProperty": "", - "StringWithLength": "way-too-long", - "EmailString": "not-an-email" - } - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - kvp => - { - Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key); - Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidListOfSubTypesProducesError(Endpoint endpoint) - { - var payload = """ - { - "ListOfSubTypes": [ - { - "RequiredProperty": "", - "StringWithLength": "way-too-long" - }, - { - "RequiredProperty": "valid", - "StringWithLength": "way-too-long" - }, - { - "RequiredProperty": "valid", - "StringWithLength": "valid" - } - ] - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - kvp => - { - Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithDerivedValidationAttribute": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithDerivedValidationAttribute", kvp.Key); - Assert.Equal("Value must be an even number", kvp.Value.Single()); - }); - } - - async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyWithMultipleAttributes": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("PropertyWithMultipleAttributes", kvp.Key); - Assert.Collection(kvp.Value, - error => - { - Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error); - }, - error => - { - Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error); - }); - }); - } - - async Task InvalidPropertyWithCustomValidationProducesError(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithRange": 42, - "IntegerWithCustomValidation": 42 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithCustomValidation", kvp.Key); - var error = Assert.Single(kvp.Value); - Assert.Equal("Can't use the same number value in two properties on the same class.", error); - }); - } - - async Task ValidInputProducesNoWarnings(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithRange": 50, - "IntegerWithRangeAndDisplayName": 50, - "PropertyWithMemberAttributes": { - "RequiredProperty": "valid", - "StringWithLength": "valid" - }, - "PropertyWithoutMemberAttributes": { - "RequiredProperty": "valid", - "StringWithLength": "valid" - }, - "PropertyWithInheritance": { - "RequiredProperty": "valid", - "StringWithLength": "valid", - "EmailString": "test@example.com" - }, - "ListOfSubTypes": [], - "IntegerWithDerivedValidationAttribute": 2, - "IntegerWithCustomValidation": 0, - "PropertyWithMultipleAttributes": 12 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - await endpoint.RequestDelegate(context); - - Assert.Equal(200, context.Response.StatusCode); - } - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs deleted file mode 100644 index 70f1d725bc70..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.IValidatableObject.cs +++ /dev/null @@ -1,208 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateIValidatableObject() - { - var source = """ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); -builder.Services.AddSingleton(); -builder.Services.AddKeyedSingleton("serviceKey"); -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.MapPost("/validatable-object", ( - ComplexValidatableType model, - // Demonstrates that parameters that are annotated with [FromService] are not processed - // by the source generator and not emitted as ValidatableTypes in the generated code. - [FromServices] IRangeService rangeService, - [FromKeyedServices("serviceKey")] TestService testService) => Results.Ok(rangeService.GetMinimum())); - -app.Run(); - -public class ComplexValidatableType: IValidatableObject -{ - [Display(Name = "Value 1")] - public int Value1 { get; set; } - - [EmailAddress] - [Required] - public required string Value2 { get; set; } = "test@example.com"; - - public ValidatableSubType SubType { get; set; } = new ValidatableSubType(); - - public IEnumerable Validate(ValidationContext validationContext) - { - var rangeService = (IRangeService?)validationContext.GetService(typeof(IRangeService)); - var minimum = rangeService?.GetMinimum(); - var maximum = rangeService?.GetMaximum(); - if (Value1 < minimum || Value1 > maximum) - { - yield return new ValidationResult($"The field {nameof(Value1)} must be between {minimum} and {maximum}.", [nameof(Value1)]); - } - } -} - -public class SubType -{ - [Required] - // This gets ignored since it has an unsupported constructor name - [Display(ShortName = "SubType")] - public string RequiredProperty { get; set; } = "some-value"; - - [StringLength(10)] - public string? StringWithLength { get; set; } -} - -public class ValidatableSubType : SubType, IValidatableObject -{ - public string Value3 { get; set; } = "some-value"; - - public IEnumerable Validate(ValidationContext validationContext) - { - if (Value3 != "some-value") - { - yield return new ValidationResult($"The field {validationContext.DisplayName} must be 'some-value'.", [nameof(Value3)]); - } - } -} - -public interface IRangeService -{ - int GetMinimum(); - int GetMaximum(); -} - -public class RangeService : IRangeService -{ - public int GetMinimum() => 10; - public int GetMaximum() => 100; -} - -public class TestService -{ - [Range(10, 100)] - public int Value { get; set; } = 4; -} -"""; - - await Verify(source, out var compilation); - await VerifyEndpoint(compilation, "/validatable-object", async (endpoint, serviceProvider) => - { - await ValidateMethodCalledIfPropertyValidationsFail(); - await ValidateForSubtypeInvokedFirst(); - await ValidateForTopLevelInvoked(); - - async Task ValidateMethodCalledIfPropertyValidationsFail() - { - var httpContext = CreateHttpContextWithPayload(""" - { - "Value1": 5, - "Value2": "", - "SubType": { - "Value3": "foo", - "RequiredProperty": "", - "StringWithLength": "" - } - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("Value2", error.Key); - Assert.Collection(error.Value, - msg => Assert.Equal("The Value2 field is required.", msg)); - }, - error => - { - Assert.Equal("SubType.RequiredProperty", error.Key); - Assert.Equal("The RequiredProperty field is required.", error.Value.Single()); - }, - error => - { - Assert.Equal("SubType.Value3", error.Key); - Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); - }); - } - - async Task ValidateForSubtypeInvokedFirst() - { - var httpContext = CreateHttpContextWithPayload(""" - { - "Value1": 5, - "Value2": "test@test.com", - "SubType": { - "Value3": "foo", - "RequiredProperty": "some-value-2", - "StringWithLength": "element" - } - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("SubType.Value3", error.Key); - Assert.Equal("The field ValidatableSubType must be 'some-value'.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); - }); - } - - async Task ValidateForTopLevelInvoked() - { - var httpContext = CreateHttpContextWithPayload(""" - { - "Value1": 5, - "Value2": "test@test.com", - "SubType": { - "Value3": "some-value", - "RequiredProperty": "some-value-2", - "StringWithLength": "element" - } - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value1 must be between 10 and 100.", error.Value.Single()); - }); - } - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs deleted file mode 100644 index 58478ece957e..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.MultipleNamespaces.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateMultipleNamespaces() - { - // Arrange - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.MapPost("/namespace-one", (NamespaceOne.Type obj) => Results.Ok("Passed")); -app.MapPost("/namespace-two", (NamespaceTwo.Type obj) => Results.Ok("Passed")); - -app.Run(); - -namespace NamespaceOne { - public class Type - { - [StringLength(10)] - public string StringWithLength { get; set; } = string.Empty; - } -} - -namespace NamespaceTwo { - public class Type - { - [StringLength(20)] - public string StringWithLength { get; set; } = string.Empty; - } -} -"""; - await Verify(source, out var compilation); - await VerifyEndpoint(compilation, "/namespace-one", async (endpoint, serviceProvider) => - { - await InvalidStringWithLengthProducesError(endpoint); - await ValidInputProducesNoWarnings(endpoint); - - async Task InvalidStringWithLengthProducesError(Endpoint endpoint) - { - var payload = """ - { - "StringWithLength": "abcdefghijk" - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task ValidInputProducesNoWarnings(Endpoint endpoint) - { - var payload = """ - { - "StringWithLength": "abc" - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - await endpoint.RequestDelegate(context); - - Assert.Equal(200, context.Response.StatusCode); - } - }); - await VerifyEndpoint(compilation, "/namespace-two", async (endpoint, serviceProvider) => - { - await InvalidStringWithLengthProducesError(endpoint); - await ValidInputProducesNoWarnings(endpoint); - - async Task InvalidStringWithLengthProducesError(Endpoint endpoint) - { - var payload = """ - { - "StringWithLength": "abcdefghijklmnopqrstu" - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 20.", kvp.Value.Single()); - }); - } - - async Task ValidInputProducesNoWarnings(Endpoint endpoint) - { - var payload = """ - { - "StringWithLength": "abcdefghijk" - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - await endpoint.RequestDelegate(context); - - Assert.Equal(200, context.Response.StatusCode); - } - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs deleted file mode 100644 index 410c74a5cecc..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.NoOp.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task DoesNotEmitIfNoAddValidationCallExists() - { - // Arrange - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -var app = builder.Build(); - -app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed")); - -app.Run(); - -public class ComplexType -{ - [Range(10, 100)] - public int IntegerWithRange { get; set; } = 10; -} -"""; - await Verify(source, out var compilation); - // Verify that we don't validate types if no AddValidation call exists - await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => - { - var payload = """ - { - "IntegerWithRange": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - }); - } - - [Fact] - public async Task DoesNotEmitIfNotCorrectAddValidationCallExists() - { - // Arrange - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation("example"); -SomeExtensions.AddValidation(builder.Services); - -var app = builder.Build(); - -app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed")); - -app.Run(); - -public class ComplexType -{ - [Range(10, 100)] - public int IntegerWithRange { get; set; } = 10; -} - -public static class SomeExtensions -{ - public static IServiceCollection AddValidation(this IServiceCollection services, string someString) - { - // This is not the correct AddValidation method - return services; - } - - public static IServiceCollection AddValidation(this IServiceCollection services, Action? configureOptions = null) - { - // This is not the correct AddValidation method - return services; - } -} -"""; - await Verify(source, out var compilation); - // Verify that we don't validate types if no AddValidation call exists - await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => - { - var payload = """ - { - "IntegerWithRange": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - }); - } - - [Fact] - public async Task DoesNotEmitForExemptTypes() - { - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.IO.Pipelines; -using System.Threading; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.MapGet("/exempt-1", (HttpContext context) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-2", (HttpRequest request) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-3", (HttpResponse response) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-4", (IFormCollection formCollection) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-5", (IFormFileCollection formFileCollection) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-6", (IFormFile formFile) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-7", (Stream stream) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-8", (PipeReader pipeReader) => Results.Ok("Exempt Passed!")); -app.MapGet("/exempt-9", (CancellationToken cancellationToken) => Results.Ok("Exempt Passed!")); -app.MapPost("/complex-type", (ComplexType complexType) => Results.Ok("Passed")); - -app.Run(); - -public class ComplexType -{ - [Range(10, 100)] - public int IntegerWithRange { get; set; } = 10; -} -"""; - await Verify(source, out var compilation); - // Verify that we can validate non-exempt types - await VerifyEndpoint(compilation, "/complex-type", async (endpoint, serviceProvider) => - { - var payload = """ - { - "IntegerWithRange": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithRange", kvp.Key); - Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); - }); - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs deleted file mode 100644 index f05f80999147..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parameters.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateParameters() - { - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation(); -builder.Services.AddSingleton(); -builder.Services.AddKeyedSingleton("serviceKey"); - -var app = builder.Build(); - -app.MapPost("/params", ( - // Skipped from validation because it is resolved as a service by IServiceProviderIsService - TestService testService, - // Skipped from validation because it is marked as a [FromKeyedService] parameter - [FromKeyedServices("serviceKey")] TestService testService2, - [Range(10, 100)] int value1, - [Range(10, 100), Display(Name = "Valid identifier")] int value2, - [Required] string value3 = "some-value", - [CustomValidation(ErrorMessage = "Value must be an even number")] int value4 = 4, - [CustomValidation, Range(10, 100)] int value5 = 10, - // Skipped from validation because it is marked as a [FromService] parameter - [FromServices] [Range(10, 100)] int? value6 = 4, - Dictionary? testDict = null) => "OK"); - -app.Run(); - -public class CustomValidationAttribute : ValidationAttribute -{ - public override bool IsValid(object? value) => value is int number && number % 2 == 0; -} - -public class TestService -{ - [Range(10, 100)] - public int Value { get; set; } = 4; -} -"""; - await Verify(source, out var compilation); - await VerifyEndpoint(compilation, "/params", async (endpoint, serviceProvider) => - { - var context = CreateHttpContext(serviceProvider); - context.Request.QueryString = new QueryString("?value1=5&value2=5&value3=&value4=3&value5=5"); - await endpoint.RequestDelegate(context); - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("value1", error.Key); - Assert.Equal("The field value1 must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("value2", error.Key); - Assert.Equal("The field Valid identifier must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("value3", error.Key); - Assert.Equal("The value3 field is required.", error.Value.Single()); - }, - error => - { - Assert.Equal("value4", error.Key); - Assert.Equal("Value must be an even number", error.Value.Single()); - }, - error => - { - Assert.Equal("value5", error.Key); - Assert.Collection(error.Value, error => - { - Assert.Equal("The field value5 is invalid.", error); - }, - error => - { - Assert.Equal("The field value5 must be between 10 and 100.", error); - }); - }); - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs deleted file mode 100644 index 6cebd6df8584..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateTypeWithParsableProperties() - { - // Arrange - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.MapPost("/complex-type-with-parsable-properties", (ComplexTypeWithParsableProperties complexType) => Results.Ok("Passed"!)); - -app.Run(); - -public class ComplexTypeWithParsableProperties -{ - [RegularExpression("^((?!00000000-0000-0000-0000-000000000000).)*$", ErrorMessage = "Cannot use default Guid")] - public Guid? GuidWithRegularExpression { get; set; } = default; - - [Required] - public TimeOnly? TimeOnlyWithRequiredValue { get; set; } = TimeOnly.FromDateTime(DateTime.UtcNow); - - [Url(ErrorMessage = "The field Url must be a valid URL.")] - public string? Url { get; set; } = "https://example.com"; - - [Required] - [Range(typeof(DateOnly), "2023-01-01", "2025-12-31", ErrorMessage = "Date must be between 2023-01-01 and 2025-12-31")] - public DateOnly? DateOnlyWithRange { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow); - - [Range(typeof(DateTime), "2023-01-01", "2025-12-31", ErrorMessage = "DateTime must be between 2023-01-01 and 2025-12-31")] - public DateTime? DateTimeWithRange { get; set; } = DateTime.UtcNow; - - [Range(typeof(decimal), "0.1", "100.5", ErrorMessage = "Amount must be between 0.1 and 100.5")] - public decimal? DecimalWithRange { get; set; } = 50.5m; - - [Range(0, 12, ErrorMessage = "Hours must be between 0 and 12")] - public TimeSpan? TimeSpanWithHourRange { get; set; } = TimeSpan.FromHours(12); - - [Range(0, 1, ErrorMessage = "Boolean value must be 0 or 1")] - public bool BooleanWithRange { get; set; } = true; - - [RegularExpression(@"^\d+\.\d+\.\d+$", ErrorMessage = "Must be a valid version number (e.g. 1.0.0)")] - public Version? VersionWithRegex { get; set; } = new Version(1, 0, 0); -} -"""; - await Verify(source, out var compilation); - await VerifyEndpoint(compilation, "/complex-type-with-parsable-properties", async (endpoint, serviceProvider) => - { - var payload = """ - { - "TimeOnlyWithRequiredValue": null, - "IntWithRange": 150, - "StringWithLength": "AB", - "Email": "invalid-email", - "Url": "invalid-url", - "DateOnlyWithRange": "2026-05-01", - "DecimalWithRange": "150.75", - "TimeSpanWithHourRange": "22:00:00", - "VersionWithRegex": "1.0", - "EnumProperty": "Invalid" - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - - // Assert on each error with Assert.Collection - Assert.Collection(problemDetails.Errors.OrderBy(kvp => kvp.Key), - error => - { - Assert.Equal("DateOnlyWithRange", error.Key); - Assert.Contains("Date must be between 2023-01-01 and 2025-12-31", error.Value); - }, - error => - { - Assert.Equal("DecimalWithRange", error.Key); - Assert.Contains("Amount must be between 0.1 and 100.5", error.Value); - }, - error => - { - Assert.Equal("TimeOnlyWithRequiredValue", error.Key); - Assert.Contains("The TimeOnlyWithRequiredValue field is required.", error.Value); - }, - error => - { - Assert.Equal("TimeSpanWithHourRange", error.Key); - Assert.Contains("Hours must be between 0 and 12", error.Value); - }, - error => - { - Assert.Equal("Url", error.Key); - Assert.Contains("The field Url must be a valid URL.", error.Value); - }, - error => - { - Assert.Equal("VersionWithRegex", error.Key); - Assert.Contains("Must be a valid version number (e.g. 1.0.0)", error.Value); - } - ); - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Polymorphism.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Polymorphism.cs deleted file mode 100644 index 54148e784a0a..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Polymorphism.cs +++ /dev/null @@ -1,202 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidatePolymorphicTypes() - { - var source = """ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.MapPost("/basic-polymorphism", (BaseType model) => Results.Ok()); -app.MapPost("/validatable-polymorphism", (BaseValidatableType model) => Results.Ok()); -app.MapPost("/polymorphism-container", (ContainerType model) => Results.Ok()); - -app.Run(); - -public class ContainerType -{ - public BaseType BaseType { get; set; } = new BaseType(); - public BaseValidatableType BaseValidatableType { get; set; } = new BaseValidatableType(); -} - -[JsonDerivedType(typeof(BaseType), typeDiscriminator: "base")] -[JsonDerivedType(typeof(DerivedType), typeDiscriminator: "derived")] -public class BaseType -{ - [Display(Name = "Value 1")] - [Range(10, 100)] - public int Value1 { get; set; } - - [EmailAddress] - [Required] - public string Value2 { get; set; } = "test@example.com"; -} - -public class DerivedType : BaseType -{ - [Base64String] - public string? Value3 { get; set; } -} - -[JsonDerivedType(typeof(BaseValidatableType), typeDiscriminator: "base")] -[JsonDerivedType(typeof(DerivedValidatableType), typeDiscriminator: "derived")] -public class BaseValidatableType : IValidatableObject -{ - [Display(Name = "Value 1")] - public int Value1 { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if (Value1 < 10 || Value1 > 100) - { - yield return new ValidationResult("The field Value 1 must be between 10 and 100.", new[] { nameof(Value1) }); - } - } -} - -public class DerivedValidatableType : BaseValidatableType -{ - [EmailAddress] - public required string Value3 { get; set; } -} -"""; - await Verify(source, out var compilation); - - await VerifyEndpoint(compilation, "/basic-polymorphism", async (endpoint, serviceProvider) => - { - var httpContext = CreateHttpContextWithPayload(""" - { - "$type": "derived", - "Value1": 5, - "Value2": "invalid-email", - "Value3": "invalid-base64" - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("Value3", error.Key); - Assert.Equal("The Value3 field is not a valid Base64 encoding.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value2", error.Key); - Assert.Equal("The Value2 field is not a valid e-mail address.", error.Value.Single()); - }); - }); - - await VerifyEndpoint(compilation, "/validatable-polymorphism", async (endpoint, serviceProvider) => - { - var httpContext = CreateHttpContextWithPayload(""" - { - "$type": "derived", - "Value1": 5, - "Value3": "invalid-email" - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("Value3", error.Key); - Assert.Equal("The Value3 field is not a valid e-mail address.", error.Value.Single()); - }, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); - }); - - httpContext = CreateHttpContextWithPayload(""" - { - "$type": "derived", - "Value1": 5, - "Value3": "test@example.com" - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails1 = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails1.Errors, - error => - { - Assert.Equal("Value1", error.Key); - Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); - }); - }); - - await VerifyEndpoint(compilation, "/polymorphism-container", async (endpoint, serviceProvider) => - { - var httpContext = CreateHttpContextWithPayload(""" - { - "BaseType": { - "$type": "derived", - "Value1": 5, - "Value2": "invalid-email", - "Value3": "invalid-base64" - }, - "BaseValidatableType": { - "$type": "derived", - "Value1": 5, - "Value3": "test@example.com" - } - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("BaseType.Value3", error.Key); - Assert.Equal("The Value3 field is not a valid Base64 encoding.", error.Value.Single()); - }, - error => - { - Assert.Equal("BaseType.Value1", error.Key); - Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("BaseType.Value2", error.Key); - Assert.Equal("The Value2 field is not a valid e-mail address.", error.Value.Single()); - }, - error => - { - Assert.Equal("BaseValidatableType.Value1", error.Key); - Assert.Equal("The field Value 1 must be between 10 and 100.", error.Value.Single()); - }); - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs deleted file mode 100644 index 4f296c66d648..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.RecordType.cs +++ /dev/null @@ -1,371 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateRecordTypes() - { - // Arrange - var source = """ -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.MapPost("/validatable-record", (ValidatableRecord validatableRecord) => Results.Ok("Passed"!)); - -app.Run(); - -public class DerivedValidationAttribute : ValidationAttribute -{ - public override bool IsValid(object? value) => value is int number && number % 2 == 0; -} - -public record SubType([Required] string RequiredProperty = "some-value", [StringLength(10)] string? StringWithLength = default); - -public record SubTypeWithInheritance([EmailAddress] string? EmailString, string RequiredProperty, string? StringWithLength) : SubType(RequiredProperty, StringWithLength); - -public record SubTypeWithoutConstructor -{ - [Required] - public string RequiredProperty { get; set; } = "some-value"; - - [StringLength(10)] - public string? StringWithLength { get; set; } -} - -public static class CustomValidators -{ - public static ValidationResult Validate(int number, ValidationContext validationContext) - { - var parent = (ValidatableRecord)validationContext.ObjectInstance; - if (number == parent.IntegerWithRange) - { - return new ValidationResult( - "Can't use the same number value in two properties on the same class.", - new[] { validationContext.MemberName }); - } - - return ValidationResult.Success; - } -} - -public record ValidatableRecord( - [Range(10, 100)] - int IntegerWithRange = 10, - [Range(10, 100), Display(Name = "Valid identifier")] - int IntegerWithRangeAndDisplayName = 50, - SubType PropertyWithMemberAttributes = default, - SubType PropertyWithoutMemberAttributes = default, - SubTypeWithInheritance PropertyWithInheritance = default, - SubTypeWithoutConstructor PropertyOfSubtypeWithoutConstructor = default, - List ListOfSubTypes = default, - [DerivedValidation(ErrorMessage = "Value must be an even number")] - int IntegerWithDerivedValidationAttribute = 0, - [CustomValidation(typeof(CustomValidators), nameof(CustomValidators.Validate))] - int IntegerWithCustomValidation = 0, - [DerivedValidation, Range(10, 100)] - int PropertyWithMultipleAttributes = 10 -); -"""; - await Verify(source, out var compilation); - await VerifyEndpoint(compilation, "/validatable-record", async (endpoint, serviceProvider) => - { - await InvalidIntegerWithRangeProducesError(endpoint); - await InvalidIntegerWithRangeAndDisplayNameProducesError(endpoint); - await InvalidRequiredSubtypePropertyProducesError(endpoint); - await InvalidSubTypeWithInheritancePropertyProducesError(endpoint); - await InvalidListOfSubTypesProducesError(endpoint); - await InvalidPropertyWithDerivedValidationAttributeProducesError(endpoint); - await InvalidPropertyWithMultipleAttributesProducesError(endpoint); - await InvalidPropertyWithCustomValidationProducesError(endpoint); - await InvalidPropertyOfSubtypeWithoutConstructorProducesError(endpoint); - await ValidInputProducesNoWarnings(endpoint); - - async Task InvalidIntegerWithRangeProducesError(Endpoint endpoint) - { - - var payload = """ - { - "IntegerWithRange": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithRange", kvp.Key); - Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); - }); - } - - async Task InvalidIntegerWithRangeAndDisplayNameProducesError(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithRangeAndDisplayName": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key); - Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single()); - }); - } - - async Task InvalidRequiredSubtypePropertyProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyWithMemberAttributes": { - "RequiredProperty": "", - "StringWithLength": "way-too-long" - } - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - kvp => - { - Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidSubTypeWithInheritancePropertyProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyWithInheritance": { - "RequiredProperty": "", - "StringWithLength": "way-too-long", - "EmailString": "not-an-email" - } - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - kvp => - { - Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key); - Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidListOfSubTypesProducesError(Endpoint endpoint) - { - var payload = """ - { - "ListOfSubTypes": [ - { - "RequiredProperty": "", - "StringWithLength": "way-too-long" - }, - { - "RequiredProperty": "valid", - "StringWithLength": "way-too-long" - }, - { - "RequiredProperty": "valid", - "StringWithLength": "valid" - } - ] - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - kvp => - { - Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidPropertyWithDerivedValidationAttributeProducesError(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithDerivedValidationAttribute": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithDerivedValidationAttribute", kvp.Key); - Assert.Equal("Value must be an even number", kvp.Value.Single()); - }); - } - - async Task InvalidPropertyWithMultipleAttributesProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyWithMultipleAttributes": 5 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("PropertyWithMultipleAttributes", kvp.Key); - Assert.Collection(kvp.Value, - error => - { - Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error); - }, - error => - { - Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error); - }); - }); - } - - async Task InvalidPropertyWithCustomValidationProducesError(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithRange": 42, - "IntegerWithCustomValidation": 42 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, kvp => - { - Assert.Equal("IntegerWithCustomValidation", kvp.Key); - var error = Assert.Single(kvp.Value); - Assert.Equal("Can't use the same number value in two properties on the same class.", error); - }); - } - - async Task InvalidPropertyOfSubtypeWithoutConstructorProducesError(Endpoint endpoint) - { - var payload = """ - { - "PropertyOfSubtypeWithoutConstructor": { - "RequiredProperty": "", - "StringWithLength": "way-too-long" - } - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - - await endpoint.RequestDelegate(context); - - var problemDetails = await AssertBadRequest(context); - Assert.Collection(problemDetails.Errors, - kvp => - { - Assert.Equal("PropertyOfSubtypeWithoutConstructor.RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyOfSubtypeWithoutConstructor.StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task ValidInputProducesNoWarnings(Endpoint endpoint) - { - var payload = """ - { - "IntegerWithRange": 50, - "IntegerWithRangeAndDisplayName": 50, - "PropertyWithMemberAttributes": { - "RequiredProperty": "valid", - "StringWithLength": "valid" - }, - "PropertyWithoutMemberAttributes": { - "RequiredProperty": "valid", - "StringWithLength": "valid" - }, - "PropertyWithInheritance": { - "RequiredProperty": "valid", - "StringWithLength": "valid", - "EmailString": "test@example.com" - }, - "ListOfSubTypes": [], - "IntegerWithDerivedValidationAttribute": 2, - "IntegerWithCustomValidation": 0, - "PropertyWithMultipleAttributes": 12 - } - """; - var context = CreateHttpContextWithPayload(payload, serviceProvider); - await endpoint.RequestDelegate(context); - - Assert.Equal(200, context.Response.StatusCode); - } - }); - - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs deleted file mode 100644 index 4affa35f8997..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Recursion.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateRecursiveTypes() - { - var source = """ -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); -builder.Services.AddValidation(options => -{ - options.MaxDepth = 8; -}); - -var app = builder.Build(); - -app.MapPost("/recursive-type", (RecursiveType model) => Results.Ok()); - -app.Run(); - -public class RecursiveType -{ - [Range(10, 100)] - public int Value { get; set; } - public RecursiveType? Next { get; set; } -} -"""; - await Verify(source, out var compilation); - - await VerifyEndpoint(compilation, "/recursive-type", async (endpoint, serviceProvider) => - { - await ThrowsExceptionForDeeplyNestedType(endpoint); - await ValidatesTypeWithLimitedNesting(endpoint); - - async Task ThrowsExceptionForDeeplyNestedType(Endpoint endpoint) - { - var httpContext = CreateHttpContextWithPayload(""" - { - "value": 1, - "next": { - "value": 2, - "next": { - "value": 3, - "next": { - "value": 4, - "next": { - "value": 5, - "next": { - "value": 6, - "next": { - "value": 7, - "next": { - "value": 8, - "next": { - "value": 9, - "next": { - "value": 10 - } - } - } - } - } - } - } - } - } - } - """, serviceProvider); - - var exception = await Assert.ThrowsAsync(async () => await endpoint.RequestDelegate(httpContext)); - } - - async Task ValidatesTypeWithLimitedNesting(Endpoint endpoint) - { - var httpContext = CreateHttpContextWithPayload(""" - { - "value": 1, - "next": { - "value": 2, - "next": { - "value": 3, - "next": { - "value": 4, - "next": { - "value": 5, - "next": { - "value": 6, - "next": { - "value": 7, - "next": { - "value": 8 - } - } - } - } - } - } - } - } - """, serviceProvider); - - await endpoint.RequestDelegate(httpContext); - - var problemDetails = await AssertBadRequest(httpContext); - Assert.Collection(problemDetails.Errors, - error => - { - Assert.Equal("Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Next.Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Next.Next.Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Next.Next.Next.Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Next.Next.Next.Next.Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Next.Next.Next.Next.Next.Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Next.Next.Next.Next.Next.Next.Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }, - error => - { - Assert.Equal("Next.Next.Next.Next.Next.Next.Next.Value", error.Key); - Assert.Equal("The field Value must be between 10 and 100.", error.Value.Single()); - }); - } - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ValidatableType.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ValidatableType.cs deleted file mode 100644 index 0ddc3613249f..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ValidatableType.cs +++ /dev/null @@ -1,381 +0,0 @@ -#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. - -// 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 Microsoft.AspNetCore.Http.Validation; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase -{ - [Fact] - public async Task CanValidateTypesWithAttribute() - { - var source = """ -#pragma warning disable ASP0029 - -using System; -using System.ComponentModel.DataAnnotations; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -var builder = WebApplication.CreateBuilder(); - -builder.Services.AddValidation(); - -var app = builder.Build(); - -app.Run(); - -[ValidatableType] -public class ComplexType -{ - [Range(10, 100)] - public int IntegerWithRange { get; set; } = 10; - - [Range(10, 100), Display(Name = "Valid identifier")] - public int IntegerWithRangeAndDisplayName { get; set; } = 50; - - [Required] - public SubType PropertyWithMemberAttributes { get; set; } = new SubType(); - - public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType(); - - public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance(); - - public List ListOfSubTypes { get; set; } = []; - - [CustomValidation(ErrorMessage = "Value must be an even number")] - public int IntegerWithCustomValidationAttribute { get; set; } - - [CustomValidation, Range(10, 100)] - public int PropertyWithMultipleAttributes { get; set; } = 10; -} - -public class CustomValidationAttribute : ValidationAttribute -{ - public override bool IsValid(object? value) => value is int number && number % 2 == 0; -} - -public class SubType -{ - [Required] - public string RequiredProperty { get; set; } = "some-value"; - - [StringLength(10)] - public string? StringWithLength { get; set; } -} - -public class SubTypeWithInheritance : SubType -{ - [EmailAddress] - public string? EmailString { get; set; } -} -"""; - await Verify(source, out var compilation); - VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) => - { - Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo)); - - await InvalidIntegerWithRangeProducesError(validatableTypeInfo); - await InvalidIntegerWithRangeAndDisplayNameProducesError(validatableTypeInfo); - await MissingRequiredSubtypePropertyProducesError(validatableTypeInfo); - await InvalidRequiredSubtypePropertyProducesError(validatableTypeInfo); - await InvalidSubTypeWithInheritancePropertyProducesError(validatableTypeInfo); - await InvalidListOfSubTypesProducesError(validatableTypeInfo); - await InvalidPropertyWithDerivedValidationAttributeProducesError(validatableTypeInfo); - await InvalidPropertyWithMultipleAttributesProducesError(validatableTypeInfo); - await InvalidPropertyWithCustomValidationProducesError(validatableTypeInfo); - await ValidInputProducesNoWarnings(validatableTypeInfo); - - async Task InvalidIntegerWithRangeProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - type.GetProperty("IntegerWithRange")?.SetValue(instance, 5); - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, kvp => - { - Assert.Equal("IntegerWithRange", kvp.Key); - Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single()); - }); - } - - async Task InvalidIntegerWithRangeAndDisplayNameProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 5); - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, kvp => - { - Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key); - Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single()); - }); - } - - async Task MissingRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, null); - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, kvp => - { - Assert.Equal("PropertyWithMemberAttributes", kvp.Key); - Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single()); - }); - } - - async Task InvalidRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - var subType = Activator.CreateInstance(type.Assembly.GetType("SubType")!); - subType.GetType().GetProperty("RequiredProperty")?.SetValue(subType, ""); - subType.GetType().GetProperty("StringWithLength")?.SetValue(subType, "way-too-long"); - type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType); - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidSubTypeWithInheritancePropertyProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!); - inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, ""); - inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "way-too-long"); - inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "not-an-email"); - type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType); - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key); - Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidListOfSubTypesProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - var subTypeList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!)); - - // Create first invalid item - var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); - subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, ""); - subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "way-too-long"); - - // Create second invalid item - var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); - subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid"); - subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "way-too-long"); - - // Create valid item - var subType3 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); - subType3.GetType().GetProperty("RequiredProperty")?.SetValue(subType3, "valid"); - subType3.GetType().GetProperty("StringWithLength")?.SetValue(subType3, "valid"); - - // Add to list - subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType1]); - subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType2]); - subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType3]); - - type.GetProperty("ListOfSubTypes")?.SetValue(instance, subTypeList); - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, - kvp => - { - Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key); - Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }, - kvp => - { - Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key); - Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single()); - }); - } - - async Task InvalidPropertyWithDerivedValidationAttributeProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 5); // Odd number, should fail - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, kvp => - { - Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key); - Assert.Equal("Value must be an even number", kvp.Value.Single()); - }); - } - - async Task InvalidPropertyWithMultipleAttributesProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 5); - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, kvp => - { - Assert.Equal("PropertyWithMultipleAttributes", kvp.Key); - Assert.Collection(kvp.Value, - error => - { - Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error); - }, - error => - { - Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error); - }); - }); - } - - async Task InvalidPropertyWithCustomValidationProducesError(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 3); // Odd number should fail - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Collection(context.ValidationErrors, kvp => - { - Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key); - Assert.Equal("Value must be an even number", kvp.Value.Single()); - }); - } - - async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo) - { - var instance = Activator.CreateInstance(type); - - // Set all properties with valid values - type.GetProperty("IntegerWithRange")?.SetValue(instance, 50); - type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 50); - - // Create and set PropertyWithMemberAttributes - var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); - subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, "valid"); - subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "valid"); - type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType1); - - // Create and set PropertyWithoutMemberAttributes - var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!); - subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid"); - subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "valid"); - type.GetProperty("PropertyWithoutMemberAttributes")?.SetValue(instance, subType2); - - // Create and set PropertyWithInheritance - var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!); - inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, "valid"); - inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "valid"); - inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "test@example.com"); - type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType); - - // Create empty list for ListOfSubTypes - var emptyList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!)); - type.GetProperty("ListOfSubTypes")?.SetValue(instance, emptyList); - - // Set custom validation attributes - type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 2); // Even number should pass - type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 12); - - var context = new ValidateContext - { - ValidationOptions = validationOptions, - ValidationContext = new ValidationContext(instance) - }; - - await validatableInfo.ValidateAsync(instance, context, CancellationToken.None); - - Assert.Null(context.ValidationErrors); - } - }); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGeneratorTestBase.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGeneratorTestBase.cs deleted file mode 100644 index bb1cd12470d3..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGeneratorTestBase.cs +++ /dev/null @@ -1,586 +0,0 @@ -#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. - -// 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; -using System.Globalization; -using System.Reflection; -using System.Runtime.Loader; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Validation; -using Microsoft.AspNetCore.InternalTesting; -using Microsoft.AspNetCore.Routing; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Emit; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using static Microsoft.AspNetCore.Http.Generators.Tests.RequestDelegateCreationTestBase; - -namespace Microsoft.AspNetCore.Http.ValidationsGenerator.Tests; - -[UsesVerify] -public partial class ValidationsGeneratorTestBase : LoggedTestBase -{ - [GeneratedRegex(@"\[global::System\.Runtime\.CompilerServices\.InterceptsLocationAttribute\([^)]*\)\]")] - private static partial Regex InterceptsLocationRegex(); - - private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview) - .WithFeatures([new KeyValuePair("InterceptorsNamespaces", "Microsoft.AspNetCore.Http.Validation.Generated")]); - - internal static Task Verify(string source, out Compilation compilation) - { - var references = AppDomain.CurrentDomain.GetAssemblies() - .Where(assembly => !assembly.IsDynamic && !string.IsNullOrWhiteSpace(assembly.Location)) - .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) - .Concat( - [ - MetadataReference.CreateFromFile(typeof(WebApplicationBuilder).Assembly.Location), - MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location), - MetadataReference.CreateFromFile(typeof(IApplicationBuilder).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Microsoft.AspNetCore.Mvc.ApiExplorer.IApiDescriptionProvider).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Microsoft.AspNetCore.Mvc.ControllerBase).Assembly.Location), - MetadataReference.CreateFromFile(typeof(MvcCoreMvcBuilderExtensions).Assembly.Location), - MetadataReference.CreateFromFile(typeof(TypedResults).Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.Text.Json.Nodes.JsonArray).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.ValidationAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(RouteData).Assembly.Location), - MetadataReference.CreateFromFile(typeof(IFeatureCollection).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ValidateOptionsResult).Assembly.Location), - MetadataReference.CreateFromFile(typeof(IHttpMethodMetadata).Assembly.Location), - MetadataReference.CreateFromFile(typeof(IResult).Assembly.Location), - MetadataReference.CreateFromFile(typeof(HttpJsonServiceExtensions).Assembly.Location), - MetadataReference.CreateFromFile(typeof(IValidatableInfoResolver).Assembly.Location), - MetadataReference.CreateFromFile(typeof(EndpointFilterFactoryContext).Assembly.Location), - ]); - var inputCompilation = CSharpCompilation.Create("ValidationsGeneratorSample", - [CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: "Program.cs")], - references, - new CSharpCompilationOptions(OutputKind.ConsoleApplication)); - var generator = new ValidationsGenerator(); - var driver = CSharpGeneratorDriver.Create(generators: [generator.AsSourceGenerator()], parseOptions: ParseOptions); - return Verifier - .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics)) - .ScrubLinesWithReplace(line => InterceptsLocationRegex().Replace(line, "[InterceptsLocation]")) - .UseDirectory(SkipOnHelixAttribute.OnHelix() - ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "ValidationsGenerator", "snapshots") - : "snapshots"); - } - - internal static void VerifyValidatableType(Compilation compilation, string typeName, Action verifyFunc) - { - if (TryResolveServicesFromCompilation(compilation, targetAssemblyName: "Microsoft.AspNetCore.Http.Abstractions", typeName: "Microsoft.AspNetCore.Http.Validation.ValidationOptions", out var services, out var serviceType, out var outputAssemblyName) is false) - { - throw new InvalidOperationException("Could not resolve services from compilation."); - } - var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == outputAssemblyName); - var type = targetAssembly.GetType(typeName, throwOnError: false); - - // Get IOptions first - var optionsType = typeof(IOptions<>).MakeGenericType(serviceType); - var optionsInstance = services.GetService(optionsType) ?? throw new InvalidOperationException("Could not resolve IOptions."); - - // Then access the Value property - var valueProperty = optionsType.GetProperty("Value"); - var service = (ValidationOptions)valueProperty.GetValue(optionsInstance) ?? throw new InvalidOperationException("Could not resolve ValidationOptions."); - verifyFunc(service, type); - } - - internal static async Task VerifyEndpoint(Compilation compilation, string routePattern, Func verifyFunc) - { - if (TryResolveServicesFromCompilation(compilation, targetAssemblyName: "Microsoft.AspNetCore.Routing", typeName: "Microsoft.AspNetCore.Routing.EndpointDataSource", out var services, out var serviceType, out var outputAssemblyName) is false) - { - throw new InvalidOperationException("Could not resolve services from compilation."); - } - var service = services.GetService(serviceType) ?? throw new InvalidOperationException("Could not resolve EndpointDataSource."); - var endpoints = (IReadOnlyList)service.GetType().GetProperty("Endpoints", BindingFlags.Instance | BindingFlags.Public).GetValue(service); - var endpoint = endpoints.FirstOrDefault(endpoint => endpoint is RouteEndpoint routeEndpoint && routeEndpoint.RoutePattern.RawText == routePattern); - await verifyFunc(endpoint, services); - } - - private static bool TryResolveServicesFromCompilation(Compilation compilation, string targetAssemblyName, string typeName, out IServiceProvider serviceProvider, out Type serviceType, out string outputAssemblyName) - { - serviceProvider = null; - serviceType = null; - outputAssemblyName = $"TestProject-{Guid.NewGuid()}"; - var assemblyName = compilation.AssemblyName; - var symbolsName = Path.ChangeExtension(assemblyName, "pdb"); - - var output = new MemoryStream(); - var pdb = new MemoryStream(); - - var emitOptions = new EmitOptions( - debugInformationFormat: DebugInformationFormat.PortablePdb, - pdbFilePath: symbolsName, - outputNameOverride: outputAssemblyName); - - var embeddedTexts = new List(); - - foreach (var syntaxTree in compilation.SyntaxTrees) - { - var text = syntaxTree.GetText(); - var encoding = text.Encoding ?? Encoding.UTF8; - var buffer = encoding.GetBytes(text.ToString()); - var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true); - - var syntaxRootNode = (CSharpSyntaxNode)syntaxTree.GetRoot(); - var newSyntaxTree = CSharpSyntaxTree.Create(syntaxRootNode, options: ParseOptions, encoding: encoding, path: syntaxTree.FilePath); - - compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree); - - embeddedTexts.Add(EmbeddedText.FromSource(syntaxTree.FilePath, sourceText)); - } - - var result = compilation.Emit(output, pdb, options: emitOptions, embeddedTexts: embeddedTexts); - - Assert.Empty(result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); - Assert.True(result.Success); - - output.Position = 0; - pdb.Position = 0; - - var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb); - - void ConfigureHostBuilder(object hostBuilder) - { - ((IHostBuilder)hostBuilder).ConfigureServices((context, services) => - { - services.AddSingleton(); - services.AddSingleton(); - }); - } - - var waitForStartTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - void OnEntryPointExit(Exception exception) - { - // If the entry point exited, we'll try to complete the wait - if (exception != null) - { - waitForStartTcs.TrySetException(exception); - } - else - { - waitForStartTcs.TrySetResult(0); - } - } - - var factory = HostFactoryResolver.ResolveHostFactory(assembly, - stopApplication: false, - configureHostBuilder: ConfigureHostBuilder, - entrypointCompleted: OnEntryPointExit); - - if (factory == null) - { - return false; - } - - var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; - - var applicationLifetime = services.GetRequiredService(); - using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(0)); - waitForStartTcs.Task.Wait(); - var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == targetAssemblyName); - serviceType = targetAssembly.GetType(typeName, throwOnError: false); - - if (serviceType == null) - { - return false; - } - - serviceProvider = services; - return true; - } - - private sealed class NoopHostLifetime : IHostLifetime - { - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } - - private sealed class NoopServer : IServer - { - public IFeatureCollection Features { get; } = new FeatureCollection(); - public void Dispose() { } - public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull => Task.CompletedTask; - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } - - private sealed class HostFactoryResolver - { - private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; - - public const string BuildWebHost = nameof(BuildWebHost); - public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder); - public const string CreateHostBuilder = nameof(CreateHostBuilder); - private const string TimeoutEnvironmentKey = "DOTNET_HOST_FACTORY_RESOLVER_DEFAULT_TIMEOUT_IN_SECONDS"; - - // The amount of time we wait for the diagnostic source events to fire - private static readonly TimeSpan s_defaultWaitTimeout = SetupDefaultTimeout(); - - private static TimeSpan SetupDefaultTimeout() - { - if (Debugger.IsAttached) - { - return Timeout.InfiniteTimeSpan; - } - - if (uint.TryParse(Environment.GetEnvironmentVariable(TimeoutEnvironmentKey), out uint timeoutInSeconds)) - { - return TimeSpan.FromSeconds((int)timeoutInSeconds); - } - - return TimeSpan.FromMinutes(5); - } - - public static Func ResolveWebHostFactory(Assembly assembly) - { - return ResolveFactory(assembly, BuildWebHost); - } - - public static Func ResolveWebHostBuilderFactory(Assembly assembly) - { - return ResolveFactory(assembly, CreateWebHostBuilder); - } - - public static Func ResolveHostBuilderFactory(Assembly assembly) - { - return ResolveFactory(assembly, CreateHostBuilder); - } - - // This helpers encapsulates all of the complex logic required to: - // 1. Execute the entry point of the specified assembly in a different thread. - // 2. Wait for the diagnostic source events to fire - // 3. Give the caller a chance to execute logic to mutate the IHostBuilder - // 4. Resolve the instance of the applications's IHost - // 5. Allow the caller to determine if the entry point has completed - public static Func ResolveHostFactory(Assembly assembly, - TimeSpan waitTimeout = default, - bool stopApplication = true, - Action configureHostBuilder = null, - Action entrypointCompleted = null) - { - if (assembly.EntryPoint is null) - { - return null; - } - - return args => new HostingListener(args, assembly.EntryPoint, waitTimeout == default ? s_defaultWaitTimeout : waitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost(); - } - - private static Func ResolveFactory(Assembly assembly, string name) - { - var programType = assembly.EntryPoint.DeclaringType; - if (programType == null) - { - return null; - } - - var factory = programType.GetMethod(name, DeclaredOnlyLookup); - if (!IsFactory(factory)) - { - return null; - } - - return args => (T)factory!.Invoke(null, [args])!; - } - - // TReturn Factory(string[] args); - private static bool IsFactory(MethodInfo factory) - { - return factory != null - && typeof(TReturn).IsAssignableFrom(factory.ReturnType) - && factory.GetParameters().Length == 1 - && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); - } - - // Used by EF tooling without any Hosting references. Looses some return type safety checks. - public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan waitTimeout = default) - { - // Prefer the older patterns by default for back compat. - var webHostFactory = ResolveWebHostFactory(assembly); - if (webHostFactory != null) - { - return args => - { - var webHost = webHostFactory(args); - return GetServiceProvider(webHost); - }; - } - - var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly); - if (webHostBuilderFactory != null) - { - return args => - { - var webHostBuilder = webHostBuilderFactory(args); - var webHost = Build(webHostBuilder); - return GetServiceProvider(webHost); - }; - } - - var hostBuilderFactory = ResolveHostBuilderFactory(assembly); - if (hostBuilderFactory != null) - { - return args => - { - var hostBuilder = hostBuilderFactory(args); - var host = Build(hostBuilder); - return GetServiceProvider(host); - }; - } - - var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout); - if (hostFactory != null) - { - return args => - { - static bool IsApplicationNameArg(string arg) - => arg.Equals("--applicationName", StringComparison.OrdinalIgnoreCase) || - arg.Equals("/applicationName", StringComparison.OrdinalIgnoreCase); - - if (!args.Any(arg => IsApplicationNameArg(arg)) && assembly.GetName().Name is string assemblyName) - { - args = [.. args, .. new[] { "--applicationName", assemblyName }]; - } - - var host = hostFactory(args); - return GetServiceProvider(host); - }; - } - - return null; - } - - private static object Build(object builder) - { - var buildMethod = builder.GetType().GetMethod("Build"); - return buildMethod.Invoke(builder, []); - } - - private static IServiceProvider GetServiceProvider(object host) - { - if (host == null) - { - return null; - } - var hostType = host.GetType(); - var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup); - return (IServiceProvider)servicesProperty.GetValue(host); - } - - private sealed class HostingListener : IObserver, IObserver> - { - private readonly string[] _args; - private readonly MethodInfo _entryPoint; - private readonly TimeSpan _waitTimeout; - private readonly bool _stopApplication; - - private readonly TaskCompletionSource _hostTcs = new(); - private IDisposable _disposable; - private readonly Action _configure; - private readonly Action _entrypointCompleted; - private static readonly AsyncLocal _currentListener = new(); - - public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entrypointCompleted) - { - _args = args; - _entryPoint = entryPoint; - _waitTimeout = waitTimeout; - _stopApplication = stopApplication; - _configure = configure; - _entrypointCompleted = entrypointCompleted; - } - - public object CreateHost() - { - using var subscription = DiagnosticListener.AllListeners.Subscribe(this); - - // Kick off the entry point on a new thread so we don't block the current one - // in case we need to timeout the execution - var thread = new Thread(() => - { - Exception exception = null; - - try - { - // Set the async local to the instance of the HostingListener so we can filter events that - // aren't scoped to this execution of the entry point. - _currentListener.Value = this; - - var parameters = _entryPoint.GetParameters(); - if (parameters.Length == 0) - { - _entryPoint.Invoke(null, []); - } - else - { - _entryPoint.Invoke(null, new object[] { _args }); - } - - // Try to set an exception if the entry point returns gracefully, this will force - // build to throw - _hostTcs.TrySetException(new InvalidOperationException("The entry point exited without ever building an IHost.")); - } - catch (TargetInvocationException tie) when (tie.InnerException.GetType().Name == "HostAbortedException") - { - // The host was stopped by our own logic - } - catch (TargetInvocationException tie) - { - exception = tie.InnerException ?? tie; - - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(exception); - } - catch (Exception ex) - { - exception = ex; - - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(ex); - } - finally - { - // Signal that the entry point is completed - _entrypointCompleted.Invoke(exception); - } - }) - { - // Make sure this doesn't hang the process - IsBackground = true - }; - - // Start the thread - thread.Start(); - - try - { - // Wait before throwing an exception - if (!_hostTcs.Task.Wait(_waitTimeout)) - { - throw new InvalidOperationException($"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable."); - } - } - catch (AggregateException) when (_hostTcs.Task.IsCompleted) - { - // Lets this propagate out of the call to GetAwaiter().GetResult() - } - - Debug.Assert(_hostTcs.Task.IsCompleted); - - return _hostTcs.Task.GetAwaiter().GetResult(); - } - - public void OnCompleted() - { - _disposable.Dispose(); - } - - public void OnError(Exception error) - { - - } - - public void OnNext(DiagnosticListener value) - { - if (_currentListener.Value != this) - { - // Ignore events that aren't for this listener - return; - } - - if (value.Name == "Microsoft.Extensions.Hosting") - { - _disposable = value.Subscribe(this); - } - } - - public void OnNext(KeyValuePair value) - { - if (_currentListener.Value != this) - { - // Ignore events that aren't for this listener - return; - } - - if (value.Key == "HostBuilding") - { - _configure.Invoke(value.Value!); - } - - if (value.Key == "HostBuilt") - { - _hostTcs.TrySetResult(value.Value!); - - if (_stopApplication) - { - // Stop the host from running further - ThrowHostAborted(); - } - } - } - - // HostFactoryResolver is used by tools that explicitly don't want to reference Microsoft.Extensions.Hosting assemblies. - // So don't depend on the public HostAbortedException directly. Instead, load the exception type dynamically if it can - // be found. If it can't (possibly because the app is using an older version), throw a private exception with the same name. - private static void ThrowHostAborted() - { - var publicHostAbortedExceptionType = Type.GetType("Microsoft.Extensions.Hosting.HostAbortedException, Microsoft.Extensions.Hosting.Abstractions", throwOnError: false); - if (publicHostAbortedExceptionType != null) - { - throw (Exception)Activator.CreateInstance(publicHostAbortedExceptionType)!; - } - else - { - throw new HostAbortedException(); - } - } - - private sealed class HostAbortedException : Exception - { - } - } - } - - internal HttpContext CreateHttpContext(IServiceProvider serviceProvider) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = serviceProvider; - - var outStream = new MemoryStream(); - httpContext.Response.Body = outStream; - - return httpContext; - } - - internal HttpContext CreateHttpContextWithPayload(string requestData, IServiceProvider serviceProvider = null) - { - var httpContext = CreateHttpContext(serviceProvider); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - httpContext.Request.Headers["Content-Type"] = "application/json"; - - var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(requestData)); - httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - return httpContext; - } - - internal static async Task AssertBadRequest(HttpContext context) - { - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - context.Response.Body.Position = 0; - using var reader = new StreamReader(context.Response.Body); - var responseBody = await reader.ReadToEndAsync(); - return JsonSerializer.Deserialize(responseBody); - } -} diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index 5105b0ca70cd..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,244 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::SubType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "RequiredProperty", - displayName: "RequiredProperty" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "StringWithLength", - displayName: "StringWithLength" - ), - ] - ); - return true; - } - if (type == typeof(global::SubTypeWithInheritance)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubTypeWithInheritance), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubTypeWithInheritance), - propertyType: typeof(string), - name: "EmailString", - displayName: "EmailString" - ), - ] - ); - return true; - } - if (type == typeof(global::ComplexType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ComplexType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithRange", - displayName: "IntegerWithRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithRangeAndDisplayName", - displayName: "Valid identifier" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::SubType), - name: "PropertyWithMemberAttributes", - displayName: "PropertyWithMemberAttributes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::SubType), - name: "PropertyWithoutMemberAttributes", - displayName: "PropertyWithoutMemberAttributes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::SubTypeWithInheritance), - name: "PropertyWithInheritance", - displayName: "PropertyWithInheritance" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::System.Collections.Generic.List), - name: "ListOfSubTypes", - displayName: "ListOfSubTypes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithDerivedValidationAttribute", - displayName: "IntegerWithDerivedValidationAttribute" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithCustomValidation", - displayName: "IntegerWithCustomValidation" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "PropertyWithMultipleAttributes", - displayName: "PropertyWithMultipleAttributes" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index 6323e4ce9ff8..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,195 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::SubType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "RequiredProperty", - displayName: "RequiredProperty" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "StringWithLength", - displayName: "StringWithLength" - ), - ] - ); - return true; - } - if (type == typeof(global::ValidatableSubType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ValidatableSubType), - members: [] - ); - return true; - } - if (type == typeof(global::ComplexValidatableType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ComplexValidatableType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexValidatableType), - propertyType: typeof(string), - name: "Value2", - displayName: "Value2" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexValidatableType), - propertyType: typeof(global::ValidatableSubType), - name: "SubType", - displayName: "SubType" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index ccf60a0a1c89..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,175 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::NamespaceOne.Type)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::NamespaceOne.Type), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::NamespaceOne.Type), - propertyType: typeof(string), - name: "StringWithLength", - displayName: "StringWithLength" - ), - ] - ); - return true; - } - if (type == typeof(global::NamespaceTwo.Type)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::NamespaceTwo.Type), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::NamespaceTwo.Type), - propertyType: typeof(string), - name: "StringWithLength", - displayName: "StringWithLength" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index d962c3758088..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,193 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::TestService)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::TestService), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::TestService), - propertyType: typeof(int), - name: "Value", - displayName: "Value" - ), - ] - ); - return true; - } - if (type == typeof(global::System.Collections.Generic.Dictionary)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::System.Collections.Generic.Dictionary), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::System.Collections.Generic.Dictionary), - propertyType: typeof(global::System.Collections.Generic.ICollection), - name: "System.Collections.Generic.IDictionary.Values", - displayName: "System.Collections.Generic.IDictionary.Values" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::System.Collections.Generic.Dictionary), - propertyType: typeof(global::System.Collections.Generic.IEnumerable), - name: "System.Collections.Generic.IReadOnlyDictionary.Values", - displayName: "System.Collections.Generic.IReadOnlyDictionary.Values" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::System.Collections.Generic.Dictionary), - propertyType: typeof(global::TestService), - name: "this[]", - displayName: "this[]" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::System.Collections.Generic.Dictionary), - propertyType: typeof(global::System.Collections.ICollection), - name: "System.Collections.IDictionary.Values", - displayName: "System.Collections.IDictionary.Values" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index c6cd2b9a6aeb..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,225 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::DerivedType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::DerivedType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::DerivedType), - propertyType: typeof(string), - name: "Value3", - displayName: "Value3" - ), - ] - ); - return true; - } - if (type == typeof(global::BaseType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::BaseType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::BaseType), - propertyType: typeof(int), - name: "Value1", - displayName: "Value 1" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::BaseType), - propertyType: typeof(string), - name: "Value2", - displayName: "Value2" - ), - ] - ); - return true; - } - if (type == typeof(global::DerivedValidatableType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::DerivedValidatableType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::DerivedValidatableType), - propertyType: typeof(string), - name: "Value3", - displayName: "Value3" - ), - ] - ); - return true; - } - if (type == typeof(global::BaseValidatableType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::BaseValidatableType), - members: [] - ); - return true; - } - if (type == typeof(global::ContainerType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ContainerType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ContainerType), - propertyType: typeof(global::BaseType), - name: "BaseType", - displayName: "BaseType" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ContainerType), - propertyType: typeof(global::BaseValidatableType), - name: "BaseValidatableType", - displayName: "BaseValidatableType" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index 29b8c10bb51e..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,271 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::SubType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "RequiredProperty", - displayName: "RequiredProperty" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "StringWithLength", - displayName: "StringWithLength" - ), - ] - ); - return true; - } - if (type == typeof(global::SubTypeWithInheritance)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubTypeWithInheritance), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubTypeWithInheritance), - propertyType: typeof(string), - name: "EmailString", - displayName: "EmailString" - ), - ] - ); - return true; - } - if (type == typeof(global::SubTypeWithoutConstructor)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubTypeWithoutConstructor), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubTypeWithoutConstructor), - propertyType: typeof(string), - name: "RequiredProperty", - displayName: "RequiredProperty" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubTypeWithoutConstructor), - propertyType: typeof(string), - name: "StringWithLength", - displayName: "StringWithLength" - ), - ] - ); - return true; - } - if (type == typeof(global::ValidatableRecord)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ValidatableRecord), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(int), - name: "IntegerWithRange", - displayName: "IntegerWithRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(int), - name: "IntegerWithRangeAndDisplayName", - displayName: "Valid identifier" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(global::SubType), - name: "PropertyWithMemberAttributes", - displayName: "PropertyWithMemberAttributes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(global::SubType), - name: "PropertyWithoutMemberAttributes", - displayName: "PropertyWithoutMemberAttributes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(global::SubTypeWithInheritance), - name: "PropertyWithInheritance", - displayName: "PropertyWithInheritance" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(global::SubTypeWithoutConstructor), - name: "PropertyOfSubtypeWithoutConstructor", - displayName: "PropertyOfSubtypeWithoutConstructor" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(global::System.Collections.Generic.List), - name: "ListOfSubTypes", - displayName: "ListOfSubTypes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(int), - name: "IntegerWithDerivedValidationAttribute", - displayName: "IntegerWithDerivedValidationAttribute" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(int), - name: "IntegerWithCustomValidation", - displayName: "IntegerWithCustomValidation" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ValidatableRecord), - propertyType: typeof(int), - name: "PropertyWithMultipleAttributes", - displayName: "PropertyWithMultipleAttributes" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index 7625d563e861..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,166 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::RecursiveType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::RecursiveType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::RecursiveType), - propertyType: typeof(int), - name: "Value", - displayName: "Value" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::RecursiveType), - propertyType: typeof(global::RecursiveType), - name: "Next", - displayName: "Next" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index cc22f8dc30d9..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,208 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::ComplexTypeWithParsableProperties)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ComplexTypeWithParsableProperties), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(global::System.Guid?), - name: "GuidWithRegularExpression", - displayName: "GuidWithRegularExpression" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(global::System.TimeOnly?), - name: "TimeOnlyWithRequiredValue", - displayName: "TimeOnlyWithRequiredValue" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(string), - name: "Url", - displayName: "Url" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(global::System.DateOnly?), - name: "DateOnlyWithRange", - displayName: "DateOnlyWithRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(global::System.DateTime?), - name: "DateTimeWithRange", - displayName: "DateTimeWithRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(decimal?), - name: "DecimalWithRange", - displayName: "DecimalWithRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(global::System.TimeSpan?), - name: "TimeSpanWithHourRange", - displayName: "TimeSpanWithHourRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(bool), - name: "BooleanWithRange", - displayName: "BooleanWithRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexTypeWithParsableProperties), - propertyType: typeof(global::System.Version), - name: "VersionWithRegex", - displayName: "VersionWithRegex" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index 880d416cfc13..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,238 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::SubType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "RequiredProperty", - displayName: "RequiredProperty" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubType), - propertyType: typeof(string), - name: "StringWithLength", - displayName: "StringWithLength" - ), - ] - ); - return true; - } - if (type == typeof(global::SubTypeWithInheritance)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::SubTypeWithInheritance), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::SubTypeWithInheritance), - propertyType: typeof(string), - name: "EmailString", - displayName: "EmailString" - ), - ] - ); - return true; - } - if (type == typeof(global::ComplexType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ComplexType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithRange", - displayName: "IntegerWithRange" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithRangeAndDisplayName", - displayName: "Valid identifier" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::SubType), - name: "PropertyWithMemberAttributes", - displayName: "PropertyWithMemberAttributes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::SubType), - name: "PropertyWithoutMemberAttributes", - displayName: "PropertyWithoutMemberAttributes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::SubTypeWithInheritance), - name: "PropertyWithInheritance", - displayName: "PropertyWithInheritance" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(global::System.Collections.Generic.List), - name: "ListOfSubTypes", - displayName: "ListOfSubTypes" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithCustomValidationAttribute", - displayName: "IntegerWithCustomValidationAttribute" - ), - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "PropertyWithMultipleAttributes", - displayName: "PropertyWithMultipleAttributes" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs deleted file mode 100644 index be9ab29fec62..000000000000 --- a/src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ /dev/null @@ -1,160 +0,0 @@ -//HintName: ValidatableInfoResolver.g.cs -#nullable enable annotations -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable ASP0029 - -namespace System.Runtime.CompilerServices -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute : System.Attribute - { - public InterceptsLocationAttribute(int version, string data) - { - } - } -} - -namespace Microsoft.AspNetCore.Http.Validation.Generated -{ - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo - { - public GeneratedValidatablePropertyInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - global::System.Type propertyType, - string name, - string displayName) : base(containingType, propertyType, name, displayName) - { - ContainingType = containingType; - Name = name; - } - - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - internal global::System.Type ContainingType { get; } - internal string Name { get; } - - protected override global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes() - => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo - { - public GeneratedValidatableTypeInfo( - [param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] - global::System.Type type, - ValidatablePropertyInfo[] members) : base(type, members) { } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file class GeneratedValidatableInfoResolver : global::Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver - { - public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - if (type == typeof(global::ComplexType)) - { - validatableInfo = new GeneratedValidatableTypeInfo( - type: typeof(global::ComplexType), - members: [ - new GeneratedValidatablePropertyInfo( - containingType: typeof(global::ComplexType), - propertyType: typeof(int), - name: "IntegerWithRange", - displayName: "IntegerWithRange" - ), - ] - ); - return true; - } - - return false; - } - - // No-ops, rely on runtime code for ParameterInfo-based resolution - public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) - { - validatableInfo = null; - return false; - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class GeneratedServiceCollectionExtensions - { - [InterceptsLocation] - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddValidation(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::System.Action? configureOptions = null) - { - // Use non-extension method to avoid infinite recursion. - return global::Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(services, options => - { - options.Resolvers.Insert(0, new GeneratedValidatableInfoResolver()); - if (configureOptions is not null) - { - configureOptions(options); - } - }); - } - } - - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] - file static class ValidationAttributeCache - { - private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); - private static readonly global::System.Collections.Concurrent.ConcurrentDictionary _cache = new(); - - public static global::System.ComponentModel.DataAnnotations.ValidationAttribute[] GetValidationAttributes( - [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] - global::System.Type containingType, - string propertyName) - { - var key = new CacheKey(containingType, propertyName); - return _cache.GetOrAdd(key, static k => - { - var results = new global::System.Collections.Generic.List(); - - // Get attributes from the property - var property = k.ContainingType.GetProperty(k.PropertyName); - if (property != null) - { - var propertyAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(property, inherit: true); - - results.AddRange(propertyAttributes); - } - - // Check constructors for parameters that match the property name - // to handle record scenarios - foreach (var constructor in k.ContainingType.GetConstructors()) - { - // Look for parameter with matching name (case insensitive) - var parameter = global::System.Linq.Enumerable.FirstOrDefault( - constructor.GetParameters(), - p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase)); - - if (parameter != null) - { - var paramAttributes = global::System.Reflection.CustomAttributeExtensions - .GetCustomAttributes(parameter, inherit: true); - - results.AddRange(paramAttributes); - - break; - } - } - - return results.ToArray(); - }); - } - } -} \ No newline at end of file diff --git a/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj b/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj index 54d99f9254e5..663745c85991 100644 --- a/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj +++ b/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs index 973d22ed0674..6af879569e80 100644 --- a/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs +++ b/src/Http/Http/perf/Microbenchmarks/ValidatableTypesBenchmark.cs @@ -7,9 +7,9 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Http.Validation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Validation; namespace Microsoft.AspNetCore.Http.Microbenchmarks; diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 69a4e98a5599..265e3674aa4a 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -21,7 +21,6 @@ "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Abstractions\\test\\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.RequestDelegateGenerator\\Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj", - "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.ValidationsGenerator\\Microsoft.AspNetCore.Http.ValidationsGenerator.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", @@ -74,4 +73,4 @@ "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 16a710df5ce4..61036f37c6d3 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Http/Routing/src/RouteEndpointDataSource.cs b/src/Http/Routing/src/RouteEndpointDataSource.cs index cbdac85e3e8c..9586ceeff18f 100644 --- a/src/Http/Routing/src/RouteEndpointDataSource.cs +++ b/src/Http/Routing/src/RouteEndpointDataSource.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.Validation; namespace Microsoft.AspNetCore.Routing; diff --git a/src/Http/Routing/src/ValidationEndpointFilterFactory.cs b/src/Http/Routing/src/ValidationEndpointFilterFactory.cs index 73a41f0f8d57..f9e59bc88c12 100644 --- a/src/Http/Routing/src/ValidationEndpointFilterFactory.cs +++ b/src/Http/Routing/src/ValidationEndpointFilterFactory.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Validation; namespace Microsoft.AspNetCore.Http.Validation; diff --git a/src/Http/samples/MinimalValidationSample/MinimalValidationSample.csproj b/src/Http/samples/MinimalValidationSample/MinimalValidationSample.csproj index 24690f4dbd35..e01722e04559 100644 --- a/src/Http/samples/MinimalValidationSample/MinimalValidationSample.csproj +++ b/src/Http/samples/MinimalValidationSample/MinimalValidationSample.csproj @@ -4,7 +4,7 @@ $(DefaultNetCoreTargetFramework) enable true - $(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated + $(InterceptorsNamespaces);Microsoft.Extensions.Validation.Generated @@ -14,12 +14,13 @@ + - diff --git a/src/Http/samples/MinimalValidationSample/Program.cs b/src/Http/samples/MinimalValidationSample/Program.cs index 9afcebfb2b02..266e2cf74c0a 100644 --- a/src/Http/samples/MinimalValidationSample/Program.cs +++ b/src/Http/samples/MinimalValidationSample/Program.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Http.Validation; +using Microsoft.Extensions.Validation; var builder = WebApplication.CreateBuilder(args); diff --git a/src/Validation/Validations.slnf b/src/Validation/Validation.slnf similarity index 62% rename from src/Validation/Validations.slnf rename to src/Validation/Validation.slnf index 51c95f704d46..7f6b3ab900ae 100644 --- a/src/Validation/Validations.slnf +++ b/src/Validation/Validation.slnf @@ -1,11 +1,11 @@ { "solution": { - "path": "..\\..\\AspNetCore.sln", + "path": "..\\..\\AspNetCore.slnx", "projects": [ "src\\Validation\\src\\Microsoft.Extensions.Validation.csproj", "src\\Validation\\test\\Microsoft.Extensions.Validation.Tests\\Microsoft.Extensions.Validation.Tests.csproj", "src\\Validation\\gen\\Microsoft.Extensions.Validation.ValidationsGenerator.csproj", - "src\\Validation\\test\\Microsoft.Extensions.Validation.ValidationsGenerator.Tests\\Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj" + "src\\Validation\\test\\Microsoft.Extensions.Validation.GeneratorTests\\Microsoft.Extensions.Validation.GeneratorTests.csproj" ] } -} \ No newline at end of file +} diff --git a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs index 729215931d93..8deb7be3a60e 100644 --- a/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs +++ b/src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs @@ -8,7 +8,7 @@ using Microsoft.CodeAnalysis.CSharp; using System.IO; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; public sealed partial class ValidationsGenerator : IIncrementalGenerator { diff --git a/src/Validation/gen/Extensions/ISymbolExtensions.cs b/src/Validation/gen/Extensions/ISymbolExtensions.cs index 2e7d72d852fc..b1b0a35a7b03 100644 --- a/src/Validation/gen/Extensions/ISymbolExtensions.cs +++ b/src/Validation/gen/Extensions/ISymbolExtensions.cs @@ -5,7 +5,7 @@ using System.Linq; using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal static class ISymbolExtensions { diff --git a/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs index 38a4105e67f0..37186fee8a8a 100644 --- a/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs +++ b/src/Validation/gen/Extensions/ITypeSymbolExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal static class ITypeSymbolExtensions { diff --git a/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs b/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs index 5b7aa171fac8..aea74a7ab19c 100644 --- a/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs +++ b/src/Validation/gen/Extensions/IncrementalValuesProviderExtensions.cs @@ -6,7 +6,7 @@ using System.Linq; using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal static class IncrementalValuesProviderExtensions { diff --git a/src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj b/src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj index b05a37f970f6..64488ca48463 100644 --- a/src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj +++ b/src/Validation/gen/Microsoft.Extensions.Validation.ValidationsGenerator.csproj @@ -16,7 +16,7 @@ - + @@ -29,6 +29,7 @@ + - \ No newline at end of file + diff --git a/src/Validation/gen/Models/RequiredSymbols.cs b/src/Validation/gen/Models/RequiredSymbols.cs index c67e1e63c38d..51f8c92ccf9e 100644 --- a/src/Validation/gen/Models/RequiredSymbols.cs +++ b/src/Validation/gen/Models/RequiredSymbols.cs @@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal sealed record class RequiredSymbols( INamedTypeSymbol DisplayAttribute, diff --git a/src/Validation/gen/Models/ValidatableProperty.cs b/src/Validation/gen/Models/ValidatableProperty.cs index 8dee45af31cc..22b960babcad 100644 --- a/src/Validation/gen/Models/ValidatableProperty.cs +++ b/src/Validation/gen/Models/ValidatableProperty.cs @@ -4,7 +4,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal sealed record class ValidatableProperty( ITypeSymbol ContainingType, diff --git a/src/Validation/gen/Models/ValidatableType.cs b/src/Validation/gen/Models/ValidatableType.cs index 784663750929..06f3df3b6234 100644 --- a/src/Validation/gen/Models/ValidatableType.cs +++ b/src/Validation/gen/Models/ValidatableType.cs @@ -4,7 +4,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal sealed record class ValidatableType( ITypeSymbol Type, diff --git a/src/Validation/gen/Models/ValidatableTypeComparer.cs b/src/Validation/gen/Models/ValidatableTypeComparer.cs index 2338a1fa2cca..56f96a32e47c 100644 --- a/src/Validation/gen/Models/ValidatableTypeComparer.cs +++ b/src/Validation/gen/Models/ValidatableTypeComparer.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal sealed class ValidatableTypeComparer : IEqualityComparer { diff --git a/src/Validation/gen/Models/ValidationAttribute.cs b/src/Validation/gen/Models/ValidationAttribute.cs index 55adcd0835f9..3558f9fcf6c5 100644 --- a/src/Validation/gen/Models/ValidationAttribute.cs +++ b/src/Validation/gen/Models/ValidationAttribute.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; internal sealed record class ValidationAttribute( string Name, diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs b/src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs index a3fb17b099c1..23f0d30b0ec7 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.AddValidation.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; public sealed partial class ValidationsGenerator : IIncrementalGenerator { @@ -29,7 +29,7 @@ internal bool FindAddValidation(SyntaxNode syntaxNode, CancellationToken cancell var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol; if (symbol is not IMethodSymbol methodSymbol || methodSymbol.ContainingType.Name != "ValidationServiceCollectionExtensions" - || methodSymbol.ContainingAssembly.Name != "Microsoft.AspNetCore.Http.Abstractions") + || methodSymbol.ContainingAssembly.Name != "Microsoft.Extensions.Validation") { return null; } diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs index 959c9bbfb948..75566314d21b 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs @@ -8,7 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; public sealed partial class ValidationsGenerator : IIncrementalGenerator { diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs index f6c9b566b725..92b84f4b4fb3 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.EndpointsParser.cs @@ -12,7 +12,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; public sealed partial class ValidationsGenerator : IIncrementalGenerator { diff --git a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs index 30fbf38368fc..1ee0722694f4 100644 --- a/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs +++ b/src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs @@ -11,7 +11,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; public sealed partial class ValidationsGenerator : IIncrementalGenerator { diff --git a/src/Validation/gen/ValidationsGenerator.cs b/src/Validation/gen/ValidationsGenerator.cs index f67e964ab76b..5f7c2c878ac5 100644 --- a/src/Validation/gen/ValidationsGenerator.cs +++ b/src/Validation/gen/ValidationsGenerator.cs @@ -4,9 +4,9 @@ using System.Linq; using Microsoft.CodeAnalysis; -namespace Microsoft.Extensions.Validation.ValidationsGenerator; +namespace Microsoft.Extensions.Validation; -[Generator] +[Generator(LanguageNames.CSharp)] public sealed partial class ValidationsGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) diff --git a/src/Validation/src/Microsoft.Extensions.Validation.csproj b/src/Validation/src/Microsoft.Extensions.Validation.csproj index 977d1518a04e..72d50e224f42 100644 --- a/src/Validation/src/Microsoft.Extensions.Validation.csproj +++ b/src/Validation/src/Microsoft.Extensions.Validation.csproj @@ -3,7 +3,7 @@ Common validation abstractions and validation infrastructure for .NET applications. $(DefaultNetCoreTargetFramework) - false + true true validation true @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs b/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs index e6aa1e01836d..d8f0c3699dbf 100644 --- a/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs +++ b/src/Validation/src/RuntimeValidatableParameterInfoResolver.cs @@ -86,12 +86,6 @@ private static bool IsClass(Type type) type == typeof(DateOnly) || type == typeof(TimeSpan) || type == typeof(Guid) || - type == typeof(IFormFile) || - type == typeof(IFormFileCollection) || - type == typeof(IFormCollection) || - type == typeof(HttpContext) || - type == typeof(HttpRequest) || - type == typeof(HttpResponse) || type == typeof(ClaimsPrincipal) || type == typeof(CancellationToken) || type == typeof(Stream) || diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/Microsoft.Extensions.Validation.GeneratorTests.csproj similarity index 56% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/Microsoft.Extensions.Validation.GeneratorTests.csproj index 43c387d71140..96f34d7eeef6 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/Microsoft.Extensions.Validation.ValidationsGenerator.Tests.csproj +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/Microsoft.Extensions.Validation.GeneratorTests.csproj @@ -3,7 +3,6 @@ $(DefaultNetCoreTargetFramework) true - enable @@ -14,9 +13,21 @@ + + + + + + + + - \ No newline at end of file + + + + + diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ModuleInitializer.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ModuleInitializer.cs new file mode 100644 index 000000000000..2e42614606d4 --- /dev/null +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ModuleInitializer.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() => + VerifySourceGenerators.Initialize(); +} diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ComplexType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs similarity index 99% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ComplexType.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs index b694f809812c..7bdf0bea29b2 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ComplexType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ComplexType.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.IValidatableObject.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs similarity index 99% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.IValidatableObject.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs index 944aea69b187..590195468298 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.IValidatableObject.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.IValidatableObject.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.MultipleNamespaces.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.MultipleNamespaces.cs similarity index 97% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.MultipleNamespaces.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.MultipleNamespaces.cs index 7272bc84ab92..e08f8855a4a3 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.MultipleNamespaces.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.MultipleNamespaces.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.NoOp.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs similarity index 98% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.NoOp.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs index 843b6a7c2106..ba176e622450 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.NoOp.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.NoOp.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parameters.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parameters.cs similarity index 97% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parameters.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parameters.cs index 56eb8016cd4a..5b90338520eb 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parameters.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parameters.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parsable.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parsable.cs similarity index 98% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parsable.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parsable.cs index 3c3ae69658ff..6ea212ebaa16 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Parsable.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Parsable.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Polymorphism.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs similarity index 99% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Polymorphism.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs index f511aa8dad5f..39dbec97a78c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Polymorphism.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Polymorphism.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.RecordType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs similarity index 99% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.RecordType.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs index f8254fd72bc3..cfc1372b9a7a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.RecordType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.RecordType.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Recursion.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Recursion.cs similarity index 98% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Recursion.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Recursion.cs index 464a91113085..7367e2919ae0 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.Recursion.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.Recursion.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ValidatableType.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs similarity index 99% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ValidatableType.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs index 12cb575ee150..c471d99da183 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGenerator.ValidatableType.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs @@ -6,7 +6,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Validation; -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +namespace Microsoft.Extensions.Validation.GeneratorTests; public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase { diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGeneratorTestBase.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs similarity index 94% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGeneratorTestBase.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs index cd9affc4a8a4..80c24c4a6070 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/ValidationsGeneratorTestBase.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGeneratorTestBase.cs @@ -10,22 +10,28 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using System.IO.Pipelines; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Validation; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using static Microsoft.AspNetCore.Http.Generators.Tests.RequestDelegateCreationTestBase; +using Microsoft.Extensions.Validation; +using Xunit; -namespace Microsoft.Extensions.Validation.ValidationsGenerator.Tests; +namespace Microsoft.Extensions.Validation.GeneratorTests; [UsesVerify] public partial class ValidationsGeneratorTestBase : LoggedTestBase @@ -53,6 +59,8 @@ internal static Task Verify(string source, out Compilation compilation) MetadataReference.CreateFromFile(typeof(System.Text.Json.Nodes.JsonArray).Assembly.Location), MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IFormFileCollection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(PipeReader).Assembly.Location), MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.ValidationAttribute).Assembly.Location), MetadataReference.CreateFromFile(typeof(RouteData).Assembly.Location), MetadataReference.CreateFromFile(typeof(IFeatureCollection).Assembly.Location), @@ -62,6 +70,7 @@ internal static Task Verify(string source, out Compilation compilation) MetadataReference.CreateFromFile(typeof(HttpJsonServiceExtensions).Assembly.Location), MetadataReference.CreateFromFile(typeof(IValidatableInfoResolver).Assembly.Location), MetadataReference.CreateFromFile(typeof(EndpointFilterFactoryContext).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ValidationServiceCollectionExtensions).Assembly.Location), ]); var inputCompilation = CSharpCompilation.Create("ValidationsGeneratorSample", [CSharpSyntaxTree.ParseText(source, options: ParseOptions, path: "Program.cs")], @@ -72,19 +81,20 @@ internal static Task Verify(string source, out Compilation compilation) return Verifier .Verify(driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out compilation, out var diagnostics)) .ScrubLinesWithReplace(line => InterceptsLocationRegex().Replace(line, "[InterceptsLocation]")) - .UseDirectory(SkipOnHelixAttribute.OnHelix() - ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "ValidationsGenerator", "snapshots") + .UseDirectory(SkipOnHelixAttribute.OnHelix() && Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT") is { } workItemRoot + ? Path.Combine(workItemRoot, "ValidationsGenerator", "snapshots") : "snapshots"); } internal static void VerifyValidatableType(Compilation compilation, string typeName, Action verifyFunc) { - if (TryResolveServicesFromCompilation(compilation, targetAssemblyName: "Microsoft.AspNetCore.Http.Abstractions", typeName: "Microsoft.Extensions.Validation.ValidationOptions", out var services, out var serviceType, out var outputAssemblyName) is false) + if (TryResolveServicesFromCompilation(compilation, targetAssemblyName: "Microsoft.Extensions.Validation", typeName: "Microsoft.Extensions.Validation.ValidationOptions", out var services, out var serviceType, out var outputAssemblyName) is false) { throw new InvalidOperationException("Could not resolve services from compilation."); } var targetAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == outputAssemblyName); - var type = targetAssembly.GetType(typeName, throwOnError: false); + var type = targetAssembly?.GetType(typeName, throwOnError: false); + Debug.Assert(type != null, $"Could not find type {typeName} in assembly {outputAssemblyName}."); // Get IOptions first var optionsType = typeof(IOptions<>).MakeGenericType(serviceType); @@ -583,4 +593,14 @@ internal static async Task AssertBadRequest(HttpCo var responseBody = await reader.ReadToEndAsync(); return JsonSerializer.Deserialize(responseBody); } + + internal sealed class RequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature + { + public RequestBodyDetectionFeature(bool canHaveBody) + { + CanHaveBody = canHaveBody; + } + + public bool CanHaveBody { get; } + } } diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.received.cs similarity index 93% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.received.cs index 2fe89720bc38..6ed50bc4cf76 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -173,7 +173,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -191,7 +191,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.received.cs similarity index 91% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.received.cs index b7d1a9c61ed3..0e714f920f11 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -124,7 +124,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -142,7 +142,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.received.cs similarity index 91% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.received.cs index eb8ca791d504..25df8f453930 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -104,7 +104,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -122,7 +122,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.received.cs similarity index 92% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.received.cs index fb9570f39fb1..eb73f425fad1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -122,7 +122,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -140,7 +140,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.received.cs similarity index 92% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.received.cs index 86845c4ad153..a2520c96a2a2 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -154,7 +154,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -172,7 +172,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.received.cs similarity index 94% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.received.cs index a5c86c3b4b4e..a2ba7cbdc855 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -200,7 +200,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -218,7 +218,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.received.cs similarity index 90% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.received.cs index e4958777e0d3..f61ce950aac1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -95,7 +95,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -113,7 +113,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.received.cs similarity index 92% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.received.cs index c7b0cba7ed13..9641ecbc3032 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -137,7 +137,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -155,7 +155,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.received.cs similarity index 93% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.received.cs index 8d4bb3df2285..52220c7918fa 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypesWithAttribute#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -167,7 +167,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -185,7 +185,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.received.cs similarity index 90% rename from src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs rename to src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.received.cs index 34d61eb409d1..e4bf2083af57 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.ValidationsGenerator.Tests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.received.cs @@ -13,7 +13,7 @@ namespace System.Runtime.CompilerServices { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : System.Attribute { @@ -25,7 +25,7 @@ public InterceptsLocationAttribute(int version, string data) namespace Microsoft.Extensions.Validation.Generated { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.Extensions.Validation.ValidatablePropertyInfo { public GeneratedValidatablePropertyInfo( @@ -47,7 +47,7 @@ public GeneratedValidatablePropertyInfo( => ValidationAttributeCache.GetValidationAttributes(ContainingType, Name); } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file sealed class GeneratedValidatableTypeInfo : global::Microsoft.Extensions.Validation.ValidatableTypeInfo { public GeneratedValidatableTypeInfo( @@ -56,7 +56,7 @@ public GeneratedValidatableTypeInfo( ValidatablePropertyInfo[] members) : base(type, members) { } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file class GeneratedValidatableInfoResolver : global::Microsoft.Extensions.Validation.IValidatableInfoResolver { public bool TryGetValidatableTypeInfo(global::System.Type type, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Microsoft.Extensions.Validation.IValidatableInfo? validatableInfo) @@ -89,7 +89,7 @@ public bool TryGetValidatableParameterInfo(global::System.Reflection.ParameterIn } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class GeneratedServiceCollectionExtensions { [InterceptsLocation] @@ -107,7 +107,7 @@ file static class GeneratedServiceCollectionExtensions } } - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Validation.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")] file static class ValidationAttributeCache { private sealed record CacheKey([property: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] global::System.Type ContainingType, string PropertyName); diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj b/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj index 73b39892474c..f74f56690452 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj @@ -2,6 +2,7 @@ $(DefaultNetCoreTargetFramework) + enable @@ -9,4 +10,4 @@ - \ No newline at end of file + diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs index ada596165dea..eebe4032d9af 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs @@ -46,12 +46,6 @@ public void TryGetValidatableParameterInfo_WithNullName_ThrowsInvalidOperationEx [InlineData(typeof(TimeOnly))] [InlineData(typeof(DateOnly))] [InlineData(typeof(TimeSpan))] - [InlineData(typeof(IFormFile))] - [InlineData(typeof(IFormFileCollection))] - [InlineData(typeof(IFormCollection))] - [InlineData(typeof(HttpContext))] - [InlineData(typeof(HttpRequest))] - [InlineData(typeof(HttpResponse))] [InlineData(typeof(CancellationToken))] public void TryGetValidatableParameterInfo_WithSimpleTypesAndNoAttributes_ReturnsFalse(Type parameterType) { From 0813e16b04450c7cabc99ba41a8a7a242bb3fd75 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 23 May 2025 15:48:54 +0000 Subject: [PATCH 6/7] Update trimmable projects for new package --- eng/TrimmableProjects.props | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index 199d65af1b49..fbbe6f0bc44a 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -118,5 +118,6 @@ + From 74ad1e5f95aa444e6623f0434943c126c8d513a7 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 23 May 2025 17:37:22 +0000 Subject: [PATCH 7/7] Update baselines --- eng/Build.props | 2 ++ eng/ProjectReferences.props | 8 ++++---- eng/SharedFramework.Local.props | 4 ++-- eng/ShippingAssemblies.props | 6 ++++-- eng/TrimmableProjects.props | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/eng/Build.props b/eng/Build.props index dbaf0c170561..7217430d1090 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -198,6 +198,7 @@ $(RepoRoot)src\Extensions\**\*.csproj; $(RepoRoot)src\BuildAfterTargetingPack\*.csproj; $(RepoRoot)src\OpenApi\**\*.csproj; + $(RepoRoot)src\Validation\**\*.csproj; $(RepoRoot)eng\tools\HelixTestRunner\HelixTestRunner.csproj; " Exclude=" @@ -242,6 +243,7 @@ $(RepoRoot)src\Extensions\**\src\*.csproj; $(RepoRoot)src\BuildAfterTargetingPack\*.csproj; $(RepoRoot)src\OpenApi\**\src\*.csproj; + $(RepoRoot)src\Validation\**\src\*.csproj; " Exclude=" @(ProjectToBuild); diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 6efcc28f2f2b..7f5da9b33350 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -38,7 +38,6 @@ - @@ -48,8 +47,8 @@ - + @@ -118,8 +117,8 @@ - + @@ -150,13 +149,13 @@ + - @@ -171,5 +170,6 @@ + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 28efd974e7f0..6da36d432c6c 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -57,8 +57,8 @@ - + @@ -100,8 +100,8 @@ - + diff --git a/eng/ShippingAssemblies.props b/eng/ShippingAssemblies.props index d410959a9c7c..a3842c5c9d27 100644 --- a/eng/ShippingAssemblies.props +++ b/eng/ShippingAssemblies.props @@ -33,8 +33,8 @@ - + @@ -77,8 +77,8 @@ - + @@ -105,6 +105,7 @@ + @@ -165,3 +166,4 @@ + diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index fbbe6f0bc44a..1ffaf18a83bf 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -37,8 +37,8 @@ - + @@ -104,11 +104,11 @@ + -