8000 Move unified validation APIs to separate package by Copilot · Pull Request #62071 · dotnet/aspnetcore · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
10BC0
Diff view
Prev Previous commit
Next Next commit
Created project structure and moved source files
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
  • Loading branch information
2 people authored and javiercn committed Jun 12, 2025
commit 2d68f7ae41302a9644a297ef1acc3dd56781ce36
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Routing" ProjectPath="$(RepoRoot)src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.WebUtilities" ProjectPath="$(RepoRoot)src\Http\WebUtilities\src\Microsoft.AspNetCore.WebUtilities.csproj" />
<ProjectReferenceProvider Include="Microsoft.Extensions.Http.Polly" ProjectPath="$(RepoRoot)src\HttpClientFactory\Polly\src\Microsoft.Extensions.Http.Polly.csproj" />
<ProjectReferenceProvider Include="Microsoft.Extensions.Validation" ProjectPath="$(RepoRoot)src\Validation\src\Microsoft.Extensions.Validation.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Html.Abstractions" ProjectPath="$(RepoRoot)src\Html.Abstractions\src\Microsoft.AspNetCore.Html.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Identity" ProjectPath="$(RepoRoot)src\Identity\Core\src\Microsoft.AspNetCore.Identity.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" ProjectPath="$(RepoRoot)src\Identity\EntityFrameworkCore\src\Microsoft.AspNetCore.Identity.EntityFrameworkCore.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
<Reference Include="Microsoft.Net.Http.Headers" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<Reference Include="Microsoft.Extensions.Validation" />

<Compile Include="$(SharedSourceRoot)ParameterDefaultValue\*.cs" />
<Compile Include="$(SharedSourceRoot)PropertyHelper\**\*.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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))]
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All" IsImplicitlyDefined="true" Version="$(MicrosoftCodeAnalysisVersion_LatestVS)" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="All" IsImplicitlyDefined="true" Version="$(MicrosoftCodeAnalysisVersion_LatestVS)" />
<Reference Include="Microsoft.Extensions.Validation.ValidationsGenerator" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Extensions.Tests" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(SharedSourceRoot)IsExternalInit.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)HashCode.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\BoundedCacheWithFactory.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypeData.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypes.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\CodeWriter.cs" LinkBase="Shared" />
<Compile Include="$(RepoRoot)\src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.RequestDelegateGenerator\StaticRouteHandlerModel\InvocationOperationExtensions.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Diagnostics\AnalyzerDebug.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\ParsabilityHelper.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\SymbolExtensions.cs" LinkBase="Shared" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All" IsImplicitlyDefined="true" Version="$(MicrosoftCodeAnalysisVersion_LatestVS)" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="All" IsImplicitlyDefined="true" Version="$(MicrosoftCodeAnalysisVersion_LatestVS)" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
<Reference Include="Microsoft.Net.Http.Headers" />
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
<Reference Include="Microsoft.Extensions.Validation" />
</ItemGroup>

<ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions src/Validation/Validations.slnf
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
4 changes: 4 additions & 0 deletions src/Validation/build.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@ECHO OFF
SET RepoRoot=%~dp0..\..

call %RepoRoot%\eng\build.cmd -projects %RepoRoot%\src\Validation\**\*.csproj %*
6 changes: 6 additions & 0 deletions src/Validation/build.sh
Original file line number Diff line number Diff line change
@@ -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" "$@"
222 changes: 222 additions & 0 deletions src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs
Original file line number Diff line number Diff line change
@@ -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<ValidatableType> 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<ValidatableType> validatableTypes) => $$"""
#nullable enable annotations
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#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<global::Microsoft.Extensions.Validation.ValidationOptions>? 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<CacheKey, global::System.ComponentModel.DataAnnotations.ValidationAttribute[]> _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<global::System.ComponentModel.DataAnnotations.ValidationAttribute>();

// Get attributes from the property
var property = k.ContainingType.GetProperty(k.PropertyName);
if (property != null)
{
var propertyAttributes = global::System.Reflection.CustomAttributeExtensions
.GetCustomAttributes<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(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<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(parameter, inherit: true);

results.AddRange(paramAttributes);

break;
}
}

return results.ToArray();
});
}
}
}
""";

private static string EmitTypeChecks(ImmutableArray<ValidatableType> 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("),");
}
}
35 changes: 35 additions & 0 deletions src/Validation/gen/Extensions/ISymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
0