From ceec319e3a7d1710107d01599029c6a25b3bb6af Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:15:24 -0500 Subject: [PATCH 01/75] Add support for Configure method. (#3) --- .github/workflows/develop.yml | 9 +- .github/workflows/publish.yml | 7 +- GeneratedEndpoints.sln | 7 + README.md | 39 +++++ .../GeneratedEndpoints.csproj | 8 +- src/GeneratedEndpoints/MinimalApiGenerator.cs | 152 ++++++++++++++++-- .../GeneratedEndpoints.Tests.Lab.csproj | 22 +++ .../GetUserEndpoint.cs | 23 +++ tests/GeneratedEndpoints.Tests.Lab/Program.cs | 7 + .../Properties/launchSettings.json | 23 +++ .../appsettings.Development.json | 8 + .../appsettings.json | 9 ++ .../Common/ModuleInitializer.cs | 2 +- .../Common/TestHelpers.cs | 11 +- .../GeneratedEndpoints.Tests.csproj | 126 ++------------- ...ndpointHandlers_WithNamespace.verified.txt | 23 +++ ...ointHandlers_WithoutNamespace.verified.txt | 23 +++ ...ndpointHandlers_WithNamespace.verified.txt | 33 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 33 ++++ .../GeneratedEndpointsTests.cs | 23 ++- 20 files changed, 438 insertions(+), 150 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests.Lab/GeneratedEndpoints.Tests.Lab.csproj create mode 100644 tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs create mode 100644 tests/GeneratedEndpoints.Tests.Lab/Program.cs create mode 100644 tests/GeneratedEndpoints.Tests.Lab/Properties/launchSettings.json create mode 100644 tests/GeneratedEndpoints.Tests.Lab/appsettings.Development.json create mode 100644 tests/GeneratedEndpoints.Tests.Lab/appsettings.json create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index d93754c..20d7651 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -15,9 +15,6 @@ jobs: contents: read security-events: write - strategy: - fail-fast: false - steps: - name: Checkout repository uses: actions/checkout@v4 @@ -27,10 +24,10 @@ jobs: with: languages: 'csharp' - - name: Setup .NET 9.0 + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.x + dotnet-version: 10.x - name: Restore dependencies run: dotnet restore @@ -39,7 +36,7 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Test - run: dotnet test ./tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj --configuration Release --no-build --verbosity normal --framework net9.0 + run: dotnet test ./tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj --configuration Release --no-build --verbosity normal --framework net10.0 - name: Perform CodeQL analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 03b9cc0..462d270 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,17 +13,14 @@ jobs: contents: read security-events: write - strategy: - fail-fast: false - steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup .NET 9.0 + - name: Setup .NET 10.0 uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.x + dotnet-version: 10.x - name: Restore dependencies run: dotnet restore diff --git a/GeneratedEndpoints.sln b/GeneratedEndpoints.sln index ea88e6a..88e9eaf 100644 --- a/GeneratedEndpoints.sln +++ b/GeneratedEndpoints.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratedEndpoints.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratedEndpoints", "src\GeneratedEndpoints\GeneratedEndpoints.csproj", "{2F54865E-0F46-416B-A18D-C6C2ACF912B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratedEndpoints.Tests.Lab", "tests\GeneratedEndpoints.Tests.Lab\GeneratedEndpoints.Tests.Lab.csproj", "{2E39D0D0-B4C7-4A28-94E0-067E3B0902E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,8 +22,13 @@ Global {2F54865E-0F46-416B-A18D-C6C2ACF912B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F54865E-0F46-416B-A18D-C6C2ACF912B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F54865E-0F46-416B-A18D-C6C2ACF912B2}.Release|Any CPU.Build.0 = Release|Any CPU + {2E39D0D0-B4C7-4A28-94E0-067E3B0902E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E39D0D0-B4C7-4A28-94E0-067E3B0902E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E39D0D0-B4C7-4A28-94E0-067E3B0902E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E39D0D0-B4C7-4A28-94E0-067E3B0902E2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {01E8E43A-EA5C-4F28-BAAE-A3EB7861B57E} = {F1E3D1D7-B9E4-4EFF-8FD5-2A735A4695B4} + {2E39D0D0-B4C7-4A28-94E0-067E3B0902E2} = {F1E3D1D7-B9E4-4EFF-8FD5-2A735A4695B4} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 0864b24..81bbdb2 100644 --- a/README.md +++ b/README.md @@ -156,3 +156,42 @@ In this example: Every new handler will automatically appear in the generated routing table the next time the project builds—no manual `MapGet`, `MapPost`, or registration code is required. +### 4. Customize generated endpoints with `Configure` + +Some scenarios require direct access to the `IEndpointConventionBuilder` that Minimal APIs use when configuring an endpoint—for example, adding endpoint filters, OpenAPI metadata, or other advanced conventions. Handler classes can now opt-in to that level of control by providing a static `Configure` method with the following signature: + +```csharp +public static void Configure(TBuilder builder) + where TBuilder : IEndpointConventionBuilder +``` + +When the generator detects this method on a handler class it will automatically wrap every mapped endpoint from the class in a `.Configure(...)` call that invokes your method. You can use the provided builder to apply conventions that are difficult or impossible to express via attributes alone. + +```csharp +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Generated.Attributes; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace Todos.Features; + +public sealed class CreateTodo +{ + [MapPost("/todos")] + public static Ok Handle([FromBody] Todo todo) => TypedResults.Ok(todo); + + public static void Configure(TBuilder builder) + where TBuilder : IEndpointConventionBuilder + { + builder.AddEndpointFilter(new TimingFilter()); + builder.WithOpenApi(operation => + { + operation.Summary = "Creates a todo"; + return operation; + }); + } +} +``` + +The method is only generated once per handler class, so any conventions you add will automatically flow to all endpoints defined within that class. + diff --git a/src/GeneratedEndpoints/GeneratedEndpoints.csproj b/src/GeneratedEndpoints/GeneratedEndpoints.csproj index 8c260ed..2c645c6 100644 --- a/src/GeneratedEndpoints/GeneratedEndpoints.csproj +++ b/src/GeneratedEndpoints/GeneratedEndpoints.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,9 +36,9 @@ GeneratedEndpoints - 9.0.0-preview.1 - 9.0.0.0 - 9.0.0.0 + 10.0.0-preview.1 + 10.0.0.0 + 10.0.0.0 en-US false diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index ff0449d..a13941d 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -295,7 +295,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke return null; var attribute = context.Attributes[0]; - var requestHandlerClassResult = GetRequestHandlerClass(requestHandlerMethodSymbol, cancellationToken); + var requestHandlerClassResult = GetRequestHandlerClass(requestHandlerMethodSymbol, context.SemanticModel.Compilation, cancellationToken); if (requestHandlerClassResult is null) return null; @@ -499,6 +499,7 @@ private static RequestHandlerMethod GetRequestHandlerMethod(IMethodSymbol method private static (INamedTypeSymbol RequestHandlerClassSymbol, RequestHandlerClass RequestHandlerClass)? GetRequestHandlerClass( IMethodSymbol methodSymbol, + Compilation compilation, CancellationToken cancellationToken ) { @@ -510,12 +511,109 @@ CancellationToken cancellationToken var name = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var isStatic = classSymbol.IsStatic; + var endpointConventionBuilderSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); + var hasConfigureMethod = ContainsConfigureMethod(classSymbol, endpointConventionBuilderSymbol, cancellationToken); - var requestHandlerClass = new RequestHandlerClass(name, isStatic); + var requestHandlerClass = new RequestHandlerClass(name, isStatic, hasConfigureMethod); return (classSymbol, requestHandlerClass); } + private static bool ContainsConfigureMethod( + INamedTypeSymbol classSymbol, + INamedTypeSymbol? endpointConventionBuilderSymbol, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var member in classSymbol.GetMembers("Configure")) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (member is not IMethodSymbol methodSymbol) + continue; + + if (IsConfigureMethod(methodSymbol, endpointConventionBuilderSymbol)) + return true; + } + + return false; + } + + private static bool IsConfigureMethod(IMethodSymbol methodSymbol, INamedTypeSymbol? endpointConventionBuilderSymbol) + { + if (!methodSymbol.IsStatic) + return false; + + if (methodSymbol.TypeParameters.Length != 1) + return false; + + if (methodSymbol.Parameters.Length != 1) + return false; + + var builderTypeParameter = methodSymbol.TypeParameters[0]; + var builderParameter = methodSymbol.Parameters[0]; + + if (!SymbolEqualityComparer.Default.Equals(builderParameter.Type, builderTypeParameter)) + return false; + + if (!methodSymbol.ReturnsVoid) + return false; + + if (!HasEndpointConventionBuilderConstraint(builderTypeParameter, methodSymbol, endpointConventionBuilderSymbol)) + return false; + + return true; + } + + private static bool HasEndpointConventionBuilderConstraint( + ITypeParameterSymbol builderTypeParameter, + IMethodSymbol methodSymbol, + INamedTypeSymbol? endpointConventionBuilderSymbol + ) + { + var symbolMatches = builderTypeParameter.ConstraintTypes.Any(constraint => + endpointConventionBuilderSymbol is not null + ? SymbolEqualityComparer.Default.Equals(constraint, endpointConventionBuilderSymbol) + : MatchesEndpointConventionBuilder(constraint) + ); + + if (symbolMatches) + return true; + + return methodSymbol.DeclaringSyntaxReferences + .Select(reference => reference.GetSyntax()) + .OfType() + .SelectMany(methodSyntax => methodSyntax.ConstraintClauses) + .Where(clause => string.Equals(clause.Name.Identifier.ValueText, builderTypeParameter.Name, StringComparison.Ordinal)) + .SelectMany(clause => clause.Constraints.OfType()) + .Any(constraint => IsEndpointConventionBuilderIdentifier(constraint.Type)); + } + + private static bool IsEndpointConventionBuilderIdentifier(TypeSyntax typeSyntax) + { + return typeSyntax switch + { + QualifiedNameSyntax qualified => IsEndpointConventionBuilderIdentifier(qualified.Right), + AliasQualifiedNameSyntax alias => IsEndpointConventionBuilderIdentifier(alias.Name), + SimpleNameSyntax simple => string.Equals(simple.Identifier.ValueText, "IEndpointConventionBuilder", StringComparison.Ordinal), + _ => false, + }; + } + + private static bool MatchesEndpointConventionBuilder(ITypeSymbol typeSymbol) + { + if (typeSymbol is not INamedTypeSymbol namedType) + return false; + + if (!string.Equals(namedType.Name, "IEndpointConventionBuilder", StringComparison.Ordinal)) + return false; + + var containingNamespace = namedType.ContainingNamespace?.ToDisplayString() ?? string.Empty; + return string.Equals(containingNamespace, "Microsoft.AspNetCore.Builder", StringComparison.Ordinal); + } + private static EquatableImmutableArray GetRequestHandlerParameters(IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -655,6 +753,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine(); source.AppendLine("using Microsoft.AspNetCore.Builder;"); + source.AppendLine("using Microsoft.AspNetCore.Http;"); source.AppendLine("using Microsoft.AspNetCore.Mvc;"); source.AppendLine("using Microsoft.AspNetCore.Routing;"); source.AppendLine("using Microsoft.Extensions.DependencyInjection;"); @@ -700,7 +799,18 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con private static void GenerateMapRequestHandler(StringBuilder source, RequestHandler requestHandler) { - source.Append(" "); + var wrapWithConfigure = requestHandler.Class.HasConfigureMethod; + var indent = wrapWithConfigure ? " " : " "; + var continuationIndent = indent + " "; + + if (wrapWithConfigure) + { + source.Append(" "); + source.Append(requestHandler.Class.Name); + source.AppendLine(".Configure("); + } + + source.Append(indent); source.Append("builder.Map"); source.Append(requestHandler.HttpMethod is "Get" or "Post" or "Put" or "Delete" or "Patch" ? requestHandler.HttpMethod : "Methods"); source.Append('('); @@ -754,7 +864,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl if (!string.IsNullOrEmpty(requestHandler.Metadata.Name)) { source.AppendLine(); - source.Append(" .WithName("); + source.Append(continuationIndent); + source.Append(".WithName("); source.Append(StringLiteral(requestHandler.Metadata.Name)); source.Append(')'); } @@ -762,7 +873,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl if (!string.IsNullOrEmpty(requestHandler.Metadata.Summary)) { source.AppendLine(); - source.Append(" .WithSummary("); + source.Append(continuationIndent); + source.Append(".WithSummary("); source.Append(StringLiteral(requestHandler.Metadata.Summary)); source.Append(')'); } @@ -770,7 +882,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl if (!string.IsNullOrEmpty(requestHandler.Metadata.Description)) { source.AppendLine(); - source.Append(" .WithDescription("); + source.Append(continuationIndent); + source.Append(".WithDescription("); source.Append(StringLiteral(requestHandler.Metadata.Description)); source.Append(')'); } @@ -778,7 +891,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl if (requestHandler.Metadata.Tags is { Count: > 0 }) { source.AppendLine(); - source.Append(" .WithTags("); + source.Append(continuationIndent); + source.Append(".WithTags("); source.Append(string.Join(", ", requestHandler.Metadata.Tags.Value.Select(StringLiteral))); source.Append(')'); } @@ -788,23 +902,35 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.AppendLine(); if (requestHandler.AuthorizationPolicies is { Count: > 0 }) { - source.Append(" .RequireAuthorization("); + source.Append(continuationIndent); + source.Append(".RequireAuthorization("); source.Append(string.Join(", ", requestHandler.AuthorizationPolicies.Value.Select(StringLiteral))); source.Append(')'); } else { - source.Append(" .RequireAuthorization()"); + source.Append(continuationIndent); + source.Append(".RequireAuthorization()"); } } if (requestHandler.DisableAntiforgery) { source.AppendLine(); - source.Append(" .DisableAntiforgery()"); + source.Append(continuationIndent); + source.Append(".DisableAntiforgery()"); } - source.AppendLine(";"); + if (wrapWithConfigure) + { + source.AppendLine(); + source.Append(" );"); + source.AppendLine(); + } + else + { + source.AppendLine(";"); + } } private static string GetBindingSourceAttribute(BindingSource source, string? key) @@ -856,6 +982,8 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< cost += rh.AuthorizationPolicies.Value.Sum(p => 6 + p.Length); if (rh.DisableAntiforgery) cost += 24; + if (rh.Class.HasConfigureMethod) + cost += 32 + rh.Class.Name.Length; estimate += cost; } @@ -1000,7 +1128,7 @@ private readonly record struct RequestHandler( bool DisableAntiforgery ); - private readonly record struct RequestHandlerClass(string Name, bool IsStatic); + private readonly record struct RequestHandlerClass(string Name, bool IsStatic, bool HasConfigureMethod); private readonly record struct RequestHandlerMethod(string Name, bool IsStatic, bool IsAwaitable, EquatableImmutableArray Parameters); diff --git a/tests/GeneratedEndpoints.Tests.Lab/GeneratedEndpoints.Tests.Lab.csproj b/tests/GeneratedEndpoints.Tests.Lab/GeneratedEndpoints.Tests.Lab.csproj new file mode 100644 index 0000000..7ab05ac --- /dev/null +++ b/tests/GeneratedEndpoints.Tests.Lab/GeneratedEndpoints.Tests.Lab.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + false + enable + latest + false + + + + + + + + + appsettings.json + + + + diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs new file mode 100644 index 0000000..25dbbd0 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Generated.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace GeneratedEndpoints.Tests.Lab; + +internal static class GetUserEndpoint +{ + [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] + public static Results GetUser(int id) + { + if (id > 0) + return TypedResults.Ok(); + + return TypedResults.NotFound(); + } + + public static void Configure(TBuilder builder) + where TBuilder : IEndpointConventionBuilder + { + } +} diff --git a/tests/GeneratedEndpoints.Tests.Lab/Program.cs b/tests/GeneratedEndpoints.Tests.Lab/Program.cs new file mode 100644 index 0000000..2265869 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests.Lab/Program.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Builder; + +var builder = WebApplication.CreateBuilder(args); + +var app = builder.Build(); + +app.Run(); diff --git a/tests/GeneratedEndpoints.Tests.Lab/Properties/launchSettings.json b/tests/GeneratedEndpoints.Tests.Lab/Properties/launchSettings.json new file mode 100644 index 0000000..93b571a --- /dev/null +++ b/tests/GeneratedEndpoints.Tests.Lab/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5271", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7127;http://localhost:5271", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/tests/GeneratedEndpoints.Tests.Lab/appsettings.Development.json b/tests/GeneratedEndpoints.Tests.Lab/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/tests/GeneratedEndpoints.Tests.Lab/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/tests/GeneratedEndpoints.Tests.Lab/appsettings.json b/tests/GeneratedEndpoints.Tests.Lab/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests.Lab/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs b/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs index 4d55498..d9dc1e5 100644 --- a/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs +++ b/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs @@ -5,7 +5,7 @@ namespace GeneratedEndpoints.Tests.Common; public static class ModuleInitializer { - private static readonly object Lock = new(); + private static readonly Lock Lock = new(); private static bool _isInitialized; public static void Initialize() diff --git a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs index bfc4486..fcb90d6 100644 --- a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs +++ b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs @@ -9,22 +9,25 @@ public static class TestHelpers { public static GeneratorDriverRunResult RunGenerator(IEnumerable sources) { - var cSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp11).WithPreprocessorSymbols("NET7_0_OR_GREATER"); + var cSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp13); var cSharpCompilationOptions = new CSharpCompilationOptions(OutputKind.NetModule).WithNullableContextOptions(NullableContextOptions.Enable); - return IncrementalGenerator.Run(sources, cSharpParseOptions, ReferenceAssemblies.Net80, cSharpCompilationOptions); + var (_, result) = IncrementalGenerator.RunWithDiagnostics(sources, cSharpParseOptions, AspNet100.References.All, cSharpCompilationOptions); + return result; } public static IEnumerable GetSources(string source, bool withNamespace) { const string usingStatements = """ - + using Microsoft.AspNetCore.Generated.Attributes; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.HttpResults; """; if (withNamespace) yield return $""" {usingStatements} - namespace EntityFrameworkGeneratorTests; + namespace GeneratedEndpointsTests; {source} """; diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj index 3f873a6..c43a904 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable latest @@ -10,25 +10,24 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + @@ -39,103 +38,4 @@ - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - DbContextTests - ServiceLifetimeTests.cs - - - diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..9f4ba4d --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/users/{id:int}", global::GeneratedEndpointsTests.GetUserEndpoint.GetUser2) + .WithName("GetUser") + .WithSummary("Gets a user by ID.") + .WithDescription("Gets a user by ID when the ID is greater than zero.") + .WithTags("Users"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..828367c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/users/{id:int}", global::GetUserEndpoint.GetUser2) + .WithName("GetUser") + .WithSummary("Gets a user by ID.") + .WithDescription("Gets a user by ID when the ID is greater than zero.") + .WithTags("Users"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 9cd63d6..aca7c8f 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -16,14 +16,27 @@ public GeneratedEndpointsTests() [InlineData(false)] public async Task MapGet(bool withNamespace) { - var sources = TestHelpers.GetSources( - """ + var sources = TestHelpers.GetSources(""" + [Tags("Users")] + internal static class GetUserEndpoint + { + [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] + public static Results GetUser2(int id) + { + if (id > 0) + return TypedResults.Ok(); - """, - withNamespace + return TypedResults.NotFound(); + } + } + """, withNamespace ); var result = TestHelpers.RunGenerator(sources); - //await result.VerifyAsync("GeneratedSource.g.cs").UseMethodName($"{nameof(UsingDbSets)}_With{(withNamespace ? "" : "out")}Namespace"); + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapGet)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapGet)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } } From a8836badb5064c15812ca0808108efc47b22367d Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:29:19 -0500 Subject: [PATCH 02/75] Support service provider aware Configure methods (#5) --- README.md | 6 ++ src/GeneratedEndpoints/MinimalApiGenerator.cs | 95 +++++++++++++++++-- ...ndpointHandlers_WithNamespace.verified.txt | 33 +++++++ ...ointHandlers_WithoutNamespace.verified.txt | 33 +++++++ .../GeneratedEndpointsTests.cs | 30 ++++++ 5 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/README.md b/README.md index 81bbdb2..3f214c6 100644 --- a/README.md +++ b/README.md @@ -163,10 +163,16 @@ Some scenarios require direct access to the `IEndpointConventionBuilder` that Mi ```csharp public static void Configure(TBuilder builder) where TBuilder : IEndpointConventionBuilder + +// or, when you need to resolve services while configuring +public static void Configure(TBuilder builder, IServiceProvider serviceProvider) + where TBuilder : IEndpointConventionBuilder ``` When the generator detects this method on a handler class it will automatically wrap every mapped endpoint from the class in a `.Configure(...)` call that invokes your method. You can use the provided builder to apply conventions that are difficult or impossible to express via attributes alone. +If you include the optional `IServiceProvider` parameter, the generator passes the `IEndpointRouteBuilder.ServiceProvider` from the `MapEndpointHandlers` call. This makes it easy to resolve scoped services or other helpers needed to configure filters, OpenAPI metadata, or custom conventions without manually re-wiring the app's service provider. + ```csharp using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Generated.Attributes; diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index a13941d..b068dc6 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -512,21 +512,36 @@ CancellationToken cancellationToken var name = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var isStatic = classSymbol.IsStatic; var endpointConventionBuilderSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); - var hasConfigureMethod = ContainsConfigureMethod(classSymbol, endpointConventionBuilderSymbol, cancellationToken); + var serviceProviderSymbol = compilation.GetTypeByMetadataName("System.IServiceProvider"); + var configureMethodDetails = GetConfigureMethodDetails( + classSymbol, + endpointConventionBuilderSymbol, + serviceProviderSymbol, + cancellationToken + ); - var requestHandlerClass = new RequestHandlerClass(name, isStatic, hasConfigureMethod); + var requestHandlerClass = new RequestHandlerClass( + name, + isStatic, + configureMethodDetails.HasConfigureMethod, + configureMethodDetails.ConfigureMethodAcceptsServiceProvider + ); return (classSymbol, requestHandlerClass); } - private static bool ContainsConfigureMethod( + private static ConfigureMethodDetails GetConfigureMethodDetails( INamedTypeSymbol classSymbol, INamedTypeSymbol? endpointConventionBuilderSymbol, + INamedTypeSymbol? serviceProviderSymbol, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); + var hasConfigureMethod = false; + var acceptsServiceProvider = false; + foreach (var member in classSymbol.GetMembers("Configure")) { cancellationToken.ThrowIfCancellationRequested(); @@ -534,22 +549,36 @@ CancellationToken cancellationToken if (member is not IMethodSymbol methodSymbol) continue; - if (IsConfigureMethod(methodSymbol, endpointConventionBuilderSymbol)) - return true; + if (IsConfigureMethod(methodSymbol, endpointConventionBuilderSymbol, serviceProviderSymbol, out var methodAcceptsServiceProvider)) + { + hasConfigureMethod = true; + if (methodAcceptsServiceProvider) + { + acceptsServiceProvider = true; + break; + } + } } - return false; + return new ConfigureMethodDetails(hasConfigureMethod, acceptsServiceProvider); } - private static bool IsConfigureMethod(IMethodSymbol methodSymbol, INamedTypeSymbol? endpointConventionBuilderSymbol) + private static bool IsConfigureMethod( + IMethodSymbol methodSymbol, + INamedTypeSymbol? endpointConventionBuilderSymbol, + INamedTypeSymbol? serviceProviderSymbol, + out bool acceptsServiceProvider + ) { + acceptsServiceProvider = false; + if (!methodSymbol.IsStatic) return false; if (methodSymbol.TypeParameters.Length != 1) return false; - if (methodSymbol.Parameters.Length != 1) + if (methodSymbol.Parameters.Length is < 1 or > 2) return false; var builderTypeParameter = methodSymbol.TypeParameters[0]; @@ -558,6 +587,15 @@ private static bool IsConfigureMethod(IMethodSymbol methodSymbol, INamedTypeSymb if (!SymbolEqualityComparer.Default.Equals(builderParameter.Type, builderTypeParameter)) return false; + if (methodSymbol.Parameters.Length == 2) + { + var serviceProviderParameter = methodSymbol.Parameters[1]; + if (!IsServiceProviderParameter(serviceProviderParameter.Type, serviceProviderSymbol)) + return false; + + acceptsServiceProvider = true; + } + if (!methodSymbol.ReturnsVoid) return false; @@ -567,6 +605,14 @@ private static bool IsConfigureMethod(IMethodSymbol methodSymbol, INamedTypeSymb return true; } + private static bool IsServiceProviderParameter(ITypeSymbol typeSymbol, INamedTypeSymbol? serviceProviderSymbol) + { + if (serviceProviderSymbol is not null) + return SymbolEqualityComparer.Default.Equals(typeSymbol, serviceProviderSymbol); + + return MatchesServiceProvider(typeSymbol); + } + private static bool HasEndpointConventionBuilderConstraint( ITypeParameterSymbol builderTypeParameter, IMethodSymbol methodSymbol, @@ -614,6 +660,18 @@ private static bool MatchesEndpointConventionBuilder(ITypeSymbol typeSymbol) return string.Equals(containingNamespace, "Microsoft.AspNetCore.Builder", StringComparison.Ordinal); } + private static bool MatchesServiceProvider(ITypeSymbol typeSymbol) + { + if (typeSymbol is not INamedTypeSymbol namedType) + return false; + + if (!string.Equals(namedType.Name, "IServiceProvider", StringComparison.Ordinal)) + return false; + + var containingNamespace = namedType.ContainingNamespace?.ToDisplayString() ?? string.Empty; + return string.Equals(containingNamespace, "System", StringComparison.Ordinal); + } + private static EquatableImmutableArray GetRequestHandlerParameters(IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -800,6 +858,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con private static void GenerateMapRequestHandler(StringBuilder source, RequestHandler requestHandler) { var wrapWithConfigure = requestHandler.Class.HasConfigureMethod; + var configureAcceptsServiceProvider = requestHandler.Class.ConfigureMethodAcceptsServiceProvider; var indent = wrapWithConfigure ? " " : " "; var continuationIndent = indent + " "; @@ -921,6 +980,13 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(".DisableAntiforgery()"); } + if (wrapWithConfigure && configureAcceptsServiceProvider) + { + source.AppendLine(","); + source.Append(indent); + source.Append("builder.ServiceProvider"); + } + if (wrapWithConfigure) { source.AppendLine(); @@ -983,7 +1049,11 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< if (rh.DisableAntiforgery) cost += 24; if (rh.Class.HasConfigureMethod) + { cost += 32 + rh.Class.Name.Length; + if (rh.Class.ConfigureMethodAcceptsServiceProvider) + cost += 32; + } estimate += cost; } @@ -1128,7 +1198,12 @@ private readonly record struct RequestHandler( bool DisableAntiforgery ); - private readonly record struct RequestHandlerClass(string Name, bool IsStatic, bool HasConfigureMethod); + private readonly record struct RequestHandlerClass( + string Name, + bool IsStatic, + bool HasConfigureMethod, + bool ConfigureMethodAcceptsServiceProvider + ); private readonly record struct RequestHandlerMethod(string Name, bool IsStatic, bool IsAwaitable, EquatableImmutableArray Parameters); @@ -1136,6 +1211,8 @@ bool DisableAntiforgery private readonly record struct Parameter(string Name, string Type, BindingSource Source, string? Key); + private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); + private enum BindingSource { None = 0, diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..0ab0bbd --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureEndpoint.Configure( + builder.MapGet("/service-provider", global::GeneratedEndpointsTests.ConfigureEndpoint.Handle) + .WithName("Handle"), + builder.ServiceProvider + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..fa12613 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::ConfigureEndpoint.Configure( + builder.MapGet("/service-provider", global::ConfigureEndpoint.Handle) + .WithName("Handle"), + builder.ServiceProvider + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index aca7c8f..39519a1 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -39,4 +39,34 @@ await result.VerifyAsync("AddEndpointHandlers.g.cs") await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(MapGet)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapGetWithConfigureServiceProvider(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + using Microsoft.AspNetCore.Builder; + + internal static class ConfigureEndpoint + { + [MapGet("/service-provider")] + public static Ok Handle() + => TypedResults.Ok(); + + public static void Configure(TBuilder builder, System.IServiceProvider serviceProvider) + where TBuilder : IEndpointConventionBuilder + { + _ = serviceProvider; + builder.WithMetadata(new object()); + } + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapGetWithConfigureServiceProvider)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } } From 412814359c92dd24ccdd13a1f6e8aca2e11f22c1 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:33:20 -0500 Subject: [PATCH 03/75] Add MapGetWithConfigure test (#6) --- ...ndpointHandlers_WithNamespace.verified.txt | 32 +++++++++++++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 32 +++++++++++++++++++ .../GeneratedEndpointsTests.cs | 29 +++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..5187bd7 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureEndpoint.Configure( + builder.MapGet("/configure", global::GeneratedEndpointsTests.ConfigureEndpoint.Handle) + .WithName("Handle") + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..88a888a --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::ConfigureEndpoint.Configure( + builder.MapGet("/configure", global::ConfigureEndpoint.Handle) + .WithName("Handle") + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 39519a1..e314264 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -40,6 +40,35 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(MapGet)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapGetWithConfigure(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + using Microsoft.AspNetCore.Builder; + + internal static class ConfigureEndpoint + { + [MapGet("/configure")] + public static Ok Handle() + => TypedResults.Ok(); + + public static void Configure(TBuilder builder) + where TBuilder : IEndpointConventionBuilder + { + builder.WithMetadata(new object()); + } + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapGetWithConfigure)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 7d77efd7d751b9e30d1c93b13e479b100bc79e73 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:35:56 -0500 Subject: [PATCH 04/75] Add MapQuery attribute support (#7) --- README.md | 2 +- src/GeneratedEndpoints/MinimalApiGenerator.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f214c6..74b7026 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ public sealed class GetTodo Key points: -* Use `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, or `[MapConnect]` to describe the HTTP verb and route pattern. +* Use `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapQuery]`, `[MapTrace]`, or `[MapConnect]` to describe the HTTP verb and route pattern. * Optional `Name`, `Summary`, and `Description` named parameters populate the generated `.WithName`, `.WithSummary`, and `.WithDescription` metadata calls. When omitted, the generator derives the endpoint name from the method name (stripping a trailing `Async`). * Apply standard ASP.NET Core parameter binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromServices]`, `[AsParameters]`, etc.). The generator mirrors them onto the produced delegate so binding behaves exactly as declared. * Annotate the **class**, an individual **method**, or both with `[Tags]`, `[RequireAuthorization]`, or `[DisableAntiforgery]`. Class-level metadata is merged onto every generated endpoint, while method-level attributes can refine or augment the settings for a specific handler. diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index b068dc6..f8ada32 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -43,6 +43,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string MapPatchAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapPatchAttributeName}"; private const string MapPatchAttributeHint = $"{MapPatchAttributeFullyQualifiedName}.gs.cs"; + private const string MapQueryAttributeName = "MapQueryAttribute"; + private const string MapQueryAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapQueryAttributeName}"; + private const string MapQueryAttributeHint = $"{MapQueryAttributeFullyQualifiedName}.gs.cs"; + private const string MapTraceAttributeName = "MapTraceAttribute"; private const string MapTraceAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapTraceAttributeName}"; private const string MapTraceAttributeHint = $"{MapTraceAttributeFullyQualifiedName}.gs.cs"; @@ -128,6 +132,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .WhereNotNull() .Collect(); + var queryRequestHandlers = context.SyntaxProvider + .ForAttributeWithMetadataName(MapQueryAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) + .WhereNotNull() + .Collect(); + var traceRequestHandlers = context.SyntaxProvider .ForAttributeWithMetadataName(MapTraceAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) .WhereNotNull() @@ -150,6 +159,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(static (x, _) => x.Left.AddRange(x.Right)) .Combine(headRequestHandlers) .Select(static (x, _) => x.Left.AddRange(x.Right)) + .Combine(queryRequestHandlers) + .Select(static (x, _) => x.Left.AddRange(x.Right)) .Combine(traceRequestHandlers) .Select(static (x, _) => x.Left.AddRange(x.Right)) .Combine(connectRequestHandlers) @@ -170,6 +181,7 @@ private static void RegisterAttributes(IncrementalGeneratorPostInitializationCon (Name: MapOptionsAttributeName, FullyQualified: MapOptionsAttributeFullyQualifiedName, Hint: MapOptionsAttributeHint, Verb: "OPTIONS"), (Name: MapHeadAttributeName, FullyQualified: MapHeadAttributeFullyQualifiedName, Hint: MapHeadAttributeHint, Verb: "HEAD"), (Name: MapPatchAttributeName, FullyQualified: MapPatchAttributeFullyQualifiedName, Hint: MapPatchAttributeHint, Verb: "PATCH"), + (Name: MapQueryAttributeName, FullyQualified: MapQueryAttributeFullyQualifiedName, Hint: MapQueryAttributeHint, Verb: "QUERY"), (Name: MapTraceAttributeName, FullyQualified: MapTraceAttributeFullyQualifiedName, Hint: MapTraceAttributeHint, Verb: "TRACE"), (Name: MapConnectAttributeName, FullyQualified: MapConnectAttributeFullyQualifiedName, Hint: MapConnectAttributeHint, Verb: "CONNECT"), }; @@ -345,6 +357,7 @@ CancellationToken cancellationToken MapOptionsAttributeName => "OPTIONS", MapHeadAttributeName => "HEAD", MapPatchAttributeName => "Patch", + MapQueryAttributeName => "QUERY", MapTraceAttributeName => "TRACE", MapConnectAttributeName => "CONNECT", _ => "", @@ -875,7 +888,7 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append('('); source.Append(StringLiteral(requestHandler.Pattern)); source.Append(", "); - if (requestHandler.HttpMethod is "OPTIONS" or "HEAD" or "TRACE" or "CONNECT") + if (requestHandler.HttpMethod is "OPTIONS" or "HEAD" or "TRACE" or "CONNECT" or "QUERY") { source.Append("new[] { \""); source.Append(requestHandler.HttpMethod); From 9ed0d271967240ec103c38b966200f40b71be8a0 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:41:38 -0500 Subject: [PATCH 05/75] Add AllowAnonymous attribute handling (#8) --- README.md | 2 +- src/GeneratedEndpoints/MinimalApiGenerator.cs | 52 ++++++++++++++++--- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 74b7026..66eadaa 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Key points: * Use `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapQuery]`, `[MapTrace]`, or `[MapConnect]` to describe the HTTP verb and route pattern. * Optional `Name`, `Summary`, and `Description` named parameters populate the generated `.WithName`, `.WithSummary`, and `.WithDescription` metadata calls. When omitted, the generator derives the endpoint name from the method name (stripping a trailing `Async`). * Apply standard ASP.NET Core parameter binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromServices]`, `[AsParameters]`, etc.). The generator mirrors them onto the produced delegate so binding behaves exactly as declared. -* Annotate the **class**, an individual **method**, or both with `[Tags]`, `[RequireAuthorization]`, or `[DisableAntiforgery]`. Class-level metadata is merged onto every generated endpoint, while method-level attributes can refine or augment the settings for a specific handler. +* Annotate the **class**, an individual **method**, or both with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, or `[AllowAnonymous]`. Class-level metadata is merged onto every generated endpoint, while method-level attributes can refine or augment the settings for a specific handler. `[AllowAnonymous]` lets a method opt out of authorization even if the enclosing class (or other conventions) require authenticated access. * Non-static handler classes are automatically registered with dependency injection (as transient services). Their instance methods receive a scoped instance resolved from DI, while static methods continue to behave like any other static helper. #### Static handler example diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index f8ada32..e82832d 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -67,6 +67,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; + private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; + private const string AllowAnonymousAttributeFullyQualifiedName = $"{AttributesNamespace}.{AllowAnonymousAttributeName}"; + private const string AllowAnonymousAttributeHint = $"{AllowAnonymousAttributeFullyQualifiedName}.gs.cs"; + private const string RoutingNamespace = $"{BaseNamespace}.Routing"; private const string AddEndpointHandlersClassName = "EndpointServicesExtensions"; @@ -245,6 +249,23 @@ internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attri """; context.AddSource(DisableAntiforgeryAttributeHint, SourceText.From(disableAntiforgerySource, Encoding.UTF8)); + + // AllowAnonymous + var allowAnonymousSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Allows the annotated endpoint or class to bypass authorization requirements. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{AllowAnonymousAttributeName}} : global::System.Attribute + { + } + + """; + context.AddSource(AllowAnonymousAttributeHint, SourceText.From(allowAnonymousSource, Encoding.UTF8)); } private static string GenerateHttpAttributeSource(string fileHeader, string attributesNamespace, string attributeName, string summaryVerb) @@ -317,7 +338,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (httpMethod, pattern, name, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); - var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery) = + var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -325,7 +346,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var metadata = new RequestHandlerMetadata(name, summary, description, tags); var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, - authorizationPolicies, disableAntiforgery + authorizationPolicies, disableAntiforgery, allowAnonymous ); return requestHandler; @@ -397,7 +418,7 @@ CancellationToken cancellationToken } private static (EquatableImmutableArray? tags, bool requireAuthorization, EquatableImmutableArray? authorizationPolicies, bool - disableAntiforgery) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) + disableAntiforgery, bool allowAnonymous) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -405,14 +426,15 @@ private static (EquatableImmutableArray? tags, bool requireAuthorization var requireAuthorization = false; EquatableImmutableArray? authorizationPolicies = null; var disableAntiforgery = false; + var allowAnonymous = false; var classAttributes = classSymbol.GetAttributes(); - GetAdditionalRequestHandlerAttributeValues(classAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery); + GetAdditionalRequestHandlerAttributeValues(classAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous); var methodAttributes = methodSymbol.GetAttributes(); - GetAdditionalRequestHandlerAttributeValues(methodAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery); + GetAdditionalRequestHandlerAttributeValues(methodAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous); - return (tags, requireAuthorization, authorizationPolicies, disableAntiforgery); + return (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous); } private static void GetAdditionalRequestHandlerAttributeValues( @@ -420,7 +442,8 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref EquatableImmutableArray? tags, ref bool requireAuthorization, ref EquatableImmutableArray? authorizationPolicies, - ref bool disableAntiforgery + ref bool disableAntiforgery, + ref bool allowAnonymous ) { foreach (var attribute in attributes) @@ -467,6 +490,9 @@ ref bool disableAntiforgery case $"global::{DisableAntiforgeryAttributeFullyQualifiedName}": disableAntiforgery = true; break; + case $"global::{AllowAnonymousAttributeFullyQualifiedName}": + allowAnonymous = true; + break; } } } @@ -993,6 +1019,13 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(".DisableAntiforgery()"); } + if (requestHandler.AllowAnonymous) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".AllowAnonymous()"); + } + if (wrapWithConfigure && configureAcceptsServiceProvider) { source.AppendLine(","); @@ -1061,6 +1094,8 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< cost += rh.AuthorizationPolicies.Value.Sum(p => 6 + p.Length); if (rh.DisableAntiforgery) cost += 24; + if (rh.AllowAnonymous) + cost += 24; if (rh.Class.HasConfigureMethod) { cost += 32 + rh.Class.Name.Length; @@ -1208,7 +1243,8 @@ private readonly record struct RequestHandler( RequestHandlerMetadata Metadata, bool RequireAuthorization, EquatableImmutableArray? AuthorizationPolicies, - bool DisableAntiforgery + bool DisableAntiforgery, + bool AllowAnonymous ); private readonly record struct RequestHandlerClass( From f21df5dcbdba576e9f7844bcea263fee823c403b Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:02:13 -0500 Subject: [PATCH 06/75] Support generic Accepts and Produces attributes (#9) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 687 +++++++++++++++++- 1 file changed, 668 insertions(+), 19 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index e82832d..86bd414 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -71,6 +71,23 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string AllowAnonymousAttributeFullyQualifiedName = $"{AttributesNamespace}.{AllowAnonymousAttributeName}"; private const string AllowAnonymousAttributeHint = $"{AllowAnonymousAttributeFullyQualifiedName}.gs.cs"; + private const string AcceptsAttributeName = "AcceptsAttribute"; + private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}"; + private const string AcceptsAttributeHint = $"{AcceptsAttributeFullyQualifiedName}.gs.cs"; + + private const string ProducesAttributeName = "ProducesAttribute"; + private const string ProducesAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesAttributeName}"; + private const string ProducesAttributeHint = $"{ProducesAttributeFullyQualifiedName}.gs.cs"; + + private const string ProducesProblemAttributeName = "ProducesProblemAttribute"; + private const string ProducesProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesProblemAttributeName}"; + private const string ProducesProblemAttributeHint = $"{ProducesProblemAttributeFullyQualifiedName}.gs.cs"; + + private const string ProducesValidationProblemAttributeName = "ProducesValidationProblemAttribute"; + private const string ProducesValidationProblemAttributeFullyQualifiedName = + $"{AttributesNamespace}.{ProducesValidationProblemAttributeName}"; + private const string ProducesValidationProblemAttributeHint = $"{ProducesValidationProblemAttributeFullyQualifiedName}.gs.cs"; + private const string RoutingNamespace = $"{BaseNamespace}.Routing"; private const string AddEndpointHandlersClassName = "EndpointServicesExtensions"; @@ -266,6 +283,264 @@ internal sealed class {{AllowAnonymousAttributeName}} : global::System.Attribute """; context.AddSource(AllowAnonymousAttributeHint, SourceText.From(allowAnonymousSource, Encoding.UTF8)); + + // Accepts + var acceptsSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the request type and content types accepted by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type RequestType { get; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The CLR type of the request body. + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(global::System.Type requestType, string contentType = "application/json", params string[] additionalContentTypes) + { + RequestType = requestType ?? throw new global::System.ArgumentNullException(nameof(requestType)); + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. + /// + /// The CLR type of the request body. + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type RequestType => typeof(TRequest); + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Accepts attribute class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """; + context.AddSource(AcceptsAttributeHint, SourceText.From(acceptsSource, Encoding.UTF8)); + + // Produces + var producesSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType { get; } + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The CLR type of the response body. + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesAttributeName}}(global::System.Type responseType, int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) + { + ResponseType = responseType ?? throw new global::System.ArgumentNullException(nameof(responseType)); + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. + /// + /// The CLR type of the response body. + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType => typeof(TResponse); + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Produces attribute class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """; + context.AddSource(ProducesAttributeHint, SourceText.From(producesSource, Encoding.UTF8)); + + // ProducesProblem + var producesProblemSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesProblemAttributeName}}(int statusCode = 500, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """; + context.AddSource(ProducesProblemAttributeHint, SourceText.From(producesProblemSource, Encoding.UTF8)); + + // ProducesValidationProblem + var producesValidationProblemSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a validation problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesValidationProblemAttributeName}}(int statusCode = 400, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """; + context.AddSource(ProducesValidationProblemAttributeHint, SourceText.From(producesValidationProblemSource, Encoding.UTF8)); } private static string GenerateHttpAttributeSource(string fileHeader, string attributesNamespace, string attributeName, string summaryVerb) @@ -338,12 +613,22 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (httpMethod, pattern, name, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); - var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous) = - GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); + var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, accepts, produces, + producesProblem, producesValidationProblem) + = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); - var metadata = new RequestHandlerMetadata(name, summary, description, tags); + var metadata = new RequestHandlerMetadata( + name, + summary, + description, + tags, + accepts, + produces, + producesProblem, + producesValidationProblem + ); var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous @@ -417,8 +702,17 @@ CancellationToken cancellationToken return (httpMethod, pattern, name, summary, description); } - private static (EquatableImmutableArray? tags, bool requireAuthorization, EquatableImmutableArray? authorizationPolicies, bool - disableAntiforgery, bool allowAnonymous) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) + private static ( + EquatableImmutableArray? tags, + bool requireAuthorization, + EquatableImmutableArray? authorizationPolicies, + bool disableAntiforgery, + bool allowAnonymous, + EquatableImmutableArray? accepts, + EquatableImmutableArray? produces, + EquatableImmutableArray? producesProblem, + EquatableImmutableArray? producesValidationProblem + ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -428,13 +722,50 @@ private static (EquatableImmutableArray? tags, bool requireAuthorization var disableAntiforgery = false; var allowAnonymous = false; + List? accepts = null; + List? produces = null; + List? producesProblem = null; + List? producesValidationProblem = null; + var classAttributes = classSymbol.GetAttributes(); - GetAdditionalRequestHandlerAttributeValues(classAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous); + GetAdditionalRequestHandlerAttributeValues( + classAttributes, + ref tags, + ref requireAuthorization, + ref authorizationPolicies, + ref disableAntiforgery, + ref allowAnonymous, + ref accepts, + ref produces, + ref producesProblem, + ref producesValidationProblem + ); var methodAttributes = methodSymbol.GetAttributes(); - GetAdditionalRequestHandlerAttributeValues(methodAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous); + GetAdditionalRequestHandlerAttributeValues( + methodAttributes, + ref tags, + ref requireAuthorization, + ref authorizationPolicies, + ref disableAntiforgery, + ref allowAnonymous, + ref accepts, + ref produces, + ref producesProblem, + ref producesValidationProblem + ); - return (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous); + return ( + tags, + requireAuthorization, + authorizationPolicies, + disableAntiforgery, + allowAnonymous, + ToEquatableOrNull(accepts), + ToEquatableOrNull(produces), + ToEquatableOrNull(producesProblem), + ToEquatableOrNull(producesValidationProblem) + ); } private static void GetAdditionalRequestHandlerAttributeValues( @@ -443,7 +774,11 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref bool requireAuthorization, ref EquatableImmutableArray? authorizationPolicies, ref bool disableAntiforgery, - ref bool allowAnonymous + ref bool allowAnonymous, + ref List? accepts, + ref List? produces, + ref List? producesProblem, + ref List? producesValidationProblem ) { foreach (var attribute in attributes) @@ -454,6 +789,18 @@ ref bool allowAnonymous var fullyQualifiedName = attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (IsGeneratedAttribute(fullyQualifiedName, AcceptsAttributeName)) + { + TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); + continue; + } + + if (IsGeneratedAttribute(fullyQualifiedName, ProducesAttributeName)) + { + TryAddProducesMetadata(attribute, attributeClass, ref produces); + continue; + } + switch (fullyQualifiedName) { case "global::Microsoft.AspNetCore.Http.TagsAttribute": @@ -493,6 +840,38 @@ ref bool allowAnonymous case $"global::{AllowAnonymousAttributeFullyQualifiedName}": allowAnonymous = true; break; + case $"global::{ProducesProblemAttributeFullyQualifiedName}": + { + var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode + ? producesProblemStatusCode + : 500; + var contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) + : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 + ? GetStringArrayValues(attribute.ConstructorArguments[2]) + : null; + + var producesProblemList = producesProblem ??= new List(); + producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes)); + break; + } + case $"global::{ProducesValidationProblemAttributeFullyQualifiedName}": + { + var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode + ? producesValidationProblemStatusCode + : 400; + var contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) + : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 + ? GetStringArrayValues(attribute.ConstructorArguments[2]) + : null; + + var producesValidationProblemList = producesValidationProblem ??= new List(); + producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes)); + break; + } } } } @@ -503,6 +882,127 @@ private static void MergeInto(ref EquatableImmutableArray? target, IEnum target = merged.Count > 0 ? merged : null; } + private static EquatableImmutableArray? ToEquatableOrNull(List? values) + { + return values is { Count: > 0 } ? values.ToEquatableImmutableArray() : null; + } + + private static string NormalizeRequiredContentType(string? contentType, string defaultValue) + { + return string.IsNullOrWhiteSpace(contentType) ? defaultValue : contentType!.Trim(); + } + + private static string? NormalizeOptionalContentType(string? contentType) + { + return string.IsNullOrWhiteSpace(contentType) ? null : contentType!.Trim(); + } + + private static EquatableImmutableArray? GetStringArrayValues(TypedConstant typedConstant) + { + if (typedConstant.Kind != TypedConstantKind.Array || typedConstant.Values.IsDefaultOrEmpty) + return null; + + var builder = ImmutableArray.CreateBuilder(typedConstant.Values.Length); + foreach (var value in typedConstant.Values) + { + if (value.Value is string s && !string.IsNullOrWhiteSpace(s)) + builder.Add(s.Trim()); + } + + return builder.Count > 0 ? builder.ToEquatableImmutable() : null; + } + + private static bool IsGeneratedAttribute(string fullyQualifiedName, string attributeName) + { + var prefix = $"global::{AttributesNamespace}.{attributeName}"; + return fullyQualifiedName.StartsWith(prefix, StringComparison.Ordinal); + } + + private static void TryAddAcceptsMetadata( + AttributeData attribute, + INamedTypeSymbol attributeClass, + ref List? accepts) + { + string? requestType = null; + string contentType; + EquatableImmutableArray? additionalContentTypes; + + if (attributeClass.IsGenericType && attributeClass.TypeArguments.Length == 1) + { + requestType = attributeClass.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + contentType = attribute.ConstructorArguments.Length > 0 + ? NormalizeRequiredContentType(attribute.ConstructorArguments[0].Value as string, "application/json") + : "application/json"; + additionalContentTypes = attribute.ConstructorArguments.Length > 1 + ? GetStringArrayValues(attribute.ConstructorArguments[1]) + : null; + } + else if (attribute.ConstructorArguments.Length >= 1 && + attribute.ConstructorArguments[0].Value is ITypeSymbol requestTypeSymbol) + { + requestType = requestTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeRequiredContentType(attribute.ConstructorArguments[1].Value as string, "application/json") + : "application/json"; + additionalContentTypes = attribute.ConstructorArguments.Length > 2 + ? GetStringArrayValues(attribute.ConstructorArguments[2]) + : null; + } + else + { + return; + } + + var acceptsList = accepts ??= new List(); + acceptsList.Add(new AcceptsMetadata(requestType, contentType, additionalContentTypes)); + } + + private static void TryAddProducesMetadata( + AttributeData attribute, + INamedTypeSymbol attributeClass, + ref List? produces) + { + string? responseType = null; + int statusCode; + string? contentType; + EquatableImmutableArray? additionalContentTypes; + + if (attributeClass.IsGenericType && attributeClass.TypeArguments.Length == 1) + { + responseType = attributeClass.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesStatusCode + ? producesStatusCode + : 200; + contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) + : null; + additionalContentTypes = attribute.ConstructorArguments.Length > 2 + ? GetStringArrayValues(attribute.ConstructorArguments[2]) + : null; + } + else if (attribute.ConstructorArguments.Length >= 1 && + attribute.ConstructorArguments[0].Value is ITypeSymbol responseTypeSymbol) + { + responseType = responseTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + statusCode = attribute.ConstructorArguments.Length > 1 && attribute.ConstructorArguments[1].Value is int producesStatusCode + ? producesStatusCode + : 200; + contentType = attribute.ConstructorArguments.Length > 2 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[2].Value as string) + : null; + additionalContentTypes = attribute.ConstructorArguments.Length > 3 + ? GetStringArrayValues(attribute.ConstructorArguments[3]) + : null; + } + else + { + return; + } + + var producesList = produces ??= new List(); + producesList.Add(new ProducesMetadata(responseType, statusCode, contentType, additionalContentTypes)); + } + private static EquatableImmutableArray MergeUnion(EquatableImmutableArray? existing, IEnumerable values) { var list = new List(); @@ -995,6 +1495,64 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(')'); } + if (requestHandler.Metadata.Accepts is { Count: > 0 }) + { + foreach (var accepts in requestHandler.Metadata.Accepts.Value) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".Accepts<"); + source.Append(accepts.RequestType); + source.Append('>'); + source.Append('('); + source.Append(StringLiteral(accepts.ContentType)); + AppendAdditionalContentTypes(source, accepts.AdditionalContentTypes); + source.Append(')'); + } + } + + if (requestHandler.Metadata.Produces is { Count: > 0 }) + { + foreach (var produces in requestHandler.Metadata.Produces.Value) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".Produces<"); + source.Append(produces.ResponseType); + source.Append('>'); + source.Append('('); + source.Append(produces.StatusCode); + AppendOptionalContentTypes(source, produces.ContentType, produces.AdditionalContentTypes); + source.Append(')'); + } + } + + if (requestHandler.Metadata.ProducesProblem is { Count: > 0 }) + { + foreach (var producesProblem in requestHandler.Metadata.ProducesProblem.Value) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".ProducesProblem("); + source.Append(producesProblem.StatusCode); + AppendOptionalContentTypes(source, producesProblem.ContentType, producesProblem.AdditionalContentTypes); + source.Append(')'); + } + } + + if (requestHandler.Metadata.ProducesValidationProblem is { Count: > 0 }) + { + foreach (var producesValidationProblem in requestHandler.Metadata.ProducesValidationProblem.Value) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".ProducesValidationProblem("); + source.Append(producesValidationProblem.StatusCode); + AppendOptionalContentTypes(source, producesValidationProblem.ContentType, producesValidationProblem.AdditionalContentTypes); + source.Append(')'); + } + } + if (requestHandler.RequireAuthorization) { source.AppendLine(); @@ -1077,16 +1635,63 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< foreach (var p in parameters) cost += 12 + p.Type.Length + p.Name.Length; - var (name, summary, description, tags) = rh.Metadata; - if (name is { Length: > 0 }) - cost += 22 + name.Length; - if (summary is { Length: > 0 }) - cost += 24 + summary.Length; - if (description is { Length: > 0 }) - cost += 28 + description.Length; + var metadata = rh.Metadata; + if (metadata.Name is { Length: > 0 }) + cost += 22 + metadata.Name.Length; + if (metadata.Summary is { Length: > 0 }) + cost += 24 + metadata.Summary.Length; + if (metadata.Description is { Length: > 0 }) + cost += 28 + metadata.Description.Length; + + if (metadata.Tags is { Count: > 0 }) + cost += metadata.Tags.Value.Sum(tag => 6 + tag.Length); + + if (metadata.Accepts is { Count: > 0 }) + { + foreach (var accepts in metadata.Accepts.Value) + { + var additionalCost = accepts.AdditionalContentTypes is { Count: > 0 } + ? accepts.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) + : 0; + cost += 32 + accepts.RequestType.Length + accepts.ContentType.Length + additionalCost; + } + } + + if (metadata.Produces is { Count: > 0 }) + { + foreach (var produces in metadata.Produces.Value) + { + var additionalCost = produces.AdditionalContentTypes is { Count: > 0 } + ? produces.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) + : 0; + var contentTypeLength = produces.ContentType?.Length ?? 0; + cost += 40 + produces.ResponseType.Length + contentTypeLength + additionalCost; + } + } - if (tags is { Count: > 0 }) - cost += tags.Value.Sum(tag => 6 + tag.Length); + if (metadata.ProducesProblem is { Count: > 0 }) + { + foreach (var producesProblem in metadata.ProducesProblem.Value) + { + var additionalCost = producesProblem.AdditionalContentTypes is { Count: > 0 } + ? producesProblem.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) + : 0; + var contentTypeLength = producesProblem.ContentType?.Length ?? 0; + cost += 28 + contentTypeLength + additionalCost; + } + } + + if (metadata.ProducesValidationProblem is { Count: > 0 }) + { + foreach (var producesValidationProblem in metadata.ProducesValidationProblem.Value) + { + var additionalCost = producesValidationProblem.AdditionalContentTypes is { Count: > 0 } + ? producesValidationProblem.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) + : 0; + var contentTypeLength = producesValidationProblem.ContentType?.Length ?? 0; + cost += 32 + contentTypeLength + additionalCost; + } + } if (rh.RequireAuthorization) cost += 24; @@ -1220,6 +1825,28 @@ private static string StringLiteral(string? value) return sb.ToString(); } + private static void AppendAdditionalContentTypes(StringBuilder source, EquatableImmutableArray? additionalContentTypes) + { + if (additionalContentTypes is not { Count: > 0 }) + return; + + foreach (var additional in additionalContentTypes.Value) + { + source.Append(", "); + source.Append(StringLiteral(additional)); + } + } + + private static void AppendOptionalContentTypes(StringBuilder source, string? contentType, EquatableImmutableArray? additionalContentTypes) + { + if (string.IsNullOrEmpty(contentType) && additionalContentTypes is not { Count: > 0 }) + return; + + source.Append(", "); + source.Append(contentType is { Length: > 0 } ? StringLiteral(contentType) : "null"); + AppendAdditionalContentTypes(source, additionalContentTypes); + } + private static string EscapeChar(char c) { return c switch @@ -1256,7 +1883,29 @@ bool ConfigureMethodAcceptsServiceProvider private readonly record struct RequestHandlerMethod(string Name, bool IsStatic, bool IsAwaitable, EquatableImmutableArray Parameters); - private readonly record struct RequestHandlerMetadata(string? Name, string? Summary, string? Description, EquatableImmutableArray? Tags); + private readonly record struct RequestHandlerMetadata( + string? Name, + string? Summary, + string? Description, + EquatableImmutableArray? Tags, + EquatableImmutableArray? Accepts, + EquatableImmutableArray? Produces, + EquatableImmutableArray? ProducesProblem, + EquatableImmutableArray? ProducesValidationProblem + ); + + private readonly record struct AcceptsMetadata(string RequestType, string ContentType, EquatableImmutableArray? AdditionalContentTypes); + + private readonly record struct ProducesMetadata( + string ResponseType, + int StatusCode, + string? ContentType, + EquatableImmutableArray? AdditionalContentTypes + ); + + private readonly record struct ProducesProblemMetadata(int StatusCode, string? ContentType, EquatableImmutableArray? AdditionalContentTypes); + + private readonly record struct ProducesValidationProblemMetadata(int StatusCode, string? ContentType, EquatableImmutableArray? AdditionalContentTypes); private readonly record struct Parameter(string Name, string Type, BindingSource Source, string? Key); From 1e97f839fadb868049bdee2ca598aab445a2eca2 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:07:45 -0500 Subject: [PATCH 07/75] Expand lab GetUser endpoint metadata (#10) --- .../GetUserEndpoint.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index 25dbbd0..837e45b 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -1,19 +1,48 @@ -using Microsoft.AspNetCore.Builder; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; namespace GeneratedEndpoints.Tests.Lab; +[Tags("Users", "Profiles")] +[RequireAuthorization("Users.Read", "Administrators")] +[DisableAntiforgery] internal static class GetUserEndpoint { + [Tags("Featured")] + [AllowAnonymous] + [Accepts(typeof(GetUserRequest), "application/json", "application/xml")] + [Accepts("application/json", "application/xml")] + [Produces(typeof(UserProfile), StatusCodes.Status200OK, "application/json")] + [Produces(StatusCodes.Status202Accepted, "application/json")] + [ProducesProblem(StatusCodes.Status500InternalServerError, "application/problem+json")] + [ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")] [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] - public static Results GetUser(int id) + public static Results, NotFound, ValidationProblem, ProblemHttpResult> GetUser(int id) { - if (id > 0) - return TypedResults.Ok(); + if (id <= 0) + { + var errors = new Dictionary + { + [nameof(id)] = ["The ID must be greater than zero."] + }; + return TypedResults.ValidationProblem(errors); + } - return TypedResults.NotFound(); + if (id == 13) + { + return TypedResults.Problem("User data is temporarily unavailable."); + } + + if (id == 404) + { + return TypedResults.NotFound(); + } + + var profile = new UserProfile(id, $"User {id}", $"user{id}@example.com"); + return TypedResults.Ok(profile); } public static void Configure(TBuilder builder) @@ -21,3 +50,9 @@ public static void Configure(TBuilder builder) { } } + +internal sealed record GetUserRequest(int Id); + +internal sealed record GetUserMetadata(string RequestedBy, string Purpose); + +internal sealed record UserProfile(int Id, string DisplayName, string Email); From 778c8131e5aed4dc4d713aed4c6111c6cfaa13d0 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:09:27 -0500 Subject: [PATCH 08/75] docs: expand README coverage (#11) --- README.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 66eadaa..a1febe1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,21 @@ GeneratedEndpoints is a .NET source generator that automatically wires up Minima methods. This simplifies integration of HTTP handlers within Clean Architecture (CA) or Vertical Slice Architecture (VSA) by keeping endpoint definitions inside their features while generating the boilerplate mapping code. +## Capabilities at a glance + +* **Attribute-driven routing** – decorate a method with `[MapGet]`, `[MapPost]`, etc. (including OPTIONS, HEAD, TRACE, CONNECT, + and the Minimal API-specific `QUERY` verb) and the generator maps it automatically. +* **Static or instance handlers** – declare handlers in static classes or as transient services that participate in dependency + injection. +* **Metadata composition** – mix class-level and method-level attributes for tags, authorization requirements, content + negotiation, and antiforgery/anonymous settings. The generator merges everything into the produced endpoint builder. +* **Rich request/response contracts** – describe the shape of your API surface with `[Accepts]`, `[Produces]`, `[ProducesProblem]`, + and `[ProducesValidationProblem]` so OpenAPI and client tooling stay accurate. +* **Minimal boilerplate** – `AddEndpointHandlers` auto-registers instance handlers with DI, and `MapEndpointHandlers` + registers every attribute-decorated method. +* **Optional per-feature customization** – provide a `Configure` method in your feature to add filters, OpenAPI metadata, or any + other conventions using the generated `IEndpointConventionBuilder`. + [![develop](https://img.shields.io/github/actions/workflow/status/jscarle/GeneratedEndpoints/develop.yml?logo=github)](https://github.com/jscarle/GeneratedEndpoints) [![nuget](https://img.shields.io/nuget/v/GeneratedEndpoints)](https://www.nuget.org/packages/GeneratedEndpoints) [![downloads](https://img.shields.io/nuget/dt/GeneratedEndpoints)](https://www.nuget.org/packages/GeneratedEndpoints) @@ -60,7 +75,7 @@ Key points: * Use `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapQuery]`, `[MapTrace]`, or `[MapConnect]` to describe the HTTP verb and route pattern. * Optional `Name`, `Summary`, and `Description` named parameters populate the generated `.WithName`, `.WithSummary`, and `.WithDescription` metadata calls. When omitted, the generator derives the endpoint name from the method name (stripping a trailing `Async`). -* Apply standard ASP.NET Core parameter binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromServices]`, `[AsParameters]`, etc.). The generator mirrors them onto the produced delegate so binding behaves exactly as declared. +* Apply standard ASP.NET Core parameter binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator mirrors them onto the produced delegate so binding behaves exactly as declared. * Annotate the **class**, an individual **method**, or both with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, or `[AllowAnonymous]`. Class-level metadata is merged onto every generated endpoint, while method-level attributes can refine or augment the settings for a specific handler. `[AllowAnonymous]` lets a method opt out of authorization even if the enclosing class (or other conventions) require authenticated access. * Non-static handler classes are automatically registered with dependency injection (as transient services). Their instance methods receive a scoped instance resolved from DI, while static methods continue to behave like any other static helper. @@ -103,7 +118,7 @@ app.MapEndpointHandlers(); app.Run(); ``` -`AddEndpointHandlers` ensures any non-static handler types can be resolved from dependency injection, while `MapEndpointHandlers` generates Minimal API route mappings for every annotated method in the application. +`AddEndpointHandlers` ensures any non-static handler types can be resolved from dependency injection, while `MapEndpointHandlers` generates Minimal API route mappings for every annotated method in the application. Both extension methods reside in the `Microsoft.AspNetCore.Generated.Routing` namespace. ### 3. Compose additional endpoints @@ -201,3 +216,61 @@ public sealed class CreateTodo The method is only generated once per handler class, so any conventions you add will automatically flow to all endpoints defined within that class. +### 5. Describe contracts with `Accepts` and `Produces` + +GeneratedEndpoints ships with helper attributes for request and response metadata. Apply them to either a handler class or +individual methods to keep your OpenAPI description in sync with the implementation. Attributes on the class are merged into +each method, while method-level attributes can augment or override the defaults. + +```csharp +using Microsoft.AspNetCore.Generated.Attributes; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace Todos.Features; + +[Accepts("application/json", "application/xml")] +[Produces(StatusCodes.Status201Created)] +[ProducesProblem(StatusCodes.Status500InternalServerError)] +public sealed class CreateTodo +{ + [MapPost("/todos", Summary = "Create a todo")] + [ProducesValidationProblem(StatusCodes.Status400BadRequest)] + public static Created Handle([FromBody] CreateTodoRequest request) + => TypedResults.Created($"/todos/{request.Id}", request.ToTodo()); +} +``` + +The generator translates these attributes into `.Accepts`, `.Produces`, `.ProducesProblem`, and `.ProducesValidationProblem` +calls on the endpoint builder. + +### Attribute reference + +| Attribute | Scope | Purpose | +| --- | --- | --- | +| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments (`Name`, `Summary`, `Description`) fill the generated `.WithName`, `.WithSummary`, and `.WithDescription` calls. | +| `[Tags]` | Class or method | Adds tags to one or more endpoints. Multiple attributes merge without duplication. | +| `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Pass an array of policy names to enforce specific policies; when omitted the standard authorization middleware is applied. | +| `[AllowAnonymous]` | Class or method | Explicitly opts a method (or all methods in a class) into anonymous access, overriding `[RequireAuthorization]`. | +| `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint, matching the ASP.NET Core extension. | +| `[Accepts]` / `[Accepts]` | Class or method | Emits `.Accepts(contentType, additionalContentTypes...)` to document supported request bodies. Multiple attributes are allowed per endpoint. | +| `[Produces]` / `[Produces]` | Class or method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. Multiple attributes are allowed. | +| `[ProducesProblem]` | Class or method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. | +| `[ProducesValidationProblem]` | Class or method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)` when validation failures are returned. | + +> ℹ️ All metadata attributes defined on a class are applied to every annotated method inside the class. Method-level attributes +> can add additional entries (for tags, accepts, produces, etc.) or override booleans such as `[AllowAnonymous]` and +> `[DisableAntiforgery]`. + +### Authorization and security conventions + +* **Default authorization** – `[RequireAuthorization]` adds `.RequireAuthorization()` to every endpoint. Supplying policies + (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) generates `.RequireAuthorization("Todos.Read", "Todos.Write")`. +* **Allow anonymous** – `[AllowAnonymous]` on a class or method maps to `.AllowAnonymous()`, even when authorization is required elsewhere. +* **Antiforgery** – `[DisableAntiforgery]` wires through `.DisableAntiforgery()`. + +### Tips for handler classes + +* Non-static handler classes are automatically registered as transient services when you call `builder.Services.AddEndpointHandlers();`. +* You can mix static and instance methods within the same class. Instance methods receive the injected handler instance; static methods work like regular static helpers and can continue to rely on `[FromServices]` for dependencies. +* Use the optional `Configure` method for per-feature conventions. Its optional `IServiceProvider` parameter lets you resolve scoped services when adding endpoint filters or other runtime configuration. + From b7628f6c46723fb76d342e4c16eb8147fea8de90 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:10:16 -0500 Subject: [PATCH 09/75] Add comprehensive generator test coverage (#12) --- ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 71 ++++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 71 ++++++++++ .../GeneratedEndpointsTests.cs | 128 ++++++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..b3b3ebf --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddTransient(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..0cf3211 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddTransient(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..a70d5e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/complex", new[] { "CONNECT" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.ConnectComplex) + .WithName("ConnectComplex"); + + builder.MapPost("/complex", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.CreateComplexAsync) + .WithName("CreateComplex"); + + builder.MapDelete("/complex/{id:int}", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.DeleteComplex) + .WithName("DeleteComplex"); + + builder.MapMethods("/complex", new[] { "OPTIONS" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.DescribeComplex) + .WithName("DescribeComplex"); + + builder.MapMethods("/complex", new[] { "HEAD" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.HeadComplex) + .WithName("HeadComplex"); + + builder.MapPatch("/complex/{id:int}", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.PatchComplex) + .WithName("PatchComplex"); + + builder.MapMethods("/complex/query", new[] { "QUERY" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.QueryComplex) + .WithName("QueryComplex"); + + builder.MapMethods("/complex", new[] { "TRACE" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.TraceComplex) + .WithName("TraceComplex"); + + builder.MapPut("/complex/{id:int}", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.UpdateComplex) + .WithName("UpdateComplex"); + + builder.MapGet("/complex/{id:int}", static async ([FromServices] global::GeneratedEndpointsTests.ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader] string traceId, [FromBody] global::GeneratedEndpointsTests.GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, object keyed, [AsParameters] global::GeneratedEndpointsTests.AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) + .WithName("GetComplex") + .WithSummary("Gets complex data.") + .WithDescription("Uses every supported attribute.") + .WithTags("Shared", "ClassLevel", "MethodLevel") + .Accepts("application/xml", "text/xml") + .Accepts("application/custom", "text/custom") + .Produces(201, "application/json", "text/json") + .Produces(200, "application/json", "text/json") + .ProducesProblem(503, "application/problem+json") + .ProducesProblem(400, "application/problem+json", "text/plain") + .ProducesValidationProblem(409, "application/problem+json", "text/plain") + .ProducesValidationProblem(422, "application/problem+json", "text/plain") + .RequireAuthorization("PolicyA", "PolicyB", "MethodPolicy") + .DisableAntiforgery() + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..bcddd80 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/complex", new[] { "CONNECT" }, global::AllHttpMethodEndpoints.ConnectComplex) + .WithName("ConnectComplex"); + + builder.MapPost("/complex", global::AllHttpMethodEndpoints.CreateComplexAsync) + .WithName("CreateComplex"); + + builder.MapDelete("/complex/{id:int}", global::AllHttpMethodEndpoints.DeleteComplex) + .WithName("DeleteComplex"); + + builder.MapMethods("/complex", new[] { "OPTIONS" }, global::AllHttpMethodEndpoints.DescribeComplex) + .WithName("DescribeComplex"); + + builder.MapMethods("/complex", new[] { "HEAD" }, global::AllHttpMethodEndpoints.HeadComplex) + .WithName("HeadComplex"); + + builder.MapPatch("/complex/{id:int}", global::AllHttpMethodEndpoints.PatchComplex) + .WithName("PatchComplex"); + + builder.MapMethods("/complex/query", new[] { "QUERY" }, global::AllHttpMethodEndpoints.QueryComplex) + .WithName("QueryComplex"); + + builder.MapMethods("/complex", new[] { "TRACE" }, global::AllHttpMethodEndpoints.TraceComplex) + .WithName("TraceComplex"); + + builder.MapPut("/complex/{id:int}", global::AllHttpMethodEndpoints.UpdateComplex) + .WithName("UpdateComplex"); + + builder.MapGet("/complex/{id:int}", static async ([FromServices] global::ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader] string traceId, [FromBody] global::GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, object keyed, [AsParameters] global::AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) + .WithName("GetComplex") + .WithSummary("Gets complex data.") + .WithDescription("Uses every supported attribute.") + .WithTags("Shared", "ClassLevel", "MethodLevel") + .Accepts("application/xml", "text/xml") + .Accepts("application/custom", "text/custom") + .Produces(201, "application/json", "text/json") + .Produces(200, "application/json", "text/json") + .ProducesProblem(503, "application/problem+json") + .ProducesProblem(400, "application/problem+json", "text/plain") + .ProducesValidationProblem(409, "application/problem+json", "text/plain") + .ProducesValidationProblem(422, "application/problem+json", "text/plain") + .RequireAuthorization("PolicyA", "PolicyB", "MethodPolicy") + .DisableAntiforgery() + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index e314264..3519c42 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -98,4 +98,132 @@ public static void Configure(TBuilder builder, System.IServiceProvider await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(MapGetWithConfigureServiceProvider)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapAllAttributesAndHttpMethods(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + using System.Threading; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.DependencyInjection; + + [Tags("Shared", "ClassLevel")] + [RequireAuthorization("PolicyA", "PolicyB")] + [DisableAntiforgery] + [Accepts(typeof(ClassLevelRequest), "application/xml", "text/xml")] + [Microsoft.AspNetCore.Generated.Attributes.Produces(typeof(ClassLevelResponse), 201, "application/json", "text/json")] + [ProducesProblem(503, "application/problem+json")] + [ProducesValidationProblem(409, "application/problem+json", "text/plain")] + internal sealed class ComplexEndpoints + { + private readonly IServiceProvider _serviceProvider; + + public ComplexEndpoints(IServiceProvider serviceProvider) + => _serviceProvider = serviceProvider; + + public static void Configure(TBuilder builder, IServiceProvider serviceProvider) + where TBuilder : IEndpointConventionBuilder + { + _ = serviceProvider; + builder.WithMetadata("configured"); + } + + [MapGet("/complex/{id:int}", Name = nameof(GetComplex), Summary = "Gets complex data.", Description = "Uses every supported attribute.")] + [AllowAnonymous] + [Tags("MethodLevel")] + [RequireAuthorization("MethodPolicy")] + [Accepts("application/custom", "text/custom")] + [Microsoft.AspNetCore.Generated.Attributes.Produces(200, "application/json", "text/json")] + [ProducesProblem(400, "application/problem+json", "text/plain")] + [ProducesValidationProblem(422, "application/problem+json", "text/plain")] + public async Task, NotFound>> GetComplex( + [FromRoute] int id, + [FromQuery] string? filter, + [FromHeader(Name = "x-trace-id")] string? traceId, + [FromBody] GetRequest request, + [FromForm] string? formValue, + [FromServices] IServiceProvider services, + [FromKeyedServices("special")] object keyed, + [AsParameters] AdditionalParameters parameters, + CancellationToken cancellationToken) + { + _ = _serviceProvider; + _ = traceId; + _ = formValue; + _ = services; + _ = keyed; + _ = parameters; + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + return TypedResults.Ok(new GetResponse(id)); + } + } + + internal sealed record AdditionalParameters(string? Search, int? Page); + + internal sealed record ClassLevelRequest(int Value); + + internal sealed record ClassLevelResponse(int Value); + + internal sealed record GetRequest(int Value); + + internal sealed record GetResponse(int Value); + + internal static class AllHttpMethodEndpoints + { + [MapPost("/complex")] + public static async Task> CreateComplexAsync([FromBody] GetRequest request) + { + await Task.Yield(); + return TypedResults.Created($"/complex/{request.Value}", new GetResponse(request.Value)); + } + + [MapPut("/complex/{id:int}")] + public static Results UpdateComplex(int id) + => id > 0 ? TypedResults.NoContent() : TypedResults.NotFound(); + + [MapDelete("/complex/{id:int}")] + public static IResult DeleteComplex(int id) + => id > 0 ? TypedResults.Ok() : TypedResults.NotFound(); + + [MapOptions("/complex")] + public static IResult DescribeComplex() + => TypedResults.Ok(); + + [MapHead("/complex")] + public static IResult HeadComplex() + => TypedResults.Ok(); + + [MapPatch("/complex/{id:int}")] + public static IResult PatchComplex(int id) + => TypedResults.Ok(); + + [MapQuery("/complex/query")] + public static IResult QueryComplex([FromQuery] string term) + => TypedResults.Ok(term); + + [MapTrace("/complex")] + public static IResult TraceComplex() + => TypedResults.Ok(); + + [MapConnect("/complex")] + public static IResult ConnectComplex() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapAllAttributesAndHttpMethods)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapAllAttributesAndHttpMethods)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } } From b87a36bfa260be4ab8c10bd2194820aca1ac36f6 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:22:59 -0500 Subject: [PATCH 10/75] Fixed scope. --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 2 +- tests/GeneratedEndpoints.Tests.Lab/Program.cs | 5 +++++ ...ttpMethods_AddEndpointHandlers_WithNamespace.verified.txt | 2 +- ...Methods_AddEndpointHandlers_WithoutNamespace.verified.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 86bd414..129f1f2 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1305,7 +1305,7 @@ private static void GenerateAddEndpointHandlersClass(SourceProductionContext con .Select(x => x.Class.Name) .Distinct()) { - source.Append(" services.TryAddTransient<"); + source.Append(" services.TryAddScoped<"); source.Append(className); source.Append(">();"); source.AppendLine(); diff --git a/tests/GeneratedEndpoints.Tests.Lab/Program.cs b/tests/GeneratedEndpoints.Tests.Lab/Program.cs index 2265869..fc11c97 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/Program.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/Program.cs @@ -1,7 +1,12 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Generated.Routing; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddEndpointHandlers(); + var app = builder.Build(); +app.MapEndpointHandlers(); + app.Run(); diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt index b3b3ebf..50c3a2f 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt @@ -19,6 +19,6 @@ internal static class EndpointServicesExtensions { internal static void AddEndpointHandlers(this IServiceCollection services) { - services.TryAddTransient(); + services.TryAddScoped(); } } diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt index 0cf3211..06b6e7e 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -19,6 +19,6 @@ internal static class EndpointServicesExtensions { internal static void AddEndpointHandlers(this IServiceCollection services) { - services.TryAddTransient(); + services.TryAddScoped(); } } From be9a3657c45b53b0fab0979475509e2307f263a5 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:44:10 -0500 Subject: [PATCH 11/75] Use built-in AllowAnonymous attribute (#13) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 21 +------------------ .../GetUserEndpoint.cs | 1 + .../Common/TestHelpers.cs | 1 + 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 129f1f2..9103f6f 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -67,9 +67,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; - private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; - private const string AllowAnonymousAttributeFullyQualifiedName = $"{AttributesNamespace}.{AllowAnonymousAttributeName}"; - private const string AllowAnonymousAttributeHint = $"{AllowAnonymousAttributeFullyQualifiedName}.gs.cs"; + private const string AllowAnonymousAttributeFullyQualifiedName = "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute"; private const string AcceptsAttributeName = "AcceptsAttribute"; private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}"; @@ -267,23 +265,6 @@ internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attri """; context.AddSource(DisableAntiforgeryAttributeHint, SourceText.From(disableAntiforgerySource, Encoding.UTF8)); - // AllowAnonymous - var allowAnonymousSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Allows the annotated endpoint or class to bypass authorization requirements. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{AllowAnonymousAttributeName}} : global::System.Attribute - { - } - - """; - context.AddSource(AllowAnonymousAttributeHint, SourceText.From(allowAnonymousSource, Encoding.UTF8)); - // Accepts var acceptsSource = $$""" {{FileHeader}} diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index 837e45b..62957ad 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http; diff --git a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs index fcb90d6..d1a2803 100644 --- a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs +++ b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs @@ -18,6 +18,7 @@ public static GeneratorDriverRunResult RunGenerator(IEnumerable sources) public static IEnumerable GetSources(string source, bool withNamespace) { const string usingStatements = """ + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; From 1a3fd3a9ec38fcb6a88b50d8944eb5deb52cd276 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:47:12 -0500 Subject: [PATCH 12/75] Rename Produces attribute to ProducesResponse (#14) --- README.md | 8 ++++---- src/GeneratedEndpoints/MinimalApiGenerator.cs | 20 +++++++++---------- .../GetUserEndpoint.cs | 4 ++-- .../GeneratedEndpointsTests.cs | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a1febe1..a47deaf 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ by keeping endpoint definitions inside their features while generating the boile injection. * **Metadata composition** – mix class-level and method-level attributes for tags, authorization requirements, content negotiation, and antiforgery/anonymous settings. The generator merges everything into the produced endpoint builder. -* **Rich request/response contracts** – describe the shape of your API surface with `[Accepts]`, `[Produces]`, `[ProducesProblem]`, +* **Rich request/response contracts** – describe the shape of your API surface with `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` so OpenAPI and client tooling stay accurate. * **Minimal boilerplate** – `AddEndpointHandlers` auto-registers instance handlers with DI, and `MapEndpointHandlers` registers every attribute-decorated method. @@ -216,7 +216,7 @@ public sealed class CreateTodo The method is only generated once per handler class, so any conventions you add will automatically flow to all endpoints defined within that class. -### 5. Describe contracts with `Accepts` and `Produces` +### 5. Describe contracts with `Accepts` and `ProducesResponse` GeneratedEndpoints ships with helper attributes for request and response metadata. Apply them to either a handler class or individual methods to keep your OpenAPI description in sync with the implementation. Attributes on the class are merged into @@ -229,7 +229,7 @@ using Microsoft.AspNetCore.Http.HttpResults; namespace Todos.Features; [Accepts("application/json", "application/xml")] -[Produces(StatusCodes.Status201Created)] +[ProducesResponse(StatusCodes.Status201Created)] [ProducesProblem(StatusCodes.Status500InternalServerError)] public sealed class CreateTodo { @@ -253,7 +253,7 @@ calls on the endpoint builder. | `[AllowAnonymous]` | Class or method | Explicitly opts a method (or all methods in a class) into anonymous access, overriding `[RequireAuthorization]`. | | `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint, matching the ASP.NET Core extension. | | `[Accepts]` / `[Accepts]` | Class or method | Emits `.Accepts(contentType, additionalContentTypes...)` to document supported request bodies. Multiple attributes are allowed per endpoint. | -| `[Produces]` / `[Produces]` | Class or method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. Multiple attributes are allowed. | +| `[ProducesResponse]` / `[ProducesResponse]` | Class or method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. Multiple attributes are allowed. | | `[ProducesProblem]` | Class or method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. | | `[ProducesValidationProblem]` | Class or method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)` when validation failures are returned. | diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 9103f6f..c869cfc 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -73,9 +73,9 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}"; private const string AcceptsAttributeHint = $"{AcceptsAttributeFullyQualifiedName}.gs.cs"; - private const string ProducesAttributeName = "ProducesAttribute"; - private const string ProducesAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesAttributeName}"; - private const string ProducesAttributeHint = $"{ProducesAttributeFullyQualifiedName}.gs.cs"; + private const string ProducesResponseAttributeName = "ProducesResponseAttribute"; + private const string ProducesResponseAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesResponseAttributeName}"; + private const string ProducesResponseAttributeHint = $"{ProducesResponseAttributeFullyQualifiedName}.gs.cs"; private const string ProducesProblemAttributeName = "ProducesProblemAttribute"; private const string ProducesProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesProblemAttributeName}"; @@ -353,7 +353,7 @@ namespace {{AttributesNamespace}}; /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. /// [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesAttributeName}} : global::System.Attribute + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute { /// /// Gets the response type produced by the endpoint. @@ -376,13 +376,13 @@ internal sealed class {{ProducesAttributeName}} : global::System.Attribute public string[] AdditionalContentTypes { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The CLR type of the response body. /// The HTTP status code returned by the endpoint. /// The primary content type produced by the endpoint. /// Additional content types produced by the endpoint. - public {{ProducesAttributeName}}(global::System.Type responseType, int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) + public {{ProducesResponseAttributeName}}(global::System.Type responseType, int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) { ResponseType = responseType ?? throw new global::System.ArgumentNullException(nameof(responseType)); StatusCode = statusCode; @@ -396,7 +396,7 @@ internal sealed class {{ProducesAttributeName}} : global::System.Attribute /// /// The CLR type of the response body. [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesAttributeName}} : global::System.Attribute + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute { /// /// Gets the response type produced by the endpoint. @@ -424,7 +424,7 @@ internal sealed class {{ProducesAttributeName}} : global::System.Attr /// The HTTP status code returned by the endpoint. /// The primary content type produced by the endpoint. /// Additional content types produced by the endpoint. - public {{ProducesAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) + public {{ProducesResponseAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) { StatusCode = statusCode; ContentType = contentType; @@ -433,7 +433,7 @@ internal sealed class {{ProducesAttributeName}} : global::System.Attr } """; - context.AddSource(ProducesAttributeHint, SourceText.From(producesSource, Encoding.UTF8)); + context.AddSource(ProducesResponseAttributeHint, SourceText.From(producesSource, Encoding.UTF8)); // ProducesProblem var producesProblemSource = $$""" @@ -776,7 +776,7 @@ ref List? producesValidationProblem continue; } - if (IsGeneratedAttribute(fullyQualifiedName, ProducesAttributeName)) + if (IsGeneratedAttribute(fullyQualifiedName, ProducesResponseAttributeName)) { TryAddProducesMetadata(attribute, attributeClass, ref produces); continue; diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index 62957ad..1aeea98 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -16,8 +16,8 @@ internal static class GetUserEndpoint [AllowAnonymous] [Accepts(typeof(GetUserRequest), "application/json", "application/xml")] [Accepts("application/json", "application/xml")] - [Produces(typeof(UserProfile), StatusCodes.Status200OK, "application/json")] - [Produces(StatusCodes.Status202Accepted, "application/json")] + [ProducesResponse(typeof(UserProfile), StatusCodes.Status200OK, "application/json")] + [ProducesResponse(StatusCodes.Status202Accepted, "application/json")] [ProducesProblem(StatusCodes.Status500InternalServerError, "application/problem+json")] [ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")] [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 3519c42..2c3b70e 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -116,7 +116,7 @@ public async Task MapAllAttributesAndHttpMethods(bool withNamespace) [RequireAuthorization("PolicyA", "PolicyB")] [DisableAntiforgery] [Accepts(typeof(ClassLevelRequest), "application/xml", "text/xml")] - [Microsoft.AspNetCore.Generated.Attributes.Produces(typeof(ClassLevelResponse), 201, "application/json", "text/json")] + [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(typeof(ClassLevelResponse), 201, "application/json", "text/json")] [ProducesProblem(503, "application/problem+json")] [ProducesValidationProblem(409, "application/problem+json", "text/plain")] internal sealed class ComplexEndpoints @@ -138,7 +138,7 @@ public static void Configure(TBuilder builder, IServiceProvider servic [Tags("MethodLevel")] [RequireAuthorization("MethodPolicy")] [Accepts("application/custom", "text/custom")] - [Microsoft.AspNetCore.Generated.Attributes.Produces(200, "application/json", "text/json")] + [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] [ProducesProblem(400, "application/problem+json", "text/plain")] [ProducesValidationProblem(422, "application/problem+json", "text/plain")] public async Task, NotFound>> GetComplex( From 6c701adf6ea74b0ba03cdf987b6b02f789e931e7 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:52:47 -0500 Subject: [PATCH 13/75] Propagate exclude from description metadata (#15) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 28 ++++++++++++++++--- ...ndpointHandlers_WithNamespace.verified.txt | 1 + ...ointHandlers_WithoutNamespace.verified.txt | 1 + .../GeneratedEndpointsTests.cs | 2 ++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index c869cfc..6cd6336 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -594,8 +594,8 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (httpMethod, pattern, name, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); - var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, accepts, produces, - producesProblem, producesValidationProblem) + var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, + accepts, produces, producesProblem, producesValidationProblem) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -608,7 +608,8 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke accepts, produces, producesProblem, - producesValidationProblem + producesValidationProblem, + excludeFromDescription ); var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, @@ -689,6 +690,7 @@ private static ( EquatableImmutableArray? authorizationPolicies, bool disableAntiforgery, bool allowAnonymous, + bool excludeFromDescription, EquatableImmutableArray? accepts, EquatableImmutableArray? produces, EquatableImmutableArray? producesProblem, @@ -702,6 +704,7 @@ private static ( EquatableImmutableArray? authorizationPolicies = null; var disableAntiforgery = false; var allowAnonymous = false; + var excludeFromDescription = false; List? accepts = null; List? produces = null; @@ -716,6 +719,7 @@ private static ( ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous, + ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, @@ -730,6 +734,7 @@ ref producesValidationProblem ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous, + ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, @@ -742,6 +747,7 @@ ref producesValidationProblem authorizationPolicies, disableAntiforgery, allowAnonymous, + excludeFromDescription, ToEquatableOrNull(accepts), ToEquatableOrNull(produces), ToEquatableOrNull(producesProblem), @@ -756,6 +762,7 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref EquatableImmutableArray? authorizationPolicies, ref bool disableAntiforgery, ref bool allowAnonymous, + ref bool excludeFromDescription, ref List? accepts, ref List? produces, ref List? producesProblem, @@ -821,6 +828,9 @@ ref List? producesValidationProblem case $"global::{AllowAnonymousAttributeFullyQualifiedName}": allowAnonymous = true; break; + case "global::Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute": + excludeFromDescription = true; + break; case $"global::{ProducesProblemAttributeFullyQualifiedName}": { var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode @@ -1467,6 +1477,13 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(')'); } + if (requestHandler.Metadata.ExcludeFromDescription) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".ExcludeFromDescription()"); + } + if (requestHandler.Metadata.Tags is { Count: > 0 }) { source.AppendLine(); @@ -1623,6 +1640,8 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< cost += 24 + metadata.Summary.Length; if (metadata.Description is { Length: > 0 }) cost += 28 + metadata.Description.Length; + if (metadata.ExcludeFromDescription) + cost += 32; if (metadata.Tags is { Count: > 0 }) cost += metadata.Tags.Value.Sum(tag => 6 + tag.Length); @@ -1872,7 +1891,8 @@ private readonly record struct RequestHandlerMetadata( EquatableImmutableArray? Accepts, EquatableImmutableArray? Produces, EquatableImmutableArray? ProducesProblem, - EquatableImmutableArray? ProducesValidationProblem + EquatableImmutableArray? ProducesValidationProblem, + bool ExcludeFromDescription ); private readonly record struct AcceptsMetadata(string RequestType, string ContentType, EquatableImmutableArray? AdditionalContentTypes); diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt index a70d5e9..42d7e07 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt @@ -53,6 +53,7 @@ internal static class EndpointRouteBuilderExtensions .WithName("GetComplex") .WithSummary("Gets complex data.") .WithDescription("Uses every supported attribute.") + .ExcludeFromDescription() .WithTags("Shared", "ClassLevel", "MethodLevel") .Accepts("application/xml", "text/xml") .Accepts("application/custom", "text/custom") diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt index bcddd80..44f2ee6 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -53,6 +53,7 @@ internal static class EndpointRouteBuilderExtensions .WithName("GetComplex") .WithSummary("Gets complex data.") .WithDescription("Uses every supported attribute.") + .ExcludeFromDescription() .WithTags("Shared", "ClassLevel", "MethodLevel") .Accepts("application/xml", "text/xml") .Accepts("application/custom", "text/custom") diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 2c3b70e..3dbe6e2 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -119,6 +119,7 @@ public async Task MapAllAttributesAndHttpMethods(bool withNamespace) [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(typeof(ClassLevelResponse), 201, "application/json", "text/json")] [ProducesProblem(503, "application/problem+json")] [ProducesValidationProblem(409, "application/problem+json", "text/plain")] + [Microsoft.AspNetCore.Routing.ExcludeFromDescription] internal sealed class ComplexEndpoints { private readonly IServiceProvider _serviceProvider; @@ -141,6 +142,7 @@ public static void Configure(TBuilder builder, IServiceProvider servic [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] [ProducesProblem(400, "application/problem+json", "text/plain")] [ProducesValidationProblem(422, "application/problem+json", "text/plain")] + [Microsoft.AspNetCore.Routing.ExcludeFromDescription] public async Task, NotFound>> GetComplex( [FromRoute] int id, [FromQuery] string? filter, From b0d07627ddd580ef1965622e301a4403cf8c3098 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:55:23 -0500 Subject: [PATCH 14/75] Fixed namespace. --- tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs | 1 + .../GeneratedEndpoints.Tests.csproj | 6 ++++++ tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs index d1a2803..3e367d5 100644 --- a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs +++ b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs @@ -22,6 +22,7 @@ public static IEnumerable GetSources(string source, bool withNamespace) using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; + using Microsoft.AspNetCore.Routing; """; if (withNamespace) diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj index c43a904..54af1ee 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj @@ -38,4 +38,10 @@ + + + GeneratedEndpointsTests.cs + + + diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 3dbe6e2..8f94504 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -119,7 +119,7 @@ public async Task MapAllAttributesAndHttpMethods(bool withNamespace) [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(typeof(ClassLevelResponse), 201, "application/json", "text/json")] [ProducesProblem(503, "application/problem+json")] [ProducesValidationProblem(409, "application/problem+json", "text/plain")] - [Microsoft.AspNetCore.Routing.ExcludeFromDescription] + [ExcludeFromDescription] internal sealed class ComplexEndpoints { private readonly IServiceProvider _serviceProvider; @@ -142,7 +142,7 @@ public static void Configure(TBuilder builder, IServiceProvider servic [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] [ProducesProblem(400, "application/problem+json", "text/plain")] [ProducesValidationProblem(422, "application/problem+json", "text/plain")] - [Microsoft.AspNetCore.Routing.ExcludeFromDescription] + [ExcludeFromDescription] public async Task, NotFound>> GetComplex( [FromRoute] int id, [FromQuery] string? filter, From 1dfa19656995406c2e664a61c77b9b8f2643369e Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:02:38 -0500 Subject: [PATCH 15/75] Allow specifying produces response type via named parameter (#16) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 6cd6336..7636570 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -58,6 +58,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string NameAttributeNamedParameter = "Name"; private const string SummaryAttributeNamedParameter = "Summary"; private const string DescriptionAttributeNamedParameter = "Description"; + private const string ResponseTypeAttributeNamedParameter = "ResponseType"; private const string RequireAuthorizationAttributeName = "RequireAuthorizationAttribute"; private const string RequireAuthorizationAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireAuthorizationAttributeName}"; @@ -355,10 +356,10 @@ namespace {{AttributesNamespace}}; [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type ResponseType { get; } + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType { get; init; } = default!; /// /// Gets the HTTP status code returned by the endpoint. @@ -378,13 +379,11 @@ internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribu /// /// Initializes a new instance of the class. /// - /// The CLR type of the response body. /// The HTTP status code returned by the endpoint. /// The primary content type produced by the endpoint. /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(global::System.Type responseType, int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) + public {{ProducesResponseAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) { - ResponseType = responseType ?? throw new global::System.ArgumentNullException(nameof(responseType)); StatusCode = statusCode; ContentType = contentType; AdditionalContentTypes = additionalContentTypes ?? []; @@ -971,18 +970,17 @@ private static void TryAddProducesMetadata( ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; } - else if (attribute.ConstructorArguments.Length >= 1 && - attribute.ConstructorArguments[0].Value is ITypeSymbol responseTypeSymbol) + else if (GetNamedTypeSymbol(attribute, ResponseTypeAttributeNamedParameter) is { } responseTypeSymbol) { responseType = responseTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - statusCode = attribute.ConstructorArguments.Length > 1 && attribute.ConstructorArguments[1].Value is int producesStatusCode + statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesStatusCode ? producesStatusCode : 200; - contentType = attribute.ConstructorArguments.Length > 2 - ? NormalizeOptionalContentType(attribute.ConstructorArguments[2].Value as string) + contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) : null; - additionalContentTypes = attribute.ConstructorArguments.Length > 3 - ? GetStringArrayValues(attribute.ConstructorArguments[3]) + additionalContentTypes = attribute.ConstructorArguments.Length > 2 + ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; } else @@ -994,6 +992,17 @@ private static void TryAddProducesMetadata( producesList.Add(new ProducesMetadata(responseType, statusCode, contentType, additionalContentTypes)); } + private static ITypeSymbol? GetNamedTypeSymbol(AttributeData attribute, string namedParameter) + { + foreach (var namedArg in attribute.NamedArguments) + { + if (namedArg.Key == namedParameter && namedArg.Value.Value is ITypeSymbol typeSymbol) + return typeSymbol; + } + + return null; + } + private static EquatableImmutableArray MergeUnion(EquatableImmutableArray? existing, IEnumerable values) { var list = new List(); From d14bffe5a90f2d9283cb984dc02e9fe643b5d27c Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:06:56 -0500 Subject: [PATCH 16/75] Fix tests. --- tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs | 2 +- tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index 1aeea98..5d9a6ba 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -16,7 +16,7 @@ internal static class GetUserEndpoint [AllowAnonymous] [Accepts(typeof(GetUserRequest), "application/json", "application/xml")] [Accepts("application/json", "application/xml")] - [ProducesResponse(typeof(UserProfile), StatusCodes.Status200OK, "application/json")] + [ProducesResponse( StatusCodes.Status200OK, "application/json", ResponseType = typeof(UserProfile))] [ProducesResponse(StatusCodes.Status202Accepted, "application/json")] [ProducesProblem(StatusCodes.Status500InternalServerError, "application/problem+json")] [ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")] diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 8f94504..853d862 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -116,7 +116,7 @@ public async Task MapAllAttributesAndHttpMethods(bool withNamespace) [RequireAuthorization("PolicyA", "PolicyB")] [DisableAntiforgery] [Accepts(typeof(ClassLevelRequest), "application/xml", "text/xml")] - [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(typeof(ClassLevelResponse), 201, "application/json", "text/json")] + [ProducesResponse(201, "application/json", "text/json", ResponseType = typeof(ClassLevelResponse))] [ProducesProblem(503, "application/problem+json")] [ProducesValidationProblem(409, "application/problem+json", "text/plain")] [ExcludeFromDescription] From 9345004c859b426b4cbc822f7085ecdb074a876b Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:10:57 -0500 Subject: [PATCH 17/75] Allow AcceptsAttribute request type named argument (#17) --- README.md | 6 ++++++ src/GeneratedEndpoints/MinimalApiGenerator.cs | 18 ++++++++---------- .../GetUserEndpoint.cs | 2 +- .../GeneratedEndpointsTests.cs | 6 +++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a47deaf..d69ac36 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,12 @@ public sealed class CreateTodo } ``` +When you can't use the generic form (for example, the request type is only known at runtime), set the `RequestType` named argument instead: + +```csharp +[Accepts("application/xml", RequestType = typeof(CreateTodoRequest))] +``` + The generator translates these attributes into `.Accepts`, `.Produces`, `.ProducesProblem`, and `.ProducesValidationProblem` calls on the endpoint builder. diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 7636570..e0f356a 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -59,6 +59,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string SummaryAttributeNamedParameter = "Summary"; private const string DescriptionAttributeNamedParameter = "Description"; private const string ResponseTypeAttributeNamedParameter = "ResponseType"; + private const string RequestTypeAttributeNamedParameter = "RequestType"; private const string RequireAuthorizationAttributeName = "RequireAuthorizationAttribute"; private const string RequireAuthorizationAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireAuthorizationAttributeName}"; @@ -281,7 +282,7 @@ internal sealed class {{AcceptsAttributeName}} : global::System.Attribute /// /// Gets the request type accepted by the endpoint. /// - public global::System.Type RequestType { get; } + public global::System.Type RequestType { get; init; } = default!; /// /// Gets the primary content type accepted by the endpoint. @@ -296,12 +297,10 @@ internal sealed class {{AcceptsAttributeName}} : global::System.Attribute /// /// Initializes a new instance of the class. /// - /// The CLR type of the request body. /// The primary content type accepted by the endpoint. /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(global::System.Type requestType, string contentType = "application/json", params string[] additionalContentTypes) + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) { - RequestType = requestType ?? throw new global::System.ArgumentNullException(nameof(requestType)); ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; AdditionalContentTypes = additionalContentTypes ?? []; } @@ -927,15 +926,14 @@ private static void TryAddAcceptsMetadata( ? GetStringArrayValues(attribute.ConstructorArguments[1]) : null; } - else if (attribute.ConstructorArguments.Length >= 1 && - attribute.ConstructorArguments[0].Value is ITypeSymbol requestTypeSymbol) + else if (GetNamedTypeSymbol(attribute, RequestTypeAttributeNamedParameter) is { } requestTypeSymbol) { requestType = requestTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - contentType = attribute.ConstructorArguments.Length > 1 - ? NormalizeRequiredContentType(attribute.ConstructorArguments[1].Value as string, "application/json") + contentType = attribute.ConstructorArguments.Length > 0 + ? NormalizeRequiredContentType(attribute.ConstructorArguments[0].Value as string, "application/json") : "application/json"; - additionalContentTypes = attribute.ConstructorArguments.Length > 2 - ? GetStringArrayValues(attribute.ConstructorArguments[2]) + additionalContentTypes = attribute.ConstructorArguments.Length > 1 + ? GetStringArrayValues(attribute.ConstructorArguments[1]) : null; } else diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index 5d9a6ba..a5f6916 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -14,7 +14,7 @@ internal static class GetUserEndpoint { [Tags("Featured")] [AllowAnonymous] - [Accepts(typeof(GetUserRequest), "application/json", "application/xml")] + [Accepts("application/json", "application/xml", RequestType = typeof(GetUserRequest))] [Accepts("application/json", "application/xml")] [ProducesResponse( StatusCodes.Status200OK, "application/json", ResponseType = typeof(UserProfile))] [ProducesResponse(StatusCodes.Status202Accepted, "application/json")] diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 853d862..331d92d 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -115,7 +115,7 @@ public async Task MapAllAttributesAndHttpMethods(bool withNamespace) [Tags("Shared", "ClassLevel")] [RequireAuthorization("PolicyA", "PolicyB")] [DisableAntiforgery] - [Accepts(typeof(ClassLevelRequest), "application/xml", "text/xml")] + [Accepts("application/xml", "text/xml", RequestType = typeof(ClassLevelRequest))] [ProducesResponse(201, "application/json", "text/json", ResponseType = typeof(ClassLevelResponse))] [ProducesProblem(503, "application/problem+json")] [ProducesValidationProblem(409, "application/problem+json", "text/plain")] @@ -138,8 +138,8 @@ public static void Configure(TBuilder builder, IServiceProvider servic [AllowAnonymous] [Tags("MethodLevel")] [RequireAuthorization("MethodPolicy")] - [Accepts("application/custom", "text/custom")] - [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] + [Accepts("application/custom", "text/custom")] + [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] [ProducesProblem(400, "application/problem+json", "text/plain")] [ProducesValidationProblem(422, "application/problem+json", "text/plain")] [ExcludeFromDescription] From 198bb80fe459051c0d5467425e10b2084b541527 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:18:28 -0500 Subject: [PATCH 18/75] Allow null response type for non-generic attribute (#18) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index e0f356a..9f5cfad 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -358,7 +358,7 @@ internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribu /// /// Gets the response type produced by the endpoint. /// - public global::System.Type ResponseType { get; init; } = default!; + public global::System.Type? ResponseType { get; init; } = null; /// /// Gets the HTTP status code returned by the endpoint. From 5375c9b722d0bb8ca1aeff9cbe0a0b296b35545c Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:21:53 -0500 Subject: [PATCH 19/75] Add IsOptional support to AcceptsAttribute (#19) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 9f5cfad..0a84ea1 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -60,6 +60,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string DescriptionAttributeNamedParameter = "Description"; private const string ResponseTypeAttributeNamedParameter = "ResponseType"; private const string RequestTypeAttributeNamedParameter = "RequestType"; + private const string IsOptionalAttributeNamedParameter = "IsOptional"; private const string RequireAuthorizationAttributeName = "RequireAuthorizationAttribute"; private const string RequireAuthorizationAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireAuthorizationAttributeName}"; @@ -284,6 +285,11 @@ internal sealed class {{AcceptsAttributeName}} : global::System.Attribute /// public global::System.Type RequestType { get; init; } = default!; + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + /// /// Gets the primary content type accepted by the endpoint. /// @@ -318,6 +324,11 @@ internal sealed class {{AcceptsAttributeName}} : global::System.Attrib /// public global::System.Type RequestType => typeof(TRequest); + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + /// /// Gets the primary content type accepted by the endpoint. /// @@ -915,6 +926,7 @@ private static void TryAddAcceptsMetadata( string? requestType = null; string contentType; EquatableImmutableArray? additionalContentTypes; + var isOptional = GetNamedBoolValue(attribute, IsOptionalAttributeNamedParameter); if (attributeClass.IsGenericType && attributeClass.TypeArguments.Length == 1) { @@ -942,7 +954,7 @@ private static void TryAddAcceptsMetadata( } var acceptsList = accepts ??= new List(); - acceptsList.Add(new AcceptsMetadata(requestType, contentType, additionalContentTypes)); + acceptsList.Add(new AcceptsMetadata(requestType, contentType, additionalContentTypes, isOptional)); } private static void TryAddProducesMetadata( @@ -1001,6 +1013,17 @@ private static void TryAddProducesMetadata( return null; } + private static bool GetNamedBoolValue(AttributeData attribute, string namedParameter, bool defaultValue = false) + { + foreach (var namedArg in attribute.NamedArguments) + { + if (namedArg.Key == namedParameter && namedArg.Value.Value is bool boolValue) + return boolValue; + } + + return defaultValue; + } + private static EquatableImmutableArray MergeUnion(EquatableImmutableArray? existing, IEnumerable values) { var list = new List(); @@ -1510,6 +1533,10 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(accepts.RequestType); source.Append('>'); source.Append('('); + if (accepts.IsOptional) + { + source.Append("isOptional: true, "); + } source.Append(StringLiteral(accepts.ContentType)); AppendAdditionalContentTypes(source, accepts.AdditionalContentTypes); source.Append(')'); @@ -1660,7 +1687,8 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< var additionalCost = accepts.AdditionalContentTypes is { Count: > 0 } ? accepts.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) : 0; - cost += 32 + accepts.RequestType.Length + accepts.ContentType.Length + additionalCost; + var optionalCost = accepts.IsOptional ? 20 : 0; + cost += 32 + optionalCost + accepts.RequestType.Length + accepts.ContentType.Length + additionalCost; } } @@ -1902,7 +1930,11 @@ private readonly record struct RequestHandlerMetadata( bool ExcludeFromDescription ); - private readonly record struct AcceptsMetadata(string RequestType, string ContentType, EquatableImmutableArray? AdditionalContentTypes); + private readonly record struct AcceptsMetadata( + string RequestType, + string ContentType, + EquatableImmutableArray? AdditionalContentTypes, + bool IsOptional); private readonly record struct ProducesMetadata( string ResponseType, From fec8a2909810e9600ccebe4013dd4ebd3bb38d91 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:27:25 -0500 Subject: [PATCH 20/75] Cleanup. --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 23 +++++++++++-------- .../GetUserEndpoint.cs | 5 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 0a84ea1..feb2322 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -99,6 +99,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string UseEndpointHandlersMethodName = "MapEndpointHandlers"; private const string UseEndpointHandlersMethodHint = $"{RoutingNamespace}.{UseEndpointHandlersMethodName}.g.cs"; + private const string ConfigureMethodName = "Configure"; private const string AsyncSuffix = "Async"; private static readonly string FileHeader = $""" @@ -852,7 +853,7 @@ ref List? producesValidationProblem ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; - var producesProblemList = producesProblem ??= new List(); + var producesProblemList = producesProblem ??= []; producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes)); break; } @@ -868,7 +869,7 @@ ref List? producesValidationProblem ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; - var producesValidationProblemList = producesValidationProblem ??= new List(); + var producesValidationProblemList = producesValidationProblem ??= []; producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes)); break; } @@ -923,12 +924,12 @@ private static void TryAddAcceptsMetadata( INamedTypeSymbol attributeClass, ref List? accepts) { - string? requestType = null; + string? requestType; string contentType; EquatableImmutableArray? additionalContentTypes; var isOptional = GetNamedBoolValue(attribute, IsOptionalAttributeNamedParameter); - if (attributeClass.IsGenericType && attributeClass.TypeArguments.Length == 1) + if (attributeClass is { IsGenericType: true, TypeArguments.Length: 1 }) { requestType = attributeClass.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); contentType = attribute.ConstructorArguments.Length > 0 @@ -953,7 +954,7 @@ private static void TryAddAcceptsMetadata( return; } - var acceptsList = accepts ??= new List(); + var acceptsList = accepts ??= []; acceptsList.Add(new AcceptsMetadata(requestType, contentType, additionalContentTypes, isOptional)); } @@ -962,12 +963,12 @@ private static void TryAddProducesMetadata( INamedTypeSymbol attributeClass, ref List? produces) { - string? responseType = null; + string? responseType; int statusCode; string? contentType; EquatableImmutableArray? additionalContentTypes; - if (attributeClass.IsGenericType && attributeClass.TypeArguments.Length == 1) + if (attributeClass is { IsGenericType: true, TypeArguments.Length: 1 }) { responseType = attributeClass.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesStatusCode @@ -998,7 +999,7 @@ private static void TryAddProducesMetadata( return; } - var producesList = produces ??= new List(); + var producesList = produces ??= []; producesList.Add(new ProducesMetadata(responseType, statusCode, contentType, additionalContentTypes)); } @@ -1102,7 +1103,7 @@ CancellationToken cancellationToken var hasConfigureMethod = false; var acceptsServiceProvider = false; - foreach (var member in classSymbol.GetMembers("Configure")) + foreach (var member in classSymbol.GetMembers(ConfigureMethodName)) { cancellationToken.ThrowIfCancellationRequested(); @@ -1426,7 +1427,9 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl { source.Append(" "); source.Append(requestHandler.Class.Name); - source.AppendLine(".Configure("); + source.Append('.'); + source.Append(ConfigureMethodName); + source.AppendLine("("); } source.Append(indent); diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index a5f6916..cedc5e8 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; namespace GeneratedEndpoints.Tests.Lab; @@ -15,13 +16,13 @@ internal static class GetUserEndpoint [Tags("Featured")] [AllowAnonymous] [Accepts("application/json", "application/xml", RequestType = typeof(GetUserRequest))] - [Accepts("application/json", "application/xml")] + [Accepts("application/json", "application/xml", IsOptional = true)] [ProducesResponse( StatusCodes.Status200OK, "application/json", ResponseType = typeof(UserProfile))] [ProducesResponse(StatusCodes.Status202Accepted, "application/json")] [ProducesProblem(StatusCodes.Status500InternalServerError, "application/problem+json")] [ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")] [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] - public static Results, NotFound, ValidationProblem, ProblemHttpResult> GetUser(int id) + public static Results, NotFound, ValidationProblem, ProblemHttpResult> GetUser([FromQuery] int id) { if (id <= 0) { From 90a34fdba2c8eec0c6a1043136ea76b5fa14b86c Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:33:04 -0500 Subject: [PATCH 21/75] Pin to .NET 10. --- global.json | 7 +++++++ src/GeneratedEndpoints/GeneratedEndpoints.csproj | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 0000000..9a523dc --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.0", + "rollForward": "latestMajor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/GeneratedEndpoints/GeneratedEndpoints.csproj b/src/GeneratedEndpoints/GeneratedEndpoints.csproj index 2c645c6..55bf32f 100644 --- a/src/GeneratedEndpoints/GeneratedEndpoints.csproj +++ b/src/GeneratedEndpoints/GeneratedEndpoints.csproj @@ -36,7 +36,7 @@ GeneratedEndpoints - 10.0.0-preview.1 + 10.0.0 10.0.0.0 10.0.0.0 en-US From c09a4089635d6fe62f5d2c45f5c26449ea82b372 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:34:04 -0500 Subject: [PATCH 22/75] Clarify README usage details (#20) --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d69ac36..ed4c1d3 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Key points: * Optional `Name`, `Summary`, and `Description` named parameters populate the generated `.WithName`, `.WithSummary`, and `.WithDescription` metadata calls. When omitted, the generator derives the endpoint name from the method name (stripping a trailing `Async`). * Apply standard ASP.NET Core parameter binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator mirrors them onto the produced delegate so binding behaves exactly as declared. * Annotate the **class**, an individual **method**, or both with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, or `[AllowAnonymous]`. Class-level metadata is merged onto every generated endpoint, while method-level attributes can refine or augment the settings for a specific handler. `[AllowAnonymous]` lets a method opt out of authorization even if the enclosing class (or other conventions) require authenticated access. -* Non-static handler classes are automatically registered with dependency injection (as transient services). Their instance methods receive a scoped instance resolved from DI, while static methods continue to behave like any other static helper. +* Non-static handler classes are automatically registered with dependency injection (as scoped services). Their instance methods receive a scoped instance resolved from DI, while static methods continue to behave like any other static helper. #### Static handler example @@ -103,11 +103,12 @@ public static class ListTodos The generator emits extension methods in the `Microsoft.AspNetCore.Generated.Routing` namespace. Call them during startup to register handler types and map the generated endpoints. ```csharp +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Generated.Routing; var builder = WebApplication.CreateBuilder(args); -// Registers non-static handler classes with the DI container. +// Registers every non-static handler class as a scoped service. builder.Services.AddEndpointHandlers(); var app = builder.Build(); @@ -118,7 +119,9 @@ app.MapEndpointHandlers(); app.Run(); ``` -`AddEndpointHandlers` ensures any non-static handler types can be resolved from dependency injection, while `MapEndpointHandlers` generates Minimal API route mappings for every annotated method in the application. Both extension methods reside in the `Microsoft.AspNetCore.Generated.Routing` namespace. +`AddEndpointHandlers` is intentionally minimal: it calls `TryAddScoped()` once per **non-static** handler type so constructor-injected dependencies are available whenever an instance method executes. Static handler classes are skipped because they never require dependency injection. + +`MapEndpointHandlers` iterates over the same set of handler types, emits the correct `MapGet`/`MapPost`/etc. call for every annotated method, and returns the `IEndpointRouteBuilder` so you can keep chaining configuration. Instance methods are invoked through a generated delegate that pulls the handler instance from `[FromServices]`, ensuring the same scoped object handles the entire request. ### 3. Compose additional endpoints @@ -246,6 +249,8 @@ When you can't use the generic form (for example, the request type is only known [Accepts("application/xml", RequestType = typeof(CreateTodoRequest))] ``` +If the request body is optional, set `IsOptional = true` on the `[Accepts]` attribute to generate `.Accepts(..., isOptional: true)` in the resulting endpoint metadata. + The generator translates these attributes into `.Accepts`, `.Produces`, `.ProducesProblem`, and `.ProducesValidationProblem` calls on the endpoint builder. @@ -258,7 +263,8 @@ calls on the endpoint builder. | `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Pass an array of policy names to enforce specific policies; when omitted the standard authorization middleware is applied. | | `[AllowAnonymous]` | Class or method | Explicitly opts a method (or all methods in a class) into anonymous access, overriding `[RequireAuthorization]`. | | `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint, matching the ASP.NET Core extension. | -| `[Accepts]` / `[Accepts]` | Class or method | Emits `.Accepts(contentType, additionalContentTypes...)` to document supported request bodies. Multiple attributes are allowed per endpoint. | +| `[ExcludeFromDescription]` | Class or method | Generates `.ExcludeFromDescription()` so the endpoint is hidden from OpenAPI/metadata. | +| `[Accepts]` / `[Accepts]` | Class or method | Emits `.Accepts(contentType, additionalContentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes are allowed per endpoint. | | `[ProducesResponse]` / `[ProducesResponse]` | Class or method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. Multiple attributes are allowed. | | `[ProducesProblem]` | Class or method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. | | `[ProducesValidationProblem]` | Class or method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)` when validation failures are returned. | @@ -276,7 +282,7 @@ calls on the endpoint builder. ### Tips for handler classes -* Non-static handler classes are automatically registered as transient services when you call `builder.Services.AddEndpointHandlers();`. +* Non-static handler classes are automatically registered as **scoped** services when you call `builder.Services.AddEndpointHandlers();`. * You can mix static and instance methods within the same class. Instance methods receive the injected handler instance; static methods work like regular static helpers and can continue to rely on `[FromServices]` for dependencies. * Use the optional `Configure` method for per-feature conventions. Its optional `IServiceProvider` parameter lets you resolve scoped services when adding endpoint filters or other runtime configuration. From b02c98080f89b37d99172e11c1fd51ec73f4fe36 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:39:05 -0500 Subject: [PATCH 23/75] docs: expand and clarify README (#21) --- README.md | 341 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 207 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index ed4c1d3..1b6fc1c 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,54 @@ [![Banner](https://raw.githubusercontent.com/jscarle/GeneratedEndpoints/develop/Banner.png)](https://github.com/jscarle/GeneratedEndpoints) -# GeneratedEndpoints - Attribute-driven, source-generated minimal API endpoints for feature-based development +# GeneratedEndpoints -GeneratedEndpoints is a .NET source generator that automatically wires up Minimal API endpoints from attribute-annotated -methods. This simplifies integration of HTTP handlers within Clean Architecture (CA) or Vertical Slice Architecture (VSA) -by keeping endpoint definitions inside their features while generating the boilerplate mapping code. +Attribute-driven, source-generated Minimal API endpoints for feature-based development. -## Capabilities at a glance +GeneratedEndpoints is a .NET source generator that automatically wires Minimal API endpoints from attribute-decorated methods. You describe the intent of each endpoint through attributes, and the generator produces all the routing and boilerplate needed to expose it. The result is a clean, feature-centric project structure that fits Clean Architecture (CA), Vertical Slice Architecture (VSA), or any other modular approach. -* **Attribute-driven routing** – decorate a method with `[MapGet]`, `[MapPost]`, etc. (including OPTIONS, HEAD, TRACE, CONNECT, - and the Minimal API-specific `QUERY` verb) and the generator maps it automatically. -* **Static or instance handlers** – declare handlers in static classes or as transient services that participate in dependency - injection. -* **Metadata composition** – mix class-level and method-level attributes for tags, authorization requirements, content - negotiation, and antiforgery/anonymous settings. The generator merges everything into the produced endpoint builder. -* **Rich request/response contracts** – describe the shape of your API surface with `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, - and `[ProducesValidationProblem]` so OpenAPI and client tooling stay accurate. -* **Minimal boilerplate** – `AddEndpointHandlers` auto-registers instance handlers with DI, and `MapEndpointHandlers` - registers every attribute-decorated method. -* **Optional per-feature customization** – provide a `Configure` method in your feature to add filters, OpenAPI metadata, or any - other conventions using the generated `IEndpointConventionBuilder`. +--- -[![develop](https://img.shields.io/github/actions/workflow/status/jscarle/GeneratedEndpoints/develop.yml?logo=github)](https://github.com/jscarle/GeneratedEndpoints) -[![nuget](https://img.shields.io/nuget/v/GeneratedEndpoints)](https://www.nuget.org/packages/GeneratedEndpoints) -[![downloads](https://img.shields.io/nuget/dt/GeneratedEndpoints)](https://www.nuget.org/packages/GeneratedEndpoints) +## Table of contents -## Getting Started +1. [Why GeneratedEndpoints?](#why-generatedendpoints) +2. [Quick start](#quick-start) +3. [Handler styles and routing](#handler-styles-and-routing) +4. [Configuring endpoints](#configuring-endpoints) +5. [Describing requests and responses](#describing-requests-and-responses) +6. [Attribute reference](#attribute-reference) +7. [Tips, patterns, and extra examples](#tips-patterns-and-extra-examples) -### Installation +--- -Add the package to the Minimal API project that will host your endpoints. You can install it with the .NET CLI: +## Why GeneratedEndpoints? -```bash -dotnet add package GeneratedEndpoints -``` +GeneratedEndpoints focuses on three goals: + +* **Attribute-driven routing** – use `[MapGet]`, `[MapPost]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapPatch]`, `[MapTrace]`, `[MapConnect]`, and even `[MapQuery]` to describe the verb and route pattern. The generator creates the matching `Map*` call and wires up metadata like `.WithName`, `.WithSummary`, and `.WithDescription`. +* **Feature-first organization** – keep handlers close to the code they execute (for example, alongside your `Todos` feature). Non-static handler classes are automatically registered with dependency injection so you can inject EF Core DbContexts, services, etc. +* **Metadata composition** – decorate classes and methods with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, `[AllowAnonymous]`, `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]`. Class-level metadata is merged into every method, while method-level metadata can refine or override. + +The generator also emits the `AddEndpointHandlers` and `MapEndpointHandlers` extension methods that do all the registration work for you. -Once the package is referenced, the source generator will contribute its attributes and extension methods to the consuming project at build time. +--- -### 1. Define a request handler +## Quick start -Create a feature class that encapsulates the logic for a single endpoint and decorate its handler method with one of the generated HTTP verb attributes. The attributes live in the `Microsoft.AspNetCore.Generated.Attributes` namespace and map directly to Minimal API routing methods. +The fastest way to understand GeneratedEndpoints is to build a minimal feature end-to-end. -Handler classes can be expressed in whichever style best fits the feature: +### 1. Install the package -* **Instance classes** (non-static) allow constructor injection and can expose either instance or static handler methods. When an annotated method is not static the generator will call it on a resolved instance from dependency injection. -* **Static classes** make it easy to group stateless functionality. Every annotated method inside must also be static, mirroring standard C# rules. +Add the package to the Minimal API project that will host your endpoints: + +```bash +dotnet add package GeneratedEndpoints +``` -#### Instance handler example +After the reference is added, the source generator contributes its attributes and routing extensions to the consuming project at build time. + +### 2. Create a handler class + +Handlers can be static or instance classes. The following example uses a scoped handler so that EF Core can be injected through the constructor: ```csharp using Microsoft.AspNetCore.Generated.Attributes; @@ -71,36 +73,16 @@ public sealed class GetTodo } ``` -Key points: +Key ideas: -* Use `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapQuery]`, `[MapTrace]`, or `[MapConnect]` to describe the HTTP verb and route pattern. -* Optional `Name`, `Summary`, and `Description` named parameters populate the generated `.WithName`, `.WithSummary`, and `.WithDescription` metadata calls. When omitted, the generator derives the endpoint name from the method name (stripping a trailing `Async`). -* Apply standard ASP.NET Core parameter binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator mirrors them onto the produced delegate so binding behaves exactly as declared. -* Annotate the **class**, an individual **method**, or both with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, or `[AllowAnonymous]`. Class-level metadata is merged onto every generated endpoint, while method-level attributes can refine or augment the settings for a specific handler. `[AllowAnonymous]` lets a method opt out of authorization even if the enclosing class (or other conventions) require authenticated access. -* Non-static handler classes are automatically registered with dependency injection (as scoped services). Their instance methods receive a scoped instance resolved from DI, while static methods continue to behave like any other static helper. +* Choose the attribute that matches the verb (`[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`). +* Named arguments like `Summary`, `Description`, and `Name` are translated into `.WithSummary`, `.WithDescription`, and `.WithName` calls. +* Use existing ASP.NET Core binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromHeader]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator preserves them in the produced delegate. +* Metadata attributes (`[Tags]`, `[RequireAuthorization]`, `[AllowAnonymous]`, `[DisableAntiforgery]`) can be placed on the class, on a method, or on both. Class-level metadata is merged with method-level metadata. -#### Static handler example +### 3. Register handlers and map endpoints -The same attribute-driven approach works for static handler types when no dependencies are needed: - -```csharp -using Microsoft.AspNetCore.Generated.Attributes; -using Microsoft.AspNetCore.Http.HttpResults; - -namespace Todos.Features; - -public static class ListTodos -{ - [MapGet("/todos")] - [Tags("Todos")] - public static Ok> Handle() - => TypedResults.Ok(TodoStore.All); -} -``` - -### 2. Wire up the application - -The generator emits extension methods in the `Microsoft.AspNetCore.Generated.Routing` namespace. Call them during startup to register handler types and map the generated endpoints. +The generator emits two extension methods in `Microsoft.AspNetCore.Generated.Routing`. Call them during startup: ```csharp using Microsoft.AspNetCore.Builder; @@ -108,40 +90,41 @@ using Microsoft.AspNetCore.Generated.Routing; var builder = WebApplication.CreateBuilder(args); -// Registers every non-static handler class as a scoped service. -builder.Services.AddEndpointHandlers(); +builder.Services.AddEndpointHandlers(); // registers every non-static handler as scoped var app = builder.Build(); -// Maps every method decorated with a Map* attribute. -app.MapEndpointHandlers(); +app.MapEndpointHandlers(); // emits MapGet/MapPost/etc. calls for every decorated method app.Run(); ``` -`AddEndpointHandlers` is intentionally minimal: it calls `TryAddScoped()` once per **non-static** handler type so constructor-injected dependencies are available whenever an instance method executes. Static handler classes are skipped because they never require dependency injection. +`AddEndpointHandlers` calls `TryAddScoped()` for every non-static handler class. Static handler classes are skipped because they never need DI. `MapEndpointHandlers` iterates over those handler types, maps each annotated method, and returns the `IEndpointRouteBuilder` for further chaining. -`MapEndpointHandlers` iterates over the same set of handler types, emits the correct `MapGet`/`MapPost`/etc. call for every annotated method, and returns the `IEndpointRouteBuilder` so you can keep chaining configuration. Instance methods are invoked through a generated delegate that pulls the handler instance from `[FromServices]`, ensuring the same scoped object handles the entire request. +### 4. Add more endpoints -### 3. Compose additional endpoints +Every attribute-decorated method becomes an endpoint the next time the project builds. Mix synchronous and asynchronous methods, return `IResult` or typed `Results<>`, and combine static and instance handlers in the same class. Metadata from attributes composes naturally. -Add as many handler classes as needed—each annotated method becomes an endpoint. You can mix synchronous and asynchronous methods, return `IResult` or typed `Results<>`, and combine static and instance handlers in the same project. Metadata from attributes composes naturally: class-level attributes are applied to every endpoint, while method-level attributes add to (or override, when relevant) the defaults. +### 5. Run and verify -```csharp -using Microsoft.AspNetCore.Generated.Attributes; -using Microsoft.AspNetCore.Http.HttpResults; +Build the project (`dotnet build`) or run the app (`dotnet run`)—the generator will emit all routing code, DI registrations, and metadata without writing any manual `app.MapGet` or `app.MapPost` calls. -namespace Todos.Features; +--- -[Tags("Todos")] -[RequireAuthorization("Todos.Read")] +## Handler styles and routing + +GeneratedEndpoints supports multiple handler styles so you can pick the one that matches the feature you're building. + +### Instance handlers (constructor injection) + +```csharp public sealed class TodoEndpoints { private readonly TodoDbContext _db; public TodoEndpoints(TodoDbContext db) => _db = db; - [MapGet("/todos/{id}", Summary = "Retrieve a todo")] + [MapGet("/todos/{id}")] public async Task, NotFound>> GetAsync(Guid id, CancellationToken cancellationToken) { var entity = await _db.Todos.FindAsync(new object?[] { id }, cancellationToken); @@ -166,39 +149,45 @@ public sealed class TodoEndpoints } ``` -In this example: +* Non-static handler classes are registered as scoped services when you call `AddEndpointHandlers`. +* Instance methods are invoked on an injected instance. +* Static methods in the same class still work—they simply receive dependencies via `[FromServices]`. -* The class-level `[Tags]` and `[RequireAuthorization]` attributes apply to both endpoints, while the method-level `[RequireAuthorization]` adds an additional policy for the delete handler. -* `GetAsync` is an instance method that uses the injected `TodoDbContext` field, illustrating how non-static handlers can maintain state. -* `DeleteAsync` is a static method in the same class and explicitly receives its dependencies via `[FromServices]`, demonstrating that you can mix static and instance methods in a single handler type. +### Static handlers (stateless logic) -Every new handler will automatically appear in the generated routing table the next time the project builds—no manual `MapGet`, `MapPost`, or registration code is required. +```csharp +public static class ListTodos +{ + [MapGet("/todos")] + [Tags("Todos")] + public static Ok> Handle() + => TypedResults.Ok(TodoStore.All); +} +``` -### 4. Customize generated endpoints with `Configure` +Static handlers excel when no services are required. Every annotated method inside the class must also be static, just like regular C# rules. -Some scenarios require direct access to the `IEndpointConventionBuilder` that Minimal APIs use when configuring an endpoint—for example, adding endpoint filters, OpenAPI metadata, or other advanced conventions. Handler classes can now opt-in to that level of control by providing a static `Configure` method with the following signature: +### Feature layout example -```csharp -public static void Configure(TBuilder builder) - where TBuilder : IEndpointConventionBuilder +A feature folder might look like this: -// or, when you need to resolve services while configuring -public static void Configure(TBuilder builder, IServiceProvider serviceProvider) - where TBuilder : IEndpointConventionBuilder +``` +Todos/ + Features/ + GetTodo.cs + ListTodos.cs + CreateTodo.cs ``` -When the generator detects this method on a handler class it will automatically wrap every mapped endpoint from the class in a `.Configure(...)` call that invokes your method. You can use the provided builder to apply conventions that are difficult or impossible to express via attributes alone. +Each file contains exactly one handler class with attribute-decorated methods. `MapEndpointHandlers` discovers them automatically. Because everything lives next to the feature, developers can reason about behavior without scanning `Program.cs` or `Startup.cs`. -If you include the optional `IServiceProvider` parameter, the generator passes the `IEndpointRouteBuilder.ServiceProvider` from the `MapEndpointHandlers` call. This makes it easy to resolve scoped services or other helpers needed to configure filters, OpenAPI metadata, or custom conventions without manually re-wiring the app's service provider. +--- -```csharp -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Generated.Attributes; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; +## Configuring endpoints -namespace Todos.Features; +Some scenarios require more control over each endpoint's `IEndpointConventionBuilder`. The generator supports per-feature configuration through an optional static `Configure` method. +```csharp public sealed class CreateTodo { [MapPost("/todos")] @@ -217,20 +206,26 @@ public sealed class CreateTodo } ``` -The method is only generated once per handler class, so any conventions you add will automatically flow to all endpoints defined within that class. +You can also request an `IServiceProvider` when configuration depends on registered services: -### 5. Describe contracts with `Accepts` and `ProducesResponse` +```csharp +public static void Configure(TBuilder builder, IServiceProvider services) + where TBuilder : IEndpointConventionBuilder +{ + var conventions = services.GetRequiredService(); + conventions.Apply(builder); +} +``` -GeneratedEndpoints ships with helper attributes for request and response metadata. Apply them to either a handler class or -individual methods to keep your OpenAPI description in sync with the implementation. Attributes on the class are merged into -each method, while method-level attributes can augment or override the defaults. +The `Configure` method is emitted once per handler class, so every endpoint in the class receives the same conventions. -```csharp -using Microsoft.AspNetCore.Generated.Attributes; -using Microsoft.AspNetCore.Http.HttpResults; +--- -namespace Todos.Features; +## Describing requests and responses + +GeneratedEndpoints ships with attribute helpers for request/response metadata. They keep your OpenAPI description in sync with the implementation. +```csharp [Accepts("application/json", "application/xml")] [ProducesResponse(StatusCodes.Status201Created)] [ProducesProblem(StatusCodes.Status500InternalServerError)] @@ -243,46 +238,124 @@ public sealed class CreateTodo } ``` -When you can't use the generic form (for example, the request type is only known at runtime), set the `RequestType` named argument instead: - -```csharp -[Accepts("application/xml", RequestType = typeof(CreateTodoRequest))] -``` - -If the request body is optional, set `IsOptional = true` on the `[Accepts]` attribute to generate `.Accepts(..., isOptional: true)` in the resulting endpoint metadata. +* Use the generic form when the request/response type is known at compile time. For runtime types, set `RequestType` on `[Accepts]` and `ResponseType` on `[ProducesResponse]`. +* Mark `IsOptional = true` on `[Accepts]` to call `.Accepts(..., isOptional: true)`. +* Multiple `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` attributes can be applied to the same method or class. The generator creates every corresponding `.Accepts` or `.Produces` call. -The generator translates these attributes into `.Accepts`, `.Produces`, `.ProducesProblem`, and `.ProducesValidationProblem` -calls on the endpoint builder. +--- -### Attribute reference +## Attribute reference | Attribute | Scope | Purpose | | --- | --- | --- | -| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments (`Name`, `Summary`, `Description`) fill the generated `.WithName`, `.WithSummary`, and `.WithDescription` calls. | +| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName`, `.WithSummary`, and `.WithDescription` calls. | | `[Tags]` | Class or method | Adds tags to one or more endpoints. Multiple attributes merge without duplication. | -| `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Pass an array of policy names to enforce specific policies; when omitted the standard authorization middleware is applied. | -| `[AllowAnonymous]` | Class or method | Explicitly opts a method (or all methods in a class) into anonymous access, overriding `[RequireAuthorization]`. | -| `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint, matching the ASP.NET Core extension. | +| `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Passing policies (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) emits `.RequireAuthorization("Todos.Read", "Todos.Write")`. | +| `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | +| `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint. | | `[ExcludeFromDescription]` | Class or method | Generates `.ExcludeFromDescription()` so the endpoint is hidden from OpenAPI/metadata. | -| `[Accepts]` / `[Accepts]` | Class or method | Emits `.Accepts(contentType, additionalContentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes are allowed per endpoint. | -| `[ProducesResponse]` / `[ProducesResponse]` | Class or method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. Multiple attributes are allowed. | +| `[Accepts]` / `[Accepts]` | Class or method | Emits `.Accepts(contentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes allowed. | +| `[ProducesResponse]` / `[ProducesResponse]` | Class or method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. | | `[ProducesProblem]` | Class or method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. | -| `[ProducesValidationProblem]` | Class or method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)` when validation failures are returned. | +| `[ProducesValidationProblem]` | Class or method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)`. | + +> ℹ️ Metadata defined on a class is applied to every annotated method inside the class. Method-level attributes can add entries (tags, accepts, produces, etc.) or override boolean flags like `[AllowAnonymous]`. + +--- + +## Tips, patterns, and extra examples + +### Authorization and security + +* `[RequireAuthorization]` adds `.RequireAuthorization()` or `.RequireAuthorization("policy")`. +* `[AllowAnonymous]` opt-in overrides class or global authorization requirements. +* `[DisableAntiforgery]` wires `.DisableAntiforgery()` for CSRF-sensitive endpoints. + +### Handling query objects with `[AsParameters]` + +```csharp +public sealed record ListTodosQuery([FromQuery] int? Page, [FromQuery] string? Owner); + +public static class SearchTodos +{ + [MapGet("/todos/search")] + public static Ok> Handle([AsParameters] ListTodosQuery query) + { + var todos = TodoStore.Query(query.Page ?? 1, query.Owner); + return TypedResults.Ok(todos); + } +} +``` + +`[AsParameters]` lets you bundle multiple inputs into a record or class without writing manual binding logic. + +### Combining filters with `Configure` + +```csharp +public sealed class UpdateTodo +{ + [MapPut("/todos/{id}")] + [RequireAuthorization("Todos.Write")] + public async Task, NotFound>> HandleAsync(Guid id, [FromBody] UpdateTodoRequest request) + { + // ... + } + + public static void Configure(TBuilder builder, IServiceProvider services) + where TBuilder : IEndpointConventionBuilder + { + builder.AddEndpointFilter(new ValidationFilter()); + builder.AddEndpointFilterFactory((context, next) => new LoggingFilter(next, services.GetRequiredService())); + } +} +``` + +### Feature testing helper + +When testing, register handlers in a WebApplicationFactory or the new `MinimalApiApplicationBuilder`: + +```csharp +var app = MinimalApiApplication.CreateBuilder().Build(); +app.MapEndpointHandlers(); +``` + +All annotated methods become endpoints even in test hosts, so integration tests hit the same generated routing table as production. -> ℹ️ All metadata attributes defined on a class are applied to every annotated method inside the class. Method-level attributes -> can add additional entries (for tags, accepts, produces, etc.) or override booleans such as `[AllowAnonymous]` and -> `[DisableAntiforgery]`. +### Putting it all together + +```csharp +[Tags("Todos")] +[RequireAuthorization("Todos.Read")] +public sealed class TodoFeature +{ + private readonly TodoDbContext _db; -### Authorization and security conventions + public TodoFeature(TodoDbContext db) => _db = db; -* **Default authorization** – `[RequireAuthorization]` adds `.RequireAuthorization()` to every endpoint. Supplying policies - (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) generates `.RequireAuthorization("Todos.Read", "Todos.Write")`. -* **Allow anonymous** – `[AllowAnonymous]` on a class or method maps to `.AllowAnonymous()`, even when authorization is required elsewhere. -* **Antiforgery** – `[DisableAntiforgery]` wires through `.DisableAntiforgery()`. + [MapGet("/todos/{id}", Summary = "Retrieve a todo")] + public async Task, NotFound>> GetAsync(Guid id, CancellationToken cancellationToken) + { + var entity = await _db.Todos.FindAsync(new object?[] { id }, cancellationToken); + return entity is null ? TypedResults.NotFound() : TypedResults.Ok(entity); + } -### Tips for handler classes + [MapPost("/todos", Summary = "Create a todo")] + [ProducesResponse(StatusCodes.Status201Created)] + public async Task> CreateAsync([FromBody] CreateTodoRequest request, CancellationToken cancellationToken) + { + var todo = request.ToTodo(); + _db.Todos.Add(todo); + await _db.SaveChangesAsync(cancellationToken); + return TypedResults.Created($"/todos/{todo.Id}", todo); + } -* Non-static handler classes are automatically registered as **scoped** services when you call `builder.Services.AddEndpointHandlers();`. -* You can mix static and instance methods within the same class. Instance methods receive the injected handler instance; static methods work like regular static helpers and can continue to rely on `[FromServices]` for dependencies. -* Use the optional `Configure` method for per-feature conventions. Its optional `IServiceProvider` parameter lets you resolve scoped services when adding endpoint filters or other runtime configuration. + public static void Configure(TBuilder builder, IServiceProvider services) + where TBuilder : IEndpointConventionBuilder + { + var conventions = services.GetRequiredService(); + conventions.Apply(builder); + } +} +``` +With these patterns you can grow a feature-based API without ever touching the routing layer manually. Drop a new handler class into your feature folder, decorate methods with the correct attributes, and let GeneratedEndpoints handle the plumbing. From 5b32db84280dc5f5a7af8a843ea48f8368207fab Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:41:35 -0500 Subject: [PATCH 24/75] Refactor HTTP attribute definitions (#22) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 215 ++++++------------ 1 file changed, 75 insertions(+), 140 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index feb2322..e6c0677 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -15,45 +15,22 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string BaseNamespace = "Microsoft.AspNetCore.Generated"; private const string AttributesNamespace = $"{BaseNamespace}.Attributes"; - private const string MapGetAttributeName = "MapGetAttribute"; - private const string MapGetAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapGetAttributeName}"; - private const string MapGetAttributeHint = $"{MapGetAttributeFullyQualifiedName}.gs.cs"; - - private const string MapPostAttributeName = "MapPostAttribute"; - private const string MapPostAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapPostAttributeName}"; - private const string MapPostAttributeHint = $"{MapPostAttributeFullyQualifiedName}.gs.cs"; - - private const string MapPutAttributeName = "MapPutAttribute"; - private const string MapPutAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapPutAttributeName}"; - private const string MapPutAttributeHint = $"{MapPutAttributeFullyQualifiedName}.gs.cs"; - - private const string MapDeleteAttributeName = "MapDeleteAttribute"; - private const string MapDeleteAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapDeleteAttributeName}"; - private const string MapDeleteAttributeHint = $"{MapDeleteAttributeFullyQualifiedName}.gs.cs"; - - private const string MapOptionsAttributeName = "MapOptionsAttribute"; - private const string MapOptionsAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapOptionsAttributeName}"; - private const string MapOptionsAttributeHint = $"{MapOptionsAttributeFullyQualifiedName}.gs.cs"; - - private const string MapHeadAttributeName = "MapHeadAttribute"; - private const string MapHeadAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapHeadAttributeName}"; - private const string MapHeadAttributeHint = $"{MapHeadAttributeFullyQualifiedName}.gs.cs"; - - private const string MapPatchAttributeName = "MapPatchAttribute"; - private const string MapPatchAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapPatchAttributeName}"; - private const string MapPatchAttributeHint = $"{MapPatchAttributeFullyQualifiedName}.gs.cs"; - - private const string MapQueryAttributeName = "MapQueryAttribute"; - private const string MapQueryAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapQueryAttributeName}"; - private const string MapQueryAttributeHint = $"{MapQueryAttributeFullyQualifiedName}.gs.cs"; - - private const string MapTraceAttributeName = "MapTraceAttribute"; - private const string MapTraceAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapTraceAttributeName}"; - private const string MapTraceAttributeHint = $"{MapTraceAttributeFullyQualifiedName}.gs.cs"; - - private const string MapConnectAttributeName = "MapConnectAttribute"; - private const string MapConnectAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapConnectAttributeName}"; - private const string MapConnectAttributeHint = $"{MapConnectAttributeFullyQualifiedName}.gs.cs"; + private static readonly ImmutableArray HttpAttributeDefinitions = + [ + CreateHttpAttributeDefinition("MapGetAttribute", "GET"), + CreateHttpAttributeDefinition("MapPostAttribute", "POST"), + CreateHttpAttributeDefinition("MapPutAttribute", "PUT"), + CreateHttpAttributeDefinition("MapPatchAttribute", "PATCH"), + CreateHttpAttributeDefinition("MapDeleteAttribute", "DELETE"), + CreateHttpAttributeDefinition("MapOptionsAttribute", "OPTIONS"), + CreateHttpAttributeDefinition("MapHeadAttribute", "HEAD"), + CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), + CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), + CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), + ]; + + private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = + HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); private const string NameAttributeNamedParameter = "Name"; private const string SummaryAttributeNamedParameter = "Summary"; @@ -116,103 +93,55 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator #nullable enable """; + private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb) + { + var fullyQualifiedName = $"{AttributesNamespace}.{attributeName}"; + return new HttpAttributeDefinition(attributeName, fullyQualifiedName, $"{fullyQualifiedName}.gs.cs", verb); + } + public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(RegisterAttributes); - var getRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapGetAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var postRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapPostAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var putRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapPutAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var deleteRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapDeleteAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var optionsRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapOptionsAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var headRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapHeadAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var patchRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapPatchAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var queryRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapQueryAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var traceRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapTraceAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var connectRequestHandlers = context.SyntaxProvider - .ForAttributeWithMetadataName(MapConnectAttributeFullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) - .WhereNotNull() - .Collect(); - - var requestHandlers = getRequestHandlers.Combine(postRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(putRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(patchRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(deleteRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(optionsRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(headRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(queryRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(traceRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)) - .Combine(connectRequestHandlers) - .Select(static (x, _) => x.Left.AddRange(x.Right)); + var requestHandlerProviders = ImmutableArray.CreateBuilder>>( + HttpAttributeDefinitions.Length); + + foreach (var definition in HttpAttributeDefinitions) + { + var handlers = context.SyntaxProvider + .ForAttributeWithMetadataName(definition.FullyQualifiedName, RequestHandlerFilter, RequestHandlerTransform) + .WhereNotNull() + .Collect(); + + requestHandlerProviders.Add(handlers); + } + + var requestHandlers = CombineRequestHandlers(requestHandlerProviders.MoveToImmutable()); context.RegisterSourceOutput(requestHandlers, GenerateSource); } - private static void RegisterAttributes(IncrementalGeneratorPostInitializationContext context) + private static IncrementalValueProvider> CombineRequestHandlers( + ImmutableArray>> handlerProviders) { - // Definitions for HTTP method attributes - var httpAttributes = new[] + if (handlerProviders.IsDefaultOrEmpty) + throw new InvalidOperationException("No HTTP attribute definitions were provided."); + + var combined = handlerProviders[0]; + for (var i = 1; i < handlerProviders.Length; i++) { - (Name: MapGetAttributeName, FullyQualified: MapGetAttributeFullyQualifiedName, Hint: MapGetAttributeHint, Verb: "GET"), - (Name: MapPostAttributeName, FullyQualified: MapPostAttributeFullyQualifiedName, Hint: MapPostAttributeHint, Verb: "POST"), - (Name: MapPutAttributeName, FullyQualified: MapPutAttributeFullyQualifiedName, Hint: MapPutAttributeHint, Verb: "PUT"), - (Name: MapDeleteAttributeName, FullyQualified: MapDeleteAttributeFullyQualifiedName, Hint: MapDeleteAttributeHint, Verb: "DELETE"), - (Name: MapOptionsAttributeName, FullyQualified: MapOptionsAttributeFullyQualifiedName, Hint: MapOptionsAttributeHint, Verb: "OPTIONS"), - (Name: MapHeadAttributeName, FullyQualified: MapHeadAttributeFullyQualifiedName, Hint: MapHeadAttributeHint, Verb: "HEAD"), - (Name: MapPatchAttributeName, FullyQualified: MapPatchAttributeFullyQualifiedName, Hint: MapPatchAttributeHint, Verb: "PATCH"), - (Name: MapQueryAttributeName, FullyQualified: MapQueryAttributeFullyQualifiedName, Hint: MapQueryAttributeHint, Verb: "QUERY"), - (Name: MapTraceAttributeName, FullyQualified: MapTraceAttributeFullyQualifiedName, Hint: MapTraceAttributeHint, Verb: "TRACE"), - (Name: MapConnectAttributeName, FullyQualified: MapConnectAttributeFullyQualifiedName, Hint: MapConnectAttributeHint, Verb: "CONNECT"), - }; + combined = combined.Combine(handlerProviders[i]).Select(static (x, _) => x.Left.AddRange(x.Right)); + } - foreach (var (name, _, hint, verb) in httpAttributes) + return combined; + } + + private static void RegisterAttributes(IncrementalGeneratorPostInitializationContext context) + { + foreach (var definition in HttpAttributeDefinitions) { - var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, name, verb); - context.AddSource(hint, SourceText.From(source, Encoding.UTF8)); + var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, definition.Name, definition.Verb); + context.AddSource(definition.Hint, SourceText.From(source, Encoding.UTF8)); } // RequireAuthorization @@ -646,20 +575,9 @@ CancellationToken cancellationToken var attributeName = attribute.AttributeClass?.Name ?? ""; - var httpMethod = attributeName switch - { - MapGetAttributeName => "Get", - MapPostAttributeName => "Post", - MapPutAttributeName => "Put", - MapDeleteAttributeName => "Delete", - MapOptionsAttributeName => "OPTIONS", - MapHeadAttributeName => "HEAD", - MapPatchAttributeName => "Patch", - MapQueryAttributeName => "QUERY", - MapTraceAttributeName => "TRACE", - MapConnectAttributeName => "CONNECT", - _ => "", - }; + var httpMethod = HttpAttributeDefinitionsByName.TryGetValue(attributeName, out var definition) + ? definition.Verb + : ""; var pattern = (attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : "") ?? ""; @@ -1432,13 +1350,15 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.AppendLine("("); } + var mapMethodSuffix = GetMapMethodSuffix(requestHandler.HttpMethod); + source.Append(indent); source.Append("builder.Map"); - source.Append(requestHandler.HttpMethod is "Get" or "Post" or "Put" or "Delete" or "Patch" ? requestHandler.HttpMethod : "Methods"); + source.Append(mapMethodSuffix ?? "Methods"); source.Append('('); source.Append(StringLiteral(requestHandler.Pattern)); source.Append(", "); - if (requestHandler.HttpMethod is "OPTIONS" or "HEAD" or "TRACE" or "CONNECT" or "QUERY") + if (mapMethodSuffix is null) { source.Append("new[] { \""); source.Append(requestHandler.HttpMethod); @@ -1638,6 +1558,19 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } } + private static string? GetMapMethodSuffix(string httpMethod) + { + return httpMethod switch + { + "GET" => "Get", + "POST" => "Post", + "PUT" => "Put", + "DELETE" => "Delete", + "PATCH" => "Patch", + _ => null, + }; + } + private static string GetBindingSourceAttribute(BindingSource source, string? key) { return source switch @@ -1900,6 +1833,8 @@ _ when char.IsControl(c) => "\\u" + ((int)c).ToString("x4", CultureInfo.Invarian }; } + private readonly record struct HttpAttributeDefinition(string Name, string FullyQualifiedName, string Hint, string Verb); + private readonly record struct RequestHandler( RequestHandlerClass Class, RequestHandlerMethod Method, From bb4f3ab490aa698a353803b98776a2b89ee5a35b Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:42:37 -0500 Subject: [PATCH 25/75] Fix AllowAnonymous override with method RequireAuthorization (#23) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 45 ++++++++++++------- ...ndpointHandlers_WithNamespace.verified.txt | 31 +++++++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 31 +++++++++++++ .../GeneratedEndpointsTests.cs | 23 ++++++++++ 4 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index e6c0677..31ae036 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -628,11 +628,11 @@ private static ( cancellationToken.ThrowIfCancellationRequested(); EquatableImmutableArray? tags = null; - var requireAuthorization = false; + bool? requireAuthorization = null; EquatableImmutableArray? authorizationPolicies = null; - var disableAntiforgery = false; - var allowAnonymous = false; - var excludeFromDescription = false; + bool? disableAntiforgery = null; + bool? allowAnonymous = null; + bool? excludeFromDescription = null; List? accepts = null; List? produces = null; @@ -640,6 +640,8 @@ private static ( List? producesValidationProblem = null; var classAttributes = classSymbol.GetAttributes(); + var classHasAllowAnonymousAttribute = false; + var classHasRequireAuthorizationAttribute = false; GetAdditionalRequestHandlerAttributeValues( classAttributes, ref tags, @@ -651,10 +653,14 @@ private static ( ref accepts, ref produces, ref producesProblem, - ref producesValidationProblem + ref producesValidationProblem, + ref classHasAllowAnonymousAttribute, + ref classHasRequireAuthorizationAttribute ); var methodAttributes = methodSymbol.GetAttributes(); + var methodHasAllowAnonymousAttribute = false; + var methodHasRequireAuthorizationAttribute = false; GetAdditionalRequestHandlerAttributeValues( methodAttributes, ref tags, @@ -666,16 +672,21 @@ ref producesValidationProblem ref accepts, ref produces, ref producesProblem, - ref producesValidationProblem + ref producesValidationProblem, + ref methodHasAllowAnonymousAttribute, + ref methodHasRequireAuthorizationAttribute ); + if (methodHasRequireAuthorizationAttribute && !methodHasAllowAnonymousAttribute) + allowAnonymous = false; + return ( tags, - requireAuthorization, + requireAuthorization ?? false, authorizationPolicies, - disableAntiforgery, - allowAnonymous, - excludeFromDescription, + disableAntiforgery ?? false, + allowAnonymous ?? false, + excludeFromDescription ?? false, ToEquatableOrNull(accepts), ToEquatableOrNull(produces), ToEquatableOrNull(producesProblem), @@ -686,15 +697,17 @@ ref producesValidationProblem private static void GetAdditionalRequestHandlerAttributeValues( ImmutableArray attributes, ref EquatableImmutableArray? tags, - ref bool requireAuthorization, + ref bool? requireAuthorization, ref EquatableImmutableArray? authorizationPolicies, - ref bool disableAntiforgery, - ref bool allowAnonymous, - ref bool excludeFromDescription, + ref bool? disableAntiforgery, + ref bool? allowAnonymous, + ref bool? excludeFromDescription, ref List? accepts, ref List? produces, ref List? producesProblem, - ref List? producesValidationProblem + ref List? producesValidationProblem, + ref bool hasAllowAnonymousAttribute, + ref bool hasRequireAuthorizationAttribute ) { foreach (var attribute in attributes) @@ -736,6 +749,7 @@ ref List? producesValidationProblem break; case $"global::{RequireAuthorizationAttributeFullyQualifiedName}": requireAuthorization = true; + hasRequireAuthorizationAttribute = true; if (attribute.ConstructorArguments.Length == 1) { var arg = attribute.ConstructorArguments[0]; @@ -755,6 +769,7 @@ ref List? producesValidationProblem break; case $"global::{AllowAnonymousAttributeFullyQualifiedName}": allowAnonymous = true; + hasAllowAnonymousAttribute = true; break; case "global::Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute": excludeFromDescription = true; diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..e5accdf --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/allow-anon", global::GeneratedEndpointsTests.AllowAnonymousClass.Handle) + .WithName("Handle") + .RequireAuthorization(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..22be3a5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/allow-anon", global::AllowAnonymousClass.Handle) + .WithName("Handle") + .RequireAuthorization(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 331d92d..2a7750c 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -99,6 +99,29 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(MapGetWithConfigureServiceProvider)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ClassAllowAnonymousMethodRequireAuthorization(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + [AllowAnonymous] + internal sealed class AllowAnonymousClass + { + [MapGet("/allow-anon")] + [RequireAuthorization] + public static Ok Handle() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(ClassAllowAnonymousMethodRequireAuthorization)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From c666e4e0561512aeb75edd5b25d25b60795b069a Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:45:57 -0500 Subject: [PATCH 26/75] Simplify endpoint handler buffer estimate (#25) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 104 +----------------- 1 file changed, 5 insertions(+), 99 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 31ae036..4f60949 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1605,107 +1605,13 @@ private static string GetBindingSourceAttribute(BindingSource source, string? ke private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray requestHandlers) { - var estimate = 1024; + const int baseSize = 4096; + const int perHandler = 512; - foreach (var rh in requestHandlers) - { - var cost = 160; - - cost += 2 + rh.Pattern.Length; - cost += rh.Class.Name.Length + rh.Method.Name.Length; - - var parameters = rh.Method.Parameters; - foreach (var p in parameters) - cost += 12 + p.Type.Length + p.Name.Length; - - var metadata = rh.Metadata; - if (metadata.Name is { Length: > 0 }) - cost += 22 + metadata.Name.Length; - if (metadata.Summary is { Length: > 0 }) - cost += 24 + metadata.Summary.Length; - if (metadata.Description is { Length: > 0 }) - cost += 28 + metadata.Description.Length; - if (metadata.ExcludeFromDescription) - cost += 32; - - if (metadata.Tags is { Count: > 0 }) - cost += metadata.Tags.Value.Sum(tag => 6 + tag.Length); - - if (metadata.Accepts is { Count: > 0 }) - { - foreach (var accepts in metadata.Accepts.Value) - { - var additionalCost = accepts.AdditionalContentTypes is { Count: > 0 } - ? accepts.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) - : 0; - var optionalCost = accepts.IsOptional ? 20 : 0; - cost += 32 + optionalCost + accepts.RequestType.Length + accepts.ContentType.Length + additionalCost; - } - } + var estimate = baseSize + (requestHandlers.Length * perHandler); - if (metadata.Produces is { Count: > 0 }) - { - foreach (var produces in metadata.Produces.Value) - { - var additionalCost = produces.AdditionalContentTypes is { Count: > 0 } - ? produces.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) - : 0; - var contentTypeLength = produces.ContentType?.Length ?? 0; - cost += 40 + produces.ResponseType.Length + contentTypeLength + additionalCost; - } - } - - if (metadata.ProducesProblem is { Count: > 0 }) - { - foreach (var producesProblem in metadata.ProducesProblem.Value) - { - var additionalCost = producesProblem.AdditionalContentTypes is { Count: > 0 } - ? producesProblem.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) - : 0; - var contentTypeLength = producesProblem.ContentType?.Length ?? 0; - cost += 28 + contentTypeLength + additionalCost; - } - } - - if (metadata.ProducesValidationProblem is { Count: > 0 }) - { - foreach (var producesValidationProblem in metadata.ProducesValidationProblem.Value) - { - var additionalCost = producesValidationProblem.AdditionalContentTypes is { Count: > 0 } - ? producesValidationProblem.AdditionalContentTypes.Value.Sum(ct => 6 + ct.Length) - : 0; - var contentTypeLength = producesValidationProblem.ContentType?.Length ?? 0; - cost += 32 + contentTypeLength + additionalCost; - } - } - - if (rh.RequireAuthorization) - cost += 24; - if (rh.AuthorizationPolicies is { Count: > 0 }) - cost += rh.AuthorizationPolicies.Value.Sum(p => 6 + p.Length); - if (rh.DisableAntiforgery) - cost += 24; - if (rh.AllowAnonymous) - cost += 24; - if (rh.Class.HasConfigureMethod) - { - cost += 32 + rh.Class.Name.Length; - if (rh.Class.ConfigureMethodAcceptsServiceProvider) - cost += 32; - } - - estimate += cost; - } - - estimate += Math.Max(256, requestHandlers.Length * 16); - estimate = (int)(estimate * 1.12); - - estimate = estimate switch - { - < 4096 => 4096, - > 65536 => 65536, - _ => estimate, - }; + if (estimate > 65536) + estimate = 65536; return new StringBuilder(estimate); } From 238c76c846db237c461e2216e42ffb4f272e887d Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:51:20 -0500 Subject: [PATCH 27/75] Use symbol metadata for attribute detection (#26) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 191 +++++++++++------- 1 file changed, 113 insertions(+), 78 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 4f60949..527a710 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -14,6 +14,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator { private const string BaseNamespace = "Microsoft.AspNetCore.Generated"; private const string AttributesNamespace = $"{BaseNamespace}.Attributes"; + private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.'); + private static readonly string[] AspNetCoreHttpNamespaceParts = new[] { "Microsoft", "AspNetCore", "Http" }; + private static readonly string[] AspNetCoreAuthorizationNamespaceParts = new[] { "Microsoft", "AspNetCore", "Authorization" }; + private static readonly string[] AspNetCoreRoutingNamespaceParts = new[] { "Microsoft", "AspNetCore", "Routing" }; private static readonly ImmutableArray HttpAttributeDefinitions = [ @@ -47,7 +51,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; - private const string AllowAnonymousAttributeFullyQualifiedName = "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute"; + private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; private const string AcceptsAttributeName = "AcceptsAttribute"; private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}"; @@ -716,96 +720,108 @@ ref bool hasRequireAuthorizationAttribute if (attributeClass is null) continue; - var fullyQualifiedName = attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - if (IsGeneratedAttribute(fullyQualifiedName, AcceptsAttributeName)) + if (IsGeneratedAttribute(attributeClass, AcceptsAttributeName)) { TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); continue; } - if (IsGeneratedAttribute(fullyQualifiedName, ProducesResponseAttributeName)) + if (IsGeneratedAttribute(attributeClass, ProducesResponseAttributeName)) { TryAddProducesMetadata(attribute, attributeClass, ref produces); continue; } - switch (fullyQualifiedName) + if (IsAttribute(attributeClass, "TagsAttribute", AspNetCoreHttpNamespaceParts)) { - case "global::Microsoft.AspNetCore.Http.TagsAttribute": - if (attribute.ConstructorArguments.Length > 0) - { - var arg = attribute.ConstructorArguments[0]; - if (arg.Values.Length > 0) - { - var values = arg.Values - .Select(v => v.Value as string) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .Select(s => s!.Trim()); - - MergeInto(ref tags, values); - } - } - break; - case $"global::{RequireAuthorizationAttributeFullyQualifiedName}": - requireAuthorization = true; - hasRequireAuthorizationAttribute = true; - if (attribute.ConstructorArguments.Length == 1) + if (attribute.ConstructorArguments.Length > 0) + { + var arg = attribute.ConstructorArguments[0]; + if (arg.Values.Length > 0) { - var arg = attribute.ConstructorArguments[0]; - if (arg.Values.Length > 0) - { - var values = arg.Values - .Select(v => v.Value as string) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .Select(s => s!.Trim()); - - MergeInto(ref authorizationPolicies, values); - } + var values = arg.Values + .Select(v => v.Value as string) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s!.Trim()); + + MergeInto(ref tags, values); } - break; - case $"global::{DisableAntiforgeryAttributeFullyQualifiedName}": - disableAntiforgery = true; - break; - case $"global::{AllowAnonymousAttributeFullyQualifiedName}": - allowAnonymous = true; - hasAllowAnonymousAttribute = true; - break; - case "global::Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute": - excludeFromDescription = true; - break; - case $"global::{ProducesProblemAttributeFullyQualifiedName}": - { - var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode - ? producesProblemStatusCode - : 500; - var contentType = attribute.ConstructorArguments.Length > 1 - ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) - : null; - var additionalContentTypes = attribute.ConstructorArguments.Length > 2 - ? GetStringArrayValues(attribute.ConstructorArguments[2]) - : null; - - var producesProblemList = producesProblem ??= []; - producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes)); - break; } - case $"global::{ProducesValidationProblemAttributeFullyQualifiedName}": + + continue; + } + + if (IsGeneratedAttribute(attributeClass, RequireAuthorizationAttributeName)) + { + requireAuthorization = true; + hasRequireAuthorizationAttribute = true; + if (attribute.ConstructorArguments.Length == 1) { - var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode - ? producesValidationProblemStatusCode - : 400; - var contentType = attribute.ConstructorArguments.Length > 1 - ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) - : null; - var additionalContentTypes = attribute.ConstructorArguments.Length > 2 - ? GetStringArrayValues(attribute.ConstructorArguments[2]) - : null; - - var producesValidationProblemList = producesValidationProblem ??= []; - producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes)); - break; + var arg = attribute.ConstructorArguments[0]; + if (arg.Values.Length > 0) + { + var values = arg.Values + .Select(v => v.Value as string) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s!.Trim()); + + MergeInto(ref authorizationPolicies, values); + } } + + continue; + } + + if (IsGeneratedAttribute(attributeClass, DisableAntiforgeryAttributeName)) + { + disableAntiforgery = true; + continue; + } + + if (IsAttribute(attributeClass, AllowAnonymousAttributeName, AspNetCoreAuthorizationNamespaceParts)) + { + allowAnonymous = true; + hasAllowAnonymousAttribute = true; + continue; + } + + if (IsAttribute(attributeClass, "ExcludeFromDescriptionAttribute", AspNetCoreRoutingNamespaceParts)) + { + excludeFromDescription = true; + continue; + } + + if (IsGeneratedAttribute(attributeClass, ProducesProblemAttributeName)) + { + var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode + ? producesProblemStatusCode + : 500; + var contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) + : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 + ? GetStringArrayValues(attribute.ConstructorArguments[2]) + : null; + + var producesProblemList = producesProblem ??= []; + producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes)); + continue; + } + + if (IsGeneratedAttribute(attributeClass, ProducesValidationProblemAttributeName)) + { + var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode + ? producesValidationProblemStatusCode + : 400; + var contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) + : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 + ? GetStringArrayValues(attribute.ConstructorArguments[2]) + : null; + + var producesValidationProblemList = producesValidationProblem ??= []; + producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes)); } } } @@ -846,10 +862,29 @@ private static string NormalizeRequiredContentType(string? contentType, string d return builder.Count > 0 ? builder.ToEquatableImmutable() : null; } - private static bool IsGeneratedAttribute(string fullyQualifiedName, string attributeName) + private static bool IsGeneratedAttribute(INamedTypeSymbol attributeClass, string attributeName) { - var prefix = $"global::{AttributesNamespace}.{attributeName}"; - return fullyQualifiedName.StartsWith(prefix, StringComparison.Ordinal); + var definition = attributeClass.OriginalDefinition; + return definition.Name == attributeName && IsInNamespace(definition.ContainingNamespace, AttributesNamespaceParts); + } + + private static bool IsAttribute(INamedTypeSymbol attributeClass, string attributeName, string[] namespaceParts) + { + var definition = attributeClass.OriginalDefinition; + return definition.Name == attributeName && IsInNamespace(definition.ContainingNamespace, namespaceParts); + } + + private static bool IsInNamespace(INamespaceSymbol? namespaceSymbol, string[] namespaceParts) + { + for (var i = namespaceParts.Length - 1; i >= 0; i--) + { + if (namespaceSymbol is null || namespaceSymbol.Name != namespaceParts[i]) + return false; + + namespaceSymbol = namespaceSymbol.ContainingNamespace; + } + + return namespaceSymbol is null || namespaceSymbol.IsGlobalNamespace; } private static void TryAddAcceptsMetadata( From 11615fdf91788b33874cdc646434b0f6dfc7d4cd Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:53:38 -0500 Subject: [PATCH 28/75] Cleanup. --- tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj index 54af1ee..623130f 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj @@ -42,6 +42,9 @@ GeneratedEndpointsTests.cs + + GeneratedEndpointsTests.cs + From a91cb9610201fdbd545fe5b4c483e5635c5703fe Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:06:24 -0500 Subject: [PATCH 29/75] Fixed FromKeyedServices. --- .../Common/TypeSymbolExtensions.cs | 4 ++-- .../GetUserEndpoint.cs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs b/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs index 1258fbc..4548136 100644 --- a/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs +++ b/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs @@ -138,10 +138,10 @@ public static bool IsFromKeyedServicesAttribute(this ITypeSymbol symbol) MetadataName: "FromKeyedServicesAttribute", ContainingNamespace: { - Name: "Mvc", + Name: "DependencyInjection", ContainingNamespace: { - Name: "AspNetCore", + Name: "Extensions", ContainingNamespace: { Name: "Microsoft", diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index cedc5e8..cd1a146 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -5,43 +5,43 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; namespace GeneratedEndpoints.Tests.Lab; [Tags("Users", "Profiles")] [RequireAuthorization("Users.Read", "Administrators")] [DisableAntiforgery] -internal static class GetUserEndpoint +internal sealed class GetUserEndpoint(IServiceCollection services) { [Tags("Featured")] [AllowAnonymous] [Accepts("application/json", "application/xml", RequestType = typeof(GetUserRequest))] [Accepts("application/json", "application/xml", IsOptional = true)] - [ProducesResponse( StatusCodes.Status200OK, "application/json", ResponseType = typeof(UserProfile))] + [ProducesResponse(StatusCodes.Status200OK, "application/json", ResponseType = typeof(UserProfile))] [ProducesResponse(StatusCodes.Status202Accepted, "application/json")] [ProducesProblem(StatusCodes.Status500InternalServerError, "application/problem+json")] [ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")] [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] - public static Results, NotFound, ValidationProblem, ProblemHttpResult> GetUser([FromQuery] int id) + public Results, NotFound, ValidationProblem, ProblemHttpResult> GetUser( + [FromQuery] int id, + [FromKeyedServices(ServiceLifetime.Scoped)] IServiceCollection services + ) { if (id <= 0) { var errors = new Dictionary { - [nameof(id)] = ["The ID must be greater than zero."] + [nameof(id)] = ["The ID must be greater than zero."], }; return TypedResults.ValidationProblem(errors); } if (id == 13) - { return TypedResults.Problem("User data is temporarily unavailable."); - } if (id == 404) - { return TypedResults.NotFound(); - } var profile = new UserProfile(id, $"User {id}", $"user{id}@example.com"); return TypedResults.Ok(profile); From 7c3a82bdef55f852ae62bf65984ba2269100a07d Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:10:56 -0500 Subject: [PATCH 30/75] Muted warnings. --- tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index cd1a146..0239f56 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Generated.Attributes; @@ -7,12 +8,15 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +// ReSharper disable UnusedParameter.Global +#pragma warning disable CS9113 // Parameter is unread. + namespace GeneratedEndpoints.Tests.Lab; [Tags("Users", "Profiles")] [RequireAuthorization("Users.Read", "Administrators")] [DisableAntiforgery] -internal sealed class GetUserEndpoint(IServiceCollection services) +internal sealed class GetUserEndpoint(IServiceProvider serviceProvider) { [Tags("Featured")] [AllowAnonymous] From ff8b8c102e5264edf9179e9722726bce88f27e68 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:18:24 -0500 Subject: [PATCH 31/75] Preserve binding attribute names (#27) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 67 ++++++++++++++++--- ...ndpointHandlers_WithNamespace.verified.txt | 30 +++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 30 +++++++++ ...ndpointHandlers_WithNamespace.verified.txt | 2 +- ...ointHandlers_WithoutNamespace.verified.txt | 2 +- .../GeneratedEndpointsTests.cs | 26 +++++++ 6 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 527a710..854edde 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1212,6 +1212,7 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM var source = BindingSource.None; TypedConstant? typedKey = null; + string? bindingName = null; var attributes = parameter.GetAttributes(); foreach (var attribute in attributes) @@ -1221,15 +1222,27 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM continue; if (attributeClass.IsFromRouteAttribute()) + { source = BindingSource.FromRoute; + bindingName = GetBindingAttributeName(attribute) ?? bindingName; + } if (attributeClass.IsFromQueryAttribute()) + { source = BindingSource.FromQuery; + bindingName = GetBindingAttributeName(attribute) ?? bindingName; + } if (attributeClass.IsFromHeaderAttribute()) + { source = BindingSource.FromHeader; + bindingName = GetBindingAttributeName(attribute) ?? bindingName; + } if (attributeClass.IsFromBodyAttribute()) source = BindingSource.FromBody; if (attributeClass.IsFromFormAttribute()) + { source = BindingSource.FromForm; + bindingName = GetBindingAttributeName(attribute) ?? bindingName; + } if (attributeClass.IsFromServicesAttribute()) source = BindingSource.FromServices; if (attributeClass.IsFromKeyedServicesAttribute()) @@ -1244,12 +1257,40 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM var parameterName = parameter.Name; var parameterType = parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var key = typedKey.HasValue ? ConstLiteral(typedKey.Value) : null; - methodParameters.Add(new Parameter(parameterName, parameterType, source, key)); + methodParameters.Add(new Parameter(parameterName, parameterType, source, key, bindingName)); } return methodParameters.ToEquatableImmutableArray(); } + private static string? GetBindingAttributeName(AttributeData attribute) + { + foreach (var namedArg in attribute.NamedArguments) + { + if (string.Equals(namedArg.Key, NameAttributeNamedParameter, StringComparison.Ordinal) + && namedArg.Value.Value is string namedValue) + { + var normalized = NormalizeBindingName(namedValue); + if (normalized is not null) + return normalized; + } + } + + if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string constructorName) + return NormalizeBindingName(constructorName); + + return null; + } + + private static string? NormalizeBindingName(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + var trimmed = value!.Trim(); + return trimmed.Length > 0 ? trimmed : null; + } + private static void GenerateSource(SourceProductionContext context, ImmutableArray requestHandlers) { var sorted = requestHandlers.OrderBy(r => r.Class.Name, StringComparer.Ordinal) @@ -1431,7 +1472,7 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl foreach (var parameter in requestHandler.Method.Parameters) { source.Append(", "); - source.Append(GetBindingSourceAttribute(parameter.Source, parameter.Key)); + source.Append(GetBindingSourceAttribute(parameter.Source, parameter.Key, parameter.BindingName)); source.Append(parameter.Type); source.Append(' '); source.Append(parameter.Name); @@ -1621,16 +1662,16 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl }; } - private static string GetBindingSourceAttribute(BindingSource source, string? key) + private static string GetBindingSourceAttribute(BindingSource source, string? key, string? bindingName) { return source switch { BindingSource.None => "", - BindingSource.FromRoute => "[FromRoute] ", - BindingSource.FromQuery => "[FromQuery] ", - BindingSource.FromHeader => "[FromHeader] ", - BindingSource.FromBody => "[FromBody] ", - BindingSource.FromForm => "[FromForm] ", + BindingSource.FromRoute => FormatBindingAttribute("FromRoute", bindingName), + BindingSource.FromQuery => FormatBindingAttribute("FromQuery", bindingName), + BindingSource.FromHeader => FormatBindingAttribute("FromHeader", bindingName), + BindingSource.FromBody => FormatBindingAttribute("FromBody", bindingName), + BindingSource.FromForm => FormatBindingAttribute("FromForm", bindingName), BindingSource.FromServices => "[FromServices] ", BindingSource.FromKeyedServices => $"[FromKeyedServices({key})] ", BindingSource.AsParameters => "[AsParameters] ", @@ -1638,6 +1679,14 @@ private static string GetBindingSourceAttribute(BindingSource source, string? ke }; } + private static string FormatBindingAttribute(string attributeName, string? bindingName) + { + if (bindingName is null) + return $"[{attributeName}] "; + + return $"[{attributeName}(Name = {StringLiteral(bindingName)})] "; + } + private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray requestHandlers) { const int baseSize = 4096; @@ -1841,7 +1890,7 @@ private readonly record struct ProducesMetadata( private readonly record struct ProducesValidationProblemMetadata(int StatusCode, string? ContentType, EquatableImmutableArray? AdditionalContentTypes); - private readonly record struct Parameter(string Name, string Type, BindingSource Source, string? Key); + private readonly record struct Parameter(string Name, string Type, BindingSource Source, string? Key, string? BindingName); private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..f743001 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/binding/{routeId}", static ([FromServices] global::GeneratedEndpointsTests.BindingNameEndpoints handler, [FromRoute(Name = "route-id")] int routeId, [FromQuery(Name = "filter-term")] string filter, [FromHeader(Name = "x-custom-header")] string traceId) => handler.Handle(routeId, filter, traceId)) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..2b881cc --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/binding/{routeId}", static ([FromServices] global::BindingNameEndpoints handler, [FromRoute(Name = "route-id")] int routeId, [FromQuery(Name = "filter-term")] string filter, [FromHeader(Name = "x-custom-header")] string traceId) => handler.Handle(routeId, filter, traceId)) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt index 42d7e07..7a88dc5 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt @@ -49,7 +49,7 @@ internal static class EndpointRouteBuilderExtensions builder.MapPut("/complex/{id:int}", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.UpdateComplex) .WithName("UpdateComplex"); - builder.MapGet("/complex/{id:int}", static async ([FromServices] global::GeneratedEndpointsTests.ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader] string traceId, [FromBody] global::GeneratedEndpointsTests.GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, object keyed, [AsParameters] global::GeneratedEndpointsTests.AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) + builder.MapGet("/complex/{id:int}", static async ([FromServices] global::GeneratedEndpointsTests.ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader(Name = "x-trace-id")] string traceId, [FromBody] global::GeneratedEndpointsTests.GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, [FromKeyedServices("special")] object keyed, [AsParameters] global::GeneratedEndpointsTests.AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) .WithName("GetComplex") .WithSummary("Gets complex data.") .WithDescription("Uses every supported attribute.") diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt index 44f2ee6..2506682 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -49,7 +49,7 @@ internal static class EndpointRouteBuilderExtensions builder.MapPut("/complex/{id:int}", global::AllHttpMethodEndpoints.UpdateComplex) .WithName("UpdateComplex"); - builder.MapGet("/complex/{id:int}", static async ([FromServices] global::ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader] string traceId, [FromBody] global::GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, object keyed, [AsParameters] global::AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) + builder.MapGet("/complex/{id:int}", static async ([FromServices] global::ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader(Name = "x-trace-id")] string traceId, [FromBody] global::GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, [FromKeyedServices("special")] object keyed, [AsParameters] global::AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) .WithName("GetComplex") .WithSummary("Gets complex data.") .WithDescription("Uses every supported attribute.") diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 2a7750c..c04283d 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -122,6 +122,32 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(ClassAllowAnonymousMethodRequireAuthorization)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task BindingAttributeNamesArePreserved(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + using Microsoft.AspNetCore.Mvc; + + internal sealed class BindingNameEndpoints + { + [MapGet("/binding/{routeId}")] + public Ok Handle( + [FromRoute(Name = "route-id")] int routeId, + [FromQuery(Name = "filter-term")] string filter, + [FromHeader(Name = "x-custom-header")] string traceId) + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(BindingAttributeNamesArePreserved)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 18a6dfe28c943f273edf21985368ae54c4120072 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:33:14 -0500 Subject: [PATCH 32/75] Restrict metadata attributes to methods (#28) --- README.md | 20 +++++++++---------- src/GeneratedEndpoints/MinimalApiGenerator.cs | 12 +++++------ .../GeneratedEndpointsTests.cs | 12 +++++------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1b6fc1c..a6b147d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ GeneratedEndpoints focuses on three goals: * **Attribute-driven routing** – use `[MapGet]`, `[MapPost]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapPatch]`, `[MapTrace]`, `[MapConnect]`, and even `[MapQuery]` to describe the verb and route pattern. The generator creates the matching `Map*` call and wires up metadata like `.WithName`, `.WithSummary`, and `.WithDescription`. * **Feature-first organization** – keep handlers close to the code they execute (for example, alongside your `Todos` feature). Non-static handler classes are automatically registered with dependency injection so you can inject EF Core DbContexts, services, etc. -* **Metadata composition** – decorate classes and methods with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, `[AllowAnonymous]`, `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]`. Class-level metadata is merged into every method, while method-level metadata can refine or override. +* **Metadata composition** – decorate classes and methods with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, `[AllowAnonymous]`, and `[ExcludeFromDescription]`. Apply `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` directly to the methods they describe. Class-level metadata is merged into every method, while method-level metadata can refine or override. The generator also emits the `AddEndpointHandlers` and `MapEndpointHandlers` extension methods that do all the registration work for you. @@ -78,7 +78,7 @@ Key ideas: * Choose the attribute that matches the verb (`[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`). * Named arguments like `Summary`, `Description`, and `Name` are translated into `.WithSummary`, `.WithDescription`, and `.WithName` calls. * Use existing ASP.NET Core binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromHeader]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator preserves them in the produced delegate. -* Metadata attributes (`[Tags]`, `[RequireAuthorization]`, `[AllowAnonymous]`, `[DisableAntiforgery]`) can be placed on the class, on a method, or on both. Class-level metadata is merged with method-level metadata. +* Metadata attributes (`[Tags]`, `[RequireAuthorization]`, `[AllowAnonymous]`, `[DisableAntiforgery]`, `[ExcludeFromDescription]`) can be placed on the class, on a method, or on both. Class-level metadata is merged with method-level metadata. Request/response attributes (`[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, `[ProducesValidationProblem]`) must be applied directly to the method they describe. ### 3. Register handlers and map endpoints @@ -226,12 +226,12 @@ The `Configure` method is emitted once per handler class, so every endpoint in t GeneratedEndpoints ships with attribute helpers for request/response metadata. They keep your OpenAPI description in sync with the implementation. ```csharp -[Accepts("application/json", "application/xml")] -[ProducesResponse(StatusCodes.Status201Created)] -[ProducesProblem(StatusCodes.Status500InternalServerError)] public sealed class CreateTodo { [MapPost("/todos", Summary = "Create a todo")] + [Accepts("application/json", "application/xml")] + [ProducesResponse(StatusCodes.Status201Created)] + [ProducesProblem(StatusCodes.Status500InternalServerError)] [ProducesValidationProblem(StatusCodes.Status400BadRequest)] public static Created Handle([FromBody] CreateTodoRequest request) => TypedResults.Created($"/todos/{request.Id}", request.ToTodo()); @@ -240,7 +240,7 @@ public sealed class CreateTodo * Use the generic form when the request/response type is known at compile time. For runtime types, set `RequestType` on `[Accepts]` and `ResponseType` on `[ProducesResponse]`. * Mark `IsOptional = true` on `[Accepts]` to call `.Accepts(..., isOptional: true)`. -* Multiple `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` attributes can be applied to the same method or class. The generator creates every corresponding `.Accepts` or `.Produces` call. +* Multiple `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` attributes can be applied to the same method. The generator creates every corresponding `.Accepts` or `.Produces` call. --- @@ -254,10 +254,10 @@ public sealed class CreateTodo | `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | | `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint. | | `[ExcludeFromDescription]` | Class or method | Generates `.ExcludeFromDescription()` so the endpoint is hidden from OpenAPI/metadata. | -| `[Accepts]` / `[Accepts]` | Class or method | Emits `.Accepts(contentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes allowed. | -| `[ProducesResponse]` / `[ProducesResponse]` | Class or method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. | -| `[ProducesProblem]` | Class or method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. | -| `[ProducesValidationProblem]` | Class or method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)`. | +| `[Accepts]` / `[Accepts]` | Method | Emits `.Accepts(contentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes allowed. | +| `[ProducesResponse]` / `[ProducesResponse]` | Method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. | +| `[ProducesProblem]` | Method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. | +| `[ProducesValidationProblem]` | Method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)`. | > ℹ️ Metadata defined on a class is applied to every annotated method inside the class. Method-level attributes can add entries (tags, accepts, produces, etc.) or override boolean flags like `[AllowAnonymous]`. diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 854edde..c358dad 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -211,7 +211,7 @@ namespace {{AttributesNamespace}}; /// /// Specifies the request type and content types accepted by the annotated endpoint or class. /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] internal sealed class {{AcceptsAttributeName}} : global::System.Attribute { /// @@ -250,7 +250,7 @@ internal sealed class {{AcceptsAttributeName}} : global::System.Attribute /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. /// /// The CLR type of the request body. - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] internal sealed class {{AcceptsAttributeName}} : global::System.Attribute { /// @@ -297,7 +297,7 @@ namespace {{AttributesNamespace}}; /// /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute { /// @@ -338,7 +338,7 @@ internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribu /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. /// /// The CLR type of the response body. - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute { /// @@ -387,7 +387,7 @@ namespace {{AttributesNamespace}}; /// /// Specifies that the endpoint produces a problem details payload. /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute { /// @@ -431,7 +431,7 @@ namespace {{AttributesNamespace}}; /// /// Specifies that the endpoint produces a validation problem details payload. /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute { /// diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index c04283d..a85e830 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -164,10 +164,6 @@ public async Task MapAllAttributesAndHttpMethods(bool withNamespace) [Tags("Shared", "ClassLevel")] [RequireAuthorization("PolicyA", "PolicyB")] [DisableAntiforgery] - [Accepts("application/xml", "text/xml", RequestType = typeof(ClassLevelRequest))] - [ProducesResponse(201, "application/json", "text/json", ResponseType = typeof(ClassLevelResponse))] - [ProducesProblem(503, "application/problem+json")] - [ProducesValidationProblem(409, "application/problem+json", "text/plain")] [ExcludeFromDescription] internal sealed class ComplexEndpoints { @@ -187,9 +183,13 @@ public static void Configure(TBuilder builder, IServiceProvider servic [AllowAnonymous] [Tags("MethodLevel")] [RequireAuthorization("MethodPolicy")] - [Accepts("application/custom", "text/custom")] - [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] + [Accepts("application/xml", "text/xml", RequestType = typeof(ClassLevelRequest))] + [Accepts("application/custom", "text/custom")] + [ProducesResponse(201, "application/json", "text/json", ResponseType = typeof(ClassLevelResponse))] + [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] + [ProducesProblem(503, "application/problem+json")] [ProducesProblem(400, "application/problem+json", "text/plain")] + [ProducesValidationProblem(409, "application/problem+json", "text/plain")] [ProducesValidationProblem(422, "application/problem+json", "text/plain")] [ExcludeFromDescription] public async Task, NotFound>> GetComplex( From 6b072fbbb59767a6bb8e5825a426b40a77170e21 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:43:12 -0500 Subject: [PATCH 33/75] Add RequireCors and rate limiting support (#29) --- README.md | 4 + src/GeneratedEndpoints/MinimalApiGenerator.cs | 168 +++++++++++++++++- ...ndpointHandlers_WithNamespace.verified.txt | 35 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 35 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 32 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 32 ++++ .../GeneratedEndpointsTests.cs | 49 +++++ 7 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/README.md b/README.md index a6b147d..319b2f7 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,8 @@ public sealed class CreateTodo | `[Tags]` | Class or method | Adds tags to one or more endpoints. Multiple attributes merge without duplication. | | `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Passing policies (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) emits `.RequireAuthorization("Todos.Read", "Todos.Write")`. | | `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | +| `[RequireCors]` | Class or method | Adds `.RequireCors()` or `.RequireCors("PolicyName")` when a specific policy is provided. | +| `[RequireRateLimiting]` | Class or method | Adds `.RequireRateLimiting("PolicyName")` to enforce a named rate limiting policy. | | `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint. | | `[ExcludeFromDescription]` | Class or method | Generates `.ExcludeFromDescription()` so the endpoint is hidden from OpenAPI/metadata. | | `[Accepts]` / `[Accepts]` | Method | Emits `.Accepts(contentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes allowed. | @@ -269,6 +271,8 @@ public sealed class CreateTodo * `[RequireAuthorization]` adds `.RequireAuthorization()` or `.RequireAuthorization("policy")`. * `[AllowAnonymous]` opt-in overrides class or global authorization requirements. +* `[RequireCors]` emits `.RequireCors()` or `.RequireCors("policy")` so endpoints participate in a configured CORS policy. +* `[RequireRateLimiting]` emits `.RequireRateLimiting("policy")` to enforce ASP.NET Core rate limiting middleware. * `[DisableAntiforgery]` wires `.DisableAntiforgery()` for CSRF-sensitive endpoints. ### Handling query objects with `[AsParameters]` diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index c358dad..685d147 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -47,6 +47,14 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string RequireAuthorizationAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireAuthorizationAttributeName}"; private const string RequireAuthorizationAttributeHint = $"{RequireAuthorizationAttributeFullyQualifiedName}.gs.cs"; + private const string RequireCorsAttributeName = "RequireCorsAttribute"; + private const string RequireCorsAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireCorsAttributeName}"; + private const string RequireCorsAttributeHint = $"{RequireCorsAttributeFullyQualifiedName}.gs.cs"; + + private const string RequireRateLimitingAttributeName = "RequireRateLimitingAttribute"; + private const string RequireRateLimitingAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireRateLimitingAttributeName}"; + private const string RequireRateLimitingAttributeHint = $"{RequireRateLimitingAttributeFullyQualifiedName}.gs.cs"; + private const string DisableAntiforgeryAttributeName = "DisableAntiforgeryAttribute"; private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; @@ -185,6 +193,70 @@ internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Att """; context.AddSource(RequireAuthorizationAttributeHint, SourceText.From(requireAuthorizationSource, Encoding.UTF8)); + // RequireCors + var requireCorsSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires a configured CORS policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional CORS policy name. + /// + public string? PolicyName { get; } + + /// + /// Marks the endpoint or class as requiring the default CORS policy. + /// + public {{RequireCorsAttributeName}}() + { + } + + /// + /// Marks the endpoint or class as requiring the specified named CORS policy. + /// + public {{RequireCorsAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + """; + context.AddSource(RequireCorsAttributeHint, SourceText.From(requireCorsSource, Encoding.UTF8)); + + // RequireRateLimiting + var requireRateLimitingSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires the provided rate limiting policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The rate limiting policy to apply. + public {{RequireRateLimitingAttributeName}}(string policyName) + { + PolicyName = policyName; + } + + /// + /// Gets the rate limiting policy name. + /// + public string PolicyName { get; } + } + """; + context.AddSource(RequireRateLimitingAttributeHint, SourceText.From(requireRateLimitingSource, Encoding.UTF8)); + // DisableAntiforgery var disableAntiforgerySource = $$""" {{FileHeader}} @@ -538,7 +610,8 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (httpMethod, pattern, name, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, - accepts, produces, producesProblem, producesValidationProblem) + accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requireRateLimiting, + rateLimitingPolicyName) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -556,7 +629,8 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke ); var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, - authorizationPolicies, disableAntiforgery, allowAnonymous + authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requireRateLimiting, + rateLimitingPolicyName ); return requestHandler; @@ -626,7 +700,11 @@ private static ( EquatableImmutableArray? accepts, EquatableImmutableArray? produces, EquatableImmutableArray? producesProblem, - EquatableImmutableArray? producesValidationProblem + EquatableImmutableArray? producesValidationProblem, + bool requireCors, + string? corsPolicyName, + bool requireRateLimiting, + string? rateLimitingPolicyName ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -637,6 +715,10 @@ private static ( bool? disableAntiforgery = null; bool? allowAnonymous = null; bool? excludeFromDescription = null; + bool? requireCors = null; + string? corsPolicyName = null; + bool? requireRateLimiting = null; + string? rateLimitingPolicyName = null; List? accepts = null; List? produces = null; @@ -658,6 +740,10 @@ private static ( ref produces, ref producesProblem, ref producesValidationProblem, + ref requireCors, + ref corsPolicyName, + ref requireRateLimiting, + ref rateLimitingPolicyName, ref classHasAllowAnonymousAttribute, ref classHasRequireAuthorizationAttribute ); @@ -677,6 +763,10 @@ ref classHasRequireAuthorizationAttribute ref produces, ref producesProblem, ref producesValidationProblem, + ref requireCors, + ref corsPolicyName, + ref requireRateLimiting, + ref rateLimitingPolicyName, ref methodHasAllowAnonymousAttribute, ref methodHasRequireAuthorizationAttribute ); @@ -694,7 +784,11 @@ ref methodHasRequireAuthorizationAttribute ToEquatableOrNull(accepts), ToEquatableOrNull(produces), ToEquatableOrNull(producesProblem), - ToEquatableOrNull(producesValidationProblem) + ToEquatableOrNull(producesValidationProblem), + requireCors ?? false, + corsPolicyName, + requireRateLimiting ?? false, + rateLimitingPolicyName ); } @@ -710,6 +804,10 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref List? produces, ref List? producesProblem, ref List? producesValidationProblem, + ref bool? requireCors, + ref string? corsPolicyName, + ref bool? requireRateLimiting, + ref string? rateLimitingPolicyName, ref bool hasAllowAnonymousAttribute, ref bool hasRequireAuthorizationAttribute ) @@ -772,6 +870,30 @@ ref bool hasRequireAuthorizationAttribute continue; } + if (IsGeneratedAttribute(attributeClass, RequireCorsAttributeName)) + { + requireCors = true; + corsPolicyName = attribute.ConstructorArguments.Length > 0 + ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) + : null; + continue; + } + + if (IsGeneratedAttribute(attributeClass, RequireRateLimitingAttributeName)) + { + var policyName = attribute.ConstructorArguments.Length > 0 + ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) + : null; + + if (!string.IsNullOrEmpty(policyName)) + { + requireRateLimiting = true; + rateLimitingPolicyName = policyName; + } + + continue; + } + if (IsGeneratedAttribute(attributeClass, DisableAntiforgeryAttributeName)) { disableAntiforgery = true; @@ -847,6 +969,11 @@ private static string NormalizeRequiredContentType(string? contentType, string d return string.IsNullOrWhiteSpace(contentType) ? null : contentType!.Trim(); } + private static string? NormalizeOptionalString(string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); + } + private static EquatableImmutableArray? GetStringArrayValues(TypedConstant typedConstant) { if (typedConstant.Kind != TypedConstantKind.Array || typedConstant.Values.IsDefaultOrEmpty) @@ -1384,6 +1511,8 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine("using Microsoft.AspNetCore.Http;"); source.AppendLine("using Microsoft.AspNetCore.Mvc;"); source.AppendLine("using Microsoft.AspNetCore.Routing;"); + if (requestHandlers.Any(static handler => handler.RequireRateLimiting)) + source.AppendLine("using Microsoft.AspNetCore.RateLimiting;"); source.AppendLine("using Microsoft.Extensions.DependencyInjection;"); source.AppendLine(); @@ -1616,6 +1745,31 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } } + if (requestHandler.RequireCors) + { + source.AppendLine(); + source.Append(continuationIndent); + if (!string.IsNullOrEmpty(requestHandler.CorsPolicyName)) + { + source.Append(".RequireCors("); + source.Append(StringLiteral(requestHandler.CorsPolicyName)); + source.Append(')'); + } + else + { + source.Append(".RequireCors()"); + } + } + + if (requestHandler.RequireRateLimiting && !string.IsNullOrEmpty(requestHandler.RateLimitingPolicyName)) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".RequireRateLimiting("); + source.Append(StringLiteral(requestHandler.RateLimitingPolicyName)); + source.Append(')'); + } + if (requestHandler.DisableAntiforgery) { source.AppendLine(); @@ -1849,7 +2003,11 @@ private readonly record struct RequestHandler( bool RequireAuthorization, EquatableImmutableArray? AuthorizationPolicies, bool DisableAntiforgery, - bool AllowAnonymous + bool AllowAnonymous, + bool RequireCors, + string? CorsPolicyName, + bool RequireRateLimiting, + string? RateLimitingPolicyName ); private readonly record struct RequestHandlerClass( diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..ddca12b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/cors/default", global::GeneratedEndpointsTests.CorsEndpoints.GetDefault) + .WithName("GetDefault") + .RequireCors(); + + builder.MapGet("/cors/named", global::GeneratedEndpointsTests.CorsEndpoints.GetNamed) + .WithName("GetNamed") + .RequireCors("NamedCorsPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..a9493f5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/cors/default", global::CorsEndpoints.GetDefault) + .WithName("GetDefault") + .RequireCors(); + + builder.MapGet("/cors/named", global::CorsEndpoints.GetNamed) + .WithName("GetNamed") + .RequireCors("NamedCorsPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..582534c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/rate-limited", global::GeneratedEndpointsTests.RateLimitedEndpoints.Get) + .WithName("Get") + .RequireRateLimiting("NamedRateLimitPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..de795af --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/rate-limited", global::RateLimitedEndpoints.Get) + .WithName("Get") + .RequireRateLimiting("NamedRateLimitPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index a85e830..6cc5a98 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -122,6 +122,55 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(ClassAllowAnonymousMethodRequireAuthorization)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RequireCorsAttributes(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + internal sealed class CorsEndpoints + { + [MapGet("/cors/default")] + [RequireCors] + public static Ok GetDefault() + => TypedResults.Ok(); + + [MapGet("/cors/named")] + [RequireCors("NamedCorsPolicy")] + public static Ok GetNamed() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(RequireCorsAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RequireRateLimitingAttribute(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + internal sealed class RateLimitedEndpoints + { + [MapGet("/rate-limited")] + [RequireRateLimiting("NamedRateLimitPolicy")] + public static Ok Get() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(RequireRateLimitingAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From ae9f2b376b1da560cfddcd7284cba5834e67470e Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:54:11 -0500 Subject: [PATCH 34/75] Strip global prefix from fully qualified endpoint names (#30) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 61 +++++++++++++++++++ ...ndpointHandlers_WithNamespace.verified.txt | 33 ++++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 33 ++++++++++ .../GeneratedEndpointsTests.cs | 28 +++++++++ 4 files changed, 155 insertions(+) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 685d147..297efc0 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using System.Text; using GeneratedEndpoints.Common; using Microsoft.CodeAnalysis; @@ -1426,10 +1427,70 @@ private static void GenerateSource(SourceProductionContext context, ImmutableArr .ThenBy(r => r.Pattern, StringComparer.Ordinal) .ToImmutableArray(); + sorted = EnsureUniqueEndpointNames(sorted); + GenerateAddEndpointHandlersClass(context, sorted); GenerateUseEndpointHandlersClass(context, sorted); } + private static ImmutableArray EnsureUniqueEndpointNames(ImmutableArray requestHandlers) + { + var collidingHandlers = GetRequestHandlersWithNameCollisions(requestHandlers); + if (collidingHandlers.IsEmpty) + return requestHandlers; + + var builder = requestHandlers.ToBuilder(); + foreach (var index in collidingHandlers) + { + var handler = builder[index]; + var metadata = handler.Metadata with { Name = GetFullyQualifiedMethodDisplayName(handler) }; + builder[index] = handler with { Metadata = metadata }; + } + + return builder.MoveToImmutable(); + } + + private static ImmutableHashSet GetRequestHandlersWithNameCollisions(ImmutableArray requestHandlers) + { + var collidingIndices = ImmutableHashSet.CreateBuilder(); + + var groups = requestHandlers + .Select((handler, index) => (handler, index)) + .Where(static tuple => !string.IsNullOrEmpty(tuple.handler.Metadata.Name)) + .GroupBy(static tuple => tuple.handler.Metadata.Name!, StringComparer.Ordinal); + + foreach (var group in groups) + { + if (group.Count() <= 1) + continue; + + var collidingMethodGroups = group + .GroupBy(static tuple => tuple.handler.Method.Name, StringComparer.Ordinal) + .Where(static methodGroup => methodGroup.Skip(1).Any()); + + foreach (var methodGroup in collidingMethodGroups) + { + foreach (var entry in methodGroup) + collidingIndices.Add(entry.index); + } + } + + return collidingIndices.ToImmutable(); + } + + private static string GetFullyQualifiedMethodDisplayName(RequestHandler requestHandler) + { + var className = requestHandler.Class.Name; + const string GlobalPrefix = "global::"; + if (className.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + className = className.Substring(GlobalPrefix.Length); + + if (className.IndexOf('+') >= 0) + className = className.Replace('+', '.'); + + return string.Concat(className, ".", requestHandler.Method.Name); + } + private static void GenerateAddEndpointHandlersClass(SourceProductionContext context, ImmutableArray requestHandlers) { var source = GetAddEndpointHandlersStringBuilder(requestHandlers); diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..9b5bd26 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/first", global::GeneratedEndpointsTests.FirstEndpoint.Handle) + .WithName("GeneratedEndpointsTests.FirstEndpoint.Handle"); + + builder.MapGet("/second", global::GeneratedEndpointsTests.SecondEndpoint.Handle) + .WithName("GeneratedEndpointsTests.SecondEndpoint.Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..26d3618 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/first", global::FirstEndpoint.Handle) + .WithName("FirstEndpoint.Handle"); + + builder.MapGet("/second", global::SecondEndpoint.Handle) + .WithName("SecondEndpoint.Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 6cc5a98..9979baf 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -40,6 +40,34 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(MapGet)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MethodsWithSameNameAreFullyQualifiedWhenNamesCollide(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + internal sealed class FirstEndpoint + { + [MapGet("/first")] + public static Ok Handle() + => TypedResults.Ok(); + } + + internal sealed class SecondEndpoint + { + [MapGet("/second")] + public static Ok Handle() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MethodsWithSameNameAreFullyQualifiedWhenNamesCollide)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 07544804a8341c64fa657152d83343653e20a3fa Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:14:18 -0500 Subject: [PATCH 35/75] Add support for EndpointFilter attributes (#31) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 120 +++++++++++++++++- ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 32 +++++ ...ointHandlers_WithoutNamespace.verified.txt | 32 +++++ ...ndpointHandlers_WithNamespace.verified.txt | 23 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 23 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 23 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 23 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++ ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++ .../GeneratedEndpointsTests.cs | 59 +++++++++ 18 files changed, 568 insertions(+), 7 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 297efc0..f289283 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -62,6 +62,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; + private const string EndpointFilterAttributeName = "EndpointFilterAttribute"; + private const string EndpointFilterAttributeFullyQualifiedName = $"{AttributesNamespace}.{EndpointFilterAttributeName}"; + private const string EndpointFilterAttributeHint = $"{EndpointFilterAttributeFullyQualifiedName}.gs.cs"; + private const string AcceptsAttributeName = "AcceptsAttribute"; private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}"; private const string AcceptsAttributeHint = $"{AcceptsAttributeFullyQualifiedName}.gs.cs"; @@ -361,6 +365,49 @@ internal sealed class {{AcceptsAttributeName}} : global::System.Attrib """; context.AddSource(AcceptsAttributeHint, SourceText.From(acceptsSource, Encoding.UTF8)); + // EndpointFilter + var endpointFilterSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies an endpoint filter type to apply to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The CLR type of the endpoint filter. + public {{EndpointFilterAttributeName}}(global::System.Type filterType) + { + FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); + } + } + + /// + /// Specifies an endpoint filter type using a generic argument. + /// + /// The CLR type of the endpoint filter. + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType => typeof(TFilter); + } + + """; + context.AddSource(EndpointFilterAttributeHint, SourceText.From(endpointFilterSource, Encoding.UTF8)); + // Produces var producesSource = $$""" {{FileHeader}} @@ -612,7 +659,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requireRateLimiting, - rateLimitingPolicyName) + rateLimitingPolicyName, endpointFilterTypes) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -631,7 +678,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requireRateLimiting, - rateLimitingPolicyName + rateLimitingPolicyName, endpointFilterTypes ); return requestHandler; @@ -705,7 +752,8 @@ private static ( bool requireCors, string? corsPolicyName, bool requireRateLimiting, - string? rateLimitingPolicyName + string? rateLimitingPolicyName, + EquatableImmutableArray? endpointFilterTypes ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -720,6 +768,7 @@ private static ( string? corsPolicyName = null; bool? requireRateLimiting = null; string? rateLimitingPolicyName = null; + List? endpointFilters = null; List? accepts = null; List? produces = null; @@ -745,6 +794,7 @@ private static ( ref corsPolicyName, ref requireRateLimiting, ref rateLimitingPolicyName, + ref endpointFilters, ref classHasAllowAnonymousAttribute, ref classHasRequireAuthorizationAttribute ); @@ -768,6 +818,7 @@ ref classHasRequireAuthorizationAttribute ref corsPolicyName, ref requireRateLimiting, ref rateLimitingPolicyName, + ref endpointFilters, ref methodHasAllowAnonymousAttribute, ref methodHasRequireAuthorizationAttribute ); @@ -789,7 +840,8 @@ ref methodHasRequireAuthorizationAttribute requireCors ?? false, corsPolicyName, requireRateLimiting ?? false, - rateLimitingPolicyName + rateLimitingPolicyName, + ToEquatableOrNull(endpointFilters) ); } @@ -809,6 +861,7 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref string? corsPolicyName, ref bool? requireRateLimiting, ref string? rateLimitingPolicyName, + ref List? endpointFilters, ref bool hasAllowAnonymousAttribute, ref bool hasRequireAuthorizationAttribute ) @@ -895,6 +948,12 @@ ref bool hasRequireAuthorizationAttribute continue; } + if (IsGeneratedAttribute(attributeClass, EndpointFilterAttributeName)) + { + TryAddEndpointFilter(attribute, attributeClass, ref endpointFilters); + continue; + } + if (IsGeneratedAttribute(attributeClass, DisableAntiforgeryAttributeName)) { disableAntiforgery = true; @@ -1099,6 +1158,38 @@ private static void TryAddProducesMetadata( producesList.Add(new ProducesMetadata(responseType, statusCode, contentType, additionalContentTypes)); } + private static void TryAddEndpointFilter( + AttributeData attribute, + INamedTypeSymbol attributeClass, + ref List? endpointFilters) + { + if (attributeClass is { IsGenericType: true, TypeArguments.Length: 1 }) + { + TryAddEndpointFilterType(attributeClass.TypeArguments[0], ref endpointFilters); + return; + } + + if (attribute.ConstructorArguments.Length == 0) + return; + + if (attribute.ConstructorArguments[0].Value is ITypeSymbol filterTypeSymbol) + TryAddEndpointFilterType(filterTypeSymbol, ref endpointFilters); + } + + private static void TryAddEndpointFilterType(ITypeSymbol? typeSymbol, ref List? endpointFilters) + { + if (typeSymbol is null or ITypeParameterSymbol or IErrorTypeSymbol) + return; + + var displayString = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + if (string.IsNullOrWhiteSpace(displayString)) + return; + + var filters = endpointFilters ??= []; + if (!filters.Contains(displayString)) + filters.Add(displayString); + } + private static ITypeSymbol? GetNamedTypeSymbol(AttributeData attribute, string namedParameter) { foreach (var namedArg in attribute.NamedArguments) @@ -1198,7 +1289,6 @@ CancellationToken cancellationToken var hasConfigureMethod = false; var acceptsServiceProvider = false; - foreach (var member in classSymbol.GetMembers(ConfigureMethodName)) { cancellationToken.ThrowIfCancellationRequested(); @@ -1845,6 +1935,18 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(".AllowAnonymous()"); } + if (requestHandler.EndpointFilterTypes is { Count: > 0 }) + { + foreach (var filterType in requestHandler.EndpointFilterTypes.Value) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".AddEndpointFilter<"); + source.Append(filterType); + source.Append(">()"); + } + } + if (wrapWithConfigure && configureAcceptsServiceProvider) { source.AppendLine(","); @@ -2068,7 +2170,8 @@ private readonly record struct RequestHandler( bool RequireCors, string? CorsPolicyName, bool RequireRateLimiting, - string? RateLimitingPolicyName + string? RateLimitingPolicyName, + EquatableImmutableArray? EndpointFilterTypes ); private readonly record struct RequestHandlerClass( @@ -2111,7 +2214,10 @@ private readonly record struct ProducesMetadata( private readonly record struct Parameter(string Name, string Type, BindingSource Source, string? Key, string? BindingName); - private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); + private readonly record struct ConfigureMethodDetails( + bool HasConfigureMethod, + bool ConfigureMethodAcceptsServiceProvider + ); private enum BindingSource { diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..b871436 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..0adf868 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..e40e3c0 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..d185d47 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..023508b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..b8ee47d --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..28dfea3 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/filters", global::GeneratedEndpointsTests.FilteredEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + .AddEndpointFilter(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..412dd52 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/filters", global::FilteredEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + .AddEndpointFilter(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..c0a7b4c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..7368a8b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..e86ba94 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..80b9edc --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 9979baf..e879ebd 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -93,6 +93,9 @@ public static void Configure(TBuilder builder) var result = TestHelpers.RunGenerator(sources); + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapGetWithConfigure)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(MapGetWithConfigure)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } @@ -123,6 +126,9 @@ public static void Configure(TBuilder builder, System.IServiceProvider var result = TestHelpers.RunGenerator(sources); + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapGetWithConfigureServiceProvider)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(MapGetWithConfigureServiceProvider)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } @@ -146,6 +152,9 @@ public static Ok Handle() var result = TestHelpers.RunGenerator(sources); + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(ClassAllowAnonymousMethodRequireAuthorization)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(ClassAllowAnonymousMethodRequireAuthorization)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } @@ -173,6 +182,9 @@ public static Ok GetNamed() var result = TestHelpers.RunGenerator(sources); + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(RequireCorsAttributes)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(RequireCorsAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } @@ -195,10 +207,54 @@ public static Ok Get() var result = TestHelpers.RunGenerator(sources); + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(RequireRateLimitingAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(RequireRateLimitingAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConfigureRegistersEndpointFilters(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + + [EndpointFilter(typeof(TimingFilter))] + internal sealed class FilteredEndpoints + { + [MapGet("/filters")] + [EndpointFilter] + public static Ok Handle() + => TypedResults.Ok(); + } + + internal sealed class TimingFilter : IEndpointFilter + { + public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + => next(context); + } + + internal sealed class ValidationFilter : IEndpointFilter + { + public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + => next(context); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(ConfigureRegistersEndpointFilters)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(ConfigureRegistersEndpointFilters)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -221,6 +277,9 @@ public Ok Handle( var result = TestHelpers.RunGenerator(sources); + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(BindingAttributeNamesArePreserved)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(BindingAttributeNamesArePreserved)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } From dcac3597ce643a3ba30fb6c462fbeb420cc3c90e Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:23:41 -0500 Subject: [PATCH 36/75] Add short circuit and request timeout attributes (#32) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 199 +++++++++++++++++- ...ndpointHandlers_WithNamespace.verified.txt | 23 ++ ...ointHandlers_WithoutNamespace.verified.txt | 23 ++ ...ndpointHandlers_WithNamespace.verified.txt | 57 +++++ ...ointHandlers_WithoutNamespace.verified.txt | 57 +++++ .../GeneratedEndpointsTests.cs | 62 ++++++ 6 files changed, 413 insertions(+), 8 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index f289283..99b6d5c 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -43,6 +43,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string ResponseTypeAttributeNamedParameter = "ResponseType"; private const string RequestTypeAttributeNamedParameter = "RequestType"; private const string IsOptionalAttributeNamedParameter = "IsOptional"; + private const string PolicyNameAttributeNamedParameter = "PolicyName"; private const string RequireAuthorizationAttributeName = "RequireAuthorizationAttribute"; private const string RequireAuthorizationAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireAuthorizationAttributeName}"; @@ -60,6 +61,18 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; + private const string ShortCircuitAttributeName = "ShortCircuitAttribute"; + private const string ShortCircuitAttributeFullyQualifiedName = $"{AttributesNamespace}.{ShortCircuitAttributeName}"; + private const string ShortCircuitAttributeHint = $"{ShortCircuitAttributeFullyQualifiedName}.gs.cs"; + + private const string DisableRequestTimeoutAttributeName = "DisableRequestTimeoutAttribute"; + private const string DisableRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableRequestTimeoutAttributeName}"; + private const string DisableRequestTimeoutAttributeHint = $"{DisableRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; + + private const string WithRequestTimeoutAttributeName = "WithRequestTimeoutAttribute"; + private const string WithRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithRequestTimeoutAttributeName}"; + private const string WithRequestTimeoutAttributeHint = $"{WithRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; + private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; private const string EndpointFilterAttributeName = "EndpointFilterAttribute"; @@ -279,6 +292,77 @@ internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attri """; context.AddSource(DisableAntiforgeryAttributeHint, SourceText.From(disableAntiforgerySource, Encoding.UTF8)); + // ShortCircuit + var shortCircuitSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Marks the annotated endpoint or class to short-circuit the request pipeline. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute + { + } + + """; + context.AddSource(ShortCircuitAttributeHint, SourceText.From(shortCircuitSource, Encoding.UTF8)); + + // DisableRequestTimeout + var disableRequestTimeoutSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Disables the request timeout for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute + { + } + + """; + context.AddSource(DisableRequestTimeoutAttributeHint, SourceText.From(disableRequestTimeoutSource, Encoding.UTF8)); + + // WithRequestTimeout + var withRequestTimeoutSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Applies the request timeout metadata to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{WithRequestTimeoutAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional request timeout policy name. + /// + public string? PolicyName { get; init; } + + /// + /// Applies the default request timeout behavior. + /// + public {{WithRequestTimeoutAttributeName}}() + { + } + + /// + /// Applies the specified request timeout policy. + /// + /// The request timeout policy name. + public {{WithRequestTimeoutAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + + """; + context.AddSource(WithRequestTimeoutAttributeHint, SourceText.From(withRequestTimeoutSource, Encoding.UTF8)); + // Accepts var acceptsSource = $$""" {{FileHeader}} @@ -659,7 +743,8 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requireRateLimiting, - rateLimitingPolicyName, endpointFilterTypes) + rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, + requestTimeoutPolicyName) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -678,7 +763,8 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requireRateLimiting, - rateLimitingPolicyName, endpointFilterTypes + rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, + requestTimeoutPolicyName ); return requestHandler; @@ -753,7 +839,11 @@ private static ( string? corsPolicyName, bool requireRateLimiting, string? rateLimitingPolicyName, - EquatableImmutableArray? endpointFilterTypes + EquatableImmutableArray? endpointFilterTypes, + bool shortCircuit, + bool disableRequestTimeout, + bool withRequestTimeout, + string? requestTimeoutPolicyName ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -769,6 +859,10 @@ private static ( bool? requireRateLimiting = null; string? rateLimitingPolicyName = null; List? endpointFilters = null; + bool? shortCircuit = null; + bool? disableRequestTimeout = null; + bool? withRequestTimeout = null; + string? requestTimeoutPolicyName = null; List? accepts = null; List? produces = null; @@ -796,7 +890,11 @@ private static ( ref rateLimitingPolicyName, ref endpointFilters, ref classHasAllowAnonymousAttribute, - ref classHasRequireAuthorizationAttribute + ref classHasRequireAuthorizationAttribute, + ref shortCircuit, + ref disableRequestTimeout, + ref withRequestTimeout, + ref requestTimeoutPolicyName ); var methodAttributes = methodSymbol.GetAttributes(); @@ -820,7 +918,11 @@ ref classHasRequireAuthorizationAttribute ref rateLimitingPolicyName, ref endpointFilters, ref methodHasAllowAnonymousAttribute, - ref methodHasRequireAuthorizationAttribute + ref methodHasRequireAuthorizationAttribute, + ref shortCircuit, + ref disableRequestTimeout, + ref withRequestTimeout, + ref requestTimeoutPolicyName ); if (methodHasRequireAuthorizationAttribute && !methodHasAllowAnonymousAttribute) @@ -841,7 +943,11 @@ ref methodHasRequireAuthorizationAttribute corsPolicyName, requireRateLimiting ?? false, rateLimitingPolicyName, - ToEquatableOrNull(endpointFilters) + ToEquatableOrNull(endpointFilters), + shortCircuit ?? false, + disableRequestTimeout ?? false, + withRequestTimeout ?? false, + (withRequestTimeout ?? false) ? requestTimeoutPolicyName : null ); } @@ -863,7 +969,11 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref string? rateLimitingPolicyName, ref List? endpointFilters, ref bool hasAllowAnonymousAttribute, - ref bool hasRequireAuthorizationAttribute + ref bool hasRequireAuthorizationAttribute, + ref bool? shortCircuit, + ref bool? disableRequestTimeout, + ref bool? withRequestTimeout, + ref string? requestTimeoutPolicyName ) { foreach (var attribute in attributes) @@ -872,6 +982,35 @@ ref bool hasRequireAuthorizationAttribute if (attributeClass is null) continue; + if (IsGeneratedAttribute(attributeClass, ShortCircuitAttributeName)) + { + shortCircuit = true; + continue; + } + + if (IsGeneratedAttribute(attributeClass, DisableRequestTimeoutAttributeName)) + { + disableRequestTimeout = true; + withRequestTimeout = false; + requestTimeoutPolicyName = null; + continue; + } + + if (IsGeneratedAttribute(attributeClass, WithRequestTimeoutAttributeName)) + { + disableRequestTimeout = false; + withRequestTimeout = true; + + string? policyName = null; + if (attribute.ConstructorArguments.Length > 0) + policyName = attribute.ConstructorArguments[0].Value as string; + + policyName ??= GetNamedStringValue(attribute, PolicyNameAttributeNamedParameter); + + requestTimeoutPolicyName = NormalizeOptionalString(policyName); + continue; + } + if (IsGeneratedAttribute(attributeClass, AcceptsAttributeName)) { TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); @@ -1212,6 +1351,17 @@ private static bool GetNamedBoolValue(AttributeData attribute, string namedParam return defaultValue; } + private static string? GetNamedStringValue(AttributeData attribute, string namedParameter) + { + foreach (var namedArg in attribute.NamedArguments) + { + if (namedArg.Key == namedParameter && namedArg.Value.Value is string stringValue) + return NormalizeOptionalString(stringValue); + } + + return null; + } + private static EquatableImmutableArray MergeUnion(EquatableImmutableArray? existing, IEnumerable values) { var list = new List(); @@ -1935,6 +2085,35 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(".AllowAnonymous()"); } + if (requestHandler.ShortCircuit) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".ShortCircuit()"); + } + + if (requestHandler.DisableRequestTimeout) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".DisableRequestTimeout()"); + } + else if (requestHandler.WithRequestTimeout) + { + source.AppendLine(); + source.Append(continuationIndent); + if (!string.IsNullOrEmpty(requestHandler.RequestTimeoutPolicyName)) + { + source.Append(".WithRequestTimeout("); + source.Append(StringLiteral(requestHandler.RequestTimeoutPolicyName)); + source.Append(')'); + } + else + { + source.Append(".WithRequestTimeout()"); + } + } + if (requestHandler.EndpointFilterTypes is { Count: > 0 }) { foreach (var filterType in requestHandler.EndpointFilterTypes.Value) @@ -2171,7 +2350,11 @@ private readonly record struct RequestHandler( string? CorsPolicyName, bool RequireRateLimiting, string? RateLimitingPolicyName, - EquatableImmutableArray? EndpointFilterTypes + EquatableImmutableArray? EndpointFilterTypes, + bool ShortCircuit, + bool DisableRequestTimeout, + bool WithRequestTimeout, + string? RequestTimeoutPolicyName ); private readonly record struct RequestHandlerClass( diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..44e4209 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/timeouts/class-disable", global::GeneratedEndpointsTests.ClassLevelDisableRequestTimeoutEndpoints.ClassDisable) + .WithName("ClassDisable") + .DisableRequestTimeout(); + + builder.MapGet("/timeouts/class-default", global::GeneratedEndpointsTests.ClassLevelTimeoutEndpoints.ClassDefault) + .WithName("ClassDefault") + .ShortCircuit() + .WithRequestTimeout(); + + builder.MapGet("/timeouts/class-override", global::GeneratedEndpointsTests.ClassLevelTimeoutEndpoints.ClassOverride) + .WithName("ClassOverride") + .ShortCircuit() + .WithRequestTimeout("ClassPolicy"); + + builder.MapGet("/timeouts/method-disable", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodDisable) + .WithName("MethodDisable") + .DisableRequestTimeout(); + + builder.MapGet("/timeouts/method-short", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodShortCircuit) + .WithName("MethodShortCircuit") + .ShortCircuit(); + + builder.MapGet("/timeouts/method-default", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodWithDefault) + .WithName("MethodWithDefault") + .WithRequestTimeout(); + + builder.MapGet("/timeouts/method-policy", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodWithPolicy) + .WithName("MethodWithPolicy") + .WithRequestTimeout("MethodPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..b08328c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/timeouts/class-disable", global::ClassLevelDisableRequestTimeoutEndpoints.ClassDisable) + .WithName("ClassDisable") + .DisableRequestTimeout(); + + builder.MapGet("/timeouts/class-default", global::ClassLevelTimeoutEndpoints.ClassDefault) + .WithName("ClassDefault") + .ShortCircuit() + .WithRequestTimeout(); + + builder.MapGet("/timeouts/class-override", global::ClassLevelTimeoutEndpoints.ClassOverride) + .WithName("ClassOverride") + .ShortCircuit() + .WithRequestTimeout("ClassPolicy"); + + builder.MapGet("/timeouts/method-disable", global::MethodLevelTimeoutEndpoints.MethodDisable) + .WithName("MethodDisable") + .DisableRequestTimeout(); + + builder.MapGet("/timeouts/method-short", global::MethodLevelTimeoutEndpoints.MethodShortCircuit) + .WithName("MethodShortCircuit") + .ShortCircuit(); + + builder.MapGet("/timeouts/method-default", global::MethodLevelTimeoutEndpoints.MethodWithDefault) + .WithName("MethodWithDefault") + .WithRequestTimeout(); + + builder.MapGet("/timeouts/method-policy", global::MethodLevelTimeoutEndpoints.MethodWithPolicy) + .WithName("MethodWithPolicy") + .WithRequestTimeout("MethodPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index e879ebd..ec7383e 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -214,6 +214,68 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(RequireRateLimitingAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ShortCircuitAndRequestTimeoutAttributes(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + [ShortCircuit] + [WithRequestTimeout] + internal static class ClassLevelTimeoutEndpoints + { + [MapGet("/timeouts/class-default")] + public static Ok ClassDefault() + => TypedResults.Ok(); + + [MapGet("/timeouts/class-override")] + [WithRequestTimeout("ClassPolicy")] + public static Ok ClassOverride() + => TypedResults.Ok(); + } + + [DisableRequestTimeout] + internal static class ClassLevelDisableRequestTimeoutEndpoints + { + [MapGet("/timeouts/class-disable")] + public static Ok ClassDisable() + => TypedResults.Ok(); + } + + internal static class MethodLevelTimeoutEndpoints + { + [MapGet("/timeouts/method-disable")] + [DisableRequestTimeout] + public static Ok MethodDisable() + => TypedResults.Ok(); + + [MapGet("/timeouts/method-default")] + [WithRequestTimeout] + public static Ok MethodWithDefault() + => TypedResults.Ok(); + + [MapGet("/timeouts/method-policy")] + [WithRequestTimeout("MethodPolicy")] + public static Ok MethodWithPolicy() + => TypedResults.Ok(); + + [MapGet("/timeouts/method-short")] + [ShortCircuit] + public static Ok MethodShortCircuit() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(ShortCircuitAndRequestTimeoutAttributes)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(ShortCircuitAndRequestTimeoutAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 54fa01f63314208d5aa283869a409c0d8cb9b0f3 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:35:48 -0500 Subject: [PATCH 37/75] Add MapFallback endpoint support (#33) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 67 +++++++++++++------ ...ndpointHandlers_WithNamespace.verified.txt | 23 +++++++ ...ointHandlers_WithoutNamespace.verified.txt | 23 +++++++ ...ndpointHandlers_WithNamespace.verified.txt | 33 +++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 33 +++++++++ .../GeneratedEndpointsTests.cs | 28 ++++++++ 6 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 99b6d5c..9cf698a 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -20,6 +20,8 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private static readonly string[] AspNetCoreAuthorizationNamespaceParts = new[] { "Microsoft", "AspNetCore", "Authorization" }; private static readonly string[] AspNetCoreRoutingNamespaceParts = new[] { "Microsoft", "AspNetCore", "Routing" }; + private const string FallbackHttpMethod = "__FALLBACK__"; + private static readonly ImmutableArray HttpAttributeDefinitions = [ CreateHttpAttributeDefinition("MapGetAttribute", "GET"), @@ -32,6 +34,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), + CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod, allowEmptyPattern: true), ]; private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = @@ -123,10 +126,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator #nullable enable """; - private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb) + private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb, bool allowEmptyPattern = false) { var fullyQualifiedName = $"{AttributesNamespace}.{attributeName}"; - return new HttpAttributeDefinition(attributeName, fullyQualifiedName, $"{fullyQualifiedName}.gs.cs", verb); + return new HttpAttributeDefinition(attributeName, fullyQualifiedName, $"{fullyQualifiedName}.gs.cs", verb, allowEmptyPattern); } public void Initialize(IncrementalGeneratorInitializationContext context) @@ -170,7 +173,9 @@ private static void RegisterAttributes(IncrementalGeneratorPostInitializationCon { foreach (var definition in HttpAttributeDefinitions) { - var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, definition.Name, definition.Verb); + var summaryVerb = definition.Verb == FallbackHttpMethod ? "fallback" : definition.Verb; + var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, definition.Name, summaryVerb, + definition.AllowEmptyPattern); context.AddSource(definition.Hint, SourceText.From(source, Encoding.UTF8)); } @@ -671,8 +676,14 @@ internal sealed class {{ProducesValidationProblemAttributeName}} : global::Syste context.AddSource(ProducesValidationProblemAttributeHint, SourceText.From(producesValidationProblemSource, Encoding.UTF8)); } - private static string GenerateHttpAttributeSource(string fileHeader, string attributesNamespace, string attributeName, string summaryVerb) + private static string GenerateHttpAttributeSource( + string fileHeader, + string attributesNamespace, + string attributeName, + string summaryVerb, + bool allowEmptyPattern) { + var patternDefaultValue = allowEmptyPattern ? " = \\\"\\\"" : string.Empty; return $$""" {{fileHeader}} @@ -708,10 +719,10 @@ internal sealed class {{attributeName}} : global::System.Attribute /// Initializes a new instance of the class. /// /// The route pattern for the endpoint. - public {{attributeName}}([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) - { - Pattern = pattern; - } + public {{attributeName}}([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern{{patternDefaultValue}}) + { + Pattern = pattern; + } } """; } @@ -1871,19 +1882,32 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.AppendLine("("); } - var mapMethodSuffix = GetMapMethodSuffix(requestHandler.HttpMethod); + var isFallback = string.Equals(requestHandler.HttpMethod, FallbackHttpMethod, StringComparison.Ordinal); + var mapMethodSuffix = isFallback ? null : GetMapMethodSuffix(requestHandler.HttpMethod); source.Append(indent); - source.Append("builder.Map"); - source.Append(mapMethodSuffix ?? "Methods"); - source.Append('('); - source.Append(StringLiteral(requestHandler.Pattern)); - source.Append(", "); - if (mapMethodSuffix is null) + if (isFallback) { - source.Append("new[] { \""); - source.Append(requestHandler.HttpMethod); - source.Append("\" }, "); + source.Append("builder.MapFallback("); + if (!string.IsNullOrEmpty(requestHandler.Pattern)) + { + source.Append(StringLiteral(requestHandler.Pattern)); + source.Append(", "); + } + } + else + { + source.Append("builder.Map"); + source.Append(mapMethodSuffix ?? "Methods"); + source.Append('('); + source.Append(StringLiteral(requestHandler.Pattern)); + source.Append(", "); + if (mapMethodSuffix is null) + { + source.Append("new[] { \""); + source.Append(requestHandler.HttpMethod); + source.Append("\" }, "); + } } if (requestHandler.Method.IsStatic) { @@ -2334,7 +2358,12 @@ _ when char.IsControl(c) => "\\u" + ((int)c).ToString("x4", CultureInfo.Invarian }; } - private readonly record struct HttpAttributeDefinition(string Name, string FullyQualifiedName, string Hint, string Verb); + private readonly record struct HttpAttributeDefinition( + string Name, + string FullyQualifiedName, + string Hint, + string Verb, + bool AllowEmptyPattern); private readonly record struct RequestHandler( RequestHandlerClass Class, diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..0740514 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback("/custom-fallback", global::GeneratedEndpointsTests.FallbackEndpoints.Custom) + .WithName("Custom"); + + builder.MapFallback(global::GeneratedEndpointsTests.FallbackEndpoints.Default) + .WithName("Default"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..9e29623 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback("/custom-fallback", global::FallbackEndpoints.Custom) + .WithName("Custom"); + + builder.MapFallback(global::FallbackEndpoints.Default) + .WithName("Default"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index ec7383e..cb9ba21 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -11,6 +11,34 @@ public GeneratedEndpointsTests() ModuleInitializer.Initialize(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapFallback(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + internal static class FallbackEndpoints + { + [MapFallback] + public static Ok Default() + => TypedResults.Ok(); + + [MapFallback("/custom-fallback")] + public static Ok Custom() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapFallback)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(MapFallback)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 4bae24a6c44e42c87582921f28c807aac5ee9bc4 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:44:53 -0500 Subject: [PATCH 38/75] Add support for WithOrder attribute (#34) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 86 ++++++++++++++++--- ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++++ ...ndpointHandlers_WithNamespace.verified.txt | 35 ++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 35 ++++++++ .../GeneratedEndpointsTests.cs | 30 +++++++ 6 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 9cf698a..3e1dcd6 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -76,6 +76,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string WithRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithRequestTimeoutAttributeName}"; private const string WithRequestTimeoutAttributeHint = $"{WithRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; + private const string WithOrderAttributeName = "WithOrderAttribute"; + private const string WithOrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithOrderAttributeName}"; + private const string WithOrderAttributeHint = $"{WithOrderAttributeFullyQualifiedName}.gs.cs"; + private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; private const string EndpointFilterAttributeName = "EndpointFilterAttribute"; @@ -333,9 +337,9 @@ internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.At // WithRequestTimeout var withRequestTimeoutSource = $$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; /// /// Applies the request timeout metadata to the annotated endpoint or class. @@ -362,15 +366,45 @@ internal sealed class {{WithRequestTimeoutAttributeName}} : global::System.Attri public {{WithRequestTimeoutAttributeName}}(string policyName) { PolicyName = policyName; - } - } + } + } - """; + """; context.AddSource(WithRequestTimeoutAttributeHint, SourceText.From(withRequestTimeoutSource, Encoding.UTF8)); + // WithOrder + var withOrderSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the order for the annotated endpoint when building conventions. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{WithOrderAttributeName}} : global::System.Attribute + { + /// + /// Gets the order that will be applied to the endpoint. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The order value to apply to the endpoint. + public {{WithOrderAttributeName}}(int order) + { + Order = order; + } + } + + """; + context.AddSource(WithOrderAttributeHint, SourceText.From(withOrderSource, Encoding.UTF8)); + // Accepts var acceptsSource = $$""" - {{FileHeader}} + {{FileHeader}} namespace {{AttributesNamespace}}; @@ -755,7 +789,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, - requestTimeoutPolicyName) + requestTimeoutPolicyName, order) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -775,7 +809,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, - requestTimeoutPolicyName + requestTimeoutPolicyName, order ); return requestHandler; @@ -854,7 +888,8 @@ private static ( bool shortCircuit, bool disableRequestTimeout, bool withRequestTimeout, - string? requestTimeoutPolicyName + string? requestTimeoutPolicyName, + int? order ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -874,6 +909,7 @@ private static ( bool? disableRequestTimeout = null; bool? withRequestTimeout = null; string? requestTimeoutPolicyName = null; + int? order = null; List? accepts = null; List? produces = null; @@ -905,7 +941,8 @@ private static ( ref shortCircuit, ref disableRequestTimeout, ref withRequestTimeout, - ref requestTimeoutPolicyName + ref requestTimeoutPolicyName, + ref order ); var methodAttributes = methodSymbol.GetAttributes(); @@ -933,7 +970,8 @@ ref requestTimeoutPolicyName ref shortCircuit, ref disableRequestTimeout, ref withRequestTimeout, - ref requestTimeoutPolicyName + ref requestTimeoutPolicyName, + ref order ); if (methodHasRequireAuthorizationAttribute && !methodHasAllowAnonymousAttribute) @@ -958,7 +996,8 @@ ref requestTimeoutPolicyName shortCircuit ?? false, disableRequestTimeout ?? false, withRequestTimeout ?? false, - (withRequestTimeout ?? false) ? requestTimeoutPolicyName : null + (withRequestTimeout ?? false) ? requestTimeoutPolicyName : null, + order ); } @@ -984,7 +1023,8 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref bool? shortCircuit, ref bool? disableRequestTimeout, ref bool? withRequestTimeout, - ref string? requestTimeoutPolicyName + ref string? requestTimeoutPolicyName, + ref int? order ) { foreach (var attribute in attributes) @@ -1022,6 +1062,14 @@ ref string? requestTimeoutPolicyName continue; } + if (IsGeneratedAttribute(attributeClass, WithOrderAttributeName)) + { + if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int orderValue) + order = orderValue; + + continue; + } + if (IsGeneratedAttribute(attributeClass, AcceptsAttributeName)) { TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); @@ -1975,6 +2023,15 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(')'); } + if (requestHandler.Order is { } orderValue) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".WithOrder("); + source.Append(orderValue); + source.Append(')'); + } + if (requestHandler.Metadata.ExcludeFromDescription) { source.AppendLine(); @@ -2383,7 +2440,8 @@ private readonly record struct RequestHandler( bool ShortCircuit, bool DisableRequestTimeout, bool WithRequestTimeout, - string? RequestTimeoutPolicyName + string? RequestTimeoutPolicyName, + int? Order ); private readonly record struct RequestHandlerClass( diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..1324ce4 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..fbd0298 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..96d1a7d --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/ordered/high", global::GeneratedEndpointsTests.OrderedEndpoints.High) + .WithName("High") + .WithOrder(5); + + builder.MapGet("/ordered/low", global::GeneratedEndpointsTests.OrderedEndpoints.Low) + .WithName("Low") + .WithOrder(-1); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..835f83f --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/ordered/high", global::OrderedEndpoints.High) + .WithName("High") + .WithOrder(5); + + builder.MapGet("/ordered/low", global::OrderedEndpoints.Low) + .WithName("Low") + .WithOrder(-1); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index cb9ba21..56afde5 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -304,6 +304,36 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(ShortCircuitAndRequestTimeoutAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WithOrderAttribute(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + internal sealed class OrderedEndpoints + { + [MapGet("/ordered/low")] + [WithOrder(-1)] + public static Ok Low() + => TypedResults.Ok(); + + [MapGet("/ordered/high")] + [WithOrder(5)] + public static Ok High() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(WithOrderAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(WithOrderAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 54e4ee119f2f5a5d47718bcdcebe9caea6a5860d Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:54:03 -0500 Subject: [PATCH 39/75] Add DisplayName metadata support (#35) --- README.md | 6 +-- src/GeneratedEndpoints/MinimalApiGenerator.cs | 37 +++++++++++++++++-- ...ndpointHandlers_WithNamespace.verified.txt | 1 + ...ointHandlers_WithoutNamespace.verified.txt | 1 + ...ndpointHandlers_WithNamespace.verified.txt | 1 + ...ointHandlers_WithoutNamespace.verified.txt | 1 + .../GeneratedEndpointsTests.cs | 4 +- 7 files changed, 43 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 319b2f7..f54a343 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ GeneratedEndpoints is a .NET source generator that automatically wires Minimal A GeneratedEndpoints focuses on three goals: -* **Attribute-driven routing** – use `[MapGet]`, `[MapPost]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapPatch]`, `[MapTrace]`, `[MapConnect]`, and even `[MapQuery]` to describe the verb and route pattern. The generator creates the matching `Map*` call and wires up metadata like `.WithName`, `.WithSummary`, and `.WithDescription`. +* **Attribute-driven routing** – use `[MapGet]`, `[MapPost]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapPatch]`, `[MapTrace]`, `[MapConnect]`, and even `[MapQuery]` to describe the verb and route pattern. The generator creates the matching `Map*` call and wires up metadata like `.WithName`, `.WithDisplayName`, `.WithSummary`, and `.WithDescription`. * **Feature-first organization** – keep handlers close to the code they execute (for example, alongside your `Todos` feature). Non-static handler classes are automatically registered with dependency injection so you can inject EF Core DbContexts, services, etc. * **Metadata composition** – decorate classes and methods with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, `[AllowAnonymous]`, and `[ExcludeFromDescription]`. Apply `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` directly to the methods they describe. Class-level metadata is merged into every method, while method-level metadata can refine or override. @@ -76,7 +76,7 @@ public sealed class GetTodo Key ideas: * Choose the attribute that matches the verb (`[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`). -* Named arguments like `Summary`, `Description`, and `Name` are translated into `.WithSummary`, `.WithDescription`, and `.WithName` calls. +* Named arguments like `DisplayName`, `Summary`, `Description`, and `Name` are translated into `.WithDisplayName`, `.WithSummary`, `.WithDescription`, and `.WithName` calls. * Use existing ASP.NET Core binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromHeader]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator preserves them in the produced delegate. * Metadata attributes (`[Tags]`, `[RequireAuthorization]`, `[AllowAnonymous]`, `[DisableAntiforgery]`, `[ExcludeFromDescription]`) can be placed on the class, on a method, or on both. Class-level metadata is merged with method-level metadata. Request/response attributes (`[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, `[ProducesValidationProblem]`) must be applied directly to the method they describe. @@ -248,7 +248,7 @@ public sealed class CreateTodo | Attribute | Scope | Purpose | | --- | --- | --- | -| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName`, `.WithSummary`, and `.WithDescription` calls. | +| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName`, `.WithDisplayName`, `.WithSummary`, and `.WithDescription` calls. | | `[Tags]` | Class or method | Adds tags to one or more endpoints. Multiple attributes merge without duplication. | | `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Passing policies (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) emits `.RequireAuthorization("Todos.Read", "Todos.Write")`. | | `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 3e1dcd6..808e84b 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -41,6 +41,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); private const string NameAttributeNamedParameter = "Name"; + private const string DisplayNameAttributeNamedParameter = "DisplayName"; private const string SummaryAttributeNamedParameter = "Summary"; private const string DescriptionAttributeNamedParameter = "Description"; private const string ResponseTypeAttributeNamedParameter = "ResponseType"; @@ -739,6 +740,11 @@ internal sealed class {{attributeName}} : global::System.Attribute /// public string Name { get; set; } = ""; + /// + /// Gets or sets the endpoint display name. + /// + public string DisplayName { get; set; } = ""; + /// /// Gets or sets the endpoint summary. /// @@ -784,7 +790,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var requestHandlerMethod = GetRequestHandlerMethod(requestHandlerMethodSymbol, cancellationToken); - var (httpMethod, pattern, name, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); + var (httpMethod, pattern, name, displayName, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requireRateLimiting, @@ -796,6 +802,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var metadata = new RequestHandlerMetadata( name, + displayName, summary, description, tags, @@ -823,7 +830,14 @@ private static string RemoveAsyncSuffix(string methodName) return methodName; } - private static (string HttpMethod, string Pattern, string? Name, string? Summary, string? Description) GetRequestHandlerAttribute( + private static ( + string HttpMethod, + string Pattern, + string? Name, + string? DisplayName, + string? Summary, + string? Description + ) GetRequestHandlerAttribute( AttributeData attribute, CancellationToken cancellationToken ) @@ -839,6 +853,7 @@ CancellationToken cancellationToken var pattern = (attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : "") ?? ""; string? name = null; + string? displayName = null; string? summary = null; string? description = null; foreach (var namedArg in attribute.NamedArguments) @@ -851,6 +866,12 @@ CancellationToken cancellationToken name = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); break; } + case DisplayNameAttributeNamedParameter: + { + var value = namedArg.Value.Value as string; + displayName = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); + break; + } case SummaryAttributeNamedParameter: { var value = namedArg.Value.Value as string; @@ -866,7 +887,7 @@ CancellationToken cancellationToken } } - return (httpMethod, pattern, name, summary, description); + return (httpMethod, pattern, name, displayName, summary, description); } private static ( @@ -2005,6 +2026,15 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(')'); } + if (!string.IsNullOrEmpty(requestHandler.Metadata.DisplayName)) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".WithDisplayName("); + source.Append(StringLiteral(requestHandler.Metadata.DisplayName)); + source.Append(')'); + } + if (!string.IsNullOrEmpty(requestHandler.Metadata.Summary)) { source.AppendLine(); @@ -2455,6 +2485,7 @@ bool ConfigureMethodAcceptsServiceProvider private readonly record struct RequestHandlerMetadata( string? Name, + string? DisplayName, string? Summary, string? Description, EquatableImmutableArray? Tags, diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt index 7a88dc5..23b3d69 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt @@ -51,6 +51,7 @@ internal static class EndpointRouteBuilderExtensions builder.MapGet("/complex/{id:int}", static async ([FromServices] global::GeneratedEndpointsTests.ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader(Name = "x-trace-id")] string traceId, [FromBody] global::GeneratedEndpointsTests.GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, [FromKeyedServices("special")] object keyed, [AsParameters] global::GeneratedEndpointsTests.AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) .WithName("GetComplex") + .WithDisplayName("Complex data endpoint") .WithSummary("Gets complex data.") .WithDescription("Uses every supported attribute.") .ExcludeFromDescription() diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt index 2506682..6707d74 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -51,6 +51,7 @@ internal static class EndpointRouteBuilderExtensions builder.MapGet("/complex/{id:int}", static async ([FromServices] global::ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader(Name = "x-trace-id")] string traceId, [FromBody] global::GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, [FromKeyedServices("special")] object keyed, [AsParameters] global::AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) .WithName("GetComplex") + .WithDisplayName("Complex data endpoint") .WithSummary("Gets complex data.") .WithDescription("Uses every supported attribute.") .ExcludeFromDescription() diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt index 9f4ba4d..92fffc3 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt @@ -24,6 +24,7 @@ internal static class EndpointRouteBuilderExtensions { builder.MapGet("/users/{id:int}", global::GeneratedEndpointsTests.GetUserEndpoint.GetUser2) .WithName("GetUser") + .WithDisplayName("User lookup endpoint") .WithSummary("Gets a user by ID.") .WithDescription("Gets a user by ID when the ID is greater than zero.") .WithTags("Users"); diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt index 828367c..6960286 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -24,6 +24,7 @@ internal static class EndpointRouteBuilderExtensions { builder.MapGet("/users/{id:int}", global::GetUserEndpoint.GetUser2) .WithName("GetUser") + .WithDisplayName("User lookup endpoint") .WithSummary("Gets a user by ID.") .WithDescription("Gets a user by ID when the ID is greater than zero.") .WithTags("Users"); diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 56afde5..7d10b9e 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -48,7 +48,7 @@ public async Task MapGet(bool withNamespace) [Tags("Users")] internal static class GetUserEndpoint { - [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] + [MapGet("/users/{id:int}", Name = nameof(GetUser), DisplayName = "User lookup endpoint", Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] public static Results GetUser2(int id) { if (id > 0) @@ -435,7 +435,7 @@ public static void Configure(TBuilder builder, IServiceProvider servic builder.WithMetadata("configured"); } - [MapGet("/complex/{id:int}", Name = nameof(GetComplex), Summary = "Gets complex data.", Description = "Uses every supported attribute.")] + [MapGet("/complex/{id:int}", Name = nameof(GetComplex), DisplayName = "Complex data endpoint", Summary = "Gets complex data.", Description = "Uses every supported attribute.")] [AllowAnonymous] [Tags("MethodLevel")] [RequireAuthorization("MethodPolicy")] From 7f36a10f8b3cb6e0506ecd8690e3c2009d996b73 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:57:00 -0500 Subject: [PATCH 40/75] Add WithGroupName attribute support (#36) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 78 +++++++++++++++++-- ...ndpointHandlers_WithNamespace.verified.txt | 23 ++++++ ...ointHandlers_WithoutNamespace.verified.txt | 23 ++++++ ...ndpointHandlers_WithNamespace.verified.txt | 35 +++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 35 +++++++++ .../GeneratedEndpointsTests.cs | 29 +++++++ 6 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 808e84b..29db4de 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -81,6 +81,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string WithOrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithOrderAttributeName}"; private const string WithOrderAttributeHint = $"{WithOrderAttributeFullyQualifiedName}.gs.cs"; + private const string WithGroupNameAttributeName = "WithGroupNameAttribute"; + private const string WithGroupNameAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithGroupNameAttributeName}"; + private const string WithGroupNameAttributeHint = $"{WithGroupNameAttributeFullyQualifiedName}.gs.cs"; + private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; private const string EndpointFilterAttributeName = "EndpointFilterAttribute"; @@ -403,6 +407,36 @@ internal sealed class {{WithOrderAttributeName}} : global::System.Attribute """; context.AddSource(WithOrderAttributeHint, SourceText.From(withOrderSource, Encoding.UTF8)); + // WithGroupName + var withGroupNameSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the endpoint group name for the annotated class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal sealed class {{WithGroupNameAttributeName}} : global::System.Attribute + { + /// + /// Gets the endpoint group name. + /// + public string EndpointGroupName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The endpoint group name to apply. + public {{WithGroupNameAttributeName}}(string endpointGroupName) + { + EndpointGroupName = endpointGroupName; + } + } + + """; + context.AddSource(WithGroupNameAttributeHint, SourceText.From(withGroupNameSource, Encoding.UTF8)); + // Accepts var acceptsSource = $$""" {{FileHeader}} @@ -795,7 +829,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, - requestTimeoutPolicyName, order) + requestTimeoutPolicyName, order, endpointGroupName) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -816,7 +850,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, - requestTimeoutPolicyName, order + requestTimeoutPolicyName, order, endpointGroupName ); return requestHandler; @@ -910,7 +944,8 @@ private static ( bool disableRequestTimeout, bool withRequestTimeout, string? requestTimeoutPolicyName, - int? order + int? order, + string? endpointGroupName ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -931,6 +966,7 @@ private static ( bool? withRequestTimeout = null; string? requestTimeoutPolicyName = null; int? order = null; + string? endpointGroupName = null; List? accepts = null; List? produces = null; @@ -963,7 +999,8 @@ private static ( ref disableRequestTimeout, ref withRequestTimeout, ref requestTimeoutPolicyName, - ref order + ref order, + ref endpointGroupName ); var methodAttributes = methodSymbol.GetAttributes(); @@ -992,7 +1029,8 @@ ref order ref disableRequestTimeout, ref withRequestTimeout, ref requestTimeoutPolicyName, - ref order + ref order, + ref endpointGroupName ); if (methodHasRequireAuthorizationAttribute && !methodHasAllowAnonymousAttribute) @@ -1018,7 +1056,8 @@ ref order disableRequestTimeout ?? false, withRequestTimeout ?? false, (withRequestTimeout ?? false) ? requestTimeoutPolicyName : null, - order + order, + endpointGroupName ); } @@ -1045,7 +1084,8 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref bool? disableRequestTimeout, ref bool? withRequestTimeout, ref string? requestTimeoutPolicyName, - ref int? order + ref int? order, + ref string? endpointGroupName ) { foreach (var attribute in attributes) @@ -1091,6 +1131,18 @@ ref int? order continue; } + if (IsGeneratedAttribute(attributeClass, WithGroupNameAttributeName)) + { + if (attribute.ConstructorArguments.Length > 0) + { + var groupName = NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string); + if (!string.IsNullOrEmpty(groupName)) + endpointGroupName = groupName; + } + + continue; + } + if (IsGeneratedAttribute(attributeClass, AcceptsAttributeName)) { TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); @@ -2053,6 +2105,15 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(')'); } + if (!string.IsNullOrEmpty(requestHandler.EndpointGroupName)) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".WithGroupName("); + source.Append(StringLiteral(requestHandler.EndpointGroupName)); + source.Append(')'); + } + if (requestHandler.Order is { } orderValue) { source.AppendLine(); @@ -2471,7 +2532,8 @@ private readonly record struct RequestHandler( bool DisableRequestTimeout, bool WithRequestTimeout, string? RequestTimeoutPolicyName, - int? Order + int? Order, + string? EndpointGroupName ); private readonly record struct RequestHandlerClass( diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..1299ff2 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/grouped/first", global::GeneratedEndpointsTests.GroupedEndpoints.First) + .WithName("First") + .WithGroupName("SampleGroup"); + + builder.MapPost("/grouped/second", global::GeneratedEndpointsTests.GroupedEndpoints.Second) + .WithName("Second") + .WithGroupName("SampleGroup"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..27f809d --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/grouped/first", global::GroupedEndpoints.First) + .WithName("First") + .WithGroupName("SampleGroup"); + + builder.MapPost("/grouped/second", global::GroupedEndpoints.Second) + .WithName("Second") + .WithGroupName("SampleGroup"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 7d10b9e..284ec87 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -334,6 +334,35 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(WithOrderAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WithGroupNameAttribute(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + [WithGroupName("SampleGroup")] + internal static class GroupedEndpoints + { + [MapGet("/grouped/first")] + public static Ok First() + => TypedResults.Ok(); + + [MapPost("/grouped/second")] + public static Ok Second() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(WithGroupNameAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(WithGroupNameAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 7f1ea4c7e095b204733752ee28a253b59188bde8 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:59:40 -0500 Subject: [PATCH 41/75] Add RequireHost endpoint metadata support (#37) --- README.md | 2 + src/GeneratedEndpoints/MinimalApiGenerator.cs | 76 ++++++++++++++++++- ...ndpointHandlers_WithNamespace.verified.txt | 24 ++++++ ...ointHandlers_WithoutNamespace.verified.txt | 24 ++++++ ...ndpointHandlers_WithNamespace.verified.txt | 35 +++++++++ ...ointHandlers_WithoutNamespace.verified.txt | 35 +++++++++ .../GeneratedEndpointsTests.cs | 30 ++++++++ 7 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/README.md b/README.md index f54a343..4bff1b2 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,7 @@ public sealed class CreateTodo | `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | | `[RequireCors]` | Class or method | Adds `.RequireCors()` or `.RequireCors("PolicyName")` when a specific policy is provided. | | `[RequireRateLimiting]` | Class or method | Adds `.RequireRateLimiting("PolicyName")` to enforce a named rate limiting policy. | +| `[RequireHost]` | Class or method | Adds `.RequireHost("example.com", "*.example.com")` so endpoints only match allowed hosts. | | `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint. | | `[ExcludeFromDescription]` | Class or method | Generates `.ExcludeFromDescription()` so the endpoint is hidden from OpenAPI/metadata. | | `[Accepts]` / `[Accepts]` | Method | Emits `.Accepts(contentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes allowed. | @@ -273,6 +274,7 @@ public sealed class CreateTodo * `[AllowAnonymous]` opt-in overrides class or global authorization requirements. * `[RequireCors]` emits `.RequireCors()` or `.RequireCors("policy")` so endpoints participate in a configured CORS policy. * `[RequireRateLimiting]` emits `.RequireRateLimiting("policy")` to enforce ASP.NET Core rate limiting middleware. +* `[RequireHost]` emits `.RequireHost("host")` so endpoints only match specific hosts. * `[DisableAntiforgery]` wires `.DisableAntiforgery()` for CSRF-sensitive endpoints. ### Handling query objects with `[AsParameters]` diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 29db4de..aad6553 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -61,6 +61,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string RequireRateLimitingAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireRateLimitingAttributeName}"; private const string RequireRateLimitingAttributeHint = $"{RequireRateLimitingAttributeFullyQualifiedName}.gs.cs"; + private const string RequireHostAttributeName = "RequireHostAttribute"; + private const string RequireHostAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireHostAttributeName}"; + private const string RequireHostAttributeHint = $"{RequireHostAttributeFullyQualifiedName}.gs.cs"; + private const string DisableAntiforgeryAttributeName = "DisableAntiforgeryAttribute"; private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; @@ -289,6 +293,35 @@ internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attr """; context.AddSource(RequireRateLimitingAttributeHint, SourceText.From(requireRateLimitingSource, Encoding.UTF8)); + // RequireHost + var requireHostSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the allowed hosts for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireHostAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The hosts that are allowed to access the endpoint. + public {{RequireHostAttributeName}}(params string[] hosts) + { + Hosts = hosts ?? []; + } + + /// + /// Gets the allowed hosts. + /// + public string[] Hosts { get; } + } + """; + context.AddSource(RequireHostAttributeHint, SourceText.From(requireHostSource, Encoding.UTF8)); + // DisableAntiforgery var disableAntiforgerySource = $$""" {{FileHeader}} @@ -827,7 +860,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (httpMethod, pattern, name, displayName, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, - accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requireRateLimiting, + accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); @@ -848,7 +881,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke ); var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, - authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requireRateLimiting, + authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName ); @@ -937,6 +970,7 @@ private static ( EquatableImmutableArray? producesValidationProblem, bool requireCors, string? corsPolicyName, + EquatableImmutableArray? requiredHosts, bool requireRateLimiting, string? rateLimitingPolicyName, EquatableImmutableArray? endpointFilterTypes, @@ -958,6 +992,7 @@ private static ( bool? excludeFromDescription = null; bool? requireCors = null; string? corsPolicyName = null; + EquatableImmutableArray? requiredHosts = null; bool? requireRateLimiting = null; string? rateLimitingPolicyName = null; List? endpointFilters = null; @@ -990,6 +1025,7 @@ private static ( ref producesValidationProblem, ref requireCors, ref corsPolicyName, + ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, @@ -1020,6 +1056,7 @@ ref endpointGroupName ref producesValidationProblem, ref requireCors, ref corsPolicyName, + ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, @@ -1049,6 +1086,7 @@ ref endpointGroupName ToEquatableOrNull(producesValidationProblem), requireCors ?? false, corsPolicyName, + requiredHosts, requireRateLimiting ?? false, rateLimitingPolicyName, ToEquatableOrNull(endpointFilters), @@ -1075,6 +1113,7 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref List? producesValidationProblem, ref bool? requireCors, ref string? corsPolicyName, + ref EquatableImmutableArray? requiredHosts, ref bool? requireRateLimiting, ref string? rateLimitingPolicyName, ref List? endpointFilters, @@ -1204,6 +1243,29 @@ ref string? endpointGroupName continue; } + if (IsGeneratedAttribute(attributeClass, RequireHostAttributeName)) + { + if (attribute.ConstructorArguments.Length == 1) + { + var arg = attribute.ConstructorArguments[0]; + if (arg.Kind == TypedConstantKind.Array && arg.Values.Length > 0) + { + var values = arg.Values + .Select(v => v.Value as string) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s!.Trim()); + + MergeInto(ref requiredHosts, values); + } + else if (arg.Value is string singleHost && !string.IsNullOrWhiteSpace(singleHost)) + { + MergeInto(ref requiredHosts, new[] { singleHost }); + } + } + + continue; + } + if (IsGeneratedAttribute(attributeClass, RequireRateLimitingAttributeName)) { var policyName = attribute.ConstructorArguments.Length > 0 @@ -2234,6 +2296,15 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } } + if (requestHandler.RequiredHosts is { Count: > 0 }) + { + source.AppendLine(); + source.Append(continuationIndent); + source.Append(".RequireHost("); + source.Append(string.Join(", ", requestHandler.RequiredHosts.Value.Select(StringLiteral))); + source.Append(')'); + } + if (requestHandler.RequireRateLimiting && !string.IsNullOrEmpty(requestHandler.RateLimitingPolicyName)) { source.AppendLine(); @@ -2525,6 +2596,7 @@ private readonly record struct RequestHandler( bool AllowAnonymous, bool RequireCors, string? CorsPolicyName, + EquatableImmutableArray? RequiredHosts, bool RequireRateLimiting, string? RateLimitingPolicyName, EquatableImmutableArray? EndpointFilterTypes, diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..c457739 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..2618da5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt new file mode 100644 index 0000000..7edd9f5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/hosts/class-only", global::GeneratedEndpointsTests.HostRestrictedEndpoints.ClassOnly) + .WithName("ClassOnly") + .RequireHost("*.contoso.com"); + + builder.MapGet("/hosts/method-override", global::GeneratedEndpointsTests.HostRestrictedEndpoints.MethodOverride) + .WithName("MethodOverride") + .RequireHost("*.contoso.com", "api.contoso.com", "contoso.com"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt new file mode 100644 index 0000000..3c6c219 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/hosts/class-only", global::HostRestrictedEndpoints.ClassOnly) + .WithName("ClassOnly") + .RequireHost("*.contoso.com"); + + builder.MapGet("/hosts/method-override", global::HostRestrictedEndpoints.MethodOverride) + .WithName("MethodOverride") + .RequireHost("*.contoso.com", "api.contoso.com", "contoso.com"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 284ec87..0e9a37a 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -242,6 +242,36 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{nameof(RequireRateLimitingAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RequireHostAttributes(bool withNamespace) + { + var sources = TestHelpers.GetSources(""" + [RequireHost("*.contoso.com")] + internal sealed class HostRestrictedEndpoints + { + [MapGet("/hosts/class-only")] + public static Ok ClassOnly() + => TypedResults.Ok(); + + [MapGet("/hosts/method-override")] + [RequireHost("api.contoso.com", "contoso.com")] + public static Ok MethodOverride() + => TypedResults.Ok(); + } + """, withNamespace + ); + + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{nameof(RequireHostAttributes)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{nameof(RequireHostAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + } + [Theory] [InlineData(true)] [InlineData(false)] From 838454f28af7165866b1dace3b96bea731fce3fd Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:02:54 -0500 Subject: [PATCH 42/75] Fixed double escape. --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 2 +- .../GeneratedEndpoints.Tests.csproj | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index aad6553..0daae29 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -785,7 +785,7 @@ private static string GenerateHttpAttributeSource( string summaryVerb, bool allowEmptyPattern) { - var patternDefaultValue = allowEmptyPattern ? " = \\\"\\\"" : string.Empty; + var patternDefaultValue = allowEmptyPattern ? " = \"\"" : string.Empty; return $$""" {{fileHeader}} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj index 623130f..c43a904 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj @@ -38,13 +38,4 @@ - - - GeneratedEndpointsTests.cs - - - GeneratedEndpointsTests.cs - - - From c03999730e179b6dde64c4c72aca99da555f695d Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:08:00 -0500 Subject: [PATCH 43/75] Rename custom attributes to avoid With prefix (#38) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 60 +++++++++---------- ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 .../GeneratedEndpointsTests.cs | 26 ++++---- 10 files changed, 43 insertions(+), 43 deletions(-) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt} (100%) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 0daae29..b5fa0d7 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -77,17 +77,17 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string DisableRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableRequestTimeoutAttributeName}"; private const string DisableRequestTimeoutAttributeHint = $"{DisableRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; - private const string WithRequestTimeoutAttributeName = "WithRequestTimeoutAttribute"; - private const string WithRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithRequestTimeoutAttributeName}"; - private const string WithRequestTimeoutAttributeHint = $"{WithRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; + private const string RequestTimeoutAttributeName = "RequestTimeoutAttribute"; + private const string RequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequestTimeoutAttributeName}"; + private const string RequestTimeoutAttributeHint = $"{RequestTimeoutAttributeFullyQualifiedName}.gs.cs"; - private const string WithOrderAttributeName = "WithOrderAttribute"; - private const string WithOrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithOrderAttributeName}"; - private const string WithOrderAttributeHint = $"{WithOrderAttributeFullyQualifiedName}.gs.cs"; + private const string EndpointOrderAttributeName = "EndpointOrderAttribute"; + private const string EndpointOrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{EndpointOrderAttributeName}"; + private const string EndpointOrderAttributeHint = $"{EndpointOrderAttributeFullyQualifiedName}.gs.cs"; - private const string WithGroupNameAttributeName = "WithGroupNameAttribute"; - private const string WithGroupNameAttributeFullyQualifiedName = $"{AttributesNamespace}.{WithGroupNameAttributeName}"; - private const string WithGroupNameAttributeHint = $"{WithGroupNameAttributeFullyQualifiedName}.gs.cs"; + private const string EndpointGroupMetadataAttributeName = "EndpointGroupMetadataAttribute"; + private const string EndpointGroupMetadataAttributeFullyQualifiedName = $"{AttributesNamespace}.{EndpointGroupMetadataAttributeName}"; + private const string EndpointGroupMetadataAttributeHint = $"{EndpointGroupMetadataAttributeFullyQualifiedName}.gs.cs"; private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; @@ -373,8 +373,8 @@ internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.At """; context.AddSource(DisableRequestTimeoutAttributeHint, SourceText.From(disableRequestTimeoutSource, Encoding.UTF8)); - // WithRequestTimeout - var withRequestTimeoutSource = $$""" + // RequestTimeout + var requestTimeoutSource = $$""" {{FileHeader}} namespace {{AttributesNamespace}}; @@ -383,7 +383,7 @@ namespace {{AttributesNamespace}}; /// Applies the request timeout metadata to the annotated endpoint or class. /// [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{WithRequestTimeoutAttributeName}} : global::System.Attribute + internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute { /// /// Gets the optional request timeout policy name. @@ -393,7 +393,7 @@ internal sealed class {{WithRequestTimeoutAttributeName}} : global::System.Attri /// /// Applies the default request timeout behavior. /// - public {{WithRequestTimeoutAttributeName}}() + public {{RequestTimeoutAttributeName}}() { } @@ -401,17 +401,17 @@ internal sealed class {{WithRequestTimeoutAttributeName}} : global::System.Attri /// Applies the specified request timeout policy. /// /// The request timeout policy name. - public {{WithRequestTimeoutAttributeName}}(string policyName) + public {{RequestTimeoutAttributeName}}(string policyName) { PolicyName = policyName; } } """; - context.AddSource(WithRequestTimeoutAttributeHint, SourceText.From(withRequestTimeoutSource, Encoding.UTF8)); + context.AddSource(RequestTimeoutAttributeHint, SourceText.From(requestTimeoutSource, Encoding.UTF8)); - // WithOrder - var withOrderSource = $$""" + // EndpointOrder + var endpointOrderSource = $$""" {{FileHeader}} namespace {{AttributesNamespace}}; @@ -420,7 +420,7 @@ namespace {{AttributesNamespace}}; /// Specifies the order for the annotated endpoint when building conventions. /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{WithOrderAttributeName}} : global::System.Attribute + internal sealed class {{EndpointOrderAttributeName}} : global::System.Attribute { /// /// Gets the order that will be applied to the endpoint. @@ -428,20 +428,20 @@ internal sealed class {{WithOrderAttributeName}} : global::System.Attribute public int Order { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The order value to apply to the endpoint. - public {{WithOrderAttributeName}}(int order) + public {{EndpointOrderAttributeName}}(int order) { Order = order; } } """; - context.AddSource(WithOrderAttributeHint, SourceText.From(withOrderSource, Encoding.UTF8)); + context.AddSource(EndpointOrderAttributeHint, SourceText.From(endpointOrderSource, Encoding.UTF8)); - // WithGroupName - var withGroupNameSource = $$""" + // EndpointGroupMetadata + var endpointGroupMetadataSource = $$""" {{FileHeader}} namespace {{AttributesNamespace}}; @@ -450,7 +450,7 @@ namespace {{AttributesNamespace}}; /// Specifies the endpoint group name for the annotated class. /// [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - internal sealed class {{WithGroupNameAttributeName}} : global::System.Attribute + internal sealed class {{EndpointGroupMetadataAttributeName}} : global::System.Attribute { /// /// Gets the endpoint group name. @@ -458,17 +458,17 @@ internal sealed class {{WithGroupNameAttributeName}} : global::System.Attribute public string EndpointGroupName { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The endpoint group name to apply. - public {{WithGroupNameAttributeName}}(string endpointGroupName) + public {{EndpointGroupMetadataAttributeName}}(string endpointGroupName) { EndpointGroupName = endpointGroupName; } } """; - context.AddSource(WithGroupNameAttributeHint, SourceText.From(withGroupNameSource, Encoding.UTF8)); + context.AddSource(EndpointGroupMetadataAttributeHint, SourceText.From(endpointGroupMetadataSource, Encoding.UTF8)); // Accepts var acceptsSource = $$""" @@ -1147,7 +1147,7 @@ ref string? endpointGroupName continue; } - if (IsGeneratedAttribute(attributeClass, WithRequestTimeoutAttributeName)) + if (IsGeneratedAttribute(attributeClass, RequestTimeoutAttributeName)) { disableRequestTimeout = false; withRequestTimeout = true; @@ -1162,7 +1162,7 @@ ref string? endpointGroupName continue; } - if (IsGeneratedAttribute(attributeClass, WithOrderAttributeName)) + if (IsGeneratedAttribute(attributeClass, EndpointOrderAttributeName)) { if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int orderValue) order = orderValue; @@ -1170,7 +1170,7 @@ ref string? endpointGroupName continue; } - if (IsGeneratedAttribute(attributeClass, WithGroupNameAttributeName)) + if (IsGeneratedAttribute(attributeClass, EndpointGroupMetadataAttributeName)) { if (attribute.ConstructorArguments.Length > 0) { diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithGroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.WithOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 0e9a37a..77fcea5 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -279,7 +279,7 @@ public async Task ShortCircuitAndRequestTimeoutAttributes(bool withNamespace) { var sources = TestHelpers.GetSources(""" [ShortCircuit] - [WithRequestTimeout] + [RequestTimeout] internal static class ClassLevelTimeoutEndpoints { [MapGet("/timeouts/class-default")] @@ -287,7 +287,7 @@ public static Ok ClassDefault() => TypedResults.Ok(); [MapGet("/timeouts/class-override")] - [WithRequestTimeout("ClassPolicy")] + [RequestTimeout("ClassPolicy")] public static Ok ClassOverride() => TypedResults.Ok(); } @@ -308,12 +308,12 @@ public static Ok MethodDisable() => TypedResults.Ok(); [MapGet("/timeouts/method-default")] - [WithRequestTimeout] + [RequestTimeout] public static Ok MethodWithDefault() => TypedResults.Ok(); [MapGet("/timeouts/method-policy")] - [WithRequestTimeout("MethodPolicy")] + [RequestTimeout("MethodPolicy")] public static Ok MethodWithPolicy() => TypedResults.Ok(); @@ -337,18 +337,18 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") [Theory] [InlineData(true)] [InlineData(false)] - public async Task WithOrderAttribute(bool withNamespace) + public async Task EndpointOrderAttribute(bool withNamespace) { var sources = TestHelpers.GetSources(""" internal sealed class OrderedEndpoints { [MapGet("/ordered/low")] - [WithOrder(-1)] + [EndpointOrder(-1)] public static Ok Low() => TypedResults.Ok(); [MapGet("/ordered/high")] - [WithOrder(5)] + [EndpointOrder(5)] public static Ok High() => TypedResults.Ok(); } @@ -358,19 +358,19 @@ public static Ok High() var result = TestHelpers.RunGenerator(sources); await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(WithOrderAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(EndpointOrderAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(WithOrderAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(EndpointOrderAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } [Theory] [InlineData(true)] [InlineData(false)] - public async Task WithGroupNameAttribute(bool withNamespace) + public async Task EndpointGroupMetadataAttribute(bool withNamespace) { var sources = TestHelpers.GetSources(""" - [WithGroupName("SampleGroup")] + [EndpointGroupMetadata("SampleGroup")] internal static class GroupedEndpoints { [MapGet("/grouped/first")] @@ -387,10 +387,10 @@ public static Ok Second() var result = TestHelpers.RunGenerator(sources); await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(WithGroupNameAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(EndpointGroupMetadataAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(WithGroupNameAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(EndpointGroupMetadataAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } [Theory] From 06185ba4b6c64355cfa313623456e0ceba5eeacf Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:20:46 -0500 Subject: [PATCH 44/75] Use component model display metadata attributes (#39) --- README.md | 11 +-- src/GeneratedEndpoints/MinimalApiGenerator.cs | 68 ++++++++++--------- .../GetUserEndpoint.cs | 5 +- .../GeneratedEndpointsTests.cs | 8 ++- 4 files changed, 54 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 4bff1b2..216032f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ GeneratedEndpoints is a .NET source generator that automatically wires Minimal A GeneratedEndpoints focuses on three goals: -* **Attribute-driven routing** – use `[MapGet]`, `[MapPost]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapPatch]`, `[MapTrace]`, `[MapConnect]`, and even `[MapQuery]` to describe the verb and route pattern. The generator creates the matching `Map*` call and wires up metadata like `.WithName`, `.WithDisplayName`, `.WithSummary`, and `.WithDescription`. +* **Attribute-driven routing** – use `[MapGet]`, `[MapPost]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapPatch]`, `[MapTrace]`, `[MapConnect]`, and even `[MapQuery]` to describe the verb and route pattern. The generator creates the matching `Map*` call and wires up metadata like `.WithName`, `.WithDisplayName`, `.WithSummary`, and `.WithDescription` (via `[DisplayName]`/`[Description]`). * **Feature-first organization** – keep handlers close to the code they execute (for example, alongside your `Todos` feature). Non-static handler classes are automatically registered with dependency injection so you can inject EF Core DbContexts, services, etc. * **Metadata composition** – decorate classes and methods with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, `[AllowAnonymous]`, and `[ExcludeFromDescription]`. Apply `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` directly to the methods they describe. Class-level metadata is merged into every method, while method-level metadata can refine or override. @@ -51,6 +51,7 @@ After the reference is added, the source generator contributes its attributes an Handlers can be static or instance classes. The following example uses a scoped handler so that EF Core can be injected through the constructor: ```csharp +using System.ComponentModel; using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http.HttpResults; @@ -62,7 +63,9 @@ public sealed class GetTodo public GetTodo(TodoDbContext db) => _db = db; - [MapGet("/todos/{id}", Summary = "Retrieve a todo", Description = "Returns the todo matching the provided identifier.")] + [DisplayName("Retrieve a todo")] + [Description("Returns the todo matching the provided identifier.")] + [MapGet("/todos/{id}", Summary = "Retrieve a todo")] [Tags("Todos")] [RequireAuthorization("Todos.Read")] public async Task, NotFound>> HandleAsync(Guid id, CancellationToken cancellationToken) @@ -76,7 +79,7 @@ public sealed class GetTodo Key ideas: * Choose the attribute that matches the verb (`[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`). -* Named arguments like `DisplayName`, `Summary`, `Description`, and `Name` are translated into `.WithDisplayName`, `.WithSummary`, `.WithDescription`, and `.WithName` calls. +* Named arguments like `Summary` and `Name`, plus `[DisplayName]` and `[Description]`, are translated into `.WithSummary`, `.WithName`, `.WithDisplayName`, and `.WithDescription` calls. * Use existing ASP.NET Core binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromHeader]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator preserves them in the produced delegate. * Metadata attributes (`[Tags]`, `[RequireAuthorization]`, `[AllowAnonymous]`, `[DisableAntiforgery]`, `[ExcludeFromDescription]`) can be placed on the class, on a method, or on both. Class-level metadata is merged with method-level metadata. Request/response attributes (`[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, `[ProducesValidationProblem]`) must be applied directly to the method they describe. @@ -248,7 +251,7 @@ public sealed class CreateTodo | Attribute | Scope | Purpose | | --- | --- | --- | -| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName`, `.WithDisplayName`, `.WithSummary`, and `.WithDescription` calls. | +| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName` and `.WithSummary` calls while `[DisplayName]`/`[Description]` add `.WithDisplayName`/`.WithDescription`. | | `[Tags]` | Class or method | Adds tags to one or more endpoints. Multiple attributes merge without duplication. | | `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Passing policies (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) emits `.RequireAuthorization("Todos.Read", "Todos.Write")`. | | `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index b5fa0d7..c49b146 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -19,6 +19,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private static readonly string[] AspNetCoreHttpNamespaceParts = new[] { "Microsoft", "AspNetCore", "Http" }; private static readonly string[] AspNetCoreAuthorizationNamespaceParts = new[] { "Microsoft", "AspNetCore", "Authorization" }; private static readonly string[] AspNetCoreRoutingNamespaceParts = new[] { "Microsoft", "AspNetCore", "Routing" }; + private static readonly string[] ComponentModelNamespaceParts = new[] { "System", "ComponentModel" }; private const string FallbackHttpMethod = "__FALLBACK__"; @@ -41,9 +42,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); private const string NameAttributeNamedParameter = "Name"; - private const string DisplayNameAttributeNamedParameter = "DisplayName"; private const string SummaryAttributeNamedParameter = "Summary"; - private const string DescriptionAttributeNamedParameter = "Description"; private const string ResponseTypeAttributeNamedParameter = "ResponseType"; private const string RequestTypeAttributeNamedParameter = "RequestType"; private const string IsOptionalAttributeNamedParameter = "IsOptional"; @@ -807,21 +806,11 @@ internal sealed class {{attributeName}} : global::System.Attribute /// public string Name { get; set; } = ""; - /// - /// Gets or sets the endpoint display name. - /// - public string DisplayName { get; set; } = ""; - /// /// Gets or sets the endpoint summary. /// public string Summary { get; set; } = ""; - /// - /// Gets or sets the endpoint description. - /// - public string Description { get; set; } = ""; - /// /// Initializes a new instance of the class. /// @@ -857,7 +846,9 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var requestHandlerMethod = GetRequestHandlerMethod(requestHandlerMethodSymbol, cancellationToken); - var (httpMethod, pattern, name, displayName, summary, description) = GetRequestHandlerAttribute(attribute, cancellationToken); + var (httpMethod, pattern, name, summary) = GetRequestHandlerAttribute(attribute, cancellationToken); + + var (displayName, description) = GetDisplayAndDescriptionAttributes(requestHandlerMethodSymbol); var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, @@ -901,9 +892,7 @@ private static ( string HttpMethod, string Pattern, string? Name, - string? DisplayName, - string? Summary, - string? Description + string? Summary ) GetRequestHandlerAttribute( AttributeData attribute, CancellationToken cancellationToken @@ -920,9 +909,7 @@ CancellationToken cancellationToken var pattern = (attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : "") ?? ""; string? name = null; - string? displayName = null; string? summary = null; - string? description = null; foreach (var namedArg in attribute.NamedArguments) { switch (namedArg.Key) @@ -933,28 +920,47 @@ CancellationToken cancellationToken name = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); break; } - case DisplayNameAttributeNamedParameter: - { - var value = namedArg.Value.Value as string; - displayName = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); - break; - } case SummaryAttributeNamedParameter: { var value = namedArg.Value.Value as string; summary = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); break; } - case DescriptionAttributeNamedParameter: - { - var value = namedArg.Value.Value as string; - description = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); - break; - } } } - return (httpMethod, pattern, name, displayName, summary, description); + return (httpMethod, pattern, name, summary); + } + + private static (string? DisplayName, string? Description) GetDisplayAndDescriptionAttributes(IMethodSymbol methodSymbol) + { + string? displayName = null; + string? description = null; + + foreach (var attribute in methodSymbol.GetAttributes()) + { + var attributeClass = attribute.AttributeClass; + if (attributeClass is null) + continue; + + if (IsAttribute(attributeClass, nameof(System.ComponentModel.DisplayNameAttribute), ComponentModelNamespaceParts)) + { + displayName = NormalizeOptionalString(attribute.ConstructorArguments.Length > 0 + ? attribute.ConstructorArguments[0].Value as string + : null); + + continue; + } + + if (IsAttribute(attributeClass, nameof(System.ComponentModel.DescriptionAttribute), ComponentModelNamespaceParts)) + { + description = NormalizeOptionalString(attribute.ConstructorArguments.Length > 0 + ? attribute.ConstructorArguments[0].Value as string + : null); + } + } + + return (displayName, description); } private static ( diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index 0239f56..b403f75 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Generated.Attributes; @@ -26,7 +27,9 @@ internal sealed class GetUserEndpoint(IServiceProvider serviceProvider) [ProducesResponse(StatusCodes.Status202Accepted, "application/json")] [ProducesProblem(StatusCodes.Status500InternalServerError, "application/problem+json")] [ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")] - [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] + [DisplayName("User lookup endpoint")] + [Description("Gets a user by ID when the ID is greater than zero.")] + [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.")] public Results, NotFound, ValidationProblem, ProblemHttpResult> GetUser( [FromQuery] int id, [FromKeyedServices(ServiceLifetime.Scoped)] IServiceCollection services diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 77fcea5..e3019d2 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -48,7 +48,9 @@ public async Task MapGet(bool withNamespace) [Tags("Users")] internal static class GetUserEndpoint { - [MapGet("/users/{id:int}", Name = nameof(GetUser), DisplayName = "User lookup endpoint", Summary = "Gets a user by ID.", Description = "Gets a user by ID when the ID is greater than zero.")] + [System.ComponentModel.DisplayName("User lookup endpoint")] + [System.ComponentModel.Description("Gets a user by ID when the ID is greater than zero.")] + [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.")] public static Results GetUser2(int id) { if (id > 0) @@ -494,7 +496,9 @@ public static void Configure(TBuilder builder, IServiceProvider servic builder.WithMetadata("configured"); } - [MapGet("/complex/{id:int}", Name = nameof(GetComplex), DisplayName = "Complex data endpoint", Summary = "Gets complex data.", Description = "Uses every supported attribute.")] + [System.ComponentModel.DisplayName("Complex data endpoint")] + [System.ComponentModel.Description("Uses every supported attribute.")] + [MapGet("/complex/{id:int}", Name = nameof(GetComplex), Summary = "Gets complex data.")] [AllowAnonymous] [Tags("MethodLevel")] [RequireAuthorization("MethodPolicy")] From 5bbe0921f3262f0a5f18cdd5c524a20635c6da2e Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:22:15 -0500 Subject: [PATCH 45/75] Cleanup. --- tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs | 1 + tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs index 3e367d5..e6089ac 100644 --- a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs +++ b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs @@ -23,6 +23,7 @@ public static IEnumerable GetSources(string source, bool withNamespace) using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Routing; + using System.ComponentModel; """; if (withNamespace) diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index e3019d2..8e86049 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -48,8 +48,8 @@ public async Task MapGet(bool withNamespace) [Tags("Users")] internal static class GetUserEndpoint { - [System.ComponentModel.DisplayName("User lookup endpoint")] - [System.ComponentModel.Description("Gets a user by ID when the ID is greater than zero.")] + [DisplayName("User lookup endpoint")] + [Description("Gets a user by ID when the ID is greater than zero.")] [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.")] public static Results GetUser2(int id) { @@ -496,8 +496,8 @@ public static void Configure(TBuilder builder, IServiceProvider servic builder.WithMetadata("configured"); } - [System.ComponentModel.DisplayName("Complex data endpoint")] - [System.ComponentModel.Description("Uses every supported attribute.")] + [DisplayName("Complex data endpoint")] + [Description("Uses every supported attribute.")] [MapGet("/complex/{id:int}", Name = nameof(GetComplex), Summary = "Gets complex data.")] [AllowAnonymous] [Tags("MethodLevel")] From 75f10b5f06df8e09fad0dd764a6261da53441259 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:34:17 -0500 Subject: [PATCH 46/75] Add Summary attribute and rename metadata attributes (#40) --- README.md | 17 +- src/GeneratedEndpoints/MinimalApiGenerator.cs | 206 +++++++++++------- .../GetUserEndpoint.cs | 3 +- ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 ...dpointHandlers_WithNamespace.verified.txt} | 0 ...intHandlers_WithoutNamespace.verified.txt} | 0 .../GeneratedEndpointsTests.cs | 24 +- 12 files changed, 148 insertions(+), 102 deletions(-) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt => GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt} (100%) rename tests/GeneratedEndpoints.Tests/{GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt => GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt} (100%) diff --git a/README.md b/README.md index 216032f..3b8ce20 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ public sealed class GetTodo [DisplayName("Retrieve a todo")] [Description("Returns the todo matching the provided identifier.")] - [MapGet("/todos/{id}", Summary = "Retrieve a todo")] + [Summary("Retrieve a todo")] + [MapGet("/todos/{id}")] [Tags("Todos")] [RequireAuthorization("Todos.Read")] public async Task, NotFound>> HandleAsync(Guid id, CancellationToken cancellationToken) @@ -79,7 +80,7 @@ public sealed class GetTodo Key ideas: * Choose the attribute that matches the verb (`[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`). -* Named arguments like `Summary` and `Name`, plus `[DisplayName]` and `[Description]`, are translated into `.WithSummary`, `.WithName`, `.WithDisplayName`, and `.WithDescription` calls. +* Named arguments like `Name`, plus `[Summary]`, `[DisplayName]`, and `[Description]`, are translated into `.WithName`, `.WithSummary`, `.WithDisplayName`, and `.WithDescription` calls. * Use existing ASP.NET Core binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromHeader]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator preserves them in the produced delegate. * Metadata attributes (`[Tags]`, `[RequireAuthorization]`, `[AllowAnonymous]`, `[DisableAntiforgery]`, `[ExcludeFromDescription]`) can be placed on the class, on a method, or on both. Class-level metadata is merged with method-level metadata. Request/response attributes (`[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, `[ProducesValidationProblem]`) must be applied directly to the method they describe. @@ -231,7 +232,8 @@ GeneratedEndpoints ships with attribute helpers for request/response metadata. T ```csharp public sealed class CreateTodo { - [MapPost("/todos", Summary = "Create a todo")] + [Summary("Create a todo")] + [MapPost("/todos")] [Accepts("application/json", "application/xml")] [ProducesResponse(StatusCodes.Status201Created)] [ProducesProblem(StatusCodes.Status500InternalServerError)] @@ -251,7 +253,8 @@ public sealed class CreateTodo | Attribute | Scope | Purpose | | --- | --- | --- | -| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName` and `.WithSummary` calls while `[DisplayName]`/`[Description]` add `.WithDisplayName`/`.WithDescription`. | +| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName` calls while `[Summary]`/`[DisplayName]`/`[Description]` add `.WithSummary`/`.WithDisplayName`/`.WithDescription`. | +| `[Summary]` | Class or method | Adds `.WithSummary("...")` to every annotated endpoint. Method-level summaries override class-level ones. | | `[Tags]` | Class or method | Adds tags to one or more endpoints. Multiple attributes merge without duplication. | | `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Passing policies (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) emits `.RequireAuthorization("Todos.Read", "Todos.Write")`. | | `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | @@ -341,14 +344,16 @@ public sealed class TodoFeature public TodoFeature(TodoDbContext db) => _db = db; - [MapGet("/todos/{id}", Summary = "Retrieve a todo")] + [Summary("Retrieve a todo")] + [MapGet("/todos/{id}")] public async Task, NotFound>> GetAsync(Guid id, CancellationToken cancellationToken) { var entity = await _db.Todos.FindAsync(new object?[] { id }, cancellationToken); return entity is null ? TypedResults.NotFound() : TypedResults.Ok(entity); } - [MapPost("/todos", Summary = "Create a todo")] + [Summary("Create a todo")] + [MapPost("/todos")] [ProducesResponse(StatusCodes.Status201Created)] public async Task> CreateAsync([FromBody] CreateTodoRequest request, CancellationToken cancellationToken) { diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index c49b146..f13a0fd 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -42,7 +42,6 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); private const string NameAttributeNamedParameter = "Name"; - private const string SummaryAttributeNamedParameter = "Summary"; private const string ResponseTypeAttributeNamedParameter = "ResponseType"; private const string RequestTypeAttributeNamedParameter = "RequestType"; private const string IsOptionalAttributeNamedParameter = "IsOptional"; @@ -80,13 +79,17 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string RequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequestTimeoutAttributeName}"; private const string RequestTimeoutAttributeHint = $"{RequestTimeoutAttributeFullyQualifiedName}.gs.cs"; - private const string EndpointOrderAttributeName = "EndpointOrderAttribute"; - private const string EndpointOrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{EndpointOrderAttributeName}"; - private const string EndpointOrderAttributeHint = $"{EndpointOrderAttributeFullyQualifiedName}.gs.cs"; + private const string OrderAttributeName = "OrderAttribute"; + private const string OrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{OrderAttributeName}"; + private const string OrderAttributeHint = $"{OrderAttributeFullyQualifiedName}.gs.cs"; - private const string EndpointGroupMetadataAttributeName = "EndpointGroupMetadataAttribute"; - private const string EndpointGroupMetadataAttributeFullyQualifiedName = $"{AttributesNamespace}.{EndpointGroupMetadataAttributeName}"; - private const string EndpointGroupMetadataAttributeHint = $"{EndpointGroupMetadataAttributeFullyQualifiedName}.gs.cs"; + private const string GroupNameAttributeName = "GroupNameAttribute"; + private const string GroupNameAttributeFullyQualifiedName = $"{AttributesNamespace}.{GroupNameAttributeName}"; + private const string GroupNameAttributeHint = $"{GroupNameAttributeFullyQualifiedName}.gs.cs"; + + private const string SummaryAttributeName = "SummaryAttribute"; + private const string SummaryAttributeFullyQualifiedName = $"{AttributesNamespace}.{SummaryAttributeName}"; + private const string SummaryAttributeHint = $"{SummaryAttributeFullyQualifiedName}.gs.cs"; private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; @@ -409,65 +412,95 @@ internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute """; context.AddSource(RequestTimeoutAttributeHint, SourceText.From(requestTimeoutSource, Encoding.UTF8)); - // EndpointOrder - var endpointOrderSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the order for the annotated endpoint when building conventions. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{EndpointOrderAttributeName}} : global::System.Attribute - { - /// - /// Gets the order that will be applied to the endpoint. - /// - public int Order { get; } + // Order + var orderSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the order for the annotated endpoint when building conventions. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{OrderAttributeName}} : global::System.Attribute + { + /// + /// Gets the order that will be applied to the endpoint. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The order value to apply to the endpoint. + public {{OrderAttributeName}}(int order) + { + Order = order; + } + } + + """; + context.AddSource(OrderAttributeHint, SourceText.From(orderSource, Encoding.UTF8)); + + // GroupName + var groupNameSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the endpoint group name for the annotated class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal sealed class {{GroupNameAttributeName}} : global::System.Attribute + { + /// + /// Gets the endpoint group name. + /// + public string GroupName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The endpoint group name to apply. + public {{GroupNameAttributeName}}(string groupName) + { + GroupName = groupName; + } + } - /// - /// Initializes a new instance of the class. - /// - /// The order value to apply to the endpoint. - public {{EndpointOrderAttributeName}}(int order) - { - Order = order; - } - } + """; + context.AddSource(GroupNameAttributeHint, SourceText.From(groupNameSource, Encoding.UTF8)); - """; - context.AddSource(EndpointOrderAttributeHint, SourceText.From(endpointOrderSource, Encoding.UTF8)); + // Summary + var summarySource = $$""" + {{FileHeader}} - // EndpointGroupMetadata - var endpointGroupMetadataSource = $$""" - {{FileHeader}} + namespace {{AttributesNamespace}}; - namespace {{AttributesNamespace}}; + /// + /// Specifies the summary metadata for the annotated endpoint. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{SummaryAttributeName}} : global::System.Attribute + { + /// + /// Gets the summary value for the endpoint. + /// + public string Summary { get; } - /// - /// Specifies the endpoint group name for the annotated class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - internal sealed class {{EndpointGroupMetadataAttributeName}} : global::System.Attribute - { - /// - /// Gets the endpoint group name. - /// - public string EndpointGroupName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The endpoint group name to apply. - public {{EndpointGroupMetadataAttributeName}}(string endpointGroupName) - { - EndpointGroupName = endpointGroupName; - } - } + /// + /// Initializes a new instance of the class. + /// + /// The summary to apply to the endpoint. + public {{SummaryAttributeName}}(string summary) + { + Summary = summary; + } + } - """; - context.AddSource(EndpointGroupMetadataAttributeHint, SourceText.From(endpointGroupMetadataSource, Encoding.UTF8)); + """; + context.AddSource(SummaryAttributeHint, SourceText.From(summarySource, Encoding.UTF8)); // Accepts var acceptsSource = $$""" @@ -806,11 +839,6 @@ internal sealed class {{attributeName}} : global::System.Attribute /// public string Name { get; set; } = ""; - /// - /// Gets or sets the endpoint summary. - /// - public string Summary { get; set; } = ""; - /// /// Initializes a new instance of the class. /// @@ -846,14 +874,14 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var requestHandlerMethod = GetRequestHandlerMethod(requestHandlerMethodSymbol, cancellationToken); - var (httpMethod, pattern, name, summary) = GetRequestHandlerAttribute(attribute, cancellationToken); + var (httpMethod, pattern, name) = GetRequestHandlerAttribute(attribute, cancellationToken); var (displayName, description) = GetDisplayAndDescriptionAttributes(requestHandlerMethodSymbol); var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, - requestTimeoutPolicyName, order, endpointGroupName) + requestTimeoutPolicyName, order, endpointGroupName, summary) = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); @@ -891,8 +919,7 @@ private static string RemoveAsyncSuffix(string methodName) private static ( string HttpMethod, string Pattern, - string? Name, - string? Summary + string? Name ) GetRequestHandlerAttribute( AttributeData attribute, CancellationToken cancellationToken @@ -909,7 +936,6 @@ CancellationToken cancellationToken var pattern = (attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : "") ?? ""; string? name = null; - string? summary = null; foreach (var namedArg in attribute.NamedArguments) { switch (namedArg.Key) @@ -920,16 +946,10 @@ CancellationToken cancellationToken name = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); break; } - case SummaryAttributeNamedParameter: - { - var value = namedArg.Value.Value as string; - summary = string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); - break; - } } } - return (httpMethod, pattern, name, summary); + return (httpMethod, pattern, name); } private static (string? DisplayName, string? Description) GetDisplayAndDescriptionAttributes(IMethodSymbol methodSymbol) @@ -985,7 +1005,8 @@ private static ( bool withRequestTimeout, string? requestTimeoutPolicyName, int? order, - string? endpointGroupName + string? endpointGroupName, + string? summary ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -1008,6 +1029,7 @@ private static ( string? requestTimeoutPolicyName = null; int? order = null; string? endpointGroupName = null; + string? summary = null; List? accepts = null; List? produces = null; @@ -1042,7 +1064,8 @@ private static ( ref withRequestTimeout, ref requestTimeoutPolicyName, ref order, - ref endpointGroupName + ref endpointGroupName, + ref summary ); var methodAttributes = methodSymbol.GetAttributes(); @@ -1073,7 +1096,8 @@ ref endpointGroupName ref withRequestTimeout, ref requestTimeoutPolicyName, ref order, - ref endpointGroupName + ref endpointGroupName, + ref summary ); if (methodHasRequireAuthorizationAttribute && !methodHasAllowAnonymousAttribute) @@ -1101,7 +1125,8 @@ ref endpointGroupName withRequestTimeout ?? false, (withRequestTimeout ?? false) ? requestTimeoutPolicyName : null, order, - endpointGroupName + endpointGroupName, + summary ); } @@ -1130,7 +1155,8 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref bool? withRequestTimeout, ref string? requestTimeoutPolicyName, ref int? order, - ref string? endpointGroupName + ref string? endpointGroupName, + ref string? summary ) { foreach (var attribute in attributes) @@ -1168,7 +1194,7 @@ ref string? endpointGroupName continue; } - if (IsGeneratedAttribute(attributeClass, EndpointOrderAttributeName)) + if (IsGeneratedAttribute(attributeClass, OrderAttributeName)) { if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int orderValue) order = orderValue; @@ -1176,7 +1202,7 @@ ref string? endpointGroupName continue; } - if (IsGeneratedAttribute(attributeClass, EndpointGroupMetadataAttributeName)) + if (IsGeneratedAttribute(attributeClass, GroupNameAttributeName)) { if (attribute.ConstructorArguments.Length > 0) { @@ -1188,6 +1214,18 @@ ref string? endpointGroupName continue; } + if (IsGeneratedAttribute(attributeClass, SummaryAttributeName)) + { + if (attribute.ConstructorArguments.Length > 0) + { + var summaryValue = NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string); + if (!string.IsNullOrEmpty(summaryValue)) + summary = summaryValue; + } + + continue; + } + if (IsGeneratedAttribute(attributeClass, AcceptsAttributeName)) { TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); diff --git a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs index b403f75..4452d06 100644 --- a/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs +++ b/tests/GeneratedEndpoints.Tests.Lab/GetUserEndpoint.cs @@ -29,7 +29,8 @@ internal sealed class GetUserEndpoint(IServiceProvider serviceProvider) [ProducesValidationProblem(StatusCodes.Status400BadRequest, "application/problem+json")] [DisplayName("User lookup endpoint")] [Description("Gets a user by ID when the ID is greater than zero.")] - [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.")] + [Summary("Gets a user by ID.")] + [MapGet("/users/{id:int}", Name = nameof(GetUser))] public Results, NotFound, ValidationProblem, ProblemHttpResult> GetUser( [FromQuery] int id, [FromKeyedServices(ServiceLifetime.Scoped)] IServiceCollection services diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointGroupMetadataAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt similarity index 100% rename from tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.EndpointOrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt rename to tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs index 8e86049..9596c66 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs @@ -50,7 +50,8 @@ internal static class GetUserEndpoint { [DisplayName("User lookup endpoint")] [Description("Gets a user by ID when the ID is greater than zero.")] - [MapGet("/users/{id:int}", Name = nameof(GetUser), Summary = "Gets a user by ID.")] + [Summary("Gets a user by ID.")] + [MapGet("/users/{id:int}", Name = nameof(GetUser))] public static Results GetUser2(int id) { if (id > 0) @@ -339,18 +340,18 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") [Theory] [InlineData(true)] [InlineData(false)] - public async Task EndpointOrderAttribute(bool withNamespace) + public async Task OrderAttribute(bool withNamespace) { var sources = TestHelpers.GetSources(""" internal sealed class OrderedEndpoints { [MapGet("/ordered/low")] - [EndpointOrder(-1)] + [Order(-1)] public static Ok Low() => TypedResults.Ok(); [MapGet("/ordered/high")] - [EndpointOrder(5)] + [Order(5)] public static Ok High() => TypedResults.Ok(); } @@ -360,19 +361,19 @@ public static Ok High() var result = TestHelpers.RunGenerator(sources); await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(EndpointOrderAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(OrderAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(EndpointOrderAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(OrderAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } [Theory] [InlineData(true)] [InlineData(false)] - public async Task EndpointGroupMetadataAttribute(bool withNamespace) + public async Task GroupNameAttribute(bool withNamespace) { var sources = TestHelpers.GetSources(""" - [EndpointGroupMetadata("SampleGroup")] + [GroupName("SampleGroup")] internal static class GroupedEndpoints { [MapGet("/grouped/first")] @@ -389,10 +390,10 @@ public static Ok Second() var result = TestHelpers.RunGenerator(sources); await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(EndpointGroupMetadataAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(GroupNameAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(EndpointGroupMetadataAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); + .UseMethodName($"{nameof(GroupNameAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); } [Theory] @@ -498,7 +499,8 @@ public static void Configure(TBuilder builder, IServiceProvider servic [DisplayName("Complex data endpoint")] [Description("Uses every supported attribute.")] - [MapGet("/complex/{id:int}", Name = nameof(GetComplex), Summary = "Gets complex data.")] + [Summary("Gets complex data.")] + [MapGet("/complex/{id:int}", Name = nameof(GetComplex))] [AllowAnonymous] [Tags("MethodLevel")] [RequireAuthorization("MethodPolicy")] From 8d3c4c6c3454ee5babf5162d5e620df4576049bc Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:36:07 -0500 Subject: [PATCH 47/75] Cleanup. --- .../Common/EquatableImmutableArray.cs | 2 +- src/GeneratedEndpoints/MinimalApiGenerator.cs | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/GeneratedEndpoints/Common/EquatableImmutableArray.cs b/src/GeneratedEndpoints/Common/EquatableImmutableArray.cs index d0e3b76..54c8caa 100644 --- a/src/GeneratedEndpoints/Common/EquatableImmutableArray.cs +++ b/src/GeneratedEndpoints/Common/EquatableImmutableArray.cs @@ -26,6 +26,6 @@ public static EquatableImmutableArray ToEquatableImmutableArray(this Immut /// An containing the same elements as the original enumerable. public static EquatableImmutableArray ToEquatableImmutableArray(this IEnumerable enumerable) { - return new EquatableImmutableArray(enumerable.ToImmutableArray()); + return new EquatableImmutableArray([..enumerable]); } } diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index f13a0fd..2c02cbf 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1,7 +1,6 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Text; using GeneratedEndpoints.Common; using Microsoft.CodeAnalysis; @@ -16,10 +15,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string BaseNamespace = "Microsoft.AspNetCore.Generated"; private const string AttributesNamespace = $"{BaseNamespace}.Attributes"; private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.'); - private static readonly string[] AspNetCoreHttpNamespaceParts = new[] { "Microsoft", "AspNetCore", "Http" }; - private static readonly string[] AspNetCoreAuthorizationNamespaceParts = new[] { "Microsoft", "AspNetCore", "Authorization" }; - private static readonly string[] AspNetCoreRoutingNamespaceParts = new[] { "Microsoft", "AspNetCore", "Routing" }; - private static readonly string[] ComponentModelNamespaceParts = new[] { "System", "ComponentModel" }; + private static readonly string[] AspNetCoreHttpNamespaceParts = ["Microsoft", "AspNetCore", "Http"]; + private static readonly string[] AspNetCoreAuthorizationNamespaceParts = ["Microsoft", "AspNetCore", "Authorization"]; + private static readonly string[] AspNetCoreRoutingNamespaceParts = ["Microsoft", "AspNetCore", "Routing"]; + private static readonly string[] ComponentModelNamespaceParts = ["System", "ComponentModel"]; private const string FallbackHttpMethod = "__FALLBACK__"; @@ -126,6 +125,7 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string ConfigureMethodName = "Configure"; private const string AsyncSuffix = "Async"; + private const string GlobalPrefix = "global::"; private static readonly string FileHeader = $""" //----------------------------------------------------------------------------- @@ -1292,7 +1292,7 @@ ref string? summary if (attribute.ConstructorArguments.Length == 1) { var arg = attribute.ConstructorArguments[0]; - if (arg.Kind == TypedConstantKind.Array && arg.Values.Length > 0) + if (arg is { Kind: TypedConstantKind.Array, Values.Length: > 0 }) { var values = arg.Values .Select(v => v.Value as string) @@ -1303,7 +1303,7 @@ ref string? summary } else if (arg.Value is string singleHost && !string.IsNullOrWhiteSpace(singleHost)) { - MergeInto(ref requiredHosts, new[] { singleHost }); + MergeInto(ref requiredHosts, [singleHost]); } } @@ -1959,7 +1959,6 @@ private static ImmutableHashSet GetRequestHandlersWithNameCollisions(Immuta private static string GetFullyQualifiedMethodDisplayName(RequestHandler requestHandler) { var className = requestHandler.Class.Name; - const string GlobalPrefix = "global::"; if (className.StartsWith(GlobalPrefix, StringComparison.Ordinal)) className = className.Substring(GlobalPrefix.Length); From d45f1e2b20d74f2f934e493d2225fbd2cc50c697 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 19:37:18 -0500 Subject: [PATCH 48/75] Remove generated tests and readme. --- README.md | 374 +----------------- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 30 -- ...ointHandlers_WithoutNamespace.verified.txt | 30 -- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 31 -- ...ointHandlers_WithoutNamespace.verified.txt | 31 -- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 32 -- ...ointHandlers_WithoutNamespace.verified.txt | 32 -- ...ndpointHandlers_WithNamespace.verified.txt | 23 -- ...ointHandlers_WithoutNamespace.verified.txt | 23 -- ...ndpointHandlers_WithNamespace.verified.txt | 35 -- ...ointHandlers_WithoutNamespace.verified.txt | 35 -- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 73 ---- ...ointHandlers_WithoutNamespace.verified.txt | 73 ---- ...ndpointHandlers_WithNamespace.verified.txt | 23 -- ...ointHandlers_WithoutNamespace.verified.txt | 23 -- ...ndpointHandlers_WithNamespace.verified.txt | 33 -- ...ointHandlers_WithoutNamespace.verified.txt | 33 -- ...ndpointHandlers_WithNamespace.verified.txt | 23 -- ...ointHandlers_WithoutNamespace.verified.txt | 23 -- ...ndpointHandlers_WithNamespace.verified.txt | 33 -- ...ointHandlers_WithoutNamespace.verified.txt | 33 -- ...ndpointHandlers_WithNamespace.verified.txt | 23 -- ...ointHandlers_WithoutNamespace.verified.txt | 23 -- ...ndpointHandlers_WithNamespace.verified.txt | 32 -- ...ointHandlers_WithoutNamespace.verified.txt | 32 -- ...ndpointHandlers_WithNamespace.verified.txt | 23 -- ...ointHandlers_WithoutNamespace.verified.txt | 23 -- ...ndpointHandlers_WithNamespace.verified.txt | 34 -- ...ointHandlers_WithoutNamespace.verified.txt | 34 -- ...ndpointHandlers_WithNamespace.verified.txt | 33 -- ...ointHandlers_WithoutNamespace.verified.txt | 33 -- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 35 -- ...ointHandlers_WithoutNamespace.verified.txt | 35 -- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 35 -- ...ointHandlers_WithoutNamespace.verified.txt | 35 -- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 35 -- ...ointHandlers_WithoutNamespace.verified.txt | 35 -- ...ndpointHandlers_WithNamespace.verified.txt | 24 -- ...ointHandlers_WithoutNamespace.verified.txt | 24 -- ...ndpointHandlers_WithNamespace.verified.txt | 32 -- ...ointHandlers_WithoutNamespace.verified.txt | 32 -- ...ndpointHandlers_WithNamespace.verified.txt | 23 -- ...ointHandlers_WithoutNamespace.verified.txt | 23 -- ...ndpointHandlers_WithNamespace.verified.txt | 57 --- ...ointHandlers_WithoutNamespace.verified.txt | 57 --- 59 files changed, 1 insertion(+), 2153 deletions(-) delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt diff --git a/README.md b/README.md index 3b8ce20..a539c0e 100644 --- a/README.md +++ b/README.md @@ -1,375 +1,3 @@ [![Banner](https://raw.githubusercontent.com/jscarle/GeneratedEndpoints/develop/Banner.png)](https://github.com/jscarle/GeneratedEndpoints) -# GeneratedEndpoints - -Attribute-driven, source-generated Minimal API endpoints for feature-based development. - -GeneratedEndpoints is a .NET source generator that automatically wires Minimal API endpoints from attribute-decorated methods. You describe the intent of each endpoint through attributes, and the generator produces all the routing and boilerplate needed to expose it. The result is a clean, feature-centric project structure that fits Clean Architecture (CA), Vertical Slice Architecture (VSA), or any other modular approach. - ---- - -## Table of contents - -1. [Why GeneratedEndpoints?](#why-generatedendpoints) -2. [Quick start](#quick-start) -3. [Handler styles and routing](#handler-styles-and-routing) -4. [Configuring endpoints](#configuring-endpoints) -5. [Describing requests and responses](#describing-requests-and-responses) -6. [Attribute reference](#attribute-reference) -7. [Tips, patterns, and extra examples](#tips-patterns-and-extra-examples) - ---- - -## Why GeneratedEndpoints? - -GeneratedEndpoints focuses on three goals: - -* **Attribute-driven routing** – use `[MapGet]`, `[MapPost]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapPatch]`, `[MapTrace]`, `[MapConnect]`, and even `[MapQuery]` to describe the verb and route pattern. The generator creates the matching `Map*` call and wires up metadata like `.WithName`, `.WithDisplayName`, `.WithSummary`, and `.WithDescription` (via `[DisplayName]`/`[Description]`). -* **Feature-first organization** – keep handlers close to the code they execute (for example, alongside your `Todos` feature). Non-static handler classes are automatically registered with dependency injection so you can inject EF Core DbContexts, services, etc. -* **Metadata composition** – decorate classes and methods with `[Tags]`, `[RequireAuthorization]`, `[DisableAntiforgery]`, `[AllowAnonymous]`, and `[ExcludeFromDescription]`. Apply `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` directly to the methods they describe. Class-level metadata is merged into every method, while method-level metadata can refine or override. - -The generator also emits the `AddEndpointHandlers` and `MapEndpointHandlers` extension methods that do all the registration work for you. - ---- - -## Quick start - -The fastest way to understand GeneratedEndpoints is to build a minimal feature end-to-end. - -### 1. Install the package - -Add the package to the Minimal API project that will host your endpoints: - -```bash -dotnet add package GeneratedEndpoints -``` - -After the reference is added, the source generator contributes its attributes and routing extensions to the consuming project at build time. - -### 2. Create a handler class - -Handlers can be static or instance classes. The following example uses a scoped handler so that EF Core can be injected through the constructor: - -```csharp -using System.ComponentModel; -using Microsoft.AspNetCore.Generated.Attributes; -using Microsoft.AspNetCore.Http.HttpResults; - -namespace Todos.Features; - -public sealed class GetTodo -{ - private readonly TodoDbContext _db; - - public GetTodo(TodoDbContext db) => _db = db; - - [DisplayName("Retrieve a todo")] - [Description("Returns the todo matching the provided identifier.")] - [Summary("Retrieve a todo")] - [MapGet("/todos/{id}")] - [Tags("Todos")] - [RequireAuthorization("Todos.Read")] - public async Task, NotFound>> HandleAsync(Guid id, CancellationToken cancellationToken) - { - var entity = await _db.Todos.FindAsync(new object?[] { id }, cancellationToken); - return entity is null ? TypedResults.NotFound() : TypedResults.Ok(entity); - } -} -``` - -Key ideas: - -* Choose the attribute that matches the verb (`[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`). -* Named arguments like `Name`, plus `[Summary]`, `[DisplayName]`, and `[Description]`, are translated into `.WithName`, `.WithSummary`, `.WithDisplayName`, and `.WithDescription` calls. -* Use existing ASP.NET Core binding attributes (`[FromRoute]`, `[FromQuery]`, `[FromBody]`, `[FromHeader]`, `[FromServices]`, `[FromKeyedServices]`, `[AsParameters]`, etc.). The generator preserves them in the produced delegate. -* Metadata attributes (`[Tags]`, `[RequireAuthorization]`, `[AllowAnonymous]`, `[DisableAntiforgery]`, `[ExcludeFromDescription]`) can be placed on the class, on a method, or on both. Class-level metadata is merged with method-level metadata. Request/response attributes (`[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, `[ProducesValidationProblem]`) must be applied directly to the method they describe. - -### 3. Register handlers and map endpoints - -The generator emits two extension methods in `Microsoft.AspNetCore.Generated.Routing`. Call them during startup: - -```csharp -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Generated.Routing; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddEndpointHandlers(); // registers every non-static handler as scoped - -var app = builder.Build(); - -app.MapEndpointHandlers(); // emits MapGet/MapPost/etc. calls for every decorated method - -app.Run(); -``` - -`AddEndpointHandlers` calls `TryAddScoped()` for every non-static handler class. Static handler classes are skipped because they never need DI. `MapEndpointHandlers` iterates over those handler types, maps each annotated method, and returns the `IEndpointRouteBuilder` for further chaining. - -### 4. Add more endpoints - -Every attribute-decorated method becomes an endpoint the next time the project builds. Mix synchronous and asynchronous methods, return `IResult` or typed `Results<>`, and combine static and instance handlers in the same class. Metadata from attributes composes naturally. - -### 5. Run and verify - -Build the project (`dotnet build`) or run the app (`dotnet run`)—the generator will emit all routing code, DI registrations, and metadata without writing any manual `app.MapGet` or `app.MapPost` calls. - ---- - -## Handler styles and routing - -GeneratedEndpoints supports multiple handler styles so you can pick the one that matches the feature you're building. - -### Instance handlers (constructor injection) - -```csharp -public sealed class TodoEndpoints -{ - private readonly TodoDbContext _db; - - public TodoEndpoints(TodoDbContext db) => _db = db; - - [MapGet("/todos/{id}")] - public async Task, NotFound>> GetAsync(Guid id, CancellationToken cancellationToken) - { - var entity = await _db.Todos.FindAsync(new object?[] { id }, cancellationToken); - return entity is null ? TypedResults.NotFound() : TypedResults.Ok(entity); - } - - [MapDelete("/todos/{id}")] - [RequireAuthorization("Todos.Write")] - public static async Task> DeleteAsync( - Guid id, - [FromServices] TodoDbContext db, - CancellationToken cancellationToken) - { - var entity = await db.Todos.FindAsync(new object?[] { id }, cancellationToken); - if (entity is null) - return TypedResults.NotFound(); - - db.Todos.Remove(entity); - await db.SaveChangesAsync(cancellationToken); - return TypedResults.NoContent(); - } -} -``` - -* Non-static handler classes are registered as scoped services when you call `AddEndpointHandlers`. -* Instance methods are invoked on an injected instance. -* Static methods in the same class still work—they simply receive dependencies via `[FromServices]`. - -### Static handlers (stateless logic) - -```csharp -public static class ListTodos -{ - [MapGet("/todos")] - [Tags("Todos")] - public static Ok> Handle() - => TypedResults.Ok(TodoStore.All); -} -``` - -Static handlers excel when no services are required. Every annotated method inside the class must also be static, just like regular C# rules. - -### Feature layout example - -A feature folder might look like this: - -``` -Todos/ - Features/ - GetTodo.cs - ListTodos.cs - CreateTodo.cs -``` - -Each file contains exactly one handler class with attribute-decorated methods. `MapEndpointHandlers` discovers them automatically. Because everything lives next to the feature, developers can reason about behavior without scanning `Program.cs` or `Startup.cs`. - ---- - -## Configuring endpoints - -Some scenarios require more control over each endpoint's `IEndpointConventionBuilder`. The generator supports per-feature configuration through an optional static `Configure` method. - -```csharp -public sealed class CreateTodo -{ - [MapPost("/todos")] - public static Ok Handle([FromBody] Todo todo) => TypedResults.Ok(todo); - - public static void Configure(TBuilder builder) - where TBuilder : IEndpointConventionBuilder - { - builder.AddEndpointFilter(new TimingFilter()); - builder.WithOpenApi(operation => - { - operation.Summary = "Creates a todo"; - return operation; - }); - } -} -``` - -You can also request an `IServiceProvider` when configuration depends on registered services: - -```csharp -public static void Configure(TBuilder builder, IServiceProvider services) - where TBuilder : IEndpointConventionBuilder -{ - var conventions = services.GetRequiredService(); - conventions.Apply(builder); -} -``` - -The `Configure` method is emitted once per handler class, so every endpoint in the class receives the same conventions. - ---- - -## Describing requests and responses - -GeneratedEndpoints ships with attribute helpers for request/response metadata. They keep your OpenAPI description in sync with the implementation. - -```csharp -public sealed class CreateTodo -{ - [Summary("Create a todo")] - [MapPost("/todos")] - [Accepts("application/json", "application/xml")] - [ProducesResponse(StatusCodes.Status201Created)] - [ProducesProblem(StatusCodes.Status500InternalServerError)] - [ProducesValidationProblem(StatusCodes.Status400BadRequest)] - public static Created Handle([FromBody] CreateTodoRequest request) - => TypedResults.Created($"/todos/{request.Id}", request.ToTodo()); -} -``` - -* Use the generic form when the request/response type is known at compile time. For runtime types, set `RequestType` on `[Accepts]` and `ResponseType` on `[ProducesResponse]`. -* Mark `IsOptional = true` on `[Accepts]` to call `.Accepts(..., isOptional: true)`. -* Multiple `[Accepts]`, `[ProducesResponse]`, `[ProducesProblem]`, and `[ProducesValidationProblem]` attributes can be applied to the same method. The generator creates every corresponding `.Accepts` or `.Produces` call. - ---- - -## Attribute reference - -| Attribute | Scope | Purpose | -| --- | --- | --- | -| `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapDelete]`, `[MapPatch]`, `[MapHead]`, `[MapOptions]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]` | Method | Declares an endpoint and its route pattern. Named arguments fill the generated `.WithName` calls while `[Summary]`/`[DisplayName]`/`[Description]` add `.WithSummary`/`.WithDisplayName`/`.WithDescription`. | -| `[Summary]` | Class or method | Adds `.WithSummary("...")` to every annotated endpoint. Method-level summaries override class-level ones. | -| `[Tags]` | Class or method | Adds tags to one or more endpoints. Multiple attributes merge without duplication. | -| `[RequireAuthorization]` | Class or method | Requires authorization for the endpoint. Passing policies (`[RequireAuthorization("Todos.Read", "Todos.Write")]`) emits `.RequireAuthorization("Todos.Read", "Todos.Write")`. | -| `[AllowAnonymous]` | Class or method | Explicitly opts an endpoint into anonymous access, overriding `[RequireAuthorization]`. | -| `[RequireCors]` | Class or method | Adds `.RequireCors()` or `.RequireCors("PolicyName")` when a specific policy is provided. | -| `[RequireRateLimiting]` | Class or method | Adds `.RequireRateLimiting("PolicyName")` to enforce a named rate limiting policy. | -| `[RequireHost]` | Class or method | Adds `.RequireHost("example.com", "*.example.com")` so endpoints only match allowed hosts. | -| `[DisableAntiforgery]` | Class or method | Calls `.DisableAntiforgery()` on the generated endpoint. | -| `[ExcludeFromDescription]` | Class or method | Generates `.ExcludeFromDescription()` so the endpoint is hidden from OpenAPI/metadata. | -| `[Accepts]` / `[Accepts]` | Method | Emits `.Accepts(contentTypes..., isOptional: true|false)` to document supported request bodies. Multiple attributes allowed. | -| `[ProducesResponse]` / `[ProducesResponse]` | Method | Emits `.Produces(statusCode, contentTypes...)` for each documented response type. | -| `[ProducesProblem]` | Method | Emits `.ProducesProblem(statusCode, contentTypes...)` for endpoints that return RFC 7807 problem details. | -| `[ProducesValidationProblem]` | Method | Emits `.ProducesValidationProblem(statusCode, contentTypes...)`. | - -> ℹ️ Metadata defined on a class is applied to every annotated method inside the class. Method-level attributes can add entries (tags, accepts, produces, etc.) or override boolean flags like `[AllowAnonymous]`. - ---- - -## Tips, patterns, and extra examples - -### Authorization and security - -* `[RequireAuthorization]` adds `.RequireAuthorization()` or `.RequireAuthorization("policy")`. -* `[AllowAnonymous]` opt-in overrides class or global authorization requirements. -* `[RequireCors]` emits `.RequireCors()` or `.RequireCors("policy")` so endpoints participate in a configured CORS policy. -* `[RequireRateLimiting]` emits `.RequireRateLimiting("policy")` to enforce ASP.NET Core rate limiting middleware. -* `[RequireHost]` emits `.RequireHost("host")` so endpoints only match specific hosts. -* `[DisableAntiforgery]` wires `.DisableAntiforgery()` for CSRF-sensitive endpoints. - -### Handling query objects with `[AsParameters]` - -```csharp -public sealed record ListTodosQuery([FromQuery] int? Page, [FromQuery] string? Owner); - -public static class SearchTodos -{ - [MapGet("/todos/search")] - public static Ok> Handle([AsParameters] ListTodosQuery query) - { - var todos = TodoStore.Query(query.Page ?? 1, query.Owner); - return TypedResults.Ok(todos); - } -} -``` - -`[AsParameters]` lets you bundle multiple inputs into a record or class without writing manual binding logic. - -### Combining filters with `Configure` - -```csharp -public sealed class UpdateTodo -{ - [MapPut("/todos/{id}")] - [RequireAuthorization("Todos.Write")] - public async Task, NotFound>> HandleAsync(Guid id, [FromBody] UpdateTodoRequest request) - { - // ... - } - - public static void Configure(TBuilder builder, IServiceProvider services) - where TBuilder : IEndpointConventionBuilder - { - builder.AddEndpointFilter(new ValidationFilter()); - builder.AddEndpointFilterFactory((context, next) => new LoggingFilter(next, services.GetRequiredService())); - } -} -``` - -### Feature testing helper - -When testing, register handlers in a WebApplicationFactory or the new `MinimalApiApplicationBuilder`: - -```csharp -var app = MinimalApiApplication.CreateBuilder().Build(); -app.MapEndpointHandlers(); -``` - -All annotated methods become endpoints even in test hosts, so integration tests hit the same generated routing table as production. - -### Putting it all together - -```csharp -[Tags("Todos")] -[RequireAuthorization("Todos.Read")] -public sealed class TodoFeature -{ - private readonly TodoDbContext _db; - - public TodoFeature(TodoDbContext db) => _db = db; - - [Summary("Retrieve a todo")] - [MapGet("/todos/{id}")] - public async Task, NotFound>> GetAsync(Guid id, CancellationToken cancellationToken) - { - var entity = await _db.Todos.FindAsync(new object?[] { id }, cancellationToken); - return entity is null ? TypedResults.NotFound() : TypedResults.Ok(entity); - } - - [Summary("Create a todo")] - [MapPost("/todos")] - [ProducesResponse(StatusCodes.Status201Created)] - public async Task> CreateAsync([FromBody] CreateTodoRequest request, CancellationToken cancellationToken) - { - var todo = request.ToTodo(); - _db.Todos.Add(todo); - await _db.SaveChangesAsync(cancellationToken); - return TypedResults.Created($"/todos/{todo.Id}", todo); - } - - public static void Configure(TBuilder builder, IServiceProvider services) - where TBuilder : IEndpointConventionBuilder - { - var conventions = services.GetRequiredService(); - conventions.Apply(builder); - } -} -``` - -With these patterns you can grow a feature-based API without ever touching the routing layer manually. Drop a new handler class into your feature folder, decorate methods with the correct attributes, and let GeneratedEndpoints handle the plumbing. +# GeneratedEndpoints - Attribute-driven, source-generated Minimal API endpoints for feature-based development diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index b871436..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 0adf868..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index f743001..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,30 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/binding/{routeId}", static ([FromServices] global::GeneratedEndpointsTests.BindingNameEndpoints handler, [FromRoute(Name = "route-id")] int routeId, [FromQuery(Name = "filter-term")] string filter, [FromHeader(Name = "x-custom-header")] string traceId) => handler.Handle(routeId, filter, traceId)) - .WithName("Handle"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 2b881cc..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.BindingAttributeNamesArePreserved_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,30 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/binding/{routeId}", static ([FromServices] global::BindingNameEndpoints handler, [FromRoute(Name = "route-id")] int routeId, [FromQuery(Name = "filter-term")] string filter, [FromHeader(Name = "x-custom-header")] string traceId) => handler.Handle(routeId, filter, traceId)) - .WithName("Handle"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index e40e3c0..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index d185d47..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index e5accdf..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,31 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/allow-anon", global::GeneratedEndpointsTests.AllowAnonymousClass.Handle) - .WithName("Handle") - .RequireAuthorization(); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 22be3a5..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ClassAllowAnonymousMethodRequireAuthorization_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,31 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/allow-anon", global::AllowAnonymousClass.Handle) - .WithName("Handle") - .RequireAuthorization(); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 023508b..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index b8ee47d..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 28dfea3..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,32 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/filters", global::GeneratedEndpointsTests.FilteredEndpoints.Handle) - .WithName("Handle") - .AddEndpointFilter() - .AddEndpointFilter(); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 412dd52..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ConfigureRegistersEndpointFilters_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,32 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/filters", global::FilteredEndpoints.Handle) - .WithName("Handle") - .AddEndpointFilter() - .AddEndpointFilter(); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 1299ff2..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/grouped/first", global::GeneratedEndpointsTests.GroupedEndpoints.First) - .WithName("First") - .WithGroupName("SampleGroup"); - - builder.MapPost("/grouped/second", global::GeneratedEndpointsTests.GroupedEndpoints.Second) - .WithName("Second") - .WithGroupName("SampleGroup"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 27f809d..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.GroupNameAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/grouped/first", global::GroupedEndpoints.First) - .WithName("First") - .WithGroupName("SampleGroup"); - - builder.MapPost("/grouped/second", global::GroupedEndpoints.Second) - .WithName("Second") - .WithGroupName("SampleGroup"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 50c3a2f..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 06b6e7e..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 23b3d69..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,73 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapMethods("/complex", new[] { "CONNECT" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.ConnectComplex) - .WithName("ConnectComplex"); - - builder.MapPost("/complex", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.CreateComplexAsync) - .WithName("CreateComplex"); - - builder.MapDelete("/complex/{id:int}", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.DeleteComplex) - .WithName("DeleteComplex"); - - builder.MapMethods("/complex", new[] { "OPTIONS" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.DescribeComplex) - .WithName("DescribeComplex"); - - builder.MapMethods("/complex", new[] { "HEAD" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.HeadComplex) - .WithName("HeadComplex"); - - builder.MapPatch("/complex/{id:int}", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.PatchComplex) - .WithName("PatchComplex"); - - builder.MapMethods("/complex/query", new[] { "QUERY" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.QueryComplex) - .WithName("QueryComplex"); - - builder.MapMethods("/complex", new[] { "TRACE" }, global::GeneratedEndpointsTests.AllHttpMethodEndpoints.TraceComplex) - .WithName("TraceComplex"); - - builder.MapPut("/complex/{id:int}", global::GeneratedEndpointsTests.AllHttpMethodEndpoints.UpdateComplex) - .WithName("UpdateComplex"); - - builder.MapGet("/complex/{id:int}", static async ([FromServices] global::GeneratedEndpointsTests.ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader(Name = "x-trace-id")] string traceId, [FromBody] global::GeneratedEndpointsTests.GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, [FromKeyedServices("special")] object keyed, [AsParameters] global::GeneratedEndpointsTests.AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) - .WithName("GetComplex") - .WithDisplayName("Complex data endpoint") - .WithSummary("Gets complex data.") - .WithDescription("Uses every supported attribute.") - .ExcludeFromDescription() - .WithTags("Shared", "ClassLevel", "MethodLevel") - .Accepts("application/xml", "text/xml") - .Accepts("application/custom", "text/custom") - .Produces(201, "application/json", "text/json") - .Produces(200, "application/json", "text/json") - .ProducesProblem(503, "application/problem+json") - .ProducesProblem(400, "application/problem+json", "text/plain") - .ProducesValidationProblem(409, "application/problem+json", "text/plain") - .ProducesValidationProblem(422, "application/problem+json", "text/plain") - .RequireAuthorization("PolicyA", "PolicyB", "MethodPolicy") - .DisableAntiforgery() - .AllowAnonymous(); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 6707d74..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapAllAttributesAndHttpMethods_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,73 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapMethods("/complex", new[] { "CONNECT" }, global::AllHttpMethodEndpoints.ConnectComplex) - .WithName("ConnectComplex"); - - builder.MapPost("/complex", global::AllHttpMethodEndpoints.CreateComplexAsync) - .WithName("CreateComplex"); - - builder.MapDelete("/complex/{id:int}", global::AllHttpMethodEndpoints.DeleteComplex) - .WithName("DeleteComplex"); - - builder.MapMethods("/complex", new[] { "OPTIONS" }, global::AllHttpMethodEndpoints.DescribeComplex) - .WithName("DescribeComplex"); - - builder.MapMethods("/complex", new[] { "HEAD" }, global::AllHttpMethodEndpoints.HeadComplex) - .WithName("HeadComplex"); - - builder.MapPatch("/complex/{id:int}", global::AllHttpMethodEndpoints.PatchComplex) - .WithName("PatchComplex"); - - builder.MapMethods("/complex/query", new[] { "QUERY" }, global::AllHttpMethodEndpoints.QueryComplex) - .WithName("QueryComplex"); - - builder.MapMethods("/complex", new[] { "TRACE" }, global::AllHttpMethodEndpoints.TraceComplex) - .WithName("TraceComplex"); - - builder.MapPut("/complex/{id:int}", global::AllHttpMethodEndpoints.UpdateComplex) - .WithName("UpdateComplex"); - - builder.MapGet("/complex/{id:int}", static async ([FromServices] global::ComplexEndpoints handler, [FromRoute] int id, [FromQuery] string filter, [FromHeader(Name = "x-trace-id")] string traceId, [FromBody] global::GetRequest request, [FromForm] string formValue, [FromServices] IServiceProvider services, [FromKeyedServices("special")] object keyed, [AsParameters] global::AdditionalParameters parameters, global::System.Threading.CancellationToken cancellationToken) => await handler.GetComplex(id, filter, traceId, request, formValue, services, keyed, parameters, cancellationToken)) - .WithName("GetComplex") - .WithDisplayName("Complex data endpoint") - .WithSummary("Gets complex data.") - .WithDescription("Uses every supported attribute.") - .ExcludeFromDescription() - .WithTags("Shared", "ClassLevel", "MethodLevel") - .Accepts("application/xml", "text/xml") - .Accepts("application/custom", "text/custom") - .Produces(201, "application/json", "text/json") - .Produces(200, "application/json", "text/json") - .ProducesProblem(503, "application/problem+json") - .ProducesProblem(400, "application/problem+json", "text/plain") - .ProducesValidationProblem(409, "application/problem+json", "text/plain") - .ProducesValidationProblem(422, "application/problem+json", "text/plain") - .RequireAuthorization("PolicyA", "PolicyB", "MethodPolicy") - .DisableAntiforgery() - .AllowAnonymous(); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 0740514..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapFallback("/custom-fallback", global::GeneratedEndpointsTests.FallbackEndpoints.Custom) - .WithName("Custom"); - - builder.MapFallback(global::GeneratedEndpointsTests.FallbackEndpoints.Default) - .WithName("Default"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 9e29623..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapFallback_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapFallback("/custom-fallback", global::FallbackEndpoints.Custom) - .WithName("Custom"); - - builder.MapFallback(global::FallbackEndpoints.Default) - .WithName("Default"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 0ab0bbd..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - global::GeneratedEndpointsTests.ConfigureEndpoint.Configure( - builder.MapGet("/service-provider", global::GeneratedEndpointsTests.ConfigureEndpoint.Handle) - .WithName("Handle"), - builder.ServiceProvider - ); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index fa12613..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigureServiceProvider_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - global::ConfigureEndpoint.Configure( - builder.MapGet("/service-provider", global::ConfigureEndpoint.Handle) - .WithName("Handle"), - builder.ServiceProvider - ); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 5187bd7..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,32 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - global::GeneratedEndpointsTests.ConfigureEndpoint.Configure( - builder.MapGet("/configure", global::GeneratedEndpointsTests.ConfigureEndpoint.Handle) - .WithName("Handle") - ); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 88a888a..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGetWithConfigure_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,32 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - global::ConfigureEndpoint.Configure( - builder.MapGet("/configure", global::ConfigureEndpoint.Handle) - .WithName("Handle") - ); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 92fffc3..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,34 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/users/{id:int}", global::GeneratedEndpointsTests.GetUserEndpoint.GetUser2) - .WithName("GetUser") - .WithDisplayName("User lookup endpoint") - .WithSummary("Gets a user by ID.") - .WithDescription("Gets a user by ID when the ID is greater than zero.") - .WithTags("Users"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 6960286..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MapGet_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,34 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/users/{id:int}", global::GetUserEndpoint.GetUser2) - .WithName("GetUser") - .WithDisplayName("User lookup endpoint") - .WithSummary("Gets a user by ID.") - .WithDescription("Gets a user by ID when the ID is greater than zero.") - .WithTags("Users"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 9b5bd26..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/first", global::GeneratedEndpointsTests.FirstEndpoint.Handle) - .WithName("GeneratedEndpointsTests.FirstEndpoint.Handle"); - - builder.MapGet("/second", global::GeneratedEndpointsTests.SecondEndpoint.Handle) - .WithName("GeneratedEndpointsTests.SecondEndpoint.Handle"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 26d3618..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.MethodsWithSameNameAreFullyQualifiedWhenNamesCollide_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/first", global::FirstEndpoint.Handle) - .WithName("FirstEndpoint.Handle"); - - builder.MapGet("/second", global::SecondEndpoint.Handle) - .WithName("SecondEndpoint.Handle"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 1324ce4..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index fbd0298..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 96d1a7d..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/ordered/high", global::GeneratedEndpointsTests.OrderedEndpoints.High) - .WithName("High") - .WithOrder(5); - - builder.MapGet("/ordered/low", global::GeneratedEndpointsTests.OrderedEndpoints.Low) - .WithName("Low") - .WithOrder(-1); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 835f83f..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.OrderAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/ordered/high", global::OrderedEndpoints.High) - .WithName("High") - .WithOrder(5); - - builder.MapGet("/ordered/low", global::OrderedEndpoints.Low) - .WithName("Low") - .WithOrder(-1); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index c0a7b4c..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 7368a8b..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index ddca12b..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/cors/default", global::GeneratedEndpointsTests.CorsEndpoints.GetDefault) - .WithName("GetDefault") - .RequireCors(); - - builder.MapGet("/cors/named", global::GeneratedEndpointsTests.CorsEndpoints.GetNamed) - .WithName("GetNamed") - .RequireCors("NamedCorsPolicy"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index a9493f5..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireCorsAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/cors/default", global::CorsEndpoints.GetDefault) - .WithName("GetDefault") - .RequireCors(); - - builder.MapGet("/cors/named", global::CorsEndpoints.GetNamed) - .WithName("GetNamed") - .RequireCors("NamedCorsPolicy"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index c457739..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 2618da5..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 7edd9f5..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/hosts/class-only", global::GeneratedEndpointsTests.HostRestrictedEndpoints.ClassOnly) - .WithName("ClassOnly") - .RequireHost("*.contoso.com"); - - builder.MapGet("/hosts/method-override", global::GeneratedEndpointsTests.HostRestrictedEndpoints.MethodOverride) - .WithName("MethodOverride") - .RequireHost("*.contoso.com", "api.contoso.com", "contoso.com"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 3c6c219..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireHostAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/hosts/class-only", global::HostRestrictedEndpoints.ClassOnly) - .WithName("ClassOnly") - .RequireHost("*.contoso.com"); - - builder.MapGet("/hosts/method-override", global::HostRestrictedEndpoints.MethodOverride) - .WithName("MethodOverride") - .RequireHost("*.contoso.com", "api.contoso.com", "contoso.com"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index e86ba94..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 80b9edc..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - services.TryAddScoped(); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 582534c..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,32 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.RateLimiting; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/rate-limited", global::GeneratedEndpointsTests.RateLimitedEndpoints.Get) - .WithName("Get") - .RequireRateLimiting("NamedRateLimitPolicy"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index de795af..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.RequireRateLimitingAttribute_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,32 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.RateLimiting; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/rate-limited", global::RateLimitedEndpoints.Get) - .WithName("Get") - .RequireRateLimiting("NamedRateLimitPolicy"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index 64b4329..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_AddEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointServicesExtensions -{ - internal static void AddEndpointHandlers(this IServiceCollection services) - { - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt deleted file mode 100644 index 44e4209..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithNamespace.verified.txt +++ /dev/null @@ -1,57 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/timeouts/class-disable", global::GeneratedEndpointsTests.ClassLevelDisableRequestTimeoutEndpoints.ClassDisable) - .WithName("ClassDisable") - .DisableRequestTimeout(); - - builder.MapGet("/timeouts/class-default", global::GeneratedEndpointsTests.ClassLevelTimeoutEndpoints.ClassDefault) - .WithName("ClassDefault") - .ShortCircuit() - .WithRequestTimeout(); - - builder.MapGet("/timeouts/class-override", global::GeneratedEndpointsTests.ClassLevelTimeoutEndpoints.ClassOverride) - .WithName("ClassOverride") - .ShortCircuit() - .WithRequestTimeout("ClassPolicy"); - - builder.MapGet("/timeouts/method-disable", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodDisable) - .WithName("MethodDisable") - .DisableRequestTimeout(); - - builder.MapGet("/timeouts/method-short", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodShortCircuit) - .WithName("MethodShortCircuit") - .ShortCircuit(); - - builder.MapGet("/timeouts/method-default", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodWithDefault) - .WithName("MethodWithDefault") - .WithRequestTimeout(); - - builder.MapGet("/timeouts/method-policy", global::GeneratedEndpointsTests.MethodLevelTimeoutEndpoints.MethodWithPolicy) - .WithName("MethodWithPolicy") - .WithRequestTimeout("MethodPolicy"); - - return builder; - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt deleted file mode 100644 index b08328c..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.ShortCircuitAndRequestTimeoutAttributes_MapEndpointHandlers_WithoutNamespace.verified.txt +++ /dev/null @@ -1,57 +0,0 @@ -//----------------------------------------------------------------------------- -// -// This code was generated by MinimalApiGenerator which can be found -// in the GeneratedEndpoints namespace. -// -// Changes to this file may cause incorrect behavior -// and will be lost if the code is regenerated. -// -//----------------------------------------------------------------------------- - -#nullable enable - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Generated.Routing; - -internal static class EndpointRouteBuilderExtensions -{ - internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) - { - builder.MapGet("/timeouts/class-disable", global::ClassLevelDisableRequestTimeoutEndpoints.ClassDisable) - .WithName("ClassDisable") - .DisableRequestTimeout(); - - builder.MapGet("/timeouts/class-default", global::ClassLevelTimeoutEndpoints.ClassDefault) - .WithName("ClassDefault") - .ShortCircuit() - .WithRequestTimeout(); - - builder.MapGet("/timeouts/class-override", global::ClassLevelTimeoutEndpoints.ClassOverride) - .WithName("ClassOverride") - .ShortCircuit() - .WithRequestTimeout("ClassPolicy"); - - builder.MapGet("/timeouts/method-disable", global::MethodLevelTimeoutEndpoints.MethodDisable) - .WithName("MethodDisable") - .DisableRequestTimeout(); - - builder.MapGet("/timeouts/method-short", global::MethodLevelTimeoutEndpoints.MethodShortCircuit) - .WithName("MethodShortCircuit") - .ShortCircuit(); - - builder.MapGet("/timeouts/method-default", global::MethodLevelTimeoutEndpoints.MethodWithDefault) - .WithName("MethodWithDefault") - .WithRequestTimeout(); - - builder.MapGet("/timeouts/method-policy", global::MethodLevelTimeoutEndpoints.MethodWithPolicy) - .WithName("MethodWithPolicy") - .WithRequestTimeout("MethodPolicy"); - - return builder; - } -} From 907883b0fc698e627b94a708714848b305149516 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 20:38:22 -0500 Subject: [PATCH 49/75] Add individual generator coverage (#41) --- .../GeneratedEndpointsTests.cs | 601 -------- ...E8A8FA30F_AddEndpointHandlers.verified.txt | 24 + ...E8A8FA30F_MapEndpointHandlers.verified.txt | 41 + ...2AC35B582_AddEndpointHandlers.verified.txt | 24 + ...2AC35B582_MapEndpointHandlers.verified.txt | 42 + ...41534B0BD_AddEndpointHandlers.verified.txt | 24 + ...41534B0BD_MapEndpointHandlers.verified.txt | 38 + ...4B85A7155_AddEndpointHandlers.verified.txt | 24 + ...4B85A7155_MapEndpointHandlers.verified.txt | 37 + ...575ECE261_AddEndpointHandlers.verified.txt | 24 + ...575ECE261_MapEndpointHandlers.verified.txt | 35 + ...08C7DE832_AddEndpointHandlers.verified.txt | 23 + ...08C7DE832_MapEndpointHandlers.verified.txt | 32 + ...A05F2C177_AddEndpointHandlers.verified.txt | 23 + ...A05F2C177_MapEndpointHandlers.verified.txt | 32 + ...2502B995B_AddEndpointHandlers.verified.txt | 23 + ...2502B995B_MapEndpointHandlers.verified.txt | 33 + ...DF1784969_AddEndpointHandlers.verified.txt | 23 + ...DF1784969_MapEndpointHandlers.verified.txt | 31 + ...1A0B7E7CE_AddEndpointHandlers.verified.txt | 23 + ...1A0B7E7CE_MapEndpointHandlers.verified.txt | 33 + ...0CA9A1CFC_AddEndpointHandlers.verified.txt | 24 + ...0CA9A1CFC_MapEndpointHandlers.verified.txt | 37 + ...17B97AFD0_AddEndpointHandlers.verified.txt | 24 + ...17B97AFD0_MapEndpointHandlers.verified.txt | 35 + ...FE6E1F139_AddEndpointHandlers.verified.txt | 24 + ...FE6E1F139_MapEndpointHandlers.verified.txt | 39 + ...075874154_AddEndpointHandlers.verified.txt | 24 + ...075874154_MapEndpointHandlers.verified.txt | 35 + ...9B964FDBC_AddEndpointHandlers.verified.txt | 24 + ...9B964FDBC_MapEndpointHandlers.verified.txt | 36 + ...DFFBCB949_AddEndpointHandlers.verified.txt | 23 + ...DFFBCB949_MapEndpointHandlers.verified.txt | 48 + ...E7BEC9B3F_AddEndpointHandlers.verified.txt | 23 + ...E7BEC9B3F_MapEndpointHandlers.verified.txt | 57 + ...EF512DD7F_AddEndpointHandlers.verified.txt | 23 + ...EF512DD7F_MapEndpointHandlers.verified.txt | 42 + ...8F4695C80_AddEndpointHandlers.verified.txt | 23 + ...8F4695C80_MapEndpointHandlers.verified.txt | 48 + ...DE56BC773_AddEndpointHandlers.verified.txt | 23 + ...DE56BC773_MapEndpointHandlers.verified.txt | 48 + ...EBA0910FD_AddEndpointHandlers.verified.txt | 23 + ...EBA0910FD_MapEndpointHandlers.verified.txt | 30 + ...6E889EAF2_AddEndpointHandlers.verified.txt | 23 + ...6E889EAF2_MapEndpointHandlers.verified.txt | 30 + ...225B7419F_AddEndpointHandlers.verified.txt | 23 + ...225B7419F_MapEndpointHandlers.verified.txt | 28 + ...3AB7FBEBA_AddEndpointHandlers.verified.txt | 23 + ...3AB7FBEBA_MapEndpointHandlers.verified.txt | 33 + ...17F3BAD82_AddEndpointHandlers.verified.txt | 23 + ...17F3BAD82_MapEndpointHandlers.verified.txt | 30 + .../GeneratedSourceTests.cs | 1324 +++++++++++++++++ ...Attribute_AddEndpointHandlers.verified.txt | 24 + ...Attribute_MapEndpointHandlers.verified.txt | 30 + ...tentTypes_AddEndpointHandlers.verified.txt | 24 + ...tentTypes_MapEndpointHandlers.verified.txt | 30 + ...arameters_AddEndpointHandlers.verified.txt | 24 + ...arameters_MapEndpointHandlers.verified.txt | 30 + ...dingNames_AddEndpointHandlers.verified.txt | 24 + ...dingNames_MapEndpointHandlers.verified.txt | 30 + ...Anonymous_AddEndpointHandlers.verified.txt | 24 + ...Anonymous_MapEndpointHandlers.verified.txt | 31 + ...intFilter_AddEndpointHandlers.verified.txt | 23 + ...intFilter_MapEndpointHandlers.verified.txt | 33 + ...orization_AddEndpointHandlers.verified.txt | 24 + ...orization_MapEndpointHandlers.verified.txt | 31 + ...ithPolicy_AddEndpointHandlers.verified.txt | 24 + ...ithPolicy_MapEndpointHandlers.verified.txt | 31 + ...quireCors_AddEndpointHandlers.verified.txt | 24 + ...quireCors_MapEndpointHandlers.verified.txt | 31 + ...quireHost_AddEndpointHandlers.verified.txt | 24 + ...quireHost_MapEndpointHandlers.verified.txt | 31 + ...ClassTags_AddEndpointHandlers.verified.txt | 24 + ...ClassTags_MapEndpointHandlers.verified.txt | 31 + ...sMetadata_AddEndpointHandlers.verified.txt | 23 + ...sMetadata_MapEndpointHandlers.verified.txt | 32 + ...ersFilter_AddEndpointHandlers.verified.txt | 23 + ...ersFilter_MapEndpointHandlers.verified.txt | 32 + ...eProvider_AddEndpointHandlers.verified.txt | 23 + ...eProvider_MapEndpointHandlers.verified.txt | 30 + ...Anonymous_AddEndpointHandlers.verified.txt | 24 + ...Anonymous_MapEndpointHandlers.verified.txt | 31 + ...scription_AddEndpointHandlers.verified.txt | 24 + ...scription_MapEndpointHandlers.verified.txt | 31 + ...orization_AddEndpointHandlers.verified.txt | 24 + ...orization_MapEndpointHandlers.verified.txt | 31 + ...tractTags_AddEndpointHandlers.verified.txt | 24 + ...tractTags_MapEndpointHandlers.verified.txt | 31 + ...backRoute_AddEndpointHandlers.verified.txt | 23 + ...backRoute_MapEndpointHandlers.verified.txt | 30 + ...lbackOnly_AddEndpointHandlers.verified.txt | 23 + ...lbackOnly_MapEndpointHandlers.verified.txt | 30 + ...stTimeout_AddEndpointHandlers.verified.txt | 24 + ...stTimeout_MapEndpointHandlers.verified.txt | 31 + ...Attribute_AddEndpointHandlers.verified.txt | 24 + ...Attribute_MapEndpointHandlers.verified.txt | 31 + ...scription_AddEndpointHandlers.verified.txt | 24 + ...scription_MapEndpointHandlers.verified.txt | 31 + ...dServices_AddEndpointHandlers.verified.txt | 24 + ...dServices_MapEndpointHandlers.verified.txt | 30 + ...mServices_AddEndpointHandlers.verified.txt | 24 + ...mServices_MapEndpointHandlers.verified.txt | 30 + ...Attribute_AddEndpointHandlers.verified.txt | 24 + ...Attribute_MapEndpointHandlers.verified.txt | 31 + ...intFilter_AddEndpointHandlers.verified.txt | 23 + ...intFilter_MapEndpointHandlers.verified.txt | 33 + ...GroupName_AddEndpointHandlers.verified.txt | 24 + ...GroupName_MapEndpointHandlers.verified.txt | 31 + ...tEndpoint_AddEndpointHandlers.verified.txt | 23 + ...tEndpoint_MapEndpointHandlers.verified.txt | 30 + ...eEndpoint_AddEndpointHandlers.verified.txt | 23 + ...eEndpoint_MapEndpointHandlers.verified.txt | 30 + ...tEndpoint_AddEndpointHandlers.verified.txt | 23 + ...tEndpoint_MapEndpointHandlers.verified.txt | 30 + ...dEndpoint_AddEndpointHandlers.verified.txt | 23 + ...dEndpoint_MapEndpointHandlers.verified.txt | 30 + ...sEndpoint_AddEndpointHandlers.verified.txt | 23 + ...sEndpoint_MapEndpointHandlers.verified.txt | 30 + ...hEndpoint_AddEndpointHandlers.verified.txt | 23 + ...hEndpoint_MapEndpointHandlers.verified.txt | 30 + ...tEndpoint_AddEndpointHandlers.verified.txt | 23 + ...tEndpoint_MapEndpointHandlers.verified.txt | 30 + ...tEndpoint_AddEndpointHandlers.verified.txt | 23 + ...tEndpoint_MapEndpointHandlers.verified.txt | 30 + ...yEndpoint_AddEndpointHandlers.verified.txt | 23 + ...yEndpoint_MapEndpointHandlers.verified.txt | 30 + ...eEndpoint_AddEndpointHandlers.verified.txt | 23 + ...eEndpoint_MapEndpointHandlers.verified.txt | 30 + ...Anonymous_AddEndpointHandlers.verified.txt | 24 + ...Anonymous_MapEndpointHandlers.verified.txt | 31 + ...intFilter_AddEndpointHandlers.verified.txt | 23 + ...intFilter_MapEndpointHandlers.verified.txt | 33 + ...Collision_AddEndpointHandlers.verified.txt | 23 + ...Collision_MapEndpointHandlers.verified.txt | 36 + ...orization_AddEndpointHandlers.verified.txt | 24 + ...orization_MapEndpointHandlers.verified.txt | 31 + ...ithPolicy_AddEndpointHandlers.verified.txt | 24 + ...ithPolicy_MapEndpointHandlers.verified.txt | 31 + ...quireCors_AddEndpointHandlers.verified.txt | 24 + ...quireCors_MapEndpointHandlers.verified.txt | 31 + ...quireHost_AddEndpointHandlers.verified.txt | 24 + ...quireHost_MapEndpointHandlers.verified.txt | 31 + ...ethodTags_AddEndpointHandlers.verified.txt | 24 + ...ethodTags_MapEndpointHandlers.verified.txt | 31 + ...rMetadata_AddEndpointHandlers.verified.txt | 24 + ...rMetadata_MapEndpointHandlers.verified.txt | 31 + ...Attribute_AddEndpointHandlers.verified.txt | 24 + ...Attribute_MapEndpointHandlers.verified.txt | 31 + ...Attribute_AddEndpointHandlers.verified.txt | 24 + ...Attribute_MapEndpointHandlers.verified.txt | 31 + ...tentTypes_AddEndpointHandlers.verified.txt | 24 + ...tentTypes_MapEndpointHandlers.verified.txt | 31 + ...Attribute_AddEndpointHandlers.verified.txt | 24 + ...Attribute_MapEndpointHandlers.verified.txt | 31 + ...ithPolicy_AddEndpointHandlers.verified.txt | 24 + ...ithPolicy_MapEndpointHandlers.verified.txt | 31 + ...stTimeout_AddEndpointHandlers.verified.txt | 24 + ...stTimeout_MapEndpointHandlers.verified.txt | 31 + ...ithPolicy_AddEndpointHandlers.verified.txt | 24 + ...ithPolicy_MapEndpointHandlers.verified.txt | 32 + ...eLimiting_AddEndpointHandlers.verified.txt | 24 + ...eLimiting_MapEndpointHandlers.verified.txt | 30 + ...rtCircuit_AddEndpointHandlers.verified.txt | 24 + ...rtCircuit_MapEndpointHandlers.verified.txt | 31 + ...ttributes_AddEndpointHandlers.verified.txt | 24 + ...ttributes_MapEndpointHandlers.verified.txt | 32 + 166 files changed, 5950 insertions(+), 601 deletions(-) delete mode 100644 tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_MapEndpointHandlers.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs deleted file mode 100644 index 9596c66..0000000 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpointsTests.cs +++ /dev/null @@ -1,601 +0,0 @@ -using GeneratedEndpoints.Tests.Common; -using SourceGeneratorTestHelpers.XUnit; - -namespace GeneratedEndpoints.Tests; - -[UsesVerify] -public class GeneratedEndpointsTests -{ - public GeneratedEndpointsTests() - { - ModuleInitializer.Initialize(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MapFallback(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - internal static class FallbackEndpoints - { - [MapFallback] - public static Ok Default() - => TypedResults.Ok(); - - [MapFallback("/custom-fallback")] - public static Ok Custom() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapFallback)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapFallback)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MapGet(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - [Tags("Users")] - internal static class GetUserEndpoint - { - [DisplayName("User lookup endpoint")] - [Description("Gets a user by ID when the ID is greater than zero.")] - [Summary("Gets a user by ID.")] - [MapGet("/users/{id:int}", Name = nameof(GetUser))] - public static Results GetUser2(int id) - { - if (id > 0) - return TypedResults.Ok(); - - return TypedResults.NotFound(); - } - } - """, withNamespace - ); - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapGet)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapGet)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MethodsWithSameNameAreFullyQualifiedWhenNamesCollide(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - internal sealed class FirstEndpoint - { - [MapGet("/first")] - public static Ok Handle() - => TypedResults.Ok(); - } - - internal sealed class SecondEndpoint - { - [MapGet("/second")] - public static Ok Handle() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MethodsWithSameNameAreFullyQualifiedWhenNamesCollide)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MapGetWithConfigure(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - using Microsoft.AspNetCore.Builder; - - internal static class ConfigureEndpoint - { - [MapGet("/configure")] - public static Ok Handle() - => TypedResults.Ok(); - - public static void Configure(TBuilder builder) - where TBuilder : IEndpointConventionBuilder - { - builder.WithMetadata(new object()); - } - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapGetWithConfigure)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapGetWithConfigure)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MapGetWithConfigureServiceProvider(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - using Microsoft.AspNetCore.Builder; - - internal static class ConfigureEndpoint - { - [MapGet("/service-provider")] - public static Ok Handle() - => TypedResults.Ok(); - - public static void Configure(TBuilder builder, System.IServiceProvider serviceProvider) - where TBuilder : IEndpointConventionBuilder - { - _ = serviceProvider; - builder.WithMetadata(new object()); - } - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapGetWithConfigureServiceProvider)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapGetWithConfigureServiceProvider)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ClassAllowAnonymousMethodRequireAuthorization(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - [AllowAnonymous] - internal sealed class AllowAnonymousClass - { - [MapGet("/allow-anon")] - [RequireAuthorization] - public static Ok Handle() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(ClassAllowAnonymousMethodRequireAuthorization)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(ClassAllowAnonymousMethodRequireAuthorization)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RequireCorsAttributes(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - internal sealed class CorsEndpoints - { - [MapGet("/cors/default")] - [RequireCors] - public static Ok GetDefault() - => TypedResults.Ok(); - - [MapGet("/cors/named")] - [RequireCors("NamedCorsPolicy")] - public static Ok GetNamed() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(RequireCorsAttributes)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(RequireCorsAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RequireRateLimitingAttribute(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - internal sealed class RateLimitedEndpoints - { - [MapGet("/rate-limited")] - [RequireRateLimiting("NamedRateLimitPolicy")] - public static Ok Get() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(RequireRateLimitingAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(RequireRateLimitingAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RequireHostAttributes(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - [RequireHost("*.contoso.com")] - internal sealed class HostRestrictedEndpoints - { - [MapGet("/hosts/class-only")] - public static Ok ClassOnly() - => TypedResults.Ok(); - - [MapGet("/hosts/method-override")] - [RequireHost("api.contoso.com", "contoso.com")] - public static Ok MethodOverride() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(RequireHostAttributes)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(RequireHostAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ShortCircuitAndRequestTimeoutAttributes(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - [ShortCircuit] - [RequestTimeout] - internal static class ClassLevelTimeoutEndpoints - { - [MapGet("/timeouts/class-default")] - public static Ok ClassDefault() - => TypedResults.Ok(); - - [MapGet("/timeouts/class-override")] - [RequestTimeout("ClassPolicy")] - public static Ok ClassOverride() - => TypedResults.Ok(); - } - - [DisableRequestTimeout] - internal static class ClassLevelDisableRequestTimeoutEndpoints - { - [MapGet("/timeouts/class-disable")] - public static Ok ClassDisable() - => TypedResults.Ok(); - } - - internal static class MethodLevelTimeoutEndpoints - { - [MapGet("/timeouts/method-disable")] - [DisableRequestTimeout] - public static Ok MethodDisable() - => TypedResults.Ok(); - - [MapGet("/timeouts/method-default")] - [RequestTimeout] - public static Ok MethodWithDefault() - => TypedResults.Ok(); - - [MapGet("/timeouts/method-policy")] - [RequestTimeout("MethodPolicy")] - public static Ok MethodWithPolicy() - => TypedResults.Ok(); - - [MapGet("/timeouts/method-short")] - [ShortCircuit] - public static Ok MethodShortCircuit() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(ShortCircuitAndRequestTimeoutAttributes)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(ShortCircuitAndRequestTimeoutAttributes)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task OrderAttribute(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - internal sealed class OrderedEndpoints - { - [MapGet("/ordered/low")] - [Order(-1)] - public static Ok Low() - => TypedResults.Ok(); - - [MapGet("/ordered/high")] - [Order(5)] - public static Ok High() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(OrderAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(OrderAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GroupNameAttribute(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - [GroupName("SampleGroup")] - internal static class GroupedEndpoints - { - [MapGet("/grouped/first")] - public static Ok First() - => TypedResults.Ok(); - - [MapPost("/grouped/second")] - public static Ok Second() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(GroupNameAttribute)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(GroupNameAttribute)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ConfigureRegistersEndpointFilters(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - - [EndpointFilter(typeof(TimingFilter))] - internal sealed class FilteredEndpoints - { - [MapGet("/filters")] - [EndpointFilter] - public static Ok Handle() - => TypedResults.Ok(); - } - - internal sealed class TimingFilter : IEndpointFilter - { - public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) - => next(context); - } - - internal sealed class ValidationFilter : IEndpointFilter - { - public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) - => next(context); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(ConfigureRegistersEndpointFilters)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(ConfigureRegistersEndpointFilters)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task BindingAttributeNamesArePreserved(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - using Microsoft.AspNetCore.Mvc; - - internal sealed class BindingNameEndpoints - { - [MapGet("/binding/{routeId}")] - public Ok Handle( - [FromRoute(Name = "route-id")] int routeId, - [FromQuery(Name = "filter-term")] string filter, - [FromHeader(Name = "x-custom-header")] string traceId) - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(BindingAttributeNamesArePreserved)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(BindingAttributeNamesArePreserved)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MapAllAttributesAndHttpMethods(bool withNamespace) - { - var sources = TestHelpers.GetSources(""" - using System.Threading; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.DependencyInjection; - - [Tags("Shared", "ClassLevel")] - [RequireAuthorization("PolicyA", "PolicyB")] - [DisableAntiforgery] - [ExcludeFromDescription] - internal sealed class ComplexEndpoints - { - private readonly IServiceProvider _serviceProvider; - - public ComplexEndpoints(IServiceProvider serviceProvider) - => _serviceProvider = serviceProvider; - - public static void Configure(TBuilder builder, IServiceProvider serviceProvider) - where TBuilder : IEndpointConventionBuilder - { - _ = serviceProvider; - builder.WithMetadata("configured"); - } - - [DisplayName("Complex data endpoint")] - [Description("Uses every supported attribute.")] - [Summary("Gets complex data.")] - [MapGet("/complex/{id:int}", Name = nameof(GetComplex))] - [AllowAnonymous] - [Tags("MethodLevel")] - [RequireAuthorization("MethodPolicy")] - [Accepts("application/xml", "text/xml", RequestType = typeof(ClassLevelRequest))] - [Accepts("application/custom", "text/custom")] - [ProducesResponse(201, "application/json", "text/json", ResponseType = typeof(ClassLevelResponse))] - [Microsoft.AspNetCore.Generated.Attributes.ProducesResponse(200, "application/json", "text/json")] - [ProducesProblem(503, "application/problem+json")] - [ProducesProblem(400, "application/problem+json", "text/plain")] - [ProducesValidationProblem(409, "application/problem+json", "text/plain")] - [ProducesValidationProblem(422, "application/problem+json", "text/plain")] - [ExcludeFromDescription] - public async Task, NotFound>> GetComplex( - [FromRoute] int id, - [FromQuery] string? filter, - [FromHeader(Name = "x-trace-id")] string? traceId, - [FromBody] GetRequest request, - [FromForm] string? formValue, - [FromServices] IServiceProvider services, - [FromKeyedServices("special")] object keyed, - [AsParameters] AdditionalParameters parameters, - CancellationToken cancellationToken) - { - _ = _serviceProvider; - _ = traceId; - _ = formValue; - _ = services; - _ = keyed; - _ = parameters; - await Task.Yield(); - cancellationToken.ThrowIfCancellationRequested(); - return TypedResults.Ok(new GetResponse(id)); - } - } - - internal sealed record AdditionalParameters(string? Search, int? Page); - - internal sealed record ClassLevelRequest(int Value); - - internal sealed record ClassLevelResponse(int Value); - - internal sealed record GetRequest(int Value); - - internal sealed record GetResponse(int Value); - - internal static class AllHttpMethodEndpoints - { - [MapPost("/complex")] - public static async Task> CreateComplexAsync([FromBody] GetRequest request) - { - await Task.Yield(); - return TypedResults.Created($"/complex/{request.Value}", new GetResponse(request.Value)); - } - - [MapPut("/complex/{id:int}")] - public static Results UpdateComplex(int id) - => id > 0 ? TypedResults.NoContent() : TypedResults.NotFound(); - - [MapDelete("/complex/{id:int}")] - public static IResult DeleteComplex(int id) - => id > 0 ? TypedResults.Ok() : TypedResults.NotFound(); - - [MapOptions("/complex")] - public static IResult DescribeComplex() - => TypedResults.Ok(); - - [MapHead("/complex")] - public static IResult HeadComplex() - => TypedResults.Ok(); - - [MapPatch("/complex/{id:int}")] - public static IResult PatchComplex(int id) - => TypedResults.Ok(); - - [MapQuery("/complex/query")] - public static IResult QueryComplex([FromQuery] string term) - => TypedResults.Ok(term); - - [MapTrace("/complex")] - public static IResult TraceComplex() - => TypedResults.Ok(); - - [MapConnect("/complex")] - public static IResult ConnectComplex() - => TypedResults.Ok(); - } - """, withNamespace - ); - - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapAllAttributesAndHttpMethods)}_AddEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{nameof(MapAllAttributesAndHttpMethods)}_MapEndpointHandlers_With{(withNamespace ? "" : "out")}Namespace"); - } -} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..b2993f3 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithGroupName("Docs") + .WithOrder(-5) + .ExcludeFromDescription() + .WithTags("Method", "Matrix") + .RequireAuthorization("MethodPolicy") + .RequireCors("MethodCors") + .RequireHost("api.alt.com") + .RequireRateLimiting("BurstPolicy") + .AllowAnonymous() + .DisableRequestTimeout(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..e8bc7b5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithGroupName("Reporting") + .WithOrder(5) + .ExcludeFromDescription() + .WithTags("Class", "Matrix", "Method") + .RequireAuthorization("ClassPolicy") + .RequireCors("NamedCorsPolicy") + .RequireHost("*.contoso.com", "api.contoso.com", "contoso.com") + .RequireRateLimiting("RatePolicy") + .AllowAnonymous() + .ShortCircuit() + .WithRequestTimeout("TimeoutPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..bb25ed3 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithGroupName("Operations") + .ExcludeFromDescription() + .WithTags("Class", "Matrix") + .RequireAuthorization("ClassPolicy", "MethodPolicy") + .RequireCors() + .RequireHost("*.example.com") + .AllowAnonymous() + .DisableRequestTimeout(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..82cb438 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..ed92292 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_6F94B85A7155_MapEndpointHandlers.verified.txt @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithOrder(-1) + .WithTags("Method", "Matrix") + .RequireAuthorization("MethodPolicy") + .RequireCors("MethodCors") + .RequireHost("services.contoso.com", "contoso.com") + .AllowAnonymous() + .DisableRequestTimeout(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..82cb438 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..158db91 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_9D6575ECE261_MapEndpointHandlers.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithOrder(10) + .WithTags("Class", "Matrix") + .RequireAuthorization("ClassPolicy") + .RequireHost("*.alt.com", "contoso.com") + .ShortCircuit(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d1abab8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_25B08C7DE832_MapEndpointHandlers.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + .AddEndpointFilter(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d1abab8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_401A05F2C177_MapEndpointHandlers.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + .AddEndpointFilter(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..addcc95 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_A172502B995B_MapEndpointHandlers.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::ConfigureFilterEndpoints.Configure( + builder.MapGet("/configure-filters", global::ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..146556a --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_B46DF1784969_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/configure-filters", global::ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..f5054db --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ConfigureAndFiltersMatrix_EFC1A0B7E7CE_MapEndpointHandlers.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Configure( + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..493d722 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_8330CA9A1CFC_MapEndpointHandlers.verified.txt @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .WithDisplayName("Contract endpoint") + .WithSummary("Gets detailed content.") + .WithDescription("Shows binding and contract combinations.") + .Accepts("application/json") + .ProducesValidationProblem(422, "application/problem+json") + .RequireAuthorization("ContractsPolicy") + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..7a6643b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..429595c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_89F17B97AFD0_MapEndpointHandlers.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::ContractEndpoints.Handle) + .WithName("Handle") + .ExcludeFromDescription() + .WithTags("Contracts", "Bindings") + .Produces(200, "application/problem+json") + .ProducesProblem(500, "application/problem+json") + .RequireAuthorization("ContractsPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..70f58ad --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F5FE6E1F139_MapEndpointHandlers.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .WithDisplayName("Contract endpoint") + .WithSummary("Gets detailed content.") + .WithDescription("Shows binding and contract combinations.") + .WithTags("Contracts", "Bindings") + .Accepts("application/xml") + .Produces(200, "application/json", "text/json") + .ProducesProblem(500, "application/json") + .ProducesValidationProblem(422, "application/json") + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..7a6643b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..810b914 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_9F7075874154_MapEndpointHandlers.verified.txt @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::ContractEndpoints.Handle) + .WithName("Handle") + .WithDisplayName("Contract endpoint") + .ExcludeFromDescription() + .Produces(200, "application/json") + .ProducesValidationProblem(422, "application/json") + .RequireAuthorization("ContractsPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..f4393e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.ContractsAndBindingMatrix_DDC9B964FDBC_MapEndpointHandlers.verified.txt @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .WithSummary("Gets detailed content.") + .WithDescription("Shows binding and contract combinations.") + .WithTags("Contracts", "Bindings") + .Accepts("application/json") + .ProducesProblem(500, "application/problem+json") + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..aaac7d8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_346DFFBCB949_MapEndpointHandlers.verified.txt @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/alternate", global::GeneratedEndpointsTests.AlternateEndpoints.Get) + .WithName("GeneratedEndpointsTests.AlternateEndpoints.Get"); + + builder.MapPost("/alternate", global::GeneratedEndpointsTests.AlternateEndpoints.Post) + .WithName("Post"); + + builder.MapMethods("/matrix", new[] { "CONNECT" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Connect) + .WithName("Connect"); + + builder.MapDelete("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Delete) + .WithName("Delete"); + + builder.MapGet("/matrix", global::GeneratedEndpointsTests.HttpMethodEndpoints.Get) + .WithName("GeneratedEndpointsTests.HttpMethodEndpoints.Get"); + + builder.MapMethods("/matrix", new[] { "HEAD" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Head) + .WithName("Head"); + + builder.MapMethods("/matrix/query", new[] { "QUERY" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Query) + .WithName("Query"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..05e9faf --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_434E7BEC9B3F_MapEndpointHandlers.verified.txt @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/matrix", new[] { "CONNECT" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Connect) + .WithName("Connect"); + + builder.MapDelete("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Delete) + .WithName("Delete"); + + builder.MapGet("/matrix", global::GeneratedEndpointsTests.HttpMethodEndpoints.Get) + .WithName("Get"); + + builder.MapMethods("/matrix", new[] { "HEAD" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Head) + .WithName("Head"); + + builder.MapMethods("/matrix", new[] { "OPTIONS" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Options) + .WithName("Options"); + + builder.MapPatch("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Patch) + .WithName("Patch"); + + builder.MapPost("/matrix", global::GeneratedEndpointsTests.HttpMethodEndpoints.Post) + .WithName("Post"); + + builder.MapPut("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Put) + .WithName("Put"); + + builder.MapMethods("/matrix/query", new[] { "QUERY" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Query) + .WithName("Query"); + + builder.MapMethods("/matrix", new[] { "TRACE" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Trace) + .WithName("Trace"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..01e9c67 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_541EF512DD7F_MapEndpointHandlers.verified.txt @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/matrix", new[] { "OPTIONS" }, global::HttpMethodEndpoints.Options) + .WithName("Options"); + + builder.MapPatch("/matrix/{id:int}", global::HttpMethodEndpoints.Patch) + .WithName("Patch"); + + builder.MapPost("/matrix", global::HttpMethodEndpoints.Post) + .WithName("Post"); + + builder.MapPut("/matrix/{id:int}", global::HttpMethodEndpoints.Put) + .WithName("Put"); + + builder.MapMethods("/matrix", new[] { "TRACE" }, global::HttpMethodEndpoints.Trace) + .WithName("Trace"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..9569a2a --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_DC18F4695C80_MapEndpointHandlers.verified.txt @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/alternate", global::AlternateEndpoints.Get) + .WithName("AlternateEndpoints.Get"); + + builder.MapPost("/alternate", global::AlternateEndpoints.Post) + .WithName("Post"); + + builder.MapMethods("/matrix", new[] { "CONNECT" }, global::HttpMethodEndpoints.Connect) + .WithName("Connect"); + + builder.MapDelete("/matrix/{id:int}", global::HttpMethodEndpoints.Delete) + .WithName("Delete"); + + builder.MapGet("/matrix", global::HttpMethodEndpoints.Get) + .WithName("HttpMethodEndpoints.Get"); + + builder.MapMethods("/matrix", new[] { "HEAD" }, global::HttpMethodEndpoints.Head) + .WithName("Head"); + + builder.MapMethods("/matrix/query", new[] { "QUERY" }, global::HttpMethodEndpoints.Query) + .WithName("Query"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..1d6e6bc --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.HttpMethodMatrix_F1EDE56BC773_MapEndpointHandlers.verified.txt @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/alternate", global::GeneratedEndpointsTests.AlternateEndpoints.Get) + .WithName("Get"); + + builder.MapPost("/alternate", global::GeneratedEndpointsTests.AlternateEndpoints.Post) + .WithName("GeneratedEndpointsTests.AlternateEndpoints.Post"); + + builder.MapMethods("/matrix", new[] { "OPTIONS" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Options) + .WithName("Options"); + + builder.MapPatch("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Patch) + .WithName("Patch"); + + builder.MapPost("/matrix", global::GeneratedEndpointsTests.HttpMethodEndpoints.Post) + .WithName("GeneratedEndpointsTests.HttpMethodEndpoints.Post"); + + builder.MapPut("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Put) + .WithName("Put"); + + builder.MapMethods("/matrix", new[] { "TRACE" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Trace) + .WithName("Trace"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..0b8b70e --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_370EBA0910FD_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback(global::FallbackEndpoints.Default) + .WithName("Default"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..52cf3bb --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_50C6E889EAF2_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback("/alternate-fallback", global::GeneratedEndpointsTests.FallbackEndpoints.Custom) + .WithName("Custom"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..58b2353 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_517225B7419F_MapEndpointHandlers.verified.txt @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..0740514 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_61D3AB7FBEBA_MapEndpointHandlers.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback("/custom-fallback", global::GeneratedEndpointsTests.FallbackEndpoints.Custom) + .WithName("Custom"); + + builder.MapFallback(global::GeneratedEndpointsTests.FallbackEndpoints.Default) + .WithName("Default"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..9a20c14 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.MapFallbackScenarios_A7D17F3BAD82_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback("/custom-only", global::FallbackEndpoints.Custom) + .WithName("Custom"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs new file mode 100644 index 0000000..177762d --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs @@ -0,0 +1,1324 @@ +using System.Security.Cryptography; +using System.Text; +using GeneratedEndpoints.Tests.Common; +using SourceGeneratorTestHelpers.XUnit; + +namespace GeneratedEndpoints.Tests; + +[UsesVerify] +public class GeneratedSourceTests +{ + public GeneratedSourceTests() + { + ModuleInitializer.Initialize(); + } + + [Theory] + [InlineData(true, true, true, "/custom-fallback")] + [InlineData(false, true, false, null)] + [InlineData(true, false, true, "/alternate-fallback")] + [InlineData(false, false, true, "/custom-only")] + [InlineData(true, false, false, null)] + public async Task MapFallbackScenarios(bool withNamespace, bool includeDefaultFallback, bool includeCustomFallback, string? customRoute) + { + var sources = TestHelpers.GetSources(SourceFactory.BuildFallbackSource(includeDefaultFallback, includeCustomFallback, customRoute), withNamespace); + var result = TestHelpers.RunGenerator(sources); + var scenario = ScenarioNamer.Create(nameof(MapFallbackScenarios), + ("Namespace", withNamespace), + ("Default", includeDefaultFallback), + ("Custom", includeCustomFallback), + ("Route", customRoute ?? "default")); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_AddEndpointHandlers"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_MapEndpointHandlers"); + } + + [Theory] + [InlineData(true, true, false, true, false, true, true, "*.contoso.com", "api.contoso.com", true, "NamedCorsPolicy", false, null, true, "RatePolicy", true, true, "TimeoutPolicy", false, 5, "Reporting", true)] + [InlineData(false, false, true, false, true, false, true, null, "services.contoso.com", false, null, true, "MethodCors", true, null, false, false, null, true, -1, null, false)] + [InlineData(true, true, true, true, true, true, false, "*.example.com", null, true, null, true, null, false, null, false, true, null, true, 0, "Operations", true)] + [InlineData(false, false, false, true, false, true, false, null, "*.alt.com", false, "CorsDefault", false, null, false, null, true, false, null, false, 10, null, false)] + [InlineData(true, false, true, false, true, false, true, "api.alt.com", null, true, null, true, "MethodCors", true, "BurstPolicy", false, true, "TimeoutPolicy", true, -5, "Docs", true)] + public async Task AuthorizationAndMetadataMatrix( + bool withNamespace, + bool classAllowAnonymous, + bool methodAllowAnonymous, + bool classRequireAuthorization, + bool methodRequireAuthorization, + bool classTags, + bool methodTags, + string? classHost, + string? methodHost, + bool classRequireCors, + string? classCorsPolicy, + bool methodRequireCors, + string? methodCorsPolicy, + bool requireRateLimiting, + string? rateLimitingPolicy, + bool applyShortCircuit, + bool applyRequestTimeout, + string? requestTimeoutPolicy, + bool disableRequestTimeout, + int orderValue, + string? groupName, + bool excludeFromDescription) + { + var source = SourceFactory.BuildAuthorizationMatrixSource( + classAllowAnonymous, + methodAllowAnonymous, + classRequireAuthorization, + methodRequireAuthorization, + classTags, + methodTags, + classHost, + methodHost, + classRequireCors, + classCorsPolicy, + methodRequireCors, + methodCorsPolicy, + requireRateLimiting, + rateLimitingPolicy, + applyShortCircuit, + applyRequestTimeout, + requestTimeoutPolicy, + disableRequestTimeout, + orderValue, + groupName, + excludeFromDescription); + + var sources = TestHelpers.GetSources(source, withNamespace); + var result = TestHelpers.RunGenerator(sources); + var scenario = ScenarioNamer.Create(nameof(AuthorizationAndMetadataMatrix), + ("Namespace", withNamespace), + ("ClassAnon", classAllowAnonymous), + ("MethodAnon", methodAllowAnonymous), + ("ClassAuth", classRequireAuthorization), + ("MethodAuth", methodRequireAuthorization), + ("ClassTags", classTags), + ("MethodTags", methodTags), + ("ClassHost", classHost ?? "none"), + ("MethodHost", methodHost ?? "none"), + ("ClassCors", classRequireCors), + ("MethodCors", methodRequireCors), + ("RateLimit", requireRateLimiting), + ("ShortCircuit", applyShortCircuit), + ("RequestTimeout", applyRequestTimeout), + ("DisableTimeout", disableRequestTimeout), + ("Order", orderValue), + ("Group", groupName ?? "none"), + ("Exclude", excludeFromDescription)); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_AddEndpointHandlers"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_MapEndpointHandlers"); + } + + [Theory] + [InlineData(true, true, true, true, true, true, true, "configured")] + [InlineData(false, false, true, false, true, false, true, "timed")] + [InlineData(true, true, false, true, false, true, false, "metadata")] + [InlineData(false, true, false, false, false, true, false, "analytics")] + [InlineData(true, false, false, true, false, false, true, "telemetry")] + public async Task ConfigureAndFiltersMatrix( + bool withNamespace, + bool configureWithServiceProvider, + bool configureAddsMetadata, + bool includeClassLevelFilter, + bool includeMethodLevelFilter, + bool includeGenericFilter, + bool configureRegistersFilter, + string metadataValue) + { + var source = SourceFactory.BuildConfigureAndFiltersSource( + configureWithServiceProvider, + configureAddsMetadata, + includeClassLevelFilter, + includeMethodLevelFilter, + includeGenericFilter, + configureRegistersFilter, + metadataValue); + + var sources = TestHelpers.GetSources(source, withNamespace); + var result = TestHelpers.RunGenerator(sources); + var scenario = ScenarioNamer.Create(nameof(ConfigureAndFiltersMatrix), + ("Namespace", withNamespace), + ("SvcProvider", configureWithServiceProvider), + ("Metadata", configureAddsMetadata), + ("ClassFilter", includeClassLevelFilter), + ("MethodFilter", includeMethodLevelFilter), + ("GenericFilter", includeGenericFilter), + ("ConfigureFilter", configureRegistersFilter), + ("Value", metadataValue)); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_AddEndpointHandlers"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_MapEndpointHandlers"); + } + + [Theory] + [InlineData(true, true, true, true, true, true, true, true, true, true, true, false)] + [InlineData(false, true, false, false, true, false, true, false, true, false, true, true)] + [InlineData(true, false, true, true, false, true, false, true, false, true, false, true)] + [InlineData(false, false, true, true, false, true, false, true, false, true, false, false)] + [InlineData(true, true, false, false, true, false, true, false, true, false, true, true)] + public async Task HttpMethodMatrix( + bool withNamespace, + bool includeGet, + bool includePost, + bool includePut, + bool includeDelete, + bool includeOptions, + bool includeHead, + bool includePatch, + bool includeQuery, + bool includeTrace, + bool includeConnect, + bool includeMethodNameCollision) + { + var source = SourceFactory.BuildHttpMethodMatrixSource( + includeGet, + includePost, + includePut, + includeDelete, + includeOptions, + includeHead, + includePatch, + includeQuery, + includeTrace, + includeConnect, + includeMethodNameCollision); + + var sources = TestHelpers.GetSources(source, withNamespace); + var result = TestHelpers.RunGenerator(sources); + var scenario = ScenarioNamer.Create(nameof(HttpMethodMatrix), + ("Namespace", withNamespace), + ("Get", includeGet), + ("Post", includePost), + ("Put", includePut), + ("Delete", includeDelete), + ("Options", includeOptions), + ("Head", includeHead), + ("Patch", includePatch), + ("Query", includeQuery), + ("Trace", includeTrace), + ("Connect", includeConnect), + ("Collision", includeMethodNameCollision)); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_AddEndpointHandlers"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_MapEndpointHandlers"); + } + + [Theory] + [InlineData(true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, false, "application/xml", "text/xml", "application/json", "text/json")] + [InlineData(false, false, true, false, false, true, false, true, true, false, false, false, true, true, false, true, "application/custom", null, "application/problem+json", null)] + [InlineData(true, true, false, true, true, false, true, false, false, true, true, true, false, false, true, true, null, null, null, null)] + [InlineData(false, true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, "application/xml", null, "application/json", null)] + [InlineData(true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false, null, "text/plain", null, "text/plain")] + public async Task ContractsAndBindingMatrix( + bool withNamespace, + bool includeBindingNames, + bool includeAsParameters, + bool includeFromServices, + bool includeFromKeyedServices, + bool includeAccepts, + bool includeGenericAccepts, + bool includeProducesResponse, + bool includeProducesProblem, + bool includeProducesValidationProblem, + bool includeSummaryAndDescription, + bool includeDisplayName, + bool includeTags, + bool excludeFromDescription, + bool allowAnonymous, + bool methodRequiresAuthorization, + string? acceptsContentType1, + string? acceptsContentType2, + string? producesContentType1, + string? producesContentType2) + { + var source = SourceFactory.BuildContractsAndBindingSource( + includeBindingNames, + includeAsParameters, + includeFromServices, + includeFromKeyedServices, + includeAccepts, + includeGenericAccepts, + includeProducesResponse, + includeProducesProblem, + includeProducesValidationProblem, + includeSummaryAndDescription, + includeDisplayName, + includeTags, + excludeFromDescription, + allowAnonymous, + methodRequiresAuthorization, + acceptsContentType1, + acceptsContentType2, + producesContentType1, + producesContentType2); + + var sources = TestHelpers.GetSources(source, withNamespace); + var result = TestHelpers.RunGenerator(sources); + var scenario = ScenarioNamer.Create(nameof(ContractsAndBindingMatrix), + ("Namespace", withNamespace), + ("BindingNames", includeBindingNames), + ("AsParameters", includeAsParameters), + ("Services", includeFromServices), + ("KeyedServices", includeFromKeyedServices), + ("Accepts", includeAccepts), + ("GenericAccepts", includeGenericAccepts), + ("Produces", includeProducesResponse), + ("Problem", includeProducesProblem), + ("Validation", includeProducesValidationProblem), + ("Summary", includeSummaryAndDescription), + ("DisplayName", includeDisplayName), + ("Tags", includeTags), + ("Exclude", excludeFromDescription), + ("AllowAnon", allowAnonymous), + ("MethodAuth", methodRequiresAuthorization)); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_AddEndpointHandlers"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_MapEndpointHandlers"); + } +} + +[UsesVerify] +public class IndividualTests +{ + public IndividualTests() + { + ModuleInitializer.Initialize(); + } + + [Fact] + public async Task DefaultFallbackOnly() + { + var source = FallbackScenario(includeDefault: true); + await VerifyIndividualAsync(source, nameof(DefaultFallbackOnly)); + } + + [Fact] + public async Task CustomFallbackRoute() + { + var source = FallbackScenario(includeCustom: true, customRoute: "/custom-individual"); + await VerifyIndividualAsync(source, nameof(CustomFallbackRoute)); + } + + [Fact] + public async Task ClassAllowAnonymous() + { + var source = AuthorizationScenario(classAllowAnonymous: true); + await VerifyIndividualAsync(source, nameof(ClassAllowAnonymous)); + } + + [Fact] + public async Task MethodAllowAnonymous() + { + var source = AuthorizationScenario(methodAllowAnonymous: true); + await VerifyIndividualAsync(source, nameof(MethodAllowAnonymous)); + } + + [Fact] + public async Task ClassRequireAuthorization() + { + var source = AuthorizationScenario(classRequireAuthorization: true); + await VerifyIndividualAsync(source, nameof(ClassRequireAuthorization)); + } + + [Fact] + public async Task MethodRequireAuthorization() + { + var source = AuthorizationScenario(methodRequireAuthorization: true); + await VerifyIndividualAsync(source, nameof(MethodRequireAuthorization)); + } + + [Fact] + public async Task ClassTags() + { + var source = AuthorizationScenario(classTags: true); + await VerifyIndividualAsync(source, nameof(ClassTags)); + } + + [Fact] + public async Task MethodTags() + { + var source = AuthorizationScenario(methodTags: true); + await VerifyIndividualAsync(source, nameof(MethodTags)); + } + + [Fact] + public async Task ClassRequireHost() + { + var source = AuthorizationScenario(classHost: "*.individual.com"); + await VerifyIndividualAsync(source, nameof(ClassRequireHost)); + } + + [Fact] + public async Task MethodRequireHost() + { + var source = AuthorizationScenario(methodHost: "api.individual.com"); + await VerifyIndividualAsync(source, nameof(MethodRequireHost)); + } + + [Fact] + public async Task ClassRequireCors() + { + var source = AuthorizationScenario(classRequireCors: true); + await VerifyIndividualAsync(source, nameof(ClassRequireCors)); + } + + [Fact] + public async Task ClassRequireCorsWithPolicy() + { + var source = AuthorizationScenario(classRequireCors: true, classCorsPolicy: "ClassPolicy"); + await VerifyIndividualAsync(source, nameof(ClassRequireCorsWithPolicy)); + } + + [Fact] + public async Task MethodRequireCors() + { + var source = AuthorizationScenario(methodRequireCors: true); + await VerifyIndividualAsync(source, nameof(MethodRequireCors)); + } + + [Fact] + public async Task MethodRequireCorsWithPolicy() + { + var source = AuthorizationScenario(methodRequireCors: true, methodCorsPolicy: "MethodPolicy"); + await VerifyIndividualAsync(source, nameof(MethodRequireCorsWithPolicy)); + } + + [Fact] + public async Task RequireRateLimiting() + { + var source = AuthorizationScenario(requireRateLimiting: true); + await VerifyIndividualAsync(source, nameof(RequireRateLimiting)); + } + + [Fact] + public async Task RequireRateLimitingWithPolicy() + { + var source = AuthorizationScenario(requireRateLimiting: true, rateLimitingPolicy: "BurstPolicy"); + await VerifyIndividualAsync(source, nameof(RequireRateLimitingWithPolicy)); + } + + [Fact] + public async Task ShortCircuit() + { + var source = AuthorizationScenario(applyShortCircuit: true); + await VerifyIndividualAsync(source, nameof(ShortCircuit)); + } + + [Fact] + public async Task RequestTimeout() + { + var source = AuthorizationScenario(applyRequestTimeout: true); + await VerifyIndividualAsync(source, nameof(RequestTimeout)); + } + + [Fact] + public async Task RequestTimeoutWithPolicy() + { + var source = AuthorizationScenario(applyRequestTimeout: true, requestTimeoutPolicy: "TimeoutPolicy"); + await VerifyIndividualAsync(source, nameof(RequestTimeoutWithPolicy)); + } + + [Fact] + public async Task DisableRequestTimeout() + { + var source = AuthorizationScenario(disableRequestTimeout: true); + await VerifyIndividualAsync(source, nameof(DisableRequestTimeout)); + } + + [Fact] + public async Task OrderMetadata() + { + var source = AuthorizationScenario(orderValue: 7); + await VerifyIndividualAsync(source, nameof(OrderMetadata)); + } + + [Fact] + public async Task GroupName() + { + var source = AuthorizationScenario(groupName: "IndividualGroup"); + await VerifyIndividualAsync(source, nameof(GroupName)); + } + + [Fact] + public async Task ExcludeFromDescription() + { + var source = AuthorizationScenario(excludeFromDescription: true); + await VerifyIndividualAsync(source, nameof(ExcludeFromDescription)); + } + + [Fact] + public async Task ClassEndpointFilter() + { + var source = ConfigureScenario(includeClassLevelFilter: true); + await VerifyIndividualAsync(source, nameof(ClassEndpointFilter)); + } + + [Fact] + public async Task MethodEndpointFilter() + { + var source = ConfigureScenario(includeMethodLevelFilter: true); + await VerifyIndividualAsync(source, nameof(MethodEndpointFilter)); + } + + [Fact] + public async Task GenericEndpointFilter() + { + var source = ConfigureScenario(includeGenericFilter: true); + await VerifyIndividualAsync(source, nameof(GenericEndpointFilter)); + } + + [Fact] + public async Task ConfigureWithServiceProvider() + { + var source = ConfigureScenario(configureWithServiceProvider: true); + await VerifyIndividualAsync(source, nameof(ConfigureWithServiceProvider)); + } + + [Fact] + public async Task ConfigureAddsMetadata() + { + var source = ConfigureScenario(configureAddsMetadata: true, metadataValue: "IndividualMetadata"); + await VerifyIndividualAsync(source, nameof(ConfigureAddsMetadata)); + } + + [Fact] + public async Task ConfigureRegistersFilter() + { + var source = ConfigureScenario(configureRegistersFilter: true); + await VerifyIndividualAsync(source, nameof(ConfigureRegistersFilter)); + } + + [Fact] + public async Task MapGetEndpoint() + { + var source = HttpMethodScenario(includeGet: true); + await VerifyIndividualAsync(source, nameof(MapGetEndpoint)); + } + + [Fact] + public async Task MapPostEndpoint() + { + var source = HttpMethodScenario(includePost: true); + await VerifyIndividualAsync(source, nameof(MapPostEndpoint)); + } + + [Fact] + public async Task MapPutEndpoint() + { + var source = HttpMethodScenario(includePut: true); + await VerifyIndividualAsync(source, nameof(MapPutEndpoint)); + } + + [Fact] + public async Task MapDeleteEndpoint() + { + var source = HttpMethodScenario(includeDelete: true); + await VerifyIndividualAsync(source, nameof(MapDeleteEndpoint)); + } + + [Fact] + public async Task MapOptionsEndpoint() + { + var source = HttpMethodScenario(includeOptions: true); + await VerifyIndividualAsync(source, nameof(MapOptionsEndpoint)); + } + + [Fact] + public async Task MapHeadEndpoint() + { + var source = HttpMethodScenario(includeHead: true); + await VerifyIndividualAsync(source, nameof(MapHeadEndpoint)); + } + + [Fact] + public async Task MapPatchEndpoint() + { + var source = HttpMethodScenario(includePatch: true); + await VerifyIndividualAsync(source, nameof(MapPatchEndpoint)); + } + + [Fact] + public async Task MapQueryEndpoint() + { + var source = HttpMethodScenario(includeQuery: true); + await VerifyIndividualAsync(source, nameof(MapQueryEndpoint)); + } + + [Fact] + public async Task MapTraceEndpoint() + { + var source = HttpMethodScenario(includeTrace: true); + await VerifyIndividualAsync(source, nameof(MapTraceEndpoint)); + } + + [Fact] + public async Task MapConnectEndpoint() + { + var source = HttpMethodScenario(includeConnect: true); + await VerifyIndividualAsync(source, nameof(MapConnectEndpoint)); + } + + [Fact] + public async Task MethodNameCollision() + { + var source = HttpMethodScenario(includeGet: true, includeMethodNameCollision: true); + await VerifyIndividualAsync(source, nameof(MethodNameCollision)); + } + + [Fact] + public async Task BindingNames() + { + var source = ContractScenario(includeBindingNames: true); + await VerifyIndividualAsync(source, nameof(BindingNames)); + } + + [Fact] + public async Task AsParameters() + { + var source = ContractScenario(includeAsParameters: true); + await VerifyIndividualAsync(source, nameof(AsParameters)); + } + + [Fact] + public async Task FromServices() + { + var source = ContractScenario(includeFromServices: true); + await VerifyIndividualAsync(source, nameof(FromServices)); + } + + [Fact] + public async Task FromKeyedServices() + { + var source = ContractScenario(includeFromKeyedServices: true); + await VerifyIndividualAsync(source, nameof(FromKeyedServices)); + } + + [Fact] + public async Task AcceptsAttribute() + { + var source = ContractScenario(includeAccepts: true, acceptsContentType1: "application/custom"); + await VerifyIndividualAsync(source, nameof(AcceptsAttribute)); + } + + [Fact] + public async Task AcceptsMultipleContentTypes() + { + var source = ContractScenario(includeAccepts: true, acceptsContentType1: "application/json", acceptsContentType2: "text/json"); + await VerifyIndividualAsync(source, nameof(AcceptsMultipleContentTypes)); + } + + [Fact] + public async Task GenericAcceptsAttribute() + { + var source = ContractScenario(includeGenericAccepts: true, acceptsContentType1: "application/vnd.generic"); + await VerifyIndividualAsync(source, nameof(GenericAcceptsAttribute)); + } + + [Fact] + public async Task ProducesResponseAttribute() + { + var source = ContractScenario(includeProducesResponse: true, producesContentType1: "application/json"); + await VerifyIndividualAsync(source, nameof(ProducesResponseAttribute)); + } + + [Fact] + public async Task ProducesResponseMultipleContentTypes() + { + var source = ContractScenario(includeProducesResponse: true, producesContentType1: "application/json", producesContentType2: "text/json"); + await VerifyIndividualAsync(source, nameof(ProducesResponseMultipleContentTypes)); + } + + [Fact] + public async Task ProducesProblemAttribute() + { + var source = ContractScenario(includeProducesProblem: true, producesContentType1: "application/problem+json"); + await VerifyIndividualAsync(source, nameof(ProducesProblemAttribute)); + } + + [Fact] + public async Task ProducesValidationProblemAttribute() + { + var source = ContractScenario(includeProducesValidationProblem: true); + await VerifyIndividualAsync(source, nameof(ProducesValidationProblemAttribute)); + } + + [Fact] + public async Task SummaryAndDescriptionAttributes() + { + var source = ContractScenario(includeSummaryAndDescription: true); + await VerifyIndividualAsync(source, nameof(SummaryAndDescriptionAttributes)); + } + + [Fact] + public async Task DisplayNameAttribute() + { + var source = ContractScenario(includeDisplayName: true); + await VerifyIndividualAsync(source, nameof(DisplayNameAttribute)); + } + + [Fact] + public async Task ContractTags() + { + var source = ContractScenario(includeTags: true); + await VerifyIndividualAsync(source, nameof(ContractTags)); + } + + [Fact] + public async Task ContractExcludeFromDescription() + { + var source = ContractScenario(excludeFromDescription: true); + await VerifyIndividualAsync(source, nameof(ContractExcludeFromDescription)); + } + + [Fact] + public async Task ContractAllowAnonymous() + { + var source = ContractScenario(allowAnonymous: true); + await VerifyIndividualAsync(source, nameof(ContractAllowAnonymous)); + } + + [Fact] + public async Task ContractRequireAuthorization() + { + var source = ContractScenario(methodRequiresAuthorization: true); + await VerifyIndividualAsync(source, nameof(ContractRequireAuthorization)); + } + + private static async Task VerifyIndividualAsync(string source, string scenario, bool withNamespace = true) + { + var sources = TestHelpers.GetSources(source, withNamespace); + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_AddEndpointHandlers"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_MapEndpointHandlers"); + } + + private static string FallbackScenario(bool includeDefault = false, bool includeCustom = false, string? customRoute = null) + => SourceFactory.BuildFallbackSource(includeDefault, includeCustom, customRoute); + + private static string AuthorizationScenario( + bool classAllowAnonymous = false, + bool methodAllowAnonymous = false, + bool classRequireAuthorization = false, + bool methodRequireAuthorization = false, + bool classTags = false, + bool methodTags = false, + string? classHost = null, + string? methodHost = null, + bool classRequireCors = false, + string? classCorsPolicy = null, + bool methodRequireCors = false, + string? methodCorsPolicy = null, + bool requireRateLimiting = false, + string? rateLimitingPolicy = null, + bool applyShortCircuit = false, + bool applyRequestTimeout = false, + string? requestTimeoutPolicy = null, + bool disableRequestTimeout = false, + int orderValue = 0, + string? groupName = null, + bool excludeFromDescription = false) + => SourceFactory.BuildAuthorizationMatrixSource( + classAllowAnonymous, + methodAllowAnonymous, + classRequireAuthorization, + methodRequireAuthorization, + classTags, + methodTags, + classHost, + methodHost, + classRequireCors, + classCorsPolicy, + methodRequireCors, + methodCorsPolicy, + requireRateLimiting, + rateLimitingPolicy, + applyShortCircuit, + applyRequestTimeout, + requestTimeoutPolicy, + disableRequestTimeout, + orderValue, + groupName, + excludeFromDescription); + + private static string ConfigureScenario( + bool configureWithServiceProvider = false, + bool configureAddsMetadata = false, + bool includeClassLevelFilter = false, + bool includeMethodLevelFilter = false, + bool includeGenericFilter = false, + bool configureRegistersFilter = false, + string metadataValue = "Individual") + => SourceFactory.BuildConfigureAndFiltersSource( + configureWithServiceProvider, + configureAddsMetadata, + includeClassLevelFilter, + includeMethodLevelFilter, + includeGenericFilter, + configureRegistersFilter, + metadataValue); + + private static string HttpMethodScenario( + bool includeGet = false, + bool includePost = false, + bool includePut = false, + bool includeDelete = false, + bool includeOptions = false, + bool includeHead = false, + bool includePatch = false, + bool includeQuery = false, + bool includeTrace = false, + bool includeConnect = false, + bool includeMethodNameCollision = false) + => SourceFactory.BuildHttpMethodMatrixSource( + includeGet, + includePost, + includePut, + includeDelete, + includeOptions, + includeHead, + includePatch, + includeQuery, + includeTrace, + includeConnect, + includeMethodNameCollision); + + private static string ContractScenario( + bool includeBindingNames = false, + bool includeAsParameters = false, + bool includeFromServices = false, + bool includeFromKeyedServices = false, + bool includeAccepts = false, + bool includeGenericAccepts = false, + bool includeProducesResponse = false, + bool includeProducesProblem = false, + bool includeProducesValidationProblem = false, + bool includeSummaryAndDescription = false, + bool includeDisplayName = false, + bool includeTags = false, + bool excludeFromDescription = false, + bool allowAnonymous = false, + bool methodRequiresAuthorization = false, + string? acceptsContentType1 = null, + string? acceptsContentType2 = null, + string? producesContentType1 = null, + string? producesContentType2 = null) + => SourceFactory.BuildContractsAndBindingSource( + includeBindingNames, + includeAsParameters, + includeFromServices, + includeFromKeyedServices, + includeAccepts, + includeGenericAccepts, + includeProducesResponse, + includeProducesProblem, + includeProducesValidationProblem, + includeSummaryAndDescription, + includeDisplayName, + includeTags, + excludeFromDescription, + allowAnonymous, + methodRequiresAuthorization, + acceptsContentType1, + acceptsContentType2, + producesContentType1, + producesContentType2); +} + +file static class SourceFactory +{ + public static string BuildFallbackSource(bool includeDefault, bool includeCustom, string? customRoute) + { + var builder = new StringBuilder(); + builder.AppendLine("internal static class FallbackEndpoints"); + builder.AppendLine("{"); + + if (includeDefault) + { + builder.AppendLine(" [MapFallback]"); + builder.AppendLine(" public static Ok Default() => TypedResults.Ok();"); + builder.AppendLine(); + } + + if (includeCustom) + { + var route = customRoute ?? "/custom"; + builder.AppendLine($" [MapFallback(\"{route}\")]"); + builder.AppendLine(" public static Ok Custom() => TypedResults.Ok();"); + builder.AppendLine(); + } + + builder.AppendLine("}"); + return builder.ToString(); + } + + public static string BuildAuthorizationMatrixSource( + bool classAllowAnonymous, + bool methodAllowAnonymous, + bool classRequireAuthorization, + bool methodRequireAuthorization, + bool classTags, + bool methodTags, + string? classHost, + string? methodHost, + bool classRequireCors, + string? classCorsPolicy, + bool methodRequireCors, + string? methodCorsPolicy, + bool requireRateLimiting, + string? rateLimitingPolicy, + bool applyShortCircuit, + bool applyRequestTimeout, + string? requestTimeoutPolicy, + bool disableRequestTimeout, + int orderValue, + string? groupName, + bool excludeFromDescription) + { + var builder = new StringBuilder(); + + if (classAllowAnonymous) + { + builder.AppendLine("[AllowAnonymous]"); + } + + if (classRequireAuthorization) + { + builder.AppendLine("[RequireAuthorization(\"ClassPolicy\")]"); + } + + if (classTags) + { + builder.AppendLine("[Tags(\"Class\", \"Matrix\")]"); + } + + if (!string.IsNullOrWhiteSpace(classHost)) + { + builder.AppendLine($"[RequireHost(\"{classHost}\")]"); + } + + if (classRequireCors) + { + var cors = string.IsNullOrWhiteSpace(classCorsPolicy) ? "" : $"(\"{classCorsPolicy}\")"; + builder.AppendLine($"[RequireCors{cors}]"); + } + + if (!string.IsNullOrWhiteSpace(groupName)) + { + builder.AppendLine($"[GroupName(\"{groupName}\")]"); + } + + if (applyShortCircuit) + { + builder.AppendLine("[ShortCircuit]"); + } + + if (applyRequestTimeout) + { + var timeoutArgument = string.IsNullOrWhiteSpace(requestTimeoutPolicy) + ? string.Empty + : $"(\"{requestTimeoutPolicy}\")"; + builder.AppendLine($"[RequestTimeout{timeoutArgument}]"); + } + + if (disableRequestTimeout) + { + builder.AppendLine("[DisableRequestTimeout]"); + } + + if (orderValue != 0) + { + builder.AppendLine($"[Order({orderValue})]"); + } + + if (excludeFromDescription) + { + builder.AppendLine("[ExcludeFromDescription]"); + } + + builder.AppendLine("internal sealed class AuthorizationMatrixEndpoints"); + builder.AppendLine("{"); + builder.AppendLine(" [MapGet(\"/matrix/{id:int}\", Name = \"GetMatrix\")]"); + + if (methodAllowAnonymous) + { + builder.AppendLine(" [AllowAnonymous]"); + } + + if (methodRequireAuthorization) + { + builder.AppendLine(" [RequireAuthorization(\"MethodPolicy\")]"); + } + + if (methodTags) + { + builder.AppendLine(" [Tags(\"Method\", \"Matrix\")]"); + } + + if (!string.IsNullOrWhiteSpace(methodHost)) + { + builder.AppendLine($" [RequireHost(\"{methodHost}\", \"contoso.com\")]"); + } + + if (methodRequireCors) + { + var methodCors = string.IsNullOrWhiteSpace(methodCorsPolicy) ? string.Empty : $"(\"{methodCorsPolicy}\")"; + builder.AppendLine($" [RequireCors{methodCors}]"); + } + + if (requireRateLimiting) + { + var rateLimit = string.IsNullOrWhiteSpace(rateLimitingPolicy) ? string.Empty : $"(\"{rateLimitingPolicy}\")"; + builder.AppendLine($" [RequireRateLimiting{rateLimit}]"); + } + + builder.AppendLine(" public static Ok Handle(int id) => id >= 0 ? TypedResults.Ok() : TypedResults.Ok();"); + builder.AppendLine("}"); + return builder.ToString(); + } + + public static string BuildConfigureAndFiltersSource( + bool configureWithServiceProvider, + bool configureAddsMetadata, + bool includeClassLevelFilter, + bool includeMethodLevelFilter, + bool includeGenericFilter, + bool configureRegistersFilter, + string metadataValue) + { + var builder = new StringBuilder(); + builder.AppendLine("using Microsoft.AspNetCore.Builder;"); + builder.AppendLine(); + + if (includeClassLevelFilter) + { + builder.AppendLine("[EndpointFilter(typeof(TimingFilter))]"); + } + + builder.AppendLine("internal static class ConfigureFilterEndpoints"); + builder.AppendLine("{"); + builder.AppendLine(" [MapGet(\"/configure-filters\")]"); + + if (includeMethodLevelFilter) + { + builder.AppendLine(" [EndpointFilter(typeof(ValidationFilter))]"); + } + + if (includeGenericFilter) + { + builder.AppendLine(" [EndpointFilter]"); + } + + builder.AppendLine(" public static Ok Handle() => TypedResults.Ok();"); + builder.AppendLine(); + builder.AppendLine(" public static void Configure(TBuilder builder" + (configureWithServiceProvider ? ", IServiceProvider services" : string.Empty) + ")"); + builder.AppendLine(" where TBuilder : IEndpointConventionBuilder"); + builder.AppendLine(" {"); + builder.AppendLine(" _ = builder;"); + + if (configureWithServiceProvider) + { + builder.AppendLine(" _ = services;"); + } + + if (configureAddsMetadata) + { + builder.AppendLine($" builder.WithMetadata(\"{metadataValue}\");"); + } + + if (configureRegistersFilter) + { + builder.AppendLine(" builder.AddEndpointFilterFactory((context, next) => next);"); + } + + builder.AppendLine(" }"); + builder.AppendLine("}"); + builder.AppendLine(); + builder.AppendLine("internal sealed class TimingFilter : IEndpointFilter"); + builder.AppendLine("{"); + builder.AppendLine(" public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) => next(context);"); + builder.AppendLine("}"); + builder.AppendLine(); + builder.AppendLine("internal sealed class ValidationFilter : IEndpointFilter"); + builder.AppendLine("{"); + builder.AppendLine(" public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) => next(context);"); + builder.AppendLine("}"); + + return builder.ToString(); + } + + public static string BuildHttpMethodMatrixSource( + bool includeGet, + bool includePost, + bool includePut, + bool includeDelete, + bool includeOptions, + bool includeHead, + bool includePatch, + bool includeQuery, + bool includeTrace, + bool includeConnect, + bool includeMethodNameCollision) + { + var builder = new StringBuilder(); + builder.AppendLine("using Microsoft.AspNetCore.Mvc;"); + builder.AppendLine(); + builder.AppendLine("internal static class HttpMethodEndpoints"); + builder.AppendLine("{"); + + if (includeGet) + { + builder.AppendLine(" [MapGet(\"/matrix\")] public static Ok Get() => TypedResults.Ok();"); + } + + if (includePost) + { + builder.AppendLine(" [MapPost(\"/matrix\")] public static Created Post() => TypedResults.Created(\"/matrix/1\", \"Created\");"); + } + + if (includePut) + { + builder.AppendLine(" [MapPut(\"/matrix/{id:int}\")] public static Results Put(int id) => id > 0 ? TypedResults.NoContent() : TypedResults.NotFound();"); + } + + if (includeDelete) + { + builder.AppendLine(" [MapDelete(\"/matrix/{id:int}\")] public static IResult Delete(int id) => TypedResults.Ok();"); + } + + if (includeOptions) + { + builder.AppendLine(" [MapOptions(\"/matrix\")] public static IResult Options() => TypedResults.Ok();"); + } + + if (includeHead) + { + builder.AppendLine(" [MapHead(\"/matrix\")] public static IResult Head() => TypedResults.Ok();"); + } + + if (includePatch) + { + builder.AppendLine(" [MapPatch(\"/matrix/{id:int}\")] public static IResult Patch(int id) => TypedResults.Ok();"); + } + + if (includeQuery) + { + builder.AppendLine(" [MapQuery(\"/matrix/query\")] public static IResult Query([FromQuery] string value) => TypedResults.Ok(value);"); + } + + if (includeTrace) + { + builder.AppendLine(" [MapTrace(\"/matrix\")] public static IResult Trace() => TypedResults.Ok();"); + } + + if (includeConnect) + { + builder.AppendLine(" [MapConnect(\"/matrix\")] public static IResult Connect() => TypedResults.Ok();"); + } + + builder.AppendLine("}"); + + if (includeMethodNameCollision) + { + builder.AppendLine(); + builder.AppendLine("internal static class AlternateEndpoints"); + builder.AppendLine("{"); + builder.AppendLine(" [MapGet(\"/alternate\")] public static Ok Get() => TypedResults.Ok();"); + builder.AppendLine(" [MapPost(\"/alternate\")] public static IResult Post() => TypedResults.Ok();"); + builder.AppendLine("}"); + } + + return builder.ToString(); + } + + public static string BuildContractsAndBindingSource( + bool includeBindingNames, + bool includeAsParameters, + bool includeFromServices, + bool includeFromKeyedServices, + bool includeAccepts, + bool includeGenericAccepts, + bool includeProducesResponse, + bool includeProducesProblem, + bool includeProducesValidationProblem, + bool includeSummaryAndDescription, + bool includeDisplayName, + bool includeTags, + bool excludeFromDescription, + bool allowAnonymous, + bool methodRequiresAuthorization, + string? acceptsContentType1, + string? acceptsContentType2, + string? producesContentType1, + string? producesContentType2) + { + var builder = new StringBuilder(); + builder.AppendLine("using Microsoft.AspNetCore.Mvc;"); + builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + builder.AppendLine(); + builder.AppendLine("internal sealed class ContractEndpoints"); + builder.AppendLine("{"); + + if (includeSummaryAndDescription) + { + builder.AppendLine(" [Summary(\"Gets detailed content.\")]"); + builder.AppendLine(" [Description(\"Shows binding and contract combinations.\")]"); + } + + if (includeDisplayName) + { + builder.AppendLine(" [DisplayName(\"Contract endpoint\")]"); + } + + if (includeTags) + { + builder.AppendLine(" [Tags(\"Contracts\", \"Bindings\")]"); + } + + if (excludeFromDescription) + { + builder.AppendLine(" [ExcludeFromDescription]"); + } + + if (allowAnonymous) + { + builder.AppendLine(" [AllowAnonymous]"); + } + + if (methodRequiresAuthorization) + { + builder.AppendLine(" [RequireAuthorization(\"ContractsPolicy\")]"); + } + + builder.AppendLine(" [MapGet(\"/contracts/{id:int}\")]"); + + if (includeAccepts) + { + var secondContentType = string.IsNullOrWhiteSpace(acceptsContentType2) ? string.Empty : $", \"{acceptsContentType2}\""; + builder.AppendLine($" [Accepts(\"{acceptsContentType1 ?? "application/json"}\"{secondContentType})]"); + } + + if (includeGenericAccepts) + { + builder.AppendLine($" [Accepts(\"{acceptsContentType1 ?? "application/json"}\")]"); + } + + if (includeProducesResponse) + { + var secondProduces = string.IsNullOrWhiteSpace(producesContentType2) ? string.Empty : $", \"{producesContentType2}\""; + builder.AppendLine($" [ProducesResponse(200, \"{producesContentType1 ?? "application/json"}\"{secondProduces}, ResponseType = typeof(ResponseRecord))]"); + } + + if (includeProducesProblem) + { + builder.AppendLine($" [ProducesProblem(500, \"{producesContentType1 ?? "application/problem+json"}\")]"); + } + + if (includeProducesValidationProblem) + { + builder.AppendLine($" [ProducesValidationProblem(422, \"{producesContentType1 ?? "application/problem+json"}\")]"); + } + + builder.AppendLine(" public static async Task, NotFound>> Handle("); + builder.AppendLine(includeBindingNames + ? " [FromRoute(Name = \"route-id\")] int id," + : " [FromRoute] int id,"); + builder.AppendLine(includeBindingNames + ? " [FromQuery(Name = \"filter-term\")] string? filter," + : " [FromQuery] string? filter,"); + builder.AppendLine(includeBindingNames + ? " [FromHeader(Name = \"x-trace-id\")] string? traceId," + : " [FromHeader] string? traceId,"); + builder.AppendLine(" [FromBody] RequestRecord request,"); + + if (includeAsParameters) + { + builder.AppendLine(" [AsParameters] AdditionalParameters parameters,"); + } + + if (includeFromServices) + { + builder.AppendLine(" [FromServices] IServiceProvider services,"); + } + + if (includeFromKeyedServices) + { + builder.AppendLine(" [FromKeyedServices(\"special\")] object keyed,"); + } + + builder.AppendLine(" CancellationToken cancellationToken)"); + builder.AppendLine(" {"); + builder.AppendLine(" await Task.Yield();"); + builder.AppendLine(" cancellationToken.ThrowIfCancellationRequested();"); + builder.AppendLine(" return id > 0 ? TypedResults.Ok(new ResponseRecord(id)) : TypedResults.NotFound();"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + builder.AppendLine(); + builder.AppendLine("internal sealed record RequestRecord(int Value);"); + builder.AppendLine("internal sealed record ResponseRecord(int Value);"); + + if (includeAsParameters) + { + builder.AppendLine("internal sealed record AdditionalParameters(string? Search, int? Page);"); + } + + return builder.ToString(); + } +} + +file static class ScenarioNamer +{ + public static string Create(string prefix, params (string Name, object? Value)[] parts) + { + var descriptor = new StringBuilder(); + + foreach (var (name, value) in parts) + { + descriptor.Append(name); + descriptor.Append('='); + descriptor.Append(Sanitize(value)); + descriptor.Append(';'); + } + + using var sha256 = SHA256.Create(); + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(descriptor.ToString())); + var hash = Convert.ToHexString(bytes.AsSpan(0, 6)); + return $"{prefix}_{hash}"; + } + + private static string Sanitize(object? value) + { + if (value is null) + { + return "None"; + } + + return value switch + { + bool b => b ? "On" : "Off", + string s => s, + _ => value.ToString() ?? "Value" + }; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d8a37e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsAttribute_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d8a37e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AcceptsMultipleContentTypes_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d8a37e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AsParameters_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d8a37e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.BindingNames_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..b915326 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassAllowAnonymous_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..f5054db --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassEndpointFilter_MapEndpointHandlers.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Configure( + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..b9fafc7 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireAuthorization_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireAuthorization("ClassPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..a8be8e1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCorsWithPolicy_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireCors("ClassPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d51e223 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireCors_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireCors(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..f92ec88 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassRequireHost_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireHost("*.individual.com"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..b0d8c5b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassTags_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithTags("Class", "Matrix"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..c9d22a2 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureAddsMetadata_MapEndpointHandlers.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Configure( + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..c9d22a2 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureRegistersFilter_MapEndpointHandlers.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Configure( + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..f2d6a1e --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ConfigureWithServiceProvider_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..b57ea21 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractAllowAnonymous_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..4eef24f --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractExcludeFromDescription_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .ExcludeFromDescription(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..bbf9302 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractRequireAuthorization_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .RequireAuthorization("ContractsPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..ec5e1f1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ContractTags_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .WithTags("Contracts", "Bindings"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..ae10c4a --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.CustomFallbackRoute_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback("/custom-individual", global::GeneratedEndpointsTests.FallbackEndpoints.Custom) + .WithName("Custom"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..24a42e8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.DefaultFallbackOnly_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapFallback(global::GeneratedEndpointsTests.FallbackEndpoints.Default) + .WithName("Default"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..a6e6acc --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.DisableRequestTimeout_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .DisableRequestTimeout(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..2d0e525 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.DisplayNameAttribute_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .WithDisplayName("Contract endpoint"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..dd3f8e8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ExcludeFromDescription_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .ExcludeFromDescription(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d8a37e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.FromKeyedServices_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d8a37e9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.FromServices_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..dd03570 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericAcceptsAttribute_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .Accepts("application/vnd.generic"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..3fddf3b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.GenericEndpointFilter_MapEndpointHandlers.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Configure( + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..974c923 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithGroupName("IndividualGroup"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..7b99827 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapConnectEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/matrix", new[] { "CONNECT" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Connect) + .WithName("Connect"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..0dc8dd0 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapDeleteEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapDelete("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Delete) + .WithName("Delete"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..80f6ec1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapGetEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix", global::GeneratedEndpointsTests.HttpMethodEndpoints.Get) + .WithName("Get"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..caa75d1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapHeadEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/matrix", new[] { "HEAD" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Head) + .WithName("Head"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..df2fad8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapOptionsEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/matrix", new[] { "OPTIONS" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Options) + .WithName("Options"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..58b9366 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPatchEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapPatch("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Patch) + .WithName("Patch"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..786383c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPostEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapPost("/matrix", global::GeneratedEndpointsTests.HttpMethodEndpoints.Post) + .WithName("Post"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..eaaf7c8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapPutEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapPut("/matrix/{id:int}", global::GeneratedEndpointsTests.HttpMethodEndpoints.Put) + .WithName("Put"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..e1a9b68 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapQueryEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/matrix/query", new[] { "QUERY" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Query) + .WithName("Query"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..32e2e0e --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MapTraceEndpoint_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapMethods("/matrix", new[] { "TRACE" }, global::GeneratedEndpointsTests.HttpMethodEndpoints.Trace) + .WithName("Trace"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..b915326 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodAllowAnonymous_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..3fddf3b --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodEndpointFilter_MapEndpointHandlers.verified.txt @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Configure( + builder.MapGet("/configure-filters", global::GeneratedEndpointsTests.ConfigureFilterEndpoints.Handle) + .WithName("Handle") + .AddEndpointFilter() + ); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..64b4329 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_AddEndpointHandlers.verified.txt @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..e6a1d98 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodNameCollision_MapEndpointHandlers.verified.txt @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/alternate", global::GeneratedEndpointsTests.AlternateEndpoints.Get) + .WithName("GeneratedEndpointsTests.AlternateEndpoints.Get"); + + builder.MapPost("/alternate", global::GeneratedEndpointsTests.AlternateEndpoints.Post) + .WithName("Post"); + + builder.MapGet("/matrix", global::GeneratedEndpointsTests.HttpMethodEndpoints.Get) + .WithName("GeneratedEndpointsTests.HttpMethodEndpoints.Get"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..7c0dc47 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireAuthorization_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireAuthorization("MethodPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..55b13d3 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCorsWithPolicy_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireCors("MethodPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..d51e223 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireCors_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireCors(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..f61eb7d --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodRequireHost_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireHost("api.individual.com", "contoso.com"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..369ba78 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodTags_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithTags("Method", "Matrix"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..9d860d0 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.OrderMetadata_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithOrder(7); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..97a1421 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesProblemAttribute_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .ProducesProblem(500, "application/problem+json"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..8a60c59 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseAttribute_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .Produces(200, "application/json"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..25fbbdb --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesResponseMultipleContentTypes_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .Produces(200, "application/json", "text/json"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..c8c4590 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ProducesValidationProblemAttribute_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .ProducesValidationProblem(422, "application/problem+json"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..b41ef8e --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeoutWithPolicy_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithRequestTimeout("TimeoutPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..233bfe0 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequestTimeout_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithRequestTimeout(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..447c1c6 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimitingWithPolicy_MapEndpointHandlers.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireRateLimiting("BurstPolicy"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..1fbd462 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.RequireRateLimiting_MapEndpointHandlers.verified.txt @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..e36dfde --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ShortCircuit_MapEndpointHandlers.verified.txt @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .ShortCircuit(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..6e79c25 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..4e910aa --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.SummaryAndDescriptionAttributes_MapEndpointHandlers.verified.txt @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/contracts/{id:int}", global::GeneratedEndpointsTests.ContractEndpoints.Handle) + .WithName("Handle") + .WithSummary("Gets detailed content.") + .WithDescription("Shows binding and contract combinations."); + + return builder; + } +} From bfd281d63cd5d5c0b38e96ab8646cef164ea84ff Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 20:45:31 -0500 Subject: [PATCH 50/75] Cleanup tests. --- .../Common/ModuleInitializer.cs | 2 +- .../Common/ScenarioNamer.cs | 40 + .../Common/SourceFactory.cs | 444 +++++++ .../Common/TestHelpers.cs | 4 +- .../GeneratedEndpoints.Tests.csproj | 10 +- .../GeneratedSourceTests.cs | 1030 ----------------- .../IndividualTests.cs | 555 +++++++++ 7 files changed, 1050 insertions(+), 1035 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/Common/ScenarioNamer.cs create mode 100644 tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.cs diff --git a/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs b/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs index d9dc1e5..4d55498 100644 --- a/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs +++ b/tests/GeneratedEndpoints.Tests/Common/ModuleInitializer.cs @@ -5,7 +5,7 @@ namespace GeneratedEndpoints.Tests.Common; public static class ModuleInitializer { - private static readonly Lock Lock = new(); + private static readonly object Lock = new(); private static bool _isInitialized; public static void Initialize() diff --git a/tests/GeneratedEndpoints.Tests/Common/ScenarioNamer.cs b/tests/GeneratedEndpoints.Tests/Common/ScenarioNamer.cs new file mode 100644 index 0000000..db7d771 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/Common/ScenarioNamer.cs @@ -0,0 +1,40 @@ +using System.Security.Cryptography; +using System.Text; + +namespace GeneratedEndpoints.Tests.Common; + +public static class ScenarioNamer +{ + public static string Create(string prefix, params (string Name, object? Value)[] parts) + { + var descriptor = new StringBuilder(); + + foreach (var (name, value) in parts) + { + descriptor.Append(name); + descriptor.Append('='); + descriptor.Append(Sanitize(value)); + descriptor.Append(';'); + } + + using var sha256 = SHA256.Create(); + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(descriptor.ToString())); + var hash = Convert.ToHexString(bytes.AsSpan(0, 6)); + return $"{prefix}_{hash}"; + } + + private static string Sanitize(object? value) + { + if (value is null) + { + return "None"; + } + + return value switch + { + bool b => b ? "On" : "Off", + string s => s, + _ => value.ToString() ?? "Value" + }; + } +} diff --git a/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs new file mode 100644 index 0000000..7db85c8 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs @@ -0,0 +1,444 @@ +using System.Text; + +namespace GeneratedEndpoints.Tests.Common; + +public static class SourceFactory +{ + public static string BuildFallbackSource(bool includeDefault, bool includeCustom, string? customRoute) + { + var builder = new StringBuilder(); + builder.AppendLine("internal static class FallbackEndpoints"); + builder.AppendLine("{"); + + if (includeDefault) + { + builder.AppendLine(" [MapFallback]"); + builder.AppendLine(" public static Ok Default() => TypedResults.Ok();"); + builder.AppendLine(); + } + + if (includeCustom) + { + var route = customRoute ?? "/custom"; + builder.AppendLine($" [MapFallback(\"{route}\")]"); + builder.AppendLine(" public static Ok Custom() => TypedResults.Ok();"); + builder.AppendLine(); + } + + builder.AppendLine("}"); + return builder.ToString(); + } + + public static string BuildAuthorizationMatrixSource( + bool classAllowAnonymous, + bool methodAllowAnonymous, + bool classRequireAuthorization, + bool methodRequireAuthorization, + bool classTags, + bool methodTags, + string? classHost, + string? methodHost, + bool classRequireCors, + string? classCorsPolicy, + bool methodRequireCors, + string? methodCorsPolicy, + bool requireRateLimiting, + string? rateLimitingPolicy, + bool applyShortCircuit, + bool applyRequestTimeout, + string? requestTimeoutPolicy, + bool disableRequestTimeout, + int orderValue, + string? groupName, + bool excludeFromDescription) + { + var builder = new StringBuilder(); + + if (classAllowAnonymous) + { + builder.AppendLine("[AllowAnonymous]"); + } + + if (classRequireAuthorization) + { + builder.AppendLine("[RequireAuthorization(\"ClassPolicy\")]"); + } + + if (classTags) + { + builder.AppendLine("[Tags(\"Class\", \"Matrix\")]"); + } + + if (!string.IsNullOrWhiteSpace(classHost)) + { + builder.AppendLine($"[RequireHost(\"{classHost}\")]"); + } + + if (classRequireCors) + { + var cors = string.IsNullOrWhiteSpace(classCorsPolicy) ? "" : $"(\"{classCorsPolicy}\")"; + builder.AppendLine($"[RequireCors{cors}]"); + } + + if (!string.IsNullOrWhiteSpace(groupName)) + { + builder.AppendLine($"[GroupName(\"{groupName}\")]"); + } + + if (applyShortCircuit) + { + builder.AppendLine("[ShortCircuit]"); + } + + if (applyRequestTimeout) + { + var timeoutArgument = string.IsNullOrWhiteSpace(requestTimeoutPolicy) + ? string.Empty + : $"(\"{requestTimeoutPolicy}\")"; + builder.AppendLine($"[RequestTimeout{timeoutArgument}]"); + } + + if (disableRequestTimeout) + { + builder.AppendLine("[DisableRequestTimeout]"); + } + + if (orderValue != 0) + { + builder.AppendLine($"[Order({orderValue})]"); + } + + if (excludeFromDescription) + { + builder.AppendLine("[ExcludeFromDescription]"); + } + + builder.AppendLine("internal sealed class AuthorizationMatrixEndpoints"); + builder.AppendLine("{"); + builder.AppendLine(" [MapGet(\"/matrix/{id:int}\", Name = \"GetMatrix\")]"); + + if (methodAllowAnonymous) + { + builder.AppendLine(" [AllowAnonymous]"); + } + + if (methodRequireAuthorization) + { + builder.AppendLine(" [RequireAuthorization(\"MethodPolicy\")]"); + } + + if (methodTags) + { + builder.AppendLine(" [Tags(\"Method\", \"Matrix\")]"); + } + + if (!string.IsNullOrWhiteSpace(methodHost)) + { + builder.AppendLine($" [RequireHost(\"{methodHost}\", \"contoso.com\")]"); + } + + if (methodRequireCors) + { + var methodCors = string.IsNullOrWhiteSpace(methodCorsPolicy) ? string.Empty : $"(\"{methodCorsPolicy}\")"; + builder.AppendLine($" [RequireCors{methodCors}]"); + } + + if (requireRateLimiting) + { + var rateLimit = string.IsNullOrWhiteSpace(rateLimitingPolicy) ? string.Empty : $"(\"{rateLimitingPolicy}\")"; + builder.AppendLine($" [RequireRateLimiting{rateLimit}]"); + } + + builder.AppendLine(" public static Ok Handle(int id) => id >= 0 ? TypedResults.Ok() : TypedResults.Ok();"); + builder.AppendLine("}"); + return builder.ToString(); + } + + public static string BuildConfigureAndFiltersSource( + bool configureWithServiceProvider, + bool configureAddsMetadata, + bool includeClassLevelFilter, + bool includeMethodLevelFilter, + bool includeGenericFilter, + bool configureRegistersFilter, + string metadataValue) + { + var builder = new StringBuilder(); + builder.AppendLine("using Microsoft.AspNetCore.Builder;"); + builder.AppendLine(); + + if (includeClassLevelFilter) + { + builder.AppendLine("[EndpointFilter(typeof(TimingFilter))]"); + } + + builder.AppendLine("internal static class ConfigureFilterEndpoints"); + builder.AppendLine("{"); + builder.AppendLine(" [MapGet(\"/configure-filters\")]"); + + if (includeMethodLevelFilter) + { + builder.AppendLine(" [EndpointFilter(typeof(ValidationFilter))]"); + } + + if (includeGenericFilter) + { + builder.AppendLine(" [EndpointFilter]"); + } + + builder.AppendLine(" public static Ok Handle() => TypedResults.Ok();"); + builder.AppendLine(); + builder.AppendLine(" public static void Configure(TBuilder builder" + (configureWithServiceProvider ? ", IServiceProvider services" : string.Empty) + ")"); + builder.AppendLine(" where TBuilder : IEndpointConventionBuilder"); + builder.AppendLine(" {"); + builder.AppendLine(" _ = builder;"); + + if (configureWithServiceProvider) + { + builder.AppendLine(" _ = services;"); + } + + if (configureAddsMetadata) + { + builder.AppendLine($" builder.WithMetadata(\"{metadataValue}\");"); + } + + if (configureRegistersFilter) + { + builder.AppendLine(" builder.AddEndpointFilterFactory((context, next) => next);"); + } + + builder.AppendLine(" }"); + builder.AppendLine("}"); + builder.AppendLine(); + builder.AppendLine("internal sealed class TimingFilter : IEndpointFilter"); + builder.AppendLine("{"); + builder.AppendLine(" public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) => next(context);"); + builder.AppendLine("}"); + builder.AppendLine(); + builder.AppendLine("internal sealed class ValidationFilter : IEndpointFilter"); + builder.AppendLine("{"); + builder.AppendLine(" public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) => next(context);"); + builder.AppendLine("}"); + + return builder.ToString(); + } + + public static string BuildHttpMethodMatrixSource( + bool includeGet, + bool includePost, + bool includePut, + bool includeDelete, + bool includeOptions, + bool includeHead, + bool includePatch, + bool includeQuery, + bool includeTrace, + bool includeConnect, + bool includeMethodNameCollision) + { + var builder = new StringBuilder(); + builder.AppendLine("using Microsoft.AspNetCore.Mvc;"); + builder.AppendLine(); + builder.AppendLine("internal static class HttpMethodEndpoints"); + builder.AppendLine("{"); + + if (includeGet) + { + builder.AppendLine(" [MapGet(\"/matrix\")] public static Ok Get() => TypedResults.Ok();"); + } + + if (includePost) + { + builder.AppendLine(" [MapPost(\"/matrix\")] public static Created Post() => TypedResults.Created(\"/matrix/1\", \"Created\");"); + } + + if (includePut) + { + builder.AppendLine(" [MapPut(\"/matrix/{id:int}\")] public static Results Put(int id) => id > 0 ? TypedResults.NoContent() : TypedResults.NotFound();"); + } + + if (includeDelete) + { + builder.AppendLine(" [MapDelete(\"/matrix/{id:int}\")] public static IResult Delete(int id) => TypedResults.Ok();"); + } + + if (includeOptions) + { + builder.AppendLine(" [MapOptions(\"/matrix\")] public static IResult Options() => TypedResults.Ok();"); + } + + if (includeHead) + { + builder.AppendLine(" [MapHead(\"/matrix\")] public static IResult Head() => TypedResults.Ok();"); + } + + if (includePatch) + { + builder.AppendLine(" [MapPatch(\"/matrix/{id:int}\")] public static IResult Patch(int id) => TypedResults.Ok();"); + } + + if (includeQuery) + { + builder.AppendLine(" [MapQuery(\"/matrix/query\")] public static IResult Query([FromQuery] string value) => TypedResults.Ok(value);"); + } + + if (includeTrace) + { + builder.AppendLine(" [MapTrace(\"/matrix\")] public static IResult Trace() => TypedResults.Ok();"); + } + + if (includeConnect) + { + builder.AppendLine(" [MapConnect(\"/matrix\")] public static IResult Connect() => TypedResults.Ok();"); + } + + builder.AppendLine("}"); + + if (includeMethodNameCollision) + { + builder.AppendLine(); + builder.AppendLine("internal static class AlternateEndpoints"); + builder.AppendLine("{"); + builder.AppendLine(" [MapGet(\"/alternate\")] public static Ok Get() => TypedResults.Ok();"); + builder.AppendLine(" [MapPost(\"/alternate\")] public static IResult Post() => TypedResults.Ok();"); + builder.AppendLine("}"); + } + + return builder.ToString(); + } + + public static string BuildContractsAndBindingSource( + bool includeBindingNames, + bool includeAsParameters, + bool includeFromServices, + bool includeFromKeyedServices, + bool includeAccepts, + bool includeGenericAccepts, + bool includeProducesResponse, + bool includeProducesProblem, + bool includeProducesValidationProblem, + bool includeSummaryAndDescription, + bool includeDisplayName, + bool includeTags, + bool excludeFromDescription, + bool allowAnonymous, + bool methodRequiresAuthorization, + string? acceptsContentType1, + string? acceptsContentType2, + string? producesContentType1, + string? producesContentType2) + { + var builder = new StringBuilder(); + builder.AppendLine("using Microsoft.AspNetCore.Mvc;"); + builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + builder.AppendLine(); + builder.AppendLine("internal sealed class ContractEndpoints"); + builder.AppendLine("{"); + + if (includeSummaryAndDescription) + { + builder.AppendLine(" [Summary(\"Gets detailed content.\")]"); + builder.AppendLine(" [Description(\"Shows binding and contract combinations.\")]"); + } + + if (includeDisplayName) + { + builder.AppendLine(" [DisplayName(\"Contract endpoint\")]"); + } + + if (includeTags) + { + builder.AppendLine(" [Tags(\"Contracts\", \"Bindings\")]"); + } + + if (excludeFromDescription) + { + builder.AppendLine(" [ExcludeFromDescription]"); + } + + if (allowAnonymous) + { + builder.AppendLine(" [AllowAnonymous]"); + } + + if (methodRequiresAuthorization) + { + builder.AppendLine(" [RequireAuthorization(\"ContractsPolicy\")]"); + } + + builder.AppendLine(" [MapGet(\"/contracts/{id:int}\")]"); + + if (includeAccepts) + { + var secondContentType = string.IsNullOrWhiteSpace(acceptsContentType2) ? string.Empty : $", \"{acceptsContentType2}\""; + builder.AppendLine($" [Accepts(\"{acceptsContentType1 ?? "application/json"}\"{secondContentType})]"); + } + + if (includeGenericAccepts) + { + builder.AppendLine($" [Accepts(\"{acceptsContentType1 ?? "application/json"}\")]"); + } + + if (includeProducesResponse) + { + var secondProduces = string.IsNullOrWhiteSpace(producesContentType2) ? string.Empty : $", \"{producesContentType2}\""; + builder.AppendLine($" [ProducesResponse(200, \"{producesContentType1 ?? "application/json"}\"{secondProduces}, ResponseType = typeof(ResponseRecord))]"); + } + + if (includeProducesProblem) + { + builder.AppendLine($" [ProducesProblem(500, \"{producesContentType1 ?? "application/problem+json"}\")]"); + } + + if (includeProducesValidationProblem) + { + builder.AppendLine($" [ProducesValidationProblem(422, \"{producesContentType1 ?? "application/problem+json"}\")]"); + } + + builder.AppendLine(" public static async Task, NotFound>> Handle("); + builder.AppendLine(includeBindingNames + ? " [FromRoute(Name = \"route-id\")] int id," + : " [FromRoute] int id,"); + builder.AppendLine(includeBindingNames + ? " [FromQuery(Name = \"filter-term\")] string? filter," + : " [FromQuery] string? filter,"); + builder.AppendLine(includeBindingNames + ? " [FromHeader(Name = \"x-trace-id\")] string? traceId," + : " [FromHeader] string? traceId,"); + builder.AppendLine(" [FromBody] RequestRecord request,"); + + if (includeAsParameters) + { + builder.AppendLine(" [AsParameters] AdditionalParameters parameters,"); + } + + if (includeFromServices) + { + builder.AppendLine(" [FromServices] IServiceProvider services,"); + } + + if (includeFromKeyedServices) + { + builder.AppendLine(" [FromKeyedServices(\"special\")] object keyed,"); + } + + builder.AppendLine(" CancellationToken cancellationToken)"); + builder.AppendLine(" {"); + builder.AppendLine(" await Task.Yield();"); + builder.AppendLine(" cancellationToken.ThrowIfCancellationRequested();"); + builder.AppendLine(" return id > 0 ? TypedResults.Ok(new ResponseRecord(id)) : TypedResults.NotFound();"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + builder.AppendLine(); + builder.AppendLine("internal sealed record RequestRecord(int Value);"); + builder.AppendLine("internal sealed record ResponseRecord(int Value);"); + + if (includeAsParameters) + { + builder.AppendLine("internal sealed record AdditionalParameters(string? Search, int? Page);"); + } + + return builder.ToString(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs index e6089ac..fc95fb5 100644 --- a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs +++ b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs @@ -9,9 +9,9 @@ public static class TestHelpers { public static GeneratorDriverRunResult RunGenerator(IEnumerable sources) { - var cSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp13); + var cSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp11); var cSharpCompilationOptions = new CSharpCompilationOptions(OutputKind.NetModule).WithNullableContextOptions(NullableContextOptions.Enable); - var (_, result) = IncrementalGenerator.RunWithDiagnostics(sources, cSharpParseOptions, AspNet100.References.All, cSharpCompilationOptions); + var (_, result) = IncrementalGenerator.RunWithDiagnostics(sources, cSharpParseOptions, AspNet80.References.All, cSharpCompilationOptions); return result; } diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj index c43a904..88ec984 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj @@ -1,7 +1,7 @@ - net10.0 + net8.0 enable enable latest @@ -14,7 +14,7 @@ - + all @@ -38,4 +38,10 @@ + + + GeneratedSourceTests.cs + + + diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs index 177762d..430d05b 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.cs @@ -1,5 +1,3 @@ -using System.Security.Cryptography; -using System.Text; using GeneratedEndpoints.Tests.Common; using SourceGeneratorTestHelpers.XUnit; @@ -294,1031 +292,3 @@ await result.VerifyAsync("MapEndpointHandlers.g.cs") .UseMethodName($"{scenario}_MapEndpointHandlers"); } } - -[UsesVerify] -public class IndividualTests -{ - public IndividualTests() - { - ModuleInitializer.Initialize(); - } - - [Fact] - public async Task DefaultFallbackOnly() - { - var source = FallbackScenario(includeDefault: true); - await VerifyIndividualAsync(source, nameof(DefaultFallbackOnly)); - } - - [Fact] - public async Task CustomFallbackRoute() - { - var source = FallbackScenario(includeCustom: true, customRoute: "/custom-individual"); - await VerifyIndividualAsync(source, nameof(CustomFallbackRoute)); - } - - [Fact] - public async Task ClassAllowAnonymous() - { - var source = AuthorizationScenario(classAllowAnonymous: true); - await VerifyIndividualAsync(source, nameof(ClassAllowAnonymous)); - } - - [Fact] - public async Task MethodAllowAnonymous() - { - var source = AuthorizationScenario(methodAllowAnonymous: true); - await VerifyIndividualAsync(source, nameof(MethodAllowAnonymous)); - } - - [Fact] - public async Task ClassRequireAuthorization() - { - var source = AuthorizationScenario(classRequireAuthorization: true); - await VerifyIndividualAsync(source, nameof(ClassRequireAuthorization)); - } - - [Fact] - public async Task MethodRequireAuthorization() - { - var source = AuthorizationScenario(methodRequireAuthorization: true); - await VerifyIndividualAsync(source, nameof(MethodRequireAuthorization)); - } - - [Fact] - public async Task ClassTags() - { - var source = AuthorizationScenario(classTags: true); - await VerifyIndividualAsync(source, nameof(ClassTags)); - } - - [Fact] - public async Task MethodTags() - { - var source = AuthorizationScenario(methodTags: true); - await VerifyIndividualAsync(source, nameof(MethodTags)); - } - - [Fact] - public async Task ClassRequireHost() - { - var source = AuthorizationScenario(classHost: "*.individual.com"); - await VerifyIndividualAsync(source, nameof(ClassRequireHost)); - } - - [Fact] - public async Task MethodRequireHost() - { - var source = AuthorizationScenario(methodHost: "api.individual.com"); - await VerifyIndividualAsync(source, nameof(MethodRequireHost)); - } - - [Fact] - public async Task ClassRequireCors() - { - var source = AuthorizationScenario(classRequireCors: true); - await VerifyIndividualAsync(source, nameof(ClassRequireCors)); - } - - [Fact] - public async Task ClassRequireCorsWithPolicy() - { - var source = AuthorizationScenario(classRequireCors: true, classCorsPolicy: "ClassPolicy"); - await VerifyIndividualAsync(source, nameof(ClassRequireCorsWithPolicy)); - } - - [Fact] - public async Task MethodRequireCors() - { - var source = AuthorizationScenario(methodRequireCors: true); - await VerifyIndividualAsync(source, nameof(MethodRequireCors)); - } - - [Fact] - public async Task MethodRequireCorsWithPolicy() - { - var source = AuthorizationScenario(methodRequireCors: true, methodCorsPolicy: "MethodPolicy"); - await VerifyIndividualAsync(source, nameof(MethodRequireCorsWithPolicy)); - } - - [Fact] - public async Task RequireRateLimiting() - { - var source = AuthorizationScenario(requireRateLimiting: true); - await VerifyIndividualAsync(source, nameof(RequireRateLimiting)); - } - - [Fact] - public async Task RequireRateLimitingWithPolicy() - { - var source = AuthorizationScenario(requireRateLimiting: true, rateLimitingPolicy: "BurstPolicy"); - await VerifyIndividualAsync(source, nameof(RequireRateLimitingWithPolicy)); - } - - [Fact] - public async Task ShortCircuit() - { - var source = AuthorizationScenario(applyShortCircuit: true); - await VerifyIndividualAsync(source, nameof(ShortCircuit)); - } - - [Fact] - public async Task RequestTimeout() - { - var source = AuthorizationScenario(applyRequestTimeout: true); - await VerifyIndividualAsync(source, nameof(RequestTimeout)); - } - - [Fact] - public async Task RequestTimeoutWithPolicy() - { - var source = AuthorizationScenario(applyRequestTimeout: true, requestTimeoutPolicy: "TimeoutPolicy"); - await VerifyIndividualAsync(source, nameof(RequestTimeoutWithPolicy)); - } - - [Fact] - public async Task DisableRequestTimeout() - { - var source = AuthorizationScenario(disableRequestTimeout: true); - await VerifyIndividualAsync(source, nameof(DisableRequestTimeout)); - } - - [Fact] - public async Task OrderMetadata() - { - var source = AuthorizationScenario(orderValue: 7); - await VerifyIndividualAsync(source, nameof(OrderMetadata)); - } - - [Fact] - public async Task GroupName() - { - var source = AuthorizationScenario(groupName: "IndividualGroup"); - await VerifyIndividualAsync(source, nameof(GroupName)); - } - - [Fact] - public async Task ExcludeFromDescription() - { - var source = AuthorizationScenario(excludeFromDescription: true); - await VerifyIndividualAsync(source, nameof(ExcludeFromDescription)); - } - - [Fact] - public async Task ClassEndpointFilter() - { - var source = ConfigureScenario(includeClassLevelFilter: true); - await VerifyIndividualAsync(source, nameof(ClassEndpointFilter)); - } - - [Fact] - public async Task MethodEndpointFilter() - { - var source = ConfigureScenario(includeMethodLevelFilter: true); - await VerifyIndividualAsync(source, nameof(MethodEndpointFilter)); - } - - [Fact] - public async Task GenericEndpointFilter() - { - var source = ConfigureScenario(includeGenericFilter: true); - await VerifyIndividualAsync(source, nameof(GenericEndpointFilter)); - } - - [Fact] - public async Task ConfigureWithServiceProvider() - { - var source = ConfigureScenario(configureWithServiceProvider: true); - await VerifyIndividualAsync(source, nameof(ConfigureWithServiceProvider)); - } - - [Fact] - public async Task ConfigureAddsMetadata() - { - var source = ConfigureScenario(configureAddsMetadata: true, metadataValue: "IndividualMetadata"); - await VerifyIndividualAsync(source, nameof(ConfigureAddsMetadata)); - } - - [Fact] - public async Task ConfigureRegistersFilter() - { - var source = ConfigureScenario(configureRegistersFilter: true); - await VerifyIndividualAsync(source, nameof(ConfigureRegistersFilter)); - } - - [Fact] - public async Task MapGetEndpoint() - { - var source = HttpMethodScenario(includeGet: true); - await VerifyIndividualAsync(source, nameof(MapGetEndpoint)); - } - - [Fact] - public async Task MapPostEndpoint() - { - var source = HttpMethodScenario(includePost: true); - await VerifyIndividualAsync(source, nameof(MapPostEndpoint)); - } - - [Fact] - public async Task MapPutEndpoint() - { - var source = HttpMethodScenario(includePut: true); - await VerifyIndividualAsync(source, nameof(MapPutEndpoint)); - } - - [Fact] - public async Task MapDeleteEndpoint() - { - var source = HttpMethodScenario(includeDelete: true); - await VerifyIndividualAsync(source, nameof(MapDeleteEndpoint)); - } - - [Fact] - public async Task MapOptionsEndpoint() - { - var source = HttpMethodScenario(includeOptions: true); - await VerifyIndividualAsync(source, nameof(MapOptionsEndpoint)); - } - - [Fact] - public async Task MapHeadEndpoint() - { - var source = HttpMethodScenario(includeHead: true); - await VerifyIndividualAsync(source, nameof(MapHeadEndpoint)); - } - - [Fact] - public async Task MapPatchEndpoint() - { - var source = HttpMethodScenario(includePatch: true); - await VerifyIndividualAsync(source, nameof(MapPatchEndpoint)); - } - - [Fact] - public async Task MapQueryEndpoint() - { - var source = HttpMethodScenario(includeQuery: true); - await VerifyIndividualAsync(source, nameof(MapQueryEndpoint)); - } - - [Fact] - public async Task MapTraceEndpoint() - { - var source = HttpMethodScenario(includeTrace: true); - await VerifyIndividualAsync(source, nameof(MapTraceEndpoint)); - } - - [Fact] - public async Task MapConnectEndpoint() - { - var source = HttpMethodScenario(includeConnect: true); - await VerifyIndividualAsync(source, nameof(MapConnectEndpoint)); - } - - [Fact] - public async Task MethodNameCollision() - { - var source = HttpMethodScenario(includeGet: true, includeMethodNameCollision: true); - await VerifyIndividualAsync(source, nameof(MethodNameCollision)); - } - - [Fact] - public async Task BindingNames() - { - var source = ContractScenario(includeBindingNames: true); - await VerifyIndividualAsync(source, nameof(BindingNames)); - } - - [Fact] - public async Task AsParameters() - { - var source = ContractScenario(includeAsParameters: true); - await VerifyIndividualAsync(source, nameof(AsParameters)); - } - - [Fact] - public async Task FromServices() - { - var source = ContractScenario(includeFromServices: true); - await VerifyIndividualAsync(source, nameof(FromServices)); - } - - [Fact] - public async Task FromKeyedServices() - { - var source = ContractScenario(includeFromKeyedServices: true); - await VerifyIndividualAsync(source, nameof(FromKeyedServices)); - } - - [Fact] - public async Task AcceptsAttribute() - { - var source = ContractScenario(includeAccepts: true, acceptsContentType1: "application/custom"); - await VerifyIndividualAsync(source, nameof(AcceptsAttribute)); - } - - [Fact] - public async Task AcceptsMultipleContentTypes() - { - var source = ContractScenario(includeAccepts: true, acceptsContentType1: "application/json", acceptsContentType2: "text/json"); - await VerifyIndividualAsync(source, nameof(AcceptsMultipleContentTypes)); - } - - [Fact] - public async Task GenericAcceptsAttribute() - { - var source = ContractScenario(includeGenericAccepts: true, acceptsContentType1: "application/vnd.generic"); - await VerifyIndividualAsync(source, nameof(GenericAcceptsAttribute)); - } - - [Fact] - public async Task ProducesResponseAttribute() - { - var source = ContractScenario(includeProducesResponse: true, producesContentType1: "application/json"); - await VerifyIndividualAsync(source, nameof(ProducesResponseAttribute)); - } - - [Fact] - public async Task ProducesResponseMultipleContentTypes() - { - var source = ContractScenario(includeProducesResponse: true, producesContentType1: "application/json", producesContentType2: "text/json"); - await VerifyIndividualAsync(source, nameof(ProducesResponseMultipleContentTypes)); - } - - [Fact] - public async Task ProducesProblemAttribute() - { - var source = ContractScenario(includeProducesProblem: true, producesContentType1: "application/problem+json"); - await VerifyIndividualAsync(source, nameof(ProducesProblemAttribute)); - } - - [Fact] - public async Task ProducesValidationProblemAttribute() - { - var source = ContractScenario(includeProducesValidationProblem: true); - await VerifyIndividualAsync(source, nameof(ProducesValidationProblemAttribute)); - } - - [Fact] - public async Task SummaryAndDescriptionAttributes() - { - var source = ContractScenario(includeSummaryAndDescription: true); - await VerifyIndividualAsync(source, nameof(SummaryAndDescriptionAttributes)); - } - - [Fact] - public async Task DisplayNameAttribute() - { - var source = ContractScenario(includeDisplayName: true); - await VerifyIndividualAsync(source, nameof(DisplayNameAttribute)); - } - - [Fact] - public async Task ContractTags() - { - var source = ContractScenario(includeTags: true); - await VerifyIndividualAsync(source, nameof(ContractTags)); - } - - [Fact] - public async Task ContractExcludeFromDescription() - { - var source = ContractScenario(excludeFromDescription: true); - await VerifyIndividualAsync(source, nameof(ContractExcludeFromDescription)); - } - - [Fact] - public async Task ContractAllowAnonymous() - { - var source = ContractScenario(allowAnonymous: true); - await VerifyIndividualAsync(source, nameof(ContractAllowAnonymous)); - } - - [Fact] - public async Task ContractRequireAuthorization() - { - var source = ContractScenario(methodRequiresAuthorization: true); - await VerifyIndividualAsync(source, nameof(ContractRequireAuthorization)); - } - - private static async Task VerifyIndividualAsync(string source, string scenario, bool withNamespace = true) - { - var sources = TestHelpers.GetSources(source, withNamespace); - var result = TestHelpers.RunGenerator(sources); - - await result.VerifyAsync("AddEndpointHandlers.g.cs") - .UseMethodName($"{scenario}_AddEndpointHandlers"); - - await result.VerifyAsync("MapEndpointHandlers.g.cs") - .UseMethodName($"{scenario}_MapEndpointHandlers"); - } - - private static string FallbackScenario(bool includeDefault = false, bool includeCustom = false, string? customRoute = null) - => SourceFactory.BuildFallbackSource(includeDefault, includeCustom, customRoute); - - private static string AuthorizationScenario( - bool classAllowAnonymous = false, - bool methodAllowAnonymous = false, - bool classRequireAuthorization = false, - bool methodRequireAuthorization = false, - bool classTags = false, - bool methodTags = false, - string? classHost = null, - string? methodHost = null, - bool classRequireCors = false, - string? classCorsPolicy = null, - bool methodRequireCors = false, - string? methodCorsPolicy = null, - bool requireRateLimiting = false, - string? rateLimitingPolicy = null, - bool applyShortCircuit = false, - bool applyRequestTimeout = false, - string? requestTimeoutPolicy = null, - bool disableRequestTimeout = false, - int orderValue = 0, - string? groupName = null, - bool excludeFromDescription = false) - => SourceFactory.BuildAuthorizationMatrixSource( - classAllowAnonymous, - methodAllowAnonymous, - classRequireAuthorization, - methodRequireAuthorization, - classTags, - methodTags, - classHost, - methodHost, - classRequireCors, - classCorsPolicy, - methodRequireCors, - methodCorsPolicy, - requireRateLimiting, - rateLimitingPolicy, - applyShortCircuit, - applyRequestTimeout, - requestTimeoutPolicy, - disableRequestTimeout, - orderValue, - groupName, - excludeFromDescription); - - private static string ConfigureScenario( - bool configureWithServiceProvider = false, - bool configureAddsMetadata = false, - bool includeClassLevelFilter = false, - bool includeMethodLevelFilter = false, - bool includeGenericFilter = false, - bool configureRegistersFilter = false, - string metadataValue = "Individual") - => SourceFactory.BuildConfigureAndFiltersSource( - configureWithServiceProvider, - configureAddsMetadata, - includeClassLevelFilter, - includeMethodLevelFilter, - includeGenericFilter, - configureRegistersFilter, - metadataValue); - - private static string HttpMethodScenario( - bool includeGet = false, - bool includePost = false, - bool includePut = false, - bool includeDelete = false, - bool includeOptions = false, - bool includeHead = false, - bool includePatch = false, - bool includeQuery = false, - bool includeTrace = false, - bool includeConnect = false, - bool includeMethodNameCollision = false) - => SourceFactory.BuildHttpMethodMatrixSource( - includeGet, - includePost, - includePut, - includeDelete, - includeOptions, - includeHead, - includePatch, - includeQuery, - includeTrace, - includeConnect, - includeMethodNameCollision); - - private static string ContractScenario( - bool includeBindingNames = false, - bool includeAsParameters = false, - bool includeFromServices = false, - bool includeFromKeyedServices = false, - bool includeAccepts = false, - bool includeGenericAccepts = false, - bool includeProducesResponse = false, - bool includeProducesProblem = false, - bool includeProducesValidationProblem = false, - bool includeSummaryAndDescription = false, - bool includeDisplayName = false, - bool includeTags = false, - bool excludeFromDescription = false, - bool allowAnonymous = false, - bool methodRequiresAuthorization = false, - string? acceptsContentType1 = null, - string? acceptsContentType2 = null, - string? producesContentType1 = null, - string? producesContentType2 = null) - => SourceFactory.BuildContractsAndBindingSource( - includeBindingNames, - includeAsParameters, - includeFromServices, - includeFromKeyedServices, - includeAccepts, - includeGenericAccepts, - includeProducesResponse, - includeProducesProblem, - includeProducesValidationProblem, - includeSummaryAndDescription, - includeDisplayName, - includeTags, - excludeFromDescription, - allowAnonymous, - methodRequiresAuthorization, - acceptsContentType1, - acceptsContentType2, - producesContentType1, - producesContentType2); -} - -file static class SourceFactory -{ - public static string BuildFallbackSource(bool includeDefault, bool includeCustom, string? customRoute) - { - var builder = new StringBuilder(); - builder.AppendLine("internal static class FallbackEndpoints"); - builder.AppendLine("{"); - - if (includeDefault) - { - builder.AppendLine(" [MapFallback]"); - builder.AppendLine(" public static Ok Default() => TypedResults.Ok();"); - builder.AppendLine(); - } - - if (includeCustom) - { - var route = customRoute ?? "/custom"; - builder.AppendLine($" [MapFallback(\"{route}\")]"); - builder.AppendLine(" public static Ok Custom() => TypedResults.Ok();"); - builder.AppendLine(); - } - - builder.AppendLine("}"); - return builder.ToString(); - } - - public static string BuildAuthorizationMatrixSource( - bool classAllowAnonymous, - bool methodAllowAnonymous, - bool classRequireAuthorization, - bool methodRequireAuthorization, - bool classTags, - bool methodTags, - string? classHost, - string? methodHost, - bool classRequireCors, - string? classCorsPolicy, - bool methodRequireCors, - string? methodCorsPolicy, - bool requireRateLimiting, - string? rateLimitingPolicy, - bool applyShortCircuit, - bool applyRequestTimeout, - string? requestTimeoutPolicy, - bool disableRequestTimeout, - int orderValue, - string? groupName, - bool excludeFromDescription) - { - var builder = new StringBuilder(); - - if (classAllowAnonymous) - { - builder.AppendLine("[AllowAnonymous]"); - } - - if (classRequireAuthorization) - { - builder.AppendLine("[RequireAuthorization(\"ClassPolicy\")]"); - } - - if (classTags) - { - builder.AppendLine("[Tags(\"Class\", \"Matrix\")]"); - } - - if (!string.IsNullOrWhiteSpace(classHost)) - { - builder.AppendLine($"[RequireHost(\"{classHost}\")]"); - } - - if (classRequireCors) - { - var cors = string.IsNullOrWhiteSpace(classCorsPolicy) ? "" : $"(\"{classCorsPolicy}\")"; - builder.AppendLine($"[RequireCors{cors}]"); - } - - if (!string.IsNullOrWhiteSpace(groupName)) - { - builder.AppendLine($"[GroupName(\"{groupName}\")]"); - } - - if (applyShortCircuit) - { - builder.AppendLine("[ShortCircuit]"); - } - - if (applyRequestTimeout) - { - var timeoutArgument = string.IsNullOrWhiteSpace(requestTimeoutPolicy) - ? string.Empty - : $"(\"{requestTimeoutPolicy}\")"; - builder.AppendLine($"[RequestTimeout{timeoutArgument}]"); - } - - if (disableRequestTimeout) - { - builder.AppendLine("[DisableRequestTimeout]"); - } - - if (orderValue != 0) - { - builder.AppendLine($"[Order({orderValue})]"); - } - - if (excludeFromDescription) - { - builder.AppendLine("[ExcludeFromDescription]"); - } - - builder.AppendLine("internal sealed class AuthorizationMatrixEndpoints"); - builder.AppendLine("{"); - builder.AppendLine(" [MapGet(\"/matrix/{id:int}\", Name = \"GetMatrix\")]"); - - if (methodAllowAnonymous) - { - builder.AppendLine(" [AllowAnonymous]"); - } - - if (methodRequireAuthorization) - { - builder.AppendLine(" [RequireAuthorization(\"MethodPolicy\")]"); - } - - if (methodTags) - { - builder.AppendLine(" [Tags(\"Method\", \"Matrix\")]"); - } - - if (!string.IsNullOrWhiteSpace(methodHost)) - { - builder.AppendLine($" [RequireHost(\"{methodHost}\", \"contoso.com\")]"); - } - - if (methodRequireCors) - { - var methodCors = string.IsNullOrWhiteSpace(methodCorsPolicy) ? string.Empty : $"(\"{methodCorsPolicy}\")"; - builder.AppendLine($" [RequireCors{methodCors}]"); - } - - if (requireRateLimiting) - { - var rateLimit = string.IsNullOrWhiteSpace(rateLimitingPolicy) ? string.Empty : $"(\"{rateLimitingPolicy}\")"; - builder.AppendLine($" [RequireRateLimiting{rateLimit}]"); - } - - builder.AppendLine(" public static Ok Handle(int id) => id >= 0 ? TypedResults.Ok() : TypedResults.Ok();"); - builder.AppendLine("}"); - return builder.ToString(); - } - - public static string BuildConfigureAndFiltersSource( - bool configureWithServiceProvider, - bool configureAddsMetadata, - bool includeClassLevelFilter, - bool includeMethodLevelFilter, - bool includeGenericFilter, - bool configureRegistersFilter, - string metadataValue) - { - var builder = new StringBuilder(); - builder.AppendLine("using Microsoft.AspNetCore.Builder;"); - builder.AppendLine(); - - if (includeClassLevelFilter) - { - builder.AppendLine("[EndpointFilter(typeof(TimingFilter))]"); - } - - builder.AppendLine("internal static class ConfigureFilterEndpoints"); - builder.AppendLine("{"); - builder.AppendLine(" [MapGet(\"/configure-filters\")]"); - - if (includeMethodLevelFilter) - { - builder.AppendLine(" [EndpointFilter(typeof(ValidationFilter))]"); - } - - if (includeGenericFilter) - { - builder.AppendLine(" [EndpointFilter]"); - } - - builder.AppendLine(" public static Ok Handle() => TypedResults.Ok();"); - builder.AppendLine(); - builder.AppendLine(" public static void Configure(TBuilder builder" + (configureWithServiceProvider ? ", IServiceProvider services" : string.Empty) + ")"); - builder.AppendLine(" where TBuilder : IEndpointConventionBuilder"); - builder.AppendLine(" {"); - builder.AppendLine(" _ = builder;"); - - if (configureWithServiceProvider) - { - builder.AppendLine(" _ = services;"); - } - - if (configureAddsMetadata) - { - builder.AppendLine($" builder.WithMetadata(\"{metadataValue}\");"); - } - - if (configureRegistersFilter) - { - builder.AppendLine(" builder.AddEndpointFilterFactory((context, next) => next);"); - } - - builder.AppendLine(" }"); - builder.AppendLine("}"); - builder.AppendLine(); - builder.AppendLine("internal sealed class TimingFilter : IEndpointFilter"); - builder.AppendLine("{"); - builder.AppendLine(" public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) => next(context);"); - builder.AppendLine("}"); - builder.AppendLine(); - builder.AppendLine("internal sealed class ValidationFilter : IEndpointFilter"); - builder.AppendLine("{"); - builder.AppendLine(" public ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) => next(context);"); - builder.AppendLine("}"); - - return builder.ToString(); - } - - public static string BuildHttpMethodMatrixSource( - bool includeGet, - bool includePost, - bool includePut, - bool includeDelete, - bool includeOptions, - bool includeHead, - bool includePatch, - bool includeQuery, - bool includeTrace, - bool includeConnect, - bool includeMethodNameCollision) - { - var builder = new StringBuilder(); - builder.AppendLine("using Microsoft.AspNetCore.Mvc;"); - builder.AppendLine(); - builder.AppendLine("internal static class HttpMethodEndpoints"); - builder.AppendLine("{"); - - if (includeGet) - { - builder.AppendLine(" [MapGet(\"/matrix\")] public static Ok Get() => TypedResults.Ok();"); - } - - if (includePost) - { - builder.AppendLine(" [MapPost(\"/matrix\")] public static Created Post() => TypedResults.Created(\"/matrix/1\", \"Created\");"); - } - - if (includePut) - { - builder.AppendLine(" [MapPut(\"/matrix/{id:int}\")] public static Results Put(int id) => id > 0 ? TypedResults.NoContent() : TypedResults.NotFound();"); - } - - if (includeDelete) - { - builder.AppendLine(" [MapDelete(\"/matrix/{id:int}\")] public static IResult Delete(int id) => TypedResults.Ok();"); - } - - if (includeOptions) - { - builder.AppendLine(" [MapOptions(\"/matrix\")] public static IResult Options() => TypedResults.Ok();"); - } - - if (includeHead) - { - builder.AppendLine(" [MapHead(\"/matrix\")] public static IResult Head() => TypedResults.Ok();"); - } - - if (includePatch) - { - builder.AppendLine(" [MapPatch(\"/matrix/{id:int}\")] public static IResult Patch(int id) => TypedResults.Ok();"); - } - - if (includeQuery) - { - builder.AppendLine(" [MapQuery(\"/matrix/query\")] public static IResult Query([FromQuery] string value) => TypedResults.Ok(value);"); - } - - if (includeTrace) - { - builder.AppendLine(" [MapTrace(\"/matrix\")] public static IResult Trace() => TypedResults.Ok();"); - } - - if (includeConnect) - { - builder.AppendLine(" [MapConnect(\"/matrix\")] public static IResult Connect() => TypedResults.Ok();"); - } - - builder.AppendLine("}"); - - if (includeMethodNameCollision) - { - builder.AppendLine(); - builder.AppendLine("internal static class AlternateEndpoints"); - builder.AppendLine("{"); - builder.AppendLine(" [MapGet(\"/alternate\")] public static Ok Get() => TypedResults.Ok();"); - builder.AppendLine(" [MapPost(\"/alternate\")] public static IResult Post() => TypedResults.Ok();"); - builder.AppendLine("}"); - } - - return builder.ToString(); - } - - public static string BuildContractsAndBindingSource( - bool includeBindingNames, - bool includeAsParameters, - bool includeFromServices, - bool includeFromKeyedServices, - bool includeAccepts, - bool includeGenericAccepts, - bool includeProducesResponse, - bool includeProducesProblem, - bool includeProducesValidationProblem, - bool includeSummaryAndDescription, - bool includeDisplayName, - bool includeTags, - bool excludeFromDescription, - bool allowAnonymous, - bool methodRequiresAuthorization, - string? acceptsContentType1, - string? acceptsContentType2, - string? producesContentType1, - string? producesContentType2) - { - var builder = new StringBuilder(); - builder.AppendLine("using Microsoft.AspNetCore.Mvc;"); - builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); - builder.AppendLine(); - builder.AppendLine("internal sealed class ContractEndpoints"); - builder.AppendLine("{"); - - if (includeSummaryAndDescription) - { - builder.AppendLine(" [Summary(\"Gets detailed content.\")]"); - builder.AppendLine(" [Description(\"Shows binding and contract combinations.\")]"); - } - - if (includeDisplayName) - { - builder.AppendLine(" [DisplayName(\"Contract endpoint\")]"); - } - - if (includeTags) - { - builder.AppendLine(" [Tags(\"Contracts\", \"Bindings\")]"); - } - - if (excludeFromDescription) - { - builder.AppendLine(" [ExcludeFromDescription]"); - } - - if (allowAnonymous) - { - builder.AppendLine(" [AllowAnonymous]"); - } - - if (methodRequiresAuthorization) - { - builder.AppendLine(" [RequireAuthorization(\"ContractsPolicy\")]"); - } - - builder.AppendLine(" [MapGet(\"/contracts/{id:int}\")]"); - - if (includeAccepts) - { - var secondContentType = string.IsNullOrWhiteSpace(acceptsContentType2) ? string.Empty : $", \"{acceptsContentType2}\""; - builder.AppendLine($" [Accepts(\"{acceptsContentType1 ?? "application/json"}\"{secondContentType})]"); - } - - if (includeGenericAccepts) - { - builder.AppendLine($" [Accepts(\"{acceptsContentType1 ?? "application/json"}\")]"); - } - - if (includeProducesResponse) - { - var secondProduces = string.IsNullOrWhiteSpace(producesContentType2) ? string.Empty : $", \"{producesContentType2}\""; - builder.AppendLine($" [ProducesResponse(200, \"{producesContentType1 ?? "application/json"}\"{secondProduces}, ResponseType = typeof(ResponseRecord))]"); - } - - if (includeProducesProblem) - { - builder.AppendLine($" [ProducesProblem(500, \"{producesContentType1 ?? "application/problem+json"}\")]"); - } - - if (includeProducesValidationProblem) - { - builder.AppendLine($" [ProducesValidationProblem(422, \"{producesContentType1 ?? "application/problem+json"}\")]"); - } - - builder.AppendLine(" public static async Task, NotFound>> Handle("); - builder.AppendLine(includeBindingNames - ? " [FromRoute(Name = \"route-id\")] int id," - : " [FromRoute] int id,"); - builder.AppendLine(includeBindingNames - ? " [FromQuery(Name = \"filter-term\")] string? filter," - : " [FromQuery] string? filter,"); - builder.AppendLine(includeBindingNames - ? " [FromHeader(Name = \"x-trace-id\")] string? traceId," - : " [FromHeader] string? traceId,"); - builder.AppendLine(" [FromBody] RequestRecord request,"); - - if (includeAsParameters) - { - builder.AppendLine(" [AsParameters] AdditionalParameters parameters,"); - } - - if (includeFromServices) - { - builder.AppendLine(" [FromServices] IServiceProvider services,"); - } - - if (includeFromKeyedServices) - { - builder.AppendLine(" [FromKeyedServices(\"special\")] object keyed,"); - } - - builder.AppendLine(" CancellationToken cancellationToken)"); - builder.AppendLine(" {"); - builder.AppendLine(" await Task.Yield();"); - builder.AppendLine(" cancellationToken.ThrowIfCancellationRequested();"); - builder.AppendLine(" return id > 0 ? TypedResults.Ok(new ResponseRecord(id)) : TypedResults.NotFound();"); - builder.AppendLine(" }"); - builder.AppendLine("}"); - builder.AppendLine(); - builder.AppendLine("internal sealed record RequestRecord(int Value);"); - builder.AppendLine("internal sealed record ResponseRecord(int Value);"); - - if (includeAsParameters) - { - builder.AppendLine("internal sealed record AdditionalParameters(string? Search, int? Page);"); - } - - return builder.ToString(); - } -} - -file static class ScenarioNamer -{ - public static string Create(string prefix, params (string Name, object? Value)[] parts) - { - var descriptor = new StringBuilder(); - - foreach (var (name, value) in parts) - { - descriptor.Append(name); - descriptor.Append('='); - descriptor.Append(Sanitize(value)); - descriptor.Append(';'); - } - - using var sha256 = SHA256.Create(); - var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(descriptor.ToString())); - var hash = Convert.ToHexString(bytes.AsSpan(0, 6)); - return $"{prefix}_{hash}"; - } - - private static string Sanitize(object? value) - { - if (value is null) - { - return "None"; - } - - return value switch - { - bool b => b ? "On" : "Off", - string s => s, - _ => value.ToString() ?? "Value" - }; - } -} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.cs b/tests/GeneratedEndpoints.Tests/IndividualTests.cs new file mode 100644 index 0000000..40012ef --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.cs @@ -0,0 +1,555 @@ +using GeneratedEndpoints.Tests.Common; +using SourceGeneratorTestHelpers.XUnit; + +namespace GeneratedEndpoints.Tests; + +[UsesVerify] +public class IndividualTests +{ + public IndividualTests() + { + ModuleInitializer.Initialize(); + } + + [Fact] + public async Task DefaultFallbackOnly() + { + var source = FallbackScenario(includeDefault: true); + await VerifyIndividualAsync(source, nameof(DefaultFallbackOnly)); + } + + [Fact] + public async Task CustomFallbackRoute() + { + var source = FallbackScenario(includeCustom: true, customRoute: "/custom-individual"); + await VerifyIndividualAsync(source, nameof(CustomFallbackRoute)); + } + + [Fact] + public async Task ClassAllowAnonymous() + { + var source = AuthorizationScenario(classAllowAnonymous: true); + await VerifyIndividualAsync(source, nameof(ClassAllowAnonymous)); + } + + [Fact] + public async Task MethodAllowAnonymous() + { + var source = AuthorizationScenario(methodAllowAnonymous: true); + await VerifyIndividualAsync(source, nameof(MethodAllowAnonymous)); + } + + [Fact] + public async Task ClassRequireAuthorization() + { + var source = AuthorizationScenario(classRequireAuthorization: true); + await VerifyIndividualAsync(source, nameof(ClassRequireAuthorization)); + } + + [Fact] + public async Task MethodRequireAuthorization() + { + var source = AuthorizationScenario(methodRequireAuthorization: true); + await VerifyIndividualAsync(source, nameof(MethodRequireAuthorization)); + } + + [Fact] + public async Task ClassTags() + { + var source = AuthorizationScenario(classTags: true); + await VerifyIndividualAsync(source, nameof(ClassTags)); + } + + [Fact] + public async Task MethodTags() + { + var source = AuthorizationScenario(methodTags: true); + await VerifyIndividualAsync(source, nameof(MethodTags)); + } + + [Fact] + public async Task ClassRequireHost() + { + var source = AuthorizationScenario(classHost: "*.individual.com"); + await VerifyIndividualAsync(source, nameof(ClassRequireHost)); + } + + [Fact] + public async Task MethodRequireHost() + { + var source = AuthorizationScenario(methodHost: "api.individual.com"); + await VerifyIndividualAsync(source, nameof(MethodRequireHost)); + } + + [Fact] + public async Task ClassRequireCors() + { + var source = AuthorizationScenario(classRequireCors: true); + await VerifyIndividualAsync(source, nameof(ClassRequireCors)); + } + + [Fact] + public async Task ClassRequireCorsWithPolicy() + { + var source = AuthorizationScenario(classRequireCors: true, classCorsPolicy: "ClassPolicy"); + await VerifyIndividualAsync(source, nameof(ClassRequireCorsWithPolicy)); + } + + [Fact] + public async Task MethodRequireCors() + { + var source = AuthorizationScenario(methodRequireCors: true); + await VerifyIndividualAsync(source, nameof(MethodRequireCors)); + } + + [Fact] + public async Task MethodRequireCorsWithPolicy() + { + var source = AuthorizationScenario(methodRequireCors: true, methodCorsPolicy: "MethodPolicy"); + await VerifyIndividualAsync(source, nameof(MethodRequireCorsWithPolicy)); + } + + [Fact] + public async Task RequireRateLimiting() + { + var source = AuthorizationScenario(requireRateLimiting: true); + await VerifyIndividualAsync(source, nameof(RequireRateLimiting)); + } + + [Fact] + public async Task RequireRateLimitingWithPolicy() + { + var source = AuthorizationScenario(requireRateLimiting: true, rateLimitingPolicy: "BurstPolicy"); + await VerifyIndividualAsync(source, nameof(RequireRateLimitingWithPolicy)); + } + + [Fact] + public async Task ShortCircuit() + { + var source = AuthorizationScenario(applyShortCircuit: true); + await VerifyIndividualAsync(source, nameof(ShortCircuit)); + } + + [Fact] + public async Task RequestTimeout() + { + var source = AuthorizationScenario(applyRequestTimeout: true); + await VerifyIndividualAsync(source, nameof(RequestTimeout)); + } + + [Fact] + public async Task RequestTimeoutWithPolicy() + { + var source = AuthorizationScenario(applyRequestTimeout: true, requestTimeoutPolicy: "TimeoutPolicy"); + await VerifyIndividualAsync(source, nameof(RequestTimeoutWithPolicy)); + } + + [Fact] + public async Task DisableRequestTimeout() + { + var source = AuthorizationScenario(disableRequestTimeout: true); + await VerifyIndividualAsync(source, nameof(DisableRequestTimeout)); + } + + [Fact] + public async Task OrderMetadata() + { + var source = AuthorizationScenario(orderValue: 7); + await VerifyIndividualAsync(source, nameof(OrderMetadata)); + } + + [Fact] + public async Task GroupName() + { + var source = AuthorizationScenario(groupName: "IndividualGroup"); + await VerifyIndividualAsync(source, nameof(GroupName)); + } + + [Fact] + public async Task ExcludeFromDescription() + { + var source = AuthorizationScenario(excludeFromDescription: true); + await VerifyIndividualAsync(source, nameof(ExcludeFromDescription)); + } + + [Fact] + public async Task ClassEndpointFilter() + { + var source = ConfigureScenario(includeClassLevelFilter: true); + await VerifyIndividualAsync(source, nameof(ClassEndpointFilter)); + } + + [Fact] + public async Task MethodEndpointFilter() + { + var source = ConfigureScenario(includeMethodLevelFilter: true); + await VerifyIndividualAsync(source, nameof(MethodEndpointFilter)); + } + + [Fact] + public async Task GenericEndpointFilter() + { + var source = ConfigureScenario(includeGenericFilter: true); + await VerifyIndividualAsync(source, nameof(GenericEndpointFilter)); + } + + [Fact] + public async Task ConfigureWithServiceProvider() + { + var source = ConfigureScenario(configureWithServiceProvider: true); + await VerifyIndividualAsync(source, nameof(ConfigureWithServiceProvider)); + } + + [Fact] + public async Task ConfigureAddsMetadata() + { + var source = ConfigureScenario(configureAddsMetadata: true, metadataValue: "IndividualMetadata"); + await VerifyIndividualAsync(source, nameof(ConfigureAddsMetadata)); + } + + [Fact] + public async Task ConfigureRegistersFilter() + { + var source = ConfigureScenario(configureRegistersFilter: true); + await VerifyIndividualAsync(source, nameof(ConfigureRegistersFilter)); + } + + [Fact] + public async Task MapGetEndpoint() + { + var source = HttpMethodScenario(includeGet: true); + await VerifyIndividualAsync(source, nameof(MapGetEndpoint)); + } + + [Fact] + public async Task MapPostEndpoint() + { + var source = HttpMethodScenario(includePost: true); + await VerifyIndividualAsync(source, nameof(MapPostEndpoint)); + } + + [Fact] + public async Task MapPutEndpoint() + { + var source = HttpMethodScenario(includePut: true); + await VerifyIndividualAsync(source, nameof(MapPutEndpoint)); + } + + [Fact] + public async Task MapDeleteEndpoint() + { + var source = HttpMethodScenario(includeDelete: true); + await VerifyIndividualAsync(source, nameof(MapDeleteEndpoint)); + } + + [Fact] + public async Task MapOptionsEndpoint() + { + var source = HttpMethodScenario(includeOptions: true); + await VerifyIndividualAsync(source, nameof(MapOptionsEndpoint)); + } + + [Fact] + public async Task MapHeadEndpoint() + { + var source = HttpMethodScenario(includeHead: true); + await VerifyIndividualAsync(source, nameof(MapHeadEndpoint)); + } + + [Fact] + public async Task MapPatchEndpoint() + { + var source = HttpMethodScenario(includePatch: true); + await VerifyIndividualAsync(source, nameof(MapPatchEndpoint)); + } + + [Fact] + public async Task MapQueryEndpoint() + { + var source = HttpMethodScenario(includeQuery: true); + await VerifyIndividualAsync(source, nameof(MapQueryEndpoint)); + } + + [Fact] + public async Task MapTraceEndpoint() + { + var source = HttpMethodScenario(includeTrace: true); + await VerifyIndividualAsync(source, nameof(MapTraceEndpoint)); + } + + [Fact] + public async Task MapConnectEndpoint() + { + var source = HttpMethodScenario(includeConnect: true); + await VerifyIndividualAsync(source, nameof(MapConnectEndpoint)); + } + + [Fact] + public async Task MethodNameCollision() + { + var source = HttpMethodScenario(includeGet: true, includeMethodNameCollision: true); + await VerifyIndividualAsync(source, nameof(MethodNameCollision)); + } + + [Fact] + public async Task BindingNames() + { + var source = ContractScenario(includeBindingNames: true); + await VerifyIndividualAsync(source, nameof(BindingNames)); + } + + [Fact] + public async Task AsParameters() + { + var source = ContractScenario(includeAsParameters: true); + await VerifyIndividualAsync(source, nameof(AsParameters)); + } + + [Fact] + public async Task FromServices() + { + var source = ContractScenario(includeFromServices: true); + await VerifyIndividualAsync(source, nameof(FromServices)); + } + + [Fact] + public async Task FromKeyedServices() + { + var source = ContractScenario(includeFromKeyedServices: true); + await VerifyIndividualAsync(source, nameof(FromKeyedServices)); + } + + [Fact] + public async Task AcceptsAttribute() + { + var source = ContractScenario(includeAccepts: true, acceptsContentType1: "application/custom"); + await VerifyIndividualAsync(source, nameof(AcceptsAttribute)); + } + + [Fact] + public async Task AcceptsMultipleContentTypes() + { + var source = ContractScenario(includeAccepts: true, acceptsContentType1: "application/json", acceptsContentType2: "text/json"); + await VerifyIndividualAsync(source, nameof(AcceptsMultipleContentTypes)); + } + + [Fact] + public async Task GenericAcceptsAttribute() + { + var source = ContractScenario(includeGenericAccepts: true, acceptsContentType1: "application/vnd.generic"); + await VerifyIndividualAsync(source, nameof(GenericAcceptsAttribute)); + } + + [Fact] + public async Task ProducesResponseAttribute() + { + var source = ContractScenario(includeProducesResponse: true, producesContentType1: "application/json"); + await VerifyIndividualAsync(source, nameof(ProducesResponseAttribute)); + } + + [Fact] + public async Task ProducesResponseMultipleContentTypes() + { + var source = ContractScenario(includeProducesResponse: true, producesContentType1: "application/json", producesContentType2: "text/json"); + await VerifyIndividualAsync(source, nameof(ProducesResponseMultipleContentTypes)); + } + + [Fact] + public async Task ProducesProblemAttribute() + { + var source = ContractScenario(includeProducesProblem: true, producesContentType1: "application/problem+json"); + await VerifyIndividualAsync(source, nameof(ProducesProblemAttribute)); + } + + [Fact] + public async Task ProducesValidationProblemAttribute() + { + var source = ContractScenario(includeProducesValidationProblem: true); + await VerifyIndividualAsync(source, nameof(ProducesValidationProblemAttribute)); + } + + [Fact] + public async Task SummaryAndDescriptionAttributes() + { + var source = ContractScenario(includeSummaryAndDescription: true); + await VerifyIndividualAsync(source, nameof(SummaryAndDescriptionAttributes)); + } + + [Fact] + public async Task DisplayNameAttribute() + { + var source = ContractScenario(includeDisplayName: true); + await VerifyIndividualAsync(source, nameof(DisplayNameAttribute)); + } + + [Fact] + public async Task ContractTags() + { + var source = ContractScenario(includeTags: true); + await VerifyIndividualAsync(source, nameof(ContractTags)); + } + + [Fact] + public async Task ContractExcludeFromDescription() + { + var source = ContractScenario(excludeFromDescription: true); + await VerifyIndividualAsync(source, nameof(ContractExcludeFromDescription)); + } + + [Fact] + public async Task ContractAllowAnonymous() + { + var source = ContractScenario(allowAnonymous: true); + await VerifyIndividualAsync(source, nameof(ContractAllowAnonymous)); + } + + [Fact] + public async Task ContractRequireAuthorization() + { + var source = ContractScenario(methodRequiresAuthorization: true); + await VerifyIndividualAsync(source, nameof(ContractRequireAuthorization)); + } + + private static async Task VerifyIndividualAsync(string source, string scenario, bool withNamespace = true) + { + var sources = TestHelpers.GetSources(source, withNamespace); + var result = TestHelpers.RunGenerator(sources); + + await result.VerifyAsync("AddEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_AddEndpointHandlers"); + + await result.VerifyAsync("MapEndpointHandlers.g.cs") + .UseMethodName($"{scenario}_MapEndpointHandlers"); + } + + private static string FallbackScenario(bool includeDefault = false, bool includeCustom = false, string? customRoute = null) + => SourceFactory.BuildFallbackSource(includeDefault, includeCustom, customRoute); + + private static string AuthorizationScenario( + bool classAllowAnonymous = false, + bool methodAllowAnonymous = false, + bool classRequireAuthorization = false, + bool methodRequireAuthorization = false, + bool classTags = false, + bool methodTags = false, + string? classHost = null, + string? methodHost = null, + bool classRequireCors = false, + string? classCorsPolicy = null, + bool methodRequireCors = false, + string? methodCorsPolicy = null, + bool requireRateLimiting = false, + string? rateLimitingPolicy = null, + bool applyShortCircuit = false, + bool applyRequestTimeout = false, + string? requestTimeoutPolicy = null, + bool disableRequestTimeout = false, + int orderValue = 0, + string? groupName = null, + bool excludeFromDescription = false) + => SourceFactory.BuildAuthorizationMatrixSource( + classAllowAnonymous, + methodAllowAnonymous, + classRequireAuthorization, + methodRequireAuthorization, + classTags, + methodTags, + classHost, + methodHost, + classRequireCors, + classCorsPolicy, + methodRequireCors, + methodCorsPolicy, + requireRateLimiting, + rateLimitingPolicy, + applyShortCircuit, + applyRequestTimeout, + requestTimeoutPolicy, + disableRequestTimeout, + orderValue, + groupName, + excludeFromDescription); + + private static string ConfigureScenario( + bool configureWithServiceProvider = false, + bool configureAddsMetadata = false, + bool includeClassLevelFilter = false, + bool includeMethodLevelFilter = false, + bool includeGenericFilter = false, + bool configureRegistersFilter = false, + string metadataValue = "Individual") + => SourceFactory.BuildConfigureAndFiltersSource( + configureWithServiceProvider, + configureAddsMetadata, + includeClassLevelFilter, + includeMethodLevelFilter, + includeGenericFilter, + configureRegistersFilter, + metadataValue); + + private static string HttpMethodScenario( + bool includeGet = false, + bool includePost = false, + bool includePut = false, + bool includeDelete = false, + bool includeOptions = false, + bool includeHead = false, + bool includePatch = false, + bool includeQuery = false, + bool includeTrace = false, + bool includeConnect = false, + bool includeMethodNameCollision = false) + => SourceFactory.BuildHttpMethodMatrixSource( + includeGet, + includePost, + includePut, + includeDelete, + includeOptions, + includeHead, + includePatch, + includeQuery, + includeTrace, + includeConnect, + includeMethodNameCollision); + + private static string ContractScenario( + bool includeBindingNames = false, + bool includeAsParameters = false, + bool includeFromServices = false, + bool includeFromKeyedServices = false, + bool includeAccepts = false, + bool includeGenericAccepts = false, + bool includeProducesResponse = false, + bool includeProducesProblem = false, + bool includeProducesValidationProblem = false, + bool includeSummaryAndDescription = false, + bool includeDisplayName = false, + bool includeTags = false, + bool excludeFromDescription = false, + bool allowAnonymous = false, + bool methodRequiresAuthorization = false, + string? acceptsContentType1 = null, + string? acceptsContentType2 = null, + string? producesContentType1 = null, + string? producesContentType2 = null) + => SourceFactory.BuildContractsAndBindingSource( + includeBindingNames, + includeAsParameters, + includeFromServices, + includeFromKeyedServices, + includeAccepts, + includeGenericAccepts, + includeProducesResponse, + includeProducesProblem, + includeProducesValidationProblem, + includeSummaryAndDescription, + includeDisplayName, + includeTags, + excludeFromDescription, + allowAnonymous, + methodRequiresAuthorization, + acceptsContentType1, + acceptsContentType2, + producesContentType1, + producesContentType2); +} From f2c8b7ece89d14fa6c6b7d54a5f477ff4da9819b Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:01:29 -0500 Subject: [PATCH 51/75] Expand README with usage guide (#42) --- README.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a539c0e..50fa74b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,125 @@ [![Banner](https://raw.githubusercontent.com/jscarle/GeneratedEndpoints/develop/Banner.png)](https://github.com/jscarle/GeneratedEndpoints) - # GeneratedEndpoints - Attribute-driven, source-generated Minimal API endpoints for feature-based development + +GeneratedEndpoints is a Roslyn source generator that turns small, attribute-decorated endpoint classes into fully wired Minimal API handlers. You write cohesive endpoint classes, decorate them with attributes from `Microsoft.AspNetCore.Generated.Attributes`, and the generator creates all of the registration code, endpoint metadata, and supporting plumbing at compile time. The result is a clean feature folder structure with no runtime reflection and no hand-written routing boilerplate. + +## Setup +1. Add the package to any ASP.NET Core project that hosts Minimal APIs: + ```bash + dotnet add package GeneratedEndpoints + ``` +2. Update your endpoint class files with the attribute namespace: + ```csharp + using Microsoft.AspNetCore.Generated.Attributes; + ``` +3. Ensure the application references the routing namespace emitted by the generator: + ```csharp + using Microsoft.AspNetCore.Generated.Routing; + ``` + +## Wiring the generator into Program.cs +The generator emits two extension methods that keep your Program.cs lean: +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointHandlers(); + +var app = builder.Build(); +app.MapEndpointHandlers(); + +app.Run(); +``` +`AddEndpointHandlers` registers every generated request handler as a service so that constructor injection, logging, and filters are available. `MapEndpointHandlers` discovers the generated handlers, applies their metadata, and maps them onto the `IEndpointRouteBuilder` in one call. + +## The most minimal endpoint +A minimal endpoint is a static class with a static method marked by one of the `[Map*]` attributes. The generator reads the attribute, produces the handler delegate, and automatically hooks the method into routing. +```csharp +using Microsoft.AspNetCore.Generated.Attributes; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace Contoso.Example.Endpoints; + +internal static class TimeEndpoints +{ + [MapGet("/time")] + public static Ok GetCurrentTime() => TypedResults.Ok(DateTimeOffset.UtcNow.ToString("O")); +} +``` +Build the project and the generator will emit the registration code behind the scenes. No manual `MapGet` call is required in Program.cs. + +## Expanding the endpoint with metadata +You can gradually layer richer metadata just by adding attributes. +```csharp +using Microsoft.AspNetCore.Generated.Attributes; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace Contoso.Example.Endpoints; + +[Tags("Users", "Lookup")] +internal static class UserEndpoints +{ + [Summary("Gets a user by ID.")] + [RequireAuthorization("Users.Read")] + [MapGet("/users/{id:int}")] + public static Results, NotFound> GetUser(int id) + { + if (id <= 0) + return TypedResults.NotFound(); + + return TypedResults.Ok(new UserDto(id, $"User {id}")); + } +} + +internal sealed record UserDto(int Id, string DisplayName); +``` +* `[Tags]` on the class assigns OpenAPI tags to every endpoint inside the type. +* `[Summary]` annotates the generated metadata so Swashbuckle/NSwag show friendly descriptions. +* `[RequireAuthorization]` enforces authorization policies per method without touching Program.cs. + +## Static class or sealed class with constructor injection? +Static classes are perfect for pure functions that only depend on the method parameters. When you need services, switch to a sealed class and let the generator create the scope-aware instance for you. +```csharp +internal static class HealthEndpoints +{ + [MapGet("/health")] + public static Ok Check() => TypedResults.Ok("Healthy"); +} + +internal sealed class EnvironmentEndpoints(IHostEnvironment hostEnvironment) +{ + [MapGet("/env")] + public Ok GetEnvironmentName() => TypedResults.Ok(hostEnvironment.EnvironmentName); +} +``` +Because `EnvironmentEndpoints` is sealed, the generator can new it up through dependency injection, pass in `IHostEnvironment`, and reuse the instance for the lifetime of the request. + +## Configure method requirements +Endpoint classes may include an optional `Configure` method to apply conventions after the handler is mapped. The generator looks for the following signature: +```csharp +public static void Configure(TBuilder builder) + where TBuilder : IEndpointConventionBuilder +``` +Inside `Configure` you can attach endpoint filters, metadata, or additional policies that are not already exposed via attributes. The method runs once per endpoint during startup, after the generator maps the handler but before the application starts listening. + +## Attribute reference +* `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapPatch]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`, `[MapFallback]` – Constructor: `(string pattern = "", string? displayName = null)`. Optional named parameter `Name` sets the route name. +* `[Summary]` – Constructor: `(string summary)`. +* `[RequireAuthorization]` – Overloads: no parameters to require default authorization; `(params string[] policies)` to require specific policies. +* `[AllowAnonymous]` – No parameters. Available from `Microsoft.AspNetCore.Authorization`. +* `[Tags]` – Constructor: `(params string[] tags)` from `Microsoft.AspNetCore.Http`. +* `[DisableAntiforgery]` – No parameters. +* `[RequireCors]` – Constructor: `(string? policyName = null)`. +* `[RequireHost]` – Constructor: `(params string[] hosts)`. +* `[RequireRateLimiting]` – Constructor: `(string policyName)`. +* `[DisableRequestTimeout]` – No parameters. +* `[RequestTimeout]` – Constructor: `(string? policyName = null)` to apply a named timeout policy. +* `[ShortCircuit]` – No parameters. +* `[Order]` – Constructor: `(int order)`. +* `[GroupName]` – Constructor: `(string name)`. +* `[Accepts]` – Constructor: `(string contentType = "application/json", params string[] additionalContentTypes)` with optional named parameters `RequestType` and `IsOptional`. Generic version `[Accepts]` captures the request type. +* `[ProducesResponse]` – Constructor: `(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes)` with optional named parameter `ResponseType`. Generic version `[ProducesResponse]` infers the response type. +* `[ProducesProblem]` – Constructor: `(int statusCode = 500, string? contentType = null, params string[] additionalContentTypes)`. +* `[ProducesValidationProblem]` – Constructor: `(int statusCode = 400, string? contentType = null, params string[] additionalContentTypes)`. +* `[EndpointFilter]` – Constructor: `(Type filterType)` or generic `[EndpointFilter]`. +* `[RequireCors]`, `[RequireAuthorization]`, and `[RequireRateLimiting]` support the named parameter `PolicyName` to target specific policies when applicable. +* Standard ASP.NET Core attributes `[DisplayName]`, `[Description]`, `[AllowAnonymous]`, `[RequireCors]`, `[Tags]`, and `[ExcludeFromDescription]` are also honored when applied alongside the generated attributes. From dd07b2d445180004a08cf694bd76adbd420d8ec5 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:57:03 -0500 Subject: [PATCH 52/75] Fixed readme. --- README.md | 168 ++- src/GeneratedEndpoints/MinimalApiGenerator.cs | 1137 +++++++---------- 2 files changed, 602 insertions(+), 703 deletions(-) diff --git a/README.md b/README.md index 50fa74b..3c8f8e6 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,47 @@ [![Banner](https://raw.githubusercontent.com/jscarle/GeneratedEndpoints/develop/Banner.png)](https://github.com/jscarle/GeneratedEndpoints) + # GeneratedEndpoints - Attribute-driven, source-generated Minimal API endpoints for feature-based development -GeneratedEndpoints is a Roslyn source generator that turns small, attribute-decorated endpoint classes into fully wired Minimal API handlers. You write cohesive endpoint classes, decorate them with attributes from `Microsoft.AspNetCore.Generated.Attributes`, and the generator creates all of the registration code, endpoint metadata, and supporting plumbing at compile time. The result is a clean feature folder structure with no runtime reflection and no hand-written routing boilerplate. +GeneratedEndpoints is a .NET source generator that automatically wires up Minimal API endpoints from attribute-annotated +methods. This simplifies integration of HTTP handlers within Clean Architecture (CA) or Vertical Slice Architecture (VSA) +by keeping endpoint definitions inside their features while generating the boilerplate mapping code. + +[![develop](https://img.shields.io/github/actions/workflow/status/jscarle/GeneratedEndpoints/develop.yml?logo=github)](https://github.com/jscarle/GeneratedEndpoints) +[![nuget](https://img.shields.io/nuget/v/GeneratedEndpoints)](https://www.nuget.org/packages/GeneratedEndpoints) +[![downloads](https://img.shields.io/nuget/dt/GeneratedEndpoints)](https://www.nuget.org/packages/GeneratedEndpoints) ## Setup -1. Add the package to any ASP.NET Core project that hosts Minimal APIs: - ```bash - dotnet add package GeneratedEndpoints - ``` -2. Update your endpoint class files with the attribute namespace: - ```csharp - using Microsoft.AspNetCore.Generated.Attributes; - ``` -3. Ensure the application references the routing namespace emitted by the generator: - ```csharp - using Microsoft.AspNetCore.Generated.Routing; - ``` - -## Wiring the generator into Program.cs -The generator emits two extension methods that keep your Program.cs lean: + +Add the package to any ASP.NET Core project that hosts Minimal APIs: + +```bash +dotnet add package GeneratedEndpoints +``` + +The source generator emits two extension methods: + ```csharp +using Microsoft.AspNetCore.Generated.Routing; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointHandlers(); var app = builder.Build(); + app.MapEndpointHandlers(); app.Run(); ``` -`AddEndpointHandlers` registers every generated request handler as a service so that constructor injection, logging, and filters are available. `MapEndpointHandlers` discovers the generated handlers, applies their metadata, and maps them onto the `IEndpointRouteBuilder` in one call. -## The most minimal endpoint -A minimal endpoint is a static class with a static method marked by one of the `[Map*]` attributes. The generator reads the attribute, produces the handler delegate, and automatically hooks the method into routing. +- The `AddEndpointHandlers` method registers each endpoint class with injected services as a `Scoped` service. +- The `MapEndpointHandlers` method calls the generated `Map` method for each endpoint. + +## A simple endpoint + +An endpoint is wired using a class with a method marked by one of the `[Map*]` attributes. The source generator produces the handler delegate and automatically +calls the appropriate map method on the route builder. + ```csharp using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http.HttpResults; @@ -45,10 +54,11 @@ internal static class TimeEndpoints public static Ok GetCurrentTime() => TypedResults.Ok(DateTimeOffset.UtcNow.ToString("O")); } ``` -Build the project and the generator will emit the registration code behind the scenes. No manual `MapGet` call is required in Program.cs. -## Expanding the endpoint with metadata -You can gradually layer richer metadata just by adding attributes. +## Expanding the endpoint + +You can add additional metadata to endpoints using attributes. + ```csharp using Microsoft.AspNetCore.Generated.Attributes; using Microsoft.AspNetCore.Http.HttpResults; @@ -72,54 +82,96 @@ internal static class UserEndpoints internal sealed record UserDto(int Id, string DisplayName); ``` + * `[Tags]` on the class assigns OpenAPI tags to every endpoint inside the type. -* `[Summary]` annotates the generated metadata so Swashbuckle/NSwag show friendly descriptions. -* `[RequireAuthorization]` enforces authorization policies per method without touching Program.cs. +* `[Summary]` annotates the endpoint with OpenAI summary metadata. +* `[RequireAuthorization]` enforces authorization policies on the endpoint. + +## Static vs instantiable classes + +To avoid clutering your endpoint method with service dependencies, you can specify them using a class constructor. This also allows you to reuse the same +service declaration across multiple endpoints. The source generator will generate the appropriate service injection code for each endpoint method. -## Static class or sealed class with constructor injection? -Static classes are perfect for pure functions that only depend on the method parameters. When you need services, switch to a sealed class and let the generator create the scope-aware instance for you. ```csharp +using Microsoft.AspNetCore.Generated.Attributes; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace Contoso.Example.Endpoints; + internal static class HealthEndpoints { [MapGet("/health")] - public static Ok Check() => TypedResults.Ok("Healthy"); + public static Ok Check(IHostEnvironment env) => TypedResults.Ok("Healthy, {env.EnvironmentName}"); } -internal sealed class EnvironmentEndpoints(IHostEnvironment hostEnvironment) +internal sealed class EnvironmentEndpoints(IHostEnvironment env) { [MapGet("/env")] - public Ok GetEnvironmentName() => TypedResults.Ok(hostEnvironment.EnvironmentName); + public Ok GetEnvironmentName() => TypedResults.Ok(env.EnvironmentName); } ``` -Because `EnvironmentEndpoints` is sealed, the generator can new it up through dependency injection, pass in `IHostEnvironment`, and reuse the instance for the lifetime of the request. -## Configure method requirements -Endpoint classes may include an optional `Configure` method to apply conventions after the handler is mapped. The generator looks for the following signature: +## Advanced configuration + +Endpoint classes may include an optional `Configure` method to apply conventions after the handler is mapped. The source generator will look for the following +signature: + ```csharp -public static void Configure(TBuilder builder) - where TBuilder : IEndpointConventionBuilder +using Microsoft.AspNetCore.Generated.Attributes; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace Contoso.Example.Endpoints; + +internal static class HealthEndpoints +{ + [MapGet("/health")] + public static Ok Check() => TypedResults.Ok("Healthy"); + + public static void Configure(TBuilder builder) + where TBuilder : IEndpointConventionBuilder + { + builder.WithRequestTimeout(TimeSpan.FromSeconds(5)); + } +} ``` -Inside `Configure` you can attach endpoint filters, metadata, or additional policies that are not already exposed via attributes. The method runs once per endpoint during startup, after the generator maps the handler but before the application starts listening. - -## Attribute reference -* `[MapGet]`, `[MapPost]`, `[MapPut]`, `[MapPatch]`, `[MapDelete]`, `[MapOptions]`, `[MapHead]`, `[MapTrace]`, `[MapConnect]`, `[MapQuery]`, `[MapFallback]` – Constructor: `(string pattern = "", string? displayName = null)`. Optional named parameter `Name` sets the route name. -* `[Summary]` – Constructor: `(string summary)`. -* `[RequireAuthorization]` – Overloads: no parameters to require default authorization; `(params string[] policies)` to require specific policies. -* `[AllowAnonymous]` – No parameters. Available from `Microsoft.AspNetCore.Authorization`. -* `[Tags]` – Constructor: `(params string[] tags)` from `Microsoft.AspNetCore.Http`. -* `[DisableAntiforgery]` – No parameters. -* `[RequireCors]` – Constructor: `(string? policyName = null)`. -* `[RequireHost]` – Constructor: `(params string[] hosts)`. -* `[RequireRateLimiting]` – Constructor: `(string policyName)`. -* `[DisableRequestTimeout]` – No parameters. -* `[RequestTimeout]` – Constructor: `(string? policyName = null)` to apply a named timeout policy. -* `[ShortCircuit]` – No parameters. -* `[Order]` – Constructor: `(int order)`. -* `[GroupName]` – Constructor: `(string name)`. -* `[Accepts]` – Constructor: `(string contentType = "application/json", params string[] additionalContentTypes)` with optional named parameters `RequestType` and `IsOptional`. Generic version `[Accepts]` captures the request type. -* `[ProducesResponse]` – Constructor: `(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes)` with optional named parameter `ResponseType`. Generic version `[ProducesResponse]` infers the response type. -* `[ProducesProblem]` – Constructor: `(int statusCode = 500, string? contentType = null, params string[] additionalContentTypes)`. -* `[ProducesValidationProblem]` – Constructor: `(int statusCode = 400, string? contentType = null, params string[] additionalContentTypes)`. -* `[EndpointFilter]` – Constructor: `(Type filterType)` or generic `[EndpointFilter]`. -* `[RequireCors]`, `[RequireAuthorization]`, and `[RequireRateLimiting]` support the named parameter `PolicyName` to target specific policies when applicable. -* Standard ASP.NET Core attributes `[DisplayName]`, `[Description]`, `[AllowAnonymous]`, `[RequireCors]`, `[Tags]`, and `[ExcludeFromDescription]` are also honored when applied alongside the generated attributes. + +Inside `Configure` you can continue to chain configurations that are not already exposed as attributes by the source generator. The `Configure` method is +applied to all endpoints within the class. + +## Attribute Reference + +* `[Accepts(string contentType = "application/json", params string[] additionalContentTypes, RequestType = null, IsOptional = false)]` +* `[Accepts(string contentType = "application/json", params string[] additionalContentTypes, IsOptional = false)]` +* `[AllowAnonymous]` +* `[Description(string description)]` +* `[DisableAntiforgery]` +* `[DisableRequestTimeout]` +* `[DisplayName(string displayName)]` +* `[EndpointFilter(Type filterType)]` +* `[EndpointFilter]` +* `[ExcludeFromDescription]` +* `[GroupName(string name)]` +* `[MapConnect(string pattern = "", Name = null)]` +* `[MapDelete(string pattern = "", Name = null)]` +* `[MapFallback(string pattern = "", Name = null)]` +* `[MapGet(string pattern = "", Name = null)]` +* `[MapHead(string pattern = "", Name = null)]` +* `[MapOptions(string pattern = "", Name = null)]` +* `[MapPatch(string pattern = "", Name = null)]` +* `[MapPost(string pattern = "", Name = null)]` +* `[MapPut(string pattern = "", Name = null)]` +* `[MapQuery(string pattern = "", Name = null)]` +* `[MapTrace(string pattern = "", Name = null)]` +* `[Order(int order)]` +* `[ProducesProblem(int statusCode = StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes)]` +* `[ProducesResponse(int statusCode = StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes, ResponseType = null)]` +* `[ProducesResponse(int statusCode = StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes)]` +* `[ProducesValidationProblem(int statusCode = StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes)]` +* `[RequestTimeout(string? policyName = null)]` +* `[RequireAuthorization(params string[] policies)]` +* `[RequireCors(string? policyName = null)]` +* `[RequireHost(params string[] hosts)]` +* `[RequireRateLimiting(string policyName)]` +* `[ShortCircuit]` +* `[Summary(string summary)]` +* `[Tags(params string[] tags)]` diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 2c02cbf..a5b988f 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; @@ -14,32 +15,9 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator { private const string BaseNamespace = "Microsoft.AspNetCore.Generated"; private const string AttributesNamespace = $"{BaseNamespace}.Attributes"; - private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.'); - private static readonly string[] AspNetCoreHttpNamespaceParts = ["Microsoft", "AspNetCore", "Http"]; - private static readonly string[] AspNetCoreAuthorizationNamespaceParts = ["Microsoft", "AspNetCore", "Authorization"]; - private static readonly string[] AspNetCoreRoutingNamespaceParts = ["Microsoft", "AspNetCore", "Routing"]; - private static readonly string[] ComponentModelNamespaceParts = ["System", "ComponentModel"]; private const string FallbackHttpMethod = "__FALLBACK__"; - private static readonly ImmutableArray HttpAttributeDefinitions = - [ - CreateHttpAttributeDefinition("MapGetAttribute", "GET"), - CreateHttpAttributeDefinition("MapPostAttribute", "POST"), - CreateHttpAttributeDefinition("MapPutAttribute", "PUT"), - CreateHttpAttributeDefinition("MapPatchAttribute", "PATCH"), - CreateHttpAttributeDefinition("MapDeleteAttribute", "DELETE"), - CreateHttpAttributeDefinition("MapOptionsAttribute", "OPTIONS"), - CreateHttpAttributeDefinition("MapHeadAttribute", "HEAD"), - CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), - CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), - CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), - CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod, allowEmptyPattern: true), - ]; - - private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = - HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); - private const string NameAttributeNamedParameter = "Name"; private const string ResponseTypeAttributeNamedParameter = "ResponseType"; private const string RequestTypeAttributeNamedParameter = "RequestType"; @@ -109,8 +87,9 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string ProducesProblemAttributeHint = $"{ProducesProblemAttributeFullyQualifiedName}.gs.cs"; private const string ProducesValidationProblemAttributeName = "ProducesValidationProblemAttribute"; - private const string ProducesValidationProblemAttributeFullyQualifiedName = - $"{AttributesNamespace}.{ProducesValidationProblemAttributeName}"; + + private const string ProducesValidationProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesValidationProblemAttributeName}"; + private const string ProducesValidationProblemAttributeHint = $"{ProducesValidationProblemAttributeFullyQualifiedName}.gs.cs"; private const string RoutingNamespace = $"{BaseNamespace}.Routing"; @@ -126,6 +105,29 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string ConfigureMethodName = "Configure"; private const string AsyncSuffix = "Async"; private const string GlobalPrefix = "global::"; + private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.'); + private static readonly string[] AspNetCoreHttpNamespaceParts = ["Microsoft", "AspNetCore", "Http"]; + private static readonly string[] AspNetCoreAuthorizationNamespaceParts = ["Microsoft", "AspNetCore", "Authorization"]; + private static readonly string[] AspNetCoreRoutingNamespaceParts = ["Microsoft", "AspNetCore", "Routing"]; + private static readonly string[] ComponentModelNamespaceParts = ["System", "ComponentModel"]; + + private static readonly ImmutableArray HttpAttributeDefinitions = + [ + CreateHttpAttributeDefinition("MapGetAttribute", "GET"), + CreateHttpAttributeDefinition("MapPostAttribute", "POST"), + CreateHttpAttributeDefinition("MapPutAttribute", "PUT"), + CreateHttpAttributeDefinition("MapPatchAttribute", "PATCH"), + CreateHttpAttributeDefinition("MapDeleteAttribute", "DELETE"), + CreateHttpAttributeDefinition("MapOptionsAttribute", "OPTIONS"), + CreateHttpAttributeDefinition("MapHeadAttribute", "HEAD"), + CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), + CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), + CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), + CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod, true), + ]; + + private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = + HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); private static readonly string FileHeader = $""" //----------------------------------------------------------------------------- @@ -141,18 +143,11 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator #nullable enable """; - private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb, bool allowEmptyPattern = false) - { - var fullyQualifiedName = $"{AttributesNamespace}.{attributeName}"; - return new HttpAttributeDefinition(attributeName, fullyQualifiedName, $"{fullyQualifiedName}.gs.cs", verb, allowEmptyPattern); - } - public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(RegisterAttributes); - var requestHandlerProviders = ImmutableArray.CreateBuilder>>( - HttpAttributeDefinitions.Length); + var requestHandlerProviders = ImmutableArray.CreateBuilder>>(HttpAttributeDefinitions.Length); foreach (var definition in HttpAttributeDefinitions) { @@ -169,17 +164,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(requestHandlers, GenerateSource); } + private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb, bool allowEmptyPattern = false) + { + var fullyQualifiedName = $"{AttributesNamespace}.{attributeName}"; + return new HttpAttributeDefinition(attributeName, fullyQualifiedName, $"{fullyQualifiedName}.gs.cs", verb, allowEmptyPattern); + } + private static IncrementalValueProvider> CombineRequestHandlers( - ImmutableArray>> handlerProviders) + ImmutableArray>> handlerProviders + ) { if (handlerProviders.IsDefaultOrEmpty) throw new InvalidOperationException("No HTTP attribute definitions were provided."); var combined = handlerProviders[0]; for (var i = 1; i < handlerProviders.Length; i++) - { - combined = combined.Combine(handlerProviders[i]).Select(static (x, _) => x.Left.AddRange(x.Right)); - } + combined = combined.Combine(handlerProviders[i]) + .Select(static (x, _) => x.Left.AddRange(x.Right)); return combined; } @@ -189,8 +190,7 @@ private static void RegisterAttributes(IncrementalGeneratorPostInitializationCon foreach (var definition in HttpAttributeDefinitions) { var summaryVerb = definition.Verb == FallbackHttpMethod ? "fallback" : definition.Verb; - var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, definition.Name, summaryVerb, - definition.AllowEmptyPattern); + var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, definition.Name, summaryVerb, definition.AllowEmptyPattern); context.AddSource(definition.Hint, SourceText.From(source, Encoding.UTF8)); } @@ -233,95 +233,95 @@ internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Att // RequireCors var requireCorsSource = $$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Specifies that the annotated endpoint requires a configured CORS policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional CORS policy name. - /// - public string? PolicyName { get; } - - /// - /// Marks the endpoint or class as requiring the default CORS policy. - /// - public {{RequireCorsAttributeName}}() - { - } - - /// - /// Marks the endpoint or class as requiring the specified named CORS policy. - /// - public {{RequireCorsAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } - """; + /// + /// Specifies that the annotated endpoint requires a configured CORS policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional CORS policy name. + /// + public string? PolicyName { get; } + + /// + /// Marks the endpoint or class as requiring the default CORS policy. + /// + public {{RequireCorsAttributeName}}() + { + } + + /// + /// Marks the endpoint or class as requiring the specified named CORS policy. + /// + public {{RequireCorsAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + """; context.AddSource(RequireCorsAttributeHint, SourceText.From(requireCorsSource, Encoding.UTF8)); // RequireRateLimiting var requireRateLimitingSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the annotated endpoint requires the provided rate limiting policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The rate limiting policy to apply. - public {{RequireRateLimitingAttributeName}}(string policyName) - { - PolicyName = policyName; - } - - /// - /// Gets the rate limiting policy name. - /// - public string PolicyName { get; } - } - """; + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires the provided rate limiting policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The rate limiting policy to apply. + public {{RequireRateLimitingAttributeName}}(string policyName) + { + PolicyName = policyName; + } + + /// + /// Gets the rate limiting policy name. + /// + public string PolicyName { get; } + } + """; context.AddSource(RequireRateLimitingAttributeHint, SourceText.From(requireRateLimitingSource, Encoding.UTF8)); // RequireHost var requireHostSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; + {{FileHeader}} - /// - /// Specifies the allowed hosts for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireHostAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The hosts that are allowed to access the endpoint. - public {{RequireHostAttributeName}}(params string[] hosts) - { - Hosts = hosts ?? []; - } + namespace {{AttributesNamespace}}; - /// - /// Gets the allowed hosts. - /// - public string[] Hosts { get; } - } - """; + /// + /// Specifies the allowed hosts for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireHostAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The hosts that are allowed to access the endpoint. + public {{RequireHostAttributeName}}(params string[] hosts) + { + Hosts = hosts ?? []; + } + + /// + /// Gets the allowed hosts. + /// + public string[] Hosts { get; } + } + """; context.AddSource(RequireHostAttributeHint, SourceText.From(requireHostSource, Encoding.UTF8)); // DisableAntiforgery @@ -360,232 +360,232 @@ internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute // DisableRequestTimeout var disableRequestTimeoutSource = $$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Disables the request timeout for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute - { - } + /// + /// Disables the request timeout for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute + { + } - """; + """; context.AddSource(DisableRequestTimeoutAttributeHint, SourceText.From(disableRequestTimeoutSource, Encoding.UTF8)); // RequestTimeout var requestTimeoutSource = $$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Applies the request timeout metadata to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional request timeout policy name. - /// - public string? PolicyName { get; init; } - - /// - /// Applies the default request timeout behavior. - /// - public {{RequestTimeoutAttributeName}}() - { - } - - /// - /// Applies the specified request timeout policy. - /// - /// The request timeout policy name. - public {{RequestTimeoutAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } + /// + /// Applies the request timeout metadata to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional request timeout policy name. + /// + public string? PolicyName { get; init; } + + /// + /// Applies the default request timeout behavior. + /// + public {{RequestTimeoutAttributeName}}() + { + } + + /// + /// Applies the specified request timeout policy. + /// + /// The request timeout policy name. + public {{RequestTimeoutAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } - """; + """; context.AddSource(RequestTimeoutAttributeHint, SourceText.From(requestTimeoutSource, Encoding.UTF8)); // Order var orderSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the order for the annotated endpoint when building conventions. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{OrderAttributeName}} : global::System.Attribute - { - /// - /// Gets the order that will be applied to the endpoint. - /// - public int Order { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The order value to apply to the endpoint. - public {{OrderAttributeName}}(int order) - { - Order = order; - } - } - - """; + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the order for the annotated endpoint when building conventions. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{OrderAttributeName}} : global::System.Attribute + { + /// + /// Gets the order that will be applied to the endpoint. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The order value to apply to the endpoint. + public {{OrderAttributeName}}(int order) + { + Order = order; + } + } + + """; context.AddSource(OrderAttributeHint, SourceText.From(orderSource, Encoding.UTF8)); // GroupName var groupNameSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the endpoint group name for the annotated class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal sealed class {{GroupNameAttributeName}} : global::System.Attribute + { + /// + /// Gets the endpoint group name. + /// + public string GroupName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The endpoint group name to apply. + public {{GroupNameAttributeName}}(string groupName) + { + GroupName = groupName; + } + } + + """; + context.AddSource(GroupNameAttributeHint, SourceText.From(groupNameSource, Encoding.UTF8)); + + // Summary + var summarySource = $$""" {{FileHeader}} namespace {{AttributesNamespace}}; /// - /// Specifies the endpoint group name for the annotated class. + /// Specifies the summary metadata for the annotated endpoint. /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - internal sealed class {{GroupNameAttributeName}} : global::System.Attribute + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{SummaryAttributeName}} : global::System.Attribute { /// - /// Gets the endpoint group name. + /// Gets the summary value for the endpoint. /// - public string GroupName { get; } + public string Summary { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The endpoint group name to apply. - public {{GroupNameAttributeName}}(string groupName) + /// The summary to apply to the endpoint. + public {{SummaryAttributeName}}(string summary) { - GroupName = groupName; + Summary = summary; } } """; - context.AddSource(GroupNameAttributeHint, SourceText.From(groupNameSource, Encoding.UTF8)); - - // Summary - var summarySource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the summary metadata for the annotated endpoint. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{SummaryAttributeName}} : global::System.Attribute - { - /// - /// Gets the summary value for the endpoint. - /// - public string Summary { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The summary to apply to the endpoint. - public {{SummaryAttributeName}}(string summary) - { - Summary = summary; - } - } - - """; context.AddSource(SummaryAttributeHint, SourceText.From(summarySource, Encoding.UTF8)); // Accepts var acceptsSource = $$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Specifies the request type and content types accepted by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type RequestType { get; init; } = default!; + /// + /// Specifies the request type and content types accepted by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type? RequestType { get; init; } - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } - /// - /// Initializes a new instance of the class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } + /// + /// Initializes a new instance of the class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } - /// - /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. - /// - /// The CLR type of the request body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type RequestType => typeof(TRequest); + /// + /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. + /// + /// The CLR type of the request body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type RequestType => typeof(TRequest); - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } - /// - /// Initializes a new instance of the generic Accepts attribute class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } + /// + /// Initializes a new instance of the generic Accepts attribute class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } - """; + """; context.AddSource(AcceptsAttributeHint, SourceText.From(acceptsSource, Encoding.UTF8)); // EndpointFilter @@ -633,136 +633,136 @@ internal sealed class {{EndpointFilterAttributeName}} : global::System. // Produces var producesSource = $$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type? ResponseType { get; init; } = null; + /// + /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type? ResponseType { get; init; } - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } - /// - /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. - /// - /// The CLR type of the response body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type ResponseType => typeof(TResponse); + /// + /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. + /// + /// The CLR type of the response body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType => typeof(TResponse); - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } - /// - /// Initializes a new instance of the generic Produces attribute class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = 200, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } + /// + /// Initializes a new instance of the generic Produces attribute class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } - """; + """; context.AddSource(ProducesResponseAttributeHint, SourceText.From(producesSource, Encoding.UTF8)); // ProducesProblem var producesProblemSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the endpoint produces a problem details payload. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute - { - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesProblemAttributeName}}(int statusCode = 500, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """; + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """; context.AddSource(ProducesProblemAttributeHint, SourceText.From(producesProblemSource, Encoding.UTF8)); // ProducesValidationProblem @@ -798,7 +798,7 @@ internal sealed class {{ProducesValidationProblemAttributeName}} : global::Syste /// The HTTP status code returned by the endpoint. /// The primary content type produced by the endpoint. /// Additional content types produced by the endpoint. - public {{ProducesValidationProblemAttributeName}}(int statusCode = 400, string? contentType = null, params string[] additionalContentTypes) + public {{ProducesValidationProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) { StatusCode = statusCode; ContentType = contentType; @@ -815,7 +815,8 @@ private static string GenerateHttpAttributeSource( string attributesNamespace, string attributeName, string summaryVerb, - bool allowEmptyPattern) + bool allowEmptyPattern + ) { var patternDefaultValue = allowEmptyPattern ? " = \"\"" : string.Empty; return $$""" @@ -837,7 +838,7 @@ internal sealed class {{attributeName}} : global::System.Attribute /// /// Gets or sets the endpoint name. /// - public string Name { get; set; } = ""; + public string? Name { get; init; } /// /// Initializes a new instance of the class. @@ -878,31 +879,20 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (displayName, description) = GetDisplayAndDescriptionAttributes(requestHandlerMethodSymbol); - var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, - accepts, produces, producesProblem, producesValidationProblem, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, - rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, - requestTimeoutPolicyName, order, endpointGroupName, summary) - = GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); + var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, + producesValidationProblem, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, + shortCircuit, disableRequestTimeout, withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName, summary) = + GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); - var metadata = new RequestHandlerMetadata( - name, - displayName, - summary, - description, - tags, - accepts, - produces, - producesProblem, - producesValidationProblem, + var metadata = new RequestHandlerMetadata(name, displayName, summary, description, tags, accepts, produces, producesProblem, producesValidationProblem, excludeFromDescription ); var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, - authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, - rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, - requestTimeoutPolicyName, order, endpointGroupName + authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, + endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName ); return requestHandler; @@ -916,22 +906,13 @@ private static string RemoveAsyncSuffix(string methodName) return methodName; } - private static ( - string HttpMethod, - string Pattern, - string? Name - ) GetRequestHandlerAttribute( - AttributeData attribute, - CancellationToken cancellationToken - ) + private static ( string HttpMethod, string Pattern, string? Name ) GetRequestHandlerAttribute(AttributeData attribute, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var attributeName = attribute.AttributeClass?.Name ?? ""; - var httpMethod = HttpAttributeDefinitionsByName.TryGetValue(attributeName, out var definition) - ? definition.Verb - : ""; + var httpMethod = HttpAttributeDefinitionsByName.TryGetValue(attributeName, out var definition) ? definition.Verb : ""; var pattern = (attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : "") ?? ""; @@ -963,51 +944,31 @@ private static (string? DisplayName, string? Description) GetDisplayAndDescripti if (attributeClass is null) continue; - if (IsAttribute(attributeClass, nameof(System.ComponentModel.DisplayNameAttribute), ComponentModelNamespaceParts)) + if (IsAttribute(attributeClass, nameof(DisplayNameAttribute), ComponentModelNamespaceParts)) { - displayName = NormalizeOptionalString(attribute.ConstructorArguments.Length > 0 - ? attribute.ConstructorArguments[0].Value as string - : null); + displayName = NormalizeOptionalString(attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null); continue; } - if (IsAttribute(attributeClass, nameof(System.ComponentModel.DescriptionAttribute), ComponentModelNamespaceParts)) - { - description = NormalizeOptionalString(attribute.ConstructorArguments.Length > 0 - ? attribute.ConstructorArguments[0].Value as string - : null); - } + if (IsAttribute(attributeClass, nameof(DescriptionAttribute), ComponentModelNamespaceParts)) + description = NormalizeOptionalString(attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null); } return (displayName, description); } - private static ( - EquatableImmutableArray? tags, - bool requireAuthorization, - EquatableImmutableArray? authorizationPolicies, - bool disableAntiforgery, - bool allowAnonymous, - bool excludeFromDescription, - EquatableImmutableArray? accepts, - EquatableImmutableArray? produces, - EquatableImmutableArray? producesProblem, - EquatableImmutableArray? producesValidationProblem, - bool requireCors, - string? corsPolicyName, - EquatableImmutableArray? requiredHosts, - bool requireRateLimiting, - string? rateLimitingPolicyName, - EquatableImmutableArray? endpointFilterTypes, - bool shortCircuit, - bool disableRequestTimeout, - bool withRequestTimeout, - string? requestTimeoutPolicyName, - int? order, - string? endpointGroupName, - string? summary - ) GetAdditionalRequestHandlerAttributes(INamedTypeSymbol classSymbol, IMethodSymbol methodSymbol, CancellationToken cancellationToken) + private static ( EquatableImmutableArray? tags, bool requireAuthorization, EquatableImmutableArray? authorizationPolicies, bool + disableAntiforgery, bool allowAnonymous, bool excludeFromDescription, EquatableImmutableArray? accepts, + EquatableImmutableArray? produces, EquatableImmutableArray? producesProblem, + EquatableImmutableArray? producesValidationProblem, bool requireCors, string? corsPolicyName, + EquatableImmutableArray? requiredHosts, bool requireRateLimiting, string? rateLimitingPolicyName, EquatableImmutableArray? + endpointFilterTypes, bool shortCircuit, bool disableRequestTimeout, bool withRequestTimeout, string? requestTimeoutPolicyName, int? order, string? + endpointGroupName, string? summary ) GetAdditionalRequestHandlerAttributes( + INamedTypeSymbol classSymbol, + IMethodSymbol methodSymbol, + CancellationToken cancellationToken + ) { cancellationToken.ThrowIfCancellationRequested(); @@ -1039,95 +1000,31 @@ private static ( var classAttributes = classSymbol.GetAttributes(); var classHasAllowAnonymousAttribute = false; var classHasRequireAuthorizationAttribute = false; - GetAdditionalRequestHandlerAttributeValues( - classAttributes, - ref tags, - ref requireAuthorization, - ref authorizationPolicies, - ref disableAntiforgery, - ref allowAnonymous, - ref excludeFromDescription, - ref accepts, - ref produces, - ref producesProblem, - ref producesValidationProblem, - ref requireCors, - ref corsPolicyName, - ref requiredHosts, - ref requireRateLimiting, - ref rateLimitingPolicyName, - ref endpointFilters, - ref classHasAllowAnonymousAttribute, - ref classHasRequireAuthorizationAttribute, - ref shortCircuit, - ref disableRequestTimeout, - ref withRequestTimeout, - ref requestTimeoutPolicyName, - ref order, - ref endpointGroupName, - ref summary + GetAdditionalRequestHandlerAttributeValues(classAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, + ref allowAnonymous, ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, ref producesValidationProblem, ref requireCors, + ref corsPolicyName, ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, + ref classHasAllowAnonymousAttribute, ref classHasRequireAuthorizationAttribute, ref shortCircuit, ref disableRequestTimeout, ref withRequestTimeout, + ref requestTimeoutPolicyName, ref order, ref endpointGroupName, ref summary ); var methodAttributes = methodSymbol.GetAttributes(); var methodHasAllowAnonymousAttribute = false; var methodHasRequireAuthorizationAttribute = false; - GetAdditionalRequestHandlerAttributeValues( - methodAttributes, - ref tags, - ref requireAuthorization, - ref authorizationPolicies, - ref disableAntiforgery, - ref allowAnonymous, - ref excludeFromDescription, - ref accepts, - ref produces, - ref producesProblem, - ref producesValidationProblem, - ref requireCors, - ref corsPolicyName, - ref requiredHosts, - ref requireRateLimiting, - ref rateLimitingPolicyName, - ref endpointFilters, - ref methodHasAllowAnonymousAttribute, - ref methodHasRequireAuthorizationAttribute, - ref shortCircuit, - ref disableRequestTimeout, - ref withRequestTimeout, - ref requestTimeoutPolicyName, - ref order, - ref endpointGroupName, - ref summary + GetAdditionalRequestHandlerAttributeValues(methodAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, + ref allowAnonymous, ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, ref producesValidationProblem, ref requireCors, + ref corsPolicyName, ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, + ref methodHasAllowAnonymousAttribute, ref methodHasRequireAuthorizationAttribute, ref shortCircuit, ref disableRequestTimeout, + ref withRequestTimeout, ref requestTimeoutPolicyName, ref order, ref endpointGroupName, ref summary ); if (methodHasRequireAuthorizationAttribute && !methodHasAllowAnonymousAttribute) allowAnonymous = false; - return ( - tags, - requireAuthorization ?? false, - authorizationPolicies, - disableAntiforgery ?? false, - allowAnonymous ?? false, - excludeFromDescription ?? false, - ToEquatableOrNull(accepts), - ToEquatableOrNull(produces), - ToEquatableOrNull(producesProblem), - ToEquatableOrNull(producesValidationProblem), - requireCors ?? false, - corsPolicyName, - requiredHosts, - requireRateLimiting ?? false, - rateLimitingPolicyName, - ToEquatableOrNull(endpointFilters), - shortCircuit ?? false, - disableRequestTimeout ?? false, - withRequestTimeout ?? false, - (withRequestTimeout ?? false) ? requestTimeoutPolicyName : null, - order, - endpointGroupName, - summary - ); + return (tags, requireAuthorization ?? false, authorizationPolicies, disableAntiforgery ?? false, allowAnonymous ?? false, + excludeFromDescription ?? false, ToEquatableOrNull(accepts), ToEquatableOrNull(produces), ToEquatableOrNull(producesProblem), + ToEquatableOrNull(producesValidationProblem), requireCors ?? false, corsPolicyName, requiredHosts, requireRateLimiting ?? false, + rateLimitingPolicyName, ToEquatableOrNull(endpointFilters), shortCircuit ?? false, disableRequestTimeout ?? false, withRequestTimeout ?? false, + withRequestTimeout ?? false ? requestTimeoutPolicyName : null, order, endpointGroupName, summary); } private static void GetAdditionalRequestHandlerAttributeValues( @@ -1281,9 +1178,7 @@ ref string? summary if (IsGeneratedAttribute(attributeClass, RequireCorsAttributeName)) { requireCors = true; - corsPolicyName = attribute.ConstructorArguments.Length > 0 - ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) - : null; + corsPolicyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; continue; } @@ -1312,9 +1207,7 @@ ref string? summary if (IsGeneratedAttribute(attributeClass, RequireRateLimitingAttributeName)) { - var policyName = attribute.ConstructorArguments.Length > 0 - ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) - : null; + var policyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; if (!string.IsNullOrEmpty(policyName)) { @@ -1358,9 +1251,7 @@ ref string? summary var contentType = attribute.ConstructorArguments.Length > 1 ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) : null; - var additionalContentTypes = attribute.ConstructorArguments.Length > 2 - ? GetStringArrayValues(attribute.ConstructorArguments[2]) - : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; var producesProblemList = producesProblem ??= []; producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes)); @@ -1375,9 +1266,7 @@ ref string? summary var contentType = attribute.ConstructorArguments.Length > 1 ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) : null; - var additionalContentTypes = attribute.ConstructorArguments.Length > 2 - ? GetStringArrayValues(attribute.ConstructorArguments[2]) - : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; var producesValidationProblemList = producesValidationProblem ??= []; producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes)); @@ -1451,10 +1340,7 @@ private static bool IsInNamespace(INamespaceSymbol? namespaceSymbol, string[] na return namespaceSymbol is null || namespaceSymbol.IsGlobalNamespace; } - private static void TryAddAcceptsMetadata( - AttributeData attribute, - INamedTypeSymbol attributeClass, - ref List? accepts) + private static void TryAddAcceptsMetadata(AttributeData attribute, INamedTypeSymbol attributeClass, ref List? accepts) { string? requestType; string contentType; @@ -1463,13 +1349,12 @@ private static void TryAddAcceptsMetadata( if (attributeClass is { IsGenericType: true, TypeArguments.Length: 1 }) { - requestType = attributeClass.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + requestType = attributeClass.TypeArguments[0] + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); contentType = attribute.ConstructorArguments.Length > 0 ? NormalizeRequiredContentType(attribute.ConstructorArguments[0].Value as string, "application/json") : "application/json"; - additionalContentTypes = attribute.ConstructorArguments.Length > 1 - ? GetStringArrayValues(attribute.ConstructorArguments[1]) - : null; + additionalContentTypes = attribute.ConstructorArguments.Length > 1 ? GetStringArrayValues(attribute.ConstructorArguments[1]) : null; } else if (GetNamedTypeSymbol(attribute, RequestTypeAttributeNamedParameter) is { } requestTypeSymbol) { @@ -1477,9 +1362,7 @@ private static void TryAddAcceptsMetadata( contentType = attribute.ConstructorArguments.Length > 0 ? NormalizeRequiredContentType(attribute.ConstructorArguments[0].Value as string, "application/json") : "application/json"; - additionalContentTypes = attribute.ConstructorArguments.Length > 1 - ? GetStringArrayValues(attribute.ConstructorArguments[1]) - : null; + additionalContentTypes = attribute.ConstructorArguments.Length > 1 ? GetStringArrayValues(attribute.ConstructorArguments[1]) : null; } else { @@ -1490,10 +1373,7 @@ private static void TryAddAcceptsMetadata( acceptsList.Add(new AcceptsMetadata(requestType, contentType, additionalContentTypes, isOptional)); } - private static void TryAddProducesMetadata( - AttributeData attribute, - INamedTypeSymbol attributeClass, - ref List? produces) + private static void TryAddProducesMetadata(AttributeData attribute, INamedTypeSymbol attributeClass, ref List? produces) { string? responseType; int statusCode; @@ -1502,16 +1382,13 @@ private static void TryAddProducesMetadata( if (attributeClass is { IsGenericType: true, TypeArguments.Length: 1 }) { - responseType = attributeClass.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + responseType = attributeClass.TypeArguments[0] + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesStatusCode ? producesStatusCode : 200; - contentType = attribute.ConstructorArguments.Length > 1 - ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) - : null; - additionalContentTypes = attribute.ConstructorArguments.Length > 2 - ? GetStringArrayValues(attribute.ConstructorArguments[2]) - : null; + contentType = attribute.ConstructorArguments.Length > 1 ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) : null; + additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; } else if (GetNamedTypeSymbol(attribute, ResponseTypeAttributeNamedParameter) is { } responseTypeSymbol) { @@ -1519,12 +1396,8 @@ private static void TryAddProducesMetadata( statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesStatusCode ? producesStatusCode : 200; - contentType = attribute.ConstructorArguments.Length > 1 - ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) - : null; - additionalContentTypes = attribute.ConstructorArguments.Length > 2 - ? GetStringArrayValues(attribute.ConstructorArguments[2]) - : null; + contentType = attribute.ConstructorArguments.Length > 1 ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) : null; + additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; } else { @@ -1535,10 +1408,7 @@ private static void TryAddProducesMetadata( producesList.Add(new ProducesMetadata(responseType, statusCode, contentType, additionalContentTypes)); } - private static void TryAddEndpointFilter( - AttributeData attribute, - INamedTypeSymbol attributeClass, - ref List? endpointFilters) + private static void TryAddEndpointFilter(AttributeData attribute, INamedTypeSymbol attributeClass, ref List? endpointFilters) { if (attributeClass is { IsGenericType: true, TypeArguments.Length: 1 }) { @@ -1649,17 +1519,9 @@ CancellationToken cancellationToken var isStatic = classSymbol.IsStatic; var endpointConventionBuilderSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); var serviceProviderSymbol = compilation.GetTypeByMetadataName("System.IServiceProvider"); - var configureMethodDetails = GetConfigureMethodDetails( - classSymbol, - endpointConventionBuilderSymbol, - serviceProviderSymbol, - cancellationToken - ); + var configureMethodDetails = GetConfigureMethodDetails(classSymbol, endpointConventionBuilderSymbol, serviceProviderSymbol, cancellationToken); - var requestHandlerClass = new RequestHandlerClass( - name, - isStatic, - configureMethodDetails.HasConfigureMethod, + var requestHandlerClass = new RequestHandlerClass(name, isStatic, configureMethodDetails.HasConfigureMethod, configureMethodDetails.ConfigureMethodAcceptsServiceProvider ); @@ -1873,8 +1735,7 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM { foreach (var namedArg in attribute.NamedArguments) { - if (string.Equals(namedArg.Key, NameAttributeNamedParameter, StringComparison.Ordinal) - && namedArg.Value.Value is string namedValue) + if (string.Equals(namedArg.Key, NameAttributeNamedParameter, StringComparison.Ordinal) && namedArg.Value.Value is string namedValue) { var normalized = NormalizeBindingName(namedValue); if (normalized is not null) @@ -1921,8 +1782,14 @@ private static ImmutableArray EnsureUniqueEndpointNames(Immutabl foreach (var index in collidingHandlers) { var handler = builder[index]; - var metadata = handler.Metadata with { Name = GetFullyQualifiedMethodDisplayName(handler) }; - builder[index] = handler with { Metadata = metadata }; + var metadata = handler.Metadata with + { + Name = GetFullyQualifiedMethodDisplayName(handler), + }; + builder[index] = handler with + { + Metadata = metadata, + }; } return builder.MoveToImmutable(); @@ -1932,8 +1799,7 @@ private static ImmutableHashSet GetRequestHandlersWithNameCollisions(Immuta { var collidingIndices = ImmutableHashSet.CreateBuilder(); - var groups = requestHandlers - .Select((handler, index) => (handler, index)) + var groups = requestHandlers.Select((handler, index) => (handler, index)) .Where(static tuple => !string.IsNullOrEmpty(tuple.handler.Metadata.Name)) .GroupBy(static tuple => tuple.handler.Metadata.Name!, StringComparer.Ordinal); @@ -1942,9 +1808,10 @@ private static ImmutableHashSet GetRequestHandlersWithNameCollisions(Immuta if (group.Count() <= 1) continue; - var collidingMethodGroups = group - .GroupBy(static tuple => tuple.handler.Method.Name, StringComparer.Ordinal) - .Where(static methodGroup => methodGroup.Skip(1).Any()); + var collidingMethodGroups = group.GroupBy(static tuple => tuple.handler.Method.Name, StringComparer.Ordinal) + .Where(static methodGroup => methodGroup.Skip(1) + .Any() + ); foreach (var methodGroup in collidingMethodGroups) { @@ -2245,7 +2112,6 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } if (requestHandler.Metadata.Accepts is { Count: > 0 }) - { foreach (var accepts in requestHandler.Metadata.Accepts.Value) { source.AppendLine(); @@ -2255,17 +2121,13 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append('>'); source.Append('('); if (accepts.IsOptional) - { source.Append("isOptional: true, "); - } source.Append(StringLiteral(accepts.ContentType)); AppendAdditionalContentTypes(source, accepts.AdditionalContentTypes); source.Append(')'); } - } if (requestHandler.Metadata.Produces is { Count: > 0 }) - { foreach (var produces in requestHandler.Metadata.Produces.Value) { source.AppendLine(); @@ -2278,10 +2140,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl AppendOptionalContentTypes(source, produces.ContentType, produces.AdditionalContentTypes); source.Append(')'); } - } if (requestHandler.Metadata.ProducesProblem is { Count: > 0 }) - { foreach (var producesProblem in requestHandler.Metadata.ProducesProblem.Value) { source.AppendLine(); @@ -2291,10 +2151,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl AppendOptionalContentTypes(source, producesProblem.ContentType, producesProblem.AdditionalContentTypes); source.Append(')'); } - } if (requestHandler.Metadata.ProducesValidationProblem is { Count: > 0 }) - { foreach (var producesValidationProblem in requestHandler.Metadata.ProducesValidationProblem.Value) { source.AppendLine(); @@ -2304,7 +2162,6 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl AppendOptionalContentTypes(source, producesValidationProblem.ContentType, producesValidationProblem.AdditionalContentTypes); source.Append(')'); } - } if (requestHandler.RequireAuthorization) { @@ -2401,7 +2258,6 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } if (requestHandler.EndpointFilterTypes is { Count: > 0 }) - { foreach (var filterType in requestHandler.EndpointFilterTypes.Value) { source.AppendLine(); @@ -2410,7 +2266,6 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(filterType); source.Append(">()"); } - } if (wrapWithConfigure && configureAcceptsServiceProvider) { @@ -2474,7 +2329,7 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< const int baseSize = 4096; const int perHandler = 512; - var estimate = baseSize + (requestHandlers.Length * perHandler); + var estimate = baseSize + requestHandlers.Length * perHandler; if (estimate > 65536) estimate = 65536; @@ -2620,12 +2475,7 @@ _ when char.IsControl(c) => "\\u" + ((int)c).ToString("x4", CultureInfo.Invarian }; } - private readonly record struct HttpAttributeDefinition( - string Name, - string FullyQualifiedName, - string Hint, - string Verb, - bool AllowEmptyPattern); + private readonly record struct HttpAttributeDefinition(string Name, string FullyQualifiedName, string Hint, string Verb, bool AllowEmptyPattern); private readonly record struct RequestHandler( RequestHandlerClass Class, @@ -2651,12 +2501,7 @@ private readonly record struct RequestHandler( string? EndpointGroupName ); - private readonly record struct RequestHandlerClass( - string Name, - bool IsStatic, - bool HasConfigureMethod, - bool ConfigureMethodAcceptsServiceProvider - ); + private readonly record struct RequestHandlerClass(string Name, bool IsStatic, bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); private readonly record struct RequestHandlerMethod(string Name, bool IsStatic, bool IsAwaitable, EquatableImmutableArray Parameters); @@ -2677,7 +2522,8 @@ private readonly record struct AcceptsMetadata( string RequestType, string ContentType, EquatableImmutableArray? AdditionalContentTypes, - bool IsOptional); + bool IsOptional + ); private readonly record struct ProducesMetadata( string ResponseType, @@ -2688,14 +2534,15 @@ private readonly record struct ProducesMetadata( private readonly record struct ProducesProblemMetadata(int StatusCode, string? ContentType, EquatableImmutableArray? AdditionalContentTypes); - private readonly record struct ProducesValidationProblemMetadata(int StatusCode, string? ContentType, EquatableImmutableArray? AdditionalContentTypes); + private readonly record struct ProducesValidationProblemMetadata( + int StatusCode, + string? ContentType, + EquatableImmutableArray? AdditionalContentTypes + ); private readonly record struct Parameter(string Name, string Type, BindingSource Source, string? Key, string? BindingName); - private readonly record struct ConfigureMethodDetails( - bool HasConfigureMethod, - bool ConfigureMethodAcceptsServiceProvider - ); + private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); private enum BindingSource { From 99e706ca12055c8463e92a57566cb0882fd72de1 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:41:49 -0500 Subject: [PATCH 53/75] Document MapGroup attribute (#43) --- README.md | 1 + src/GeneratedEndpoints/MinimalApiGenerator.cs | 450 ++++++++++++------ .../Common/SourceFactory.cs | 17 +- ...sMapGroup_AddEndpointHandlers.verified.txt | 24 + ...sMapGroup_MapEndpointHandlers.verified.txt | 46 ++ .../IndividualTests.cs | 28 +- 6 files changed, 425 insertions(+), 141 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_MapEndpointHandlers.verified.txt diff --git a/README.md b/README.md index 3c8f8e6..22c14f7 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ applied to all endpoints within the class. * `[MapConnect(string pattern = "", Name = null)]` * `[MapDelete(string pattern = "", Name = null)]` * `[MapFallback(string pattern = "", Name = null)]` +* `[MapGroup(string pattern)]` * `[MapGet(string pattern = "", Name = null)]` * `[MapHead(string pattern = "", Name = null)]` * `[MapOptions(string pattern = "", Name = null)]` diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index a5b988f..b0a364d 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -64,6 +64,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string GroupNameAttributeFullyQualifiedName = $"{AttributesNamespace}.{GroupNameAttributeName}"; private const string GroupNameAttributeHint = $"{GroupNameAttributeFullyQualifiedName}.gs.cs"; + private const string MapGroupAttributeName = "MapGroupAttribute"; + private const string MapGroupAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapGroupAttributeName}"; + private const string MapGroupAttributeHint = $"{MapGroupAttributeFullyQualifiedName}.gs.cs"; + private const string SummaryAttributeName = "SummaryAttribute"; private const string SummaryAttributeFullyQualifiedName = $"{AttributesNamespace}.{SummaryAttributeName}"; private const string SummaryAttributeHint = $"{SummaryAttributeFullyQualifiedName}.gs.cs"; @@ -472,6 +476,36 @@ internal sealed class {{GroupNameAttributeName}} : global::System.Attribute """; context.AddSource(GroupNameAttributeHint, SourceText.From(groupNameSource, Encoding.UTF8)); + // MapGroup + var mapGroupSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the route group for the annotated class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal sealed class {{MapGroupAttributeName}} : global::System.Attribute + { + /// + /// Gets the route group pattern. + /// + public string Pattern { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The route group pattern to apply. + public {{MapGroupAttributeName}}(string pattern) + { + Pattern = pattern; + } + } + + """; + context.AddSource(MapGroupAttributeHint, SourceText.From(mapGroupSource, Encoding.UTF8)); + // Summary var summarySource = $$""" {{FileHeader}} @@ -871,7 +905,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke if (requestHandlerClassResult is null) return null; - var (requestHandlerClassSymbol, requestHandlerClass) = requestHandlerClassResult.Value; + var (_, requestHandlerClass) = requestHandlerClassResult.Value; var requestHandlerMethod = GetRequestHandlerMethod(requestHandlerMethodSymbol, cancellationToken); @@ -879,21 +913,11 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var (displayName, description) = GetDisplayAndDescriptionAttributes(requestHandlerMethodSymbol); - var (tags, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, excludeFromDescription, accepts, produces, producesProblem, - producesValidationProblem, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, - shortCircuit, disableRequestTimeout, withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName, summary) = - GetAdditionalRequestHandlerAttributes(requestHandlerClassSymbol, requestHandlerMethodSymbol, cancellationToken); - name ??= RemoveAsyncSuffix(requestHandlerMethod.Name); - var metadata = new RequestHandlerMetadata(name, displayName, summary, description, tags, accepts, produces, producesProblem, producesValidationProblem, - excludeFromDescription - ); + var methodConfiguration = GetEndpointConfiguration(requestHandlerMethodSymbol.GetAttributes(), name, displayName, description, true); - var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, metadata, requireAuthorization, - authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, - endpointFilterTypes, shortCircuit, disableRequestTimeout, withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName - ); + var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, methodConfiguration); return requestHandler; } @@ -958,20 +982,14 @@ private static (string? DisplayName, string? Description) GetDisplayAndDescripti return (displayName, description); } - private static ( EquatableImmutableArray? tags, bool requireAuthorization, EquatableImmutableArray? authorizationPolicies, bool - disableAntiforgery, bool allowAnonymous, bool excludeFromDescription, EquatableImmutableArray? accepts, - EquatableImmutableArray? produces, EquatableImmutableArray? producesProblem, - EquatableImmutableArray? producesValidationProblem, bool requireCors, string? corsPolicyName, - EquatableImmutableArray? requiredHosts, bool requireRateLimiting, string? rateLimitingPolicyName, EquatableImmutableArray? - endpointFilterTypes, bool shortCircuit, bool disableRequestTimeout, bool withRequestTimeout, string? requestTimeoutPolicyName, int? order, string? - endpointGroupName, string? summary ) GetAdditionalRequestHandlerAttributes( - INamedTypeSymbol classSymbol, - IMethodSymbol methodSymbol, - CancellationToken cancellationToken - ) + private static EndpointConfiguration GetEndpointConfiguration( + ImmutableArray attributes, + string? name, + string? displayName, + string? description, + bool enforceMethodRequireAuthorizationRules + ) { - cancellationToken.ThrowIfCancellationRequested(); - EquatableImmutableArray? tags = null; bool? requireAuthorization = null; EquatableImmutableArray? authorizationPolicies = null; @@ -997,34 +1015,27 @@ CancellationToken cancellationToken List? producesProblem = null; List? producesValidationProblem = null; - var classAttributes = classSymbol.GetAttributes(); - var classHasAllowAnonymousAttribute = false; - var classHasRequireAuthorizationAttribute = false; - GetAdditionalRequestHandlerAttributeValues(classAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, - ref allowAnonymous, ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, ref producesValidationProblem, ref requireCors, - ref corsPolicyName, ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, - ref classHasAllowAnonymousAttribute, ref classHasRequireAuthorizationAttribute, ref shortCircuit, ref disableRequestTimeout, ref withRequestTimeout, - ref requestTimeoutPolicyName, ref order, ref endpointGroupName, ref summary - ); + var hasAllowAnonymousAttribute = false; + var hasRequireAuthorizationAttribute = false; - var methodAttributes = methodSymbol.GetAttributes(); - var methodHasAllowAnonymousAttribute = false; - var methodHasRequireAuthorizationAttribute = false; - GetAdditionalRequestHandlerAttributeValues(methodAttributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, + GetAdditionalRequestHandlerAttributeValues(attributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous, ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, ref producesValidationProblem, ref requireCors, ref corsPolicyName, ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, - ref methodHasAllowAnonymousAttribute, ref methodHasRequireAuthorizationAttribute, ref shortCircuit, ref disableRequestTimeout, - ref withRequestTimeout, ref requestTimeoutPolicyName, ref order, ref endpointGroupName, ref summary + ref hasAllowAnonymousAttribute, ref hasRequireAuthorizationAttribute, ref shortCircuit, ref disableRequestTimeout, ref withRequestTimeout, + ref requestTimeoutPolicyName, ref order, ref endpointGroupName, ref summary ); - if (methodHasRequireAuthorizationAttribute && !methodHasAllowAnonymousAttribute) + if (enforceMethodRequireAuthorizationRules && hasRequireAuthorizationAttribute && !hasAllowAnonymousAttribute) allowAnonymous = false; - return (tags, requireAuthorization ?? false, authorizationPolicies, disableAntiforgery ?? false, allowAnonymous ?? false, - excludeFromDescription ?? false, ToEquatableOrNull(accepts), ToEquatableOrNull(produces), ToEquatableOrNull(producesProblem), - ToEquatableOrNull(producesValidationProblem), requireCors ?? false, corsPolicyName, requiredHosts, requireRateLimiting ?? false, - rateLimitingPolicyName, ToEquatableOrNull(endpointFilters), shortCircuit ?? false, disableRequestTimeout ?? false, withRequestTimeout ?? false, - withRequestTimeout ?? false ? requestTimeoutPolicyName : null, order, endpointGroupName, summary); + var metadata = new RequestHandlerMetadata(name, displayName, summary, description, tags, ToEquatableOrNull(accepts), ToEquatableOrNull(produces), + ToEquatableOrNull(producesProblem), ToEquatableOrNull(producesValidationProblem), excludeFromDescription ?? false + ); + + return new EndpointConfiguration(metadata, requireAuthorization ?? false, authorizationPolicies, disableAntiforgery ?? false, + allowAnonymous ?? false, requireCors ?? false, corsPolicyName, requiredHosts, requireRateLimiting ?? false, rateLimitingPolicyName, + ToEquatableOrNull(endpointFilters), shortCircuit ?? false, disableRequestTimeout ?? false, withRequestTimeout ?? false, + withRequestTimeout ?? false ? requestTimeoutPolicyName : null, order, endpointGroupName); } private static void GetAdditionalRequestHandlerAttributeValues( @@ -1300,6 +1311,45 @@ private static string NormalizeRequiredContentType(string? contentType, string d return string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); } + private static string? GetMapGroupPattern(INamedTypeSymbol classSymbol) + { + foreach (var attribute in classSymbol.GetAttributes()) + { + var attributeClass = attribute.AttributeClass; + if (attributeClass is null) + continue; + + if (!IsGeneratedAttribute(attributeClass, MapGroupAttributeName)) + continue; + + if (attribute.ConstructorArguments.Length > 0) + { + var pattern = NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string); + if (!string.IsNullOrEmpty(pattern)) + return pattern; + } + } + + return null; + } + + private static string GetMapGroupIdentifier(string className) + { + if (className.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + className = className.Substring(GlobalPrefix.Length); + + var builder = new StringBuilder(className.Length + 8); + builder.Append('_'); + + foreach (var character in className) + { + builder.Append(char.IsLetterOrDigit(character) ? character : '_'); + } + + builder.Append("_Group"); + return builder.ToString(); + } + private static EquatableImmutableArray? GetStringArrayValues(TypedConstant typedConstant) { if (typedConstant.Kind != TypedConstantKind.Array || typedConstant.Values.IsDefaultOrEmpty) @@ -1521,8 +1571,12 @@ CancellationToken cancellationToken var serviceProviderSymbol = compilation.GetTypeByMetadataName("System.IServiceProvider"); var configureMethodDetails = GetConfigureMethodDetails(classSymbol, endpointConventionBuilderSymbol, serviceProviderSymbol, cancellationToken); + var mapGroupPattern = GetMapGroupPattern(classSymbol); + var mapGroupIdentifier = mapGroupPattern is null ? null : GetMapGroupIdentifier(name); + var classConfiguration = GetEndpointConfiguration(classSymbol.GetAttributes(), null, null, null, false); + var requestHandlerClass = new RequestHandlerClass(name, isStatic, configureMethodDetails.HasConfigureMethod, - configureMethodDetails.ConfigureMethodAcceptsServiceProvider + configureMethodDetails.ConfigureMethodAcceptsServiceProvider, mapGroupPattern, mapGroupIdentifier, classConfiguration ); return (classSymbol, requestHandlerClass); @@ -1782,14 +1836,13 @@ private static ImmutableArray EnsureUniqueEndpointNames(Immutabl foreach (var index in collidingHandlers) { var handler = builder[index]; - var metadata = handler.Metadata with + var configuration = handler.Configuration; + var metadata = configuration.Metadata with { Name = GetFullyQualifiedMethodDisplayName(handler), }; - builder[index] = handler with - { - Metadata = metadata, - }; + configuration = configuration with { Metadata = metadata }; + builder[index] = handler with { Configuration = configuration }; } return builder.MoveToImmutable(); @@ -1800,8 +1853,8 @@ private static ImmutableHashSet GetRequestHandlersWithNameCollisions(Immuta var collidingIndices = ImmutableHashSet.CreateBuilder(); var groups = requestHandlers.Select((handler, index) => (handler, index)) - .Where(static tuple => !string.IsNullOrEmpty(tuple.handler.Metadata.Name)) - .GroupBy(static tuple => tuple.handler.Metadata.Name!, StringComparer.Ordinal); + .Where(static tuple => !string.IsNullOrEmpty(tuple.handler.Configuration.Metadata.Name)) + .GroupBy(static tuple => tuple.handler.Configuration.Metadata.Name!, StringComparer.Ordinal); foreach (var group in groups) { @@ -1916,7 +1969,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine("using Microsoft.AspNetCore.Http;"); source.AppendLine("using Microsoft.AspNetCore.Mvc;"); source.AppendLine("using Microsoft.AspNetCore.Routing;"); - if (requestHandlers.Any(static handler => handler.RequireRateLimiting)) + if (requestHandlers.Any(static handler => handler.Configuration.RequireRateLimiting)) source.AppendLine("using Microsoft.AspNetCore.RateLimiting;"); source.AppendLine("using Microsoft.Extensions.DependencyInjection;"); source.AppendLine(); @@ -1939,6 +1992,25 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine(" {"); + var groupedClasses = requestHandlers.Select(static handler => handler.Class) + .Where(static handlerClass => !string.IsNullOrEmpty(handlerClass.MapGroupPattern)) + .Distinct() + .ToArray(); + + foreach (var groupedClass in groupedClasses) + { + source.Append(" var "); + source.Append(groupedClass.MapGroupBuilderIdentifier); + source.Append(" = builder.MapGroup("); + source.Append(StringLiteral(groupedClass.MapGroupPattern!)); + source.Append(')'); + AppendEndpointConfiguration(source, " ", groupedClass.Configuration, includeNameAndDisplayName: false); + source.AppendLine(";"); + } + + if (groupedClasses.Length > 0) + source.AppendLine(); + for (var index = 0; index < requestHandlers.Length; index++) { if (index > 0) @@ -1965,6 +2037,7 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl var configureAcceptsServiceProvider = requestHandler.Class.ConfigureMethodAcceptsServiceProvider; var indent = wrapWithConfigure ? " " : " "; var continuationIndent = indent + " "; + var routeBuilderIdentifier = requestHandler.Class.MapGroupBuilderIdentifier ?? "builder"; if (wrapWithConfigure) { @@ -1981,7 +2054,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(indent); if (isFallback) { - source.Append("builder.MapFallback("); + source.Append(routeBuilderIdentifier); + source.Append(".MapFallback("); if (!string.IsNullOrEmpty(requestHandler.Pattern)) { source.Append(StringLiteral(requestHandler.Pattern)); @@ -1990,7 +2064,8 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } else { - source.Append("builder.Map"); + source.Append(routeBuilderIdentifier); + source.Append(".Map"); source.Append(mapMethodSuffix ?? "Methods"); source.Append('('); source.Append(StringLiteral(requestHandler.Pattern)); @@ -2041,81 +2116,110 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } source.Append(')'); - if (!string.IsNullOrEmpty(requestHandler.Metadata.Name)) + var configuration = requestHandler.Configuration; + if (requestHandler.Class.MapGroupPattern is null) + configuration = MergeEndpointConfigurations(requestHandler.Class.Configuration, configuration); + + AppendEndpointConfiguration(source, continuationIndent, configuration, includeNameAndDisplayName: true); + + if (wrapWithConfigure && configureAcceptsServiceProvider) + { + source.AppendLine(","); + source.Append(indent); + source.Append("builder.ServiceProvider"); + } + + if (wrapWithConfigure) + { + source.AppendLine(); + source.Append(" );"); + source.AppendLine(); + } + else + { + source.AppendLine(";"); + } + } + + private static void AppendEndpointConfiguration(StringBuilder source, string indent, EndpointConfiguration configuration, bool includeNameAndDisplayName) + { + var metadata = configuration.Metadata; + + if (includeNameAndDisplayName && !string.IsNullOrEmpty(metadata.Name)) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".WithName("); - source.Append(StringLiteral(requestHandler.Metadata.Name)); + source.Append(StringLiteral(metadata.Name)); source.Append(')'); } - if (!string.IsNullOrEmpty(requestHandler.Metadata.DisplayName)) + if (includeNameAndDisplayName && !string.IsNullOrEmpty(metadata.DisplayName)) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".WithDisplayName("); - source.Append(StringLiteral(requestHandler.Metadata.DisplayName)); + source.Append(StringLiteral(metadata.DisplayName)); source.Append(')'); } - if (!string.IsNullOrEmpty(requestHandler.Metadata.Summary)) + if (!string.IsNullOrEmpty(metadata.Summary)) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".WithSummary("); - source.Append(StringLiteral(requestHandler.Metadata.Summary)); + source.Append(StringLiteral(metadata.Summary)); source.Append(')'); } - if (!string.IsNullOrEmpty(requestHandler.Metadata.Description)) + if (!string.IsNullOrEmpty(metadata.Description)) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".WithDescription("); - source.Append(StringLiteral(requestHandler.Metadata.Description)); + source.Append(StringLiteral(metadata.Description)); source.Append(')'); } - if (!string.IsNullOrEmpty(requestHandler.EndpointGroupName)) + if (!string.IsNullOrEmpty(configuration.EndpointGroupName)) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".WithGroupName("); - source.Append(StringLiteral(requestHandler.EndpointGroupName)); + source.Append(StringLiteral(configuration.EndpointGroupName)); source.Append(')'); } - if (requestHandler.Order is { } orderValue) + if (configuration.Order is { } orderValue) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".WithOrder("); source.Append(orderValue); source.Append(')'); } - if (requestHandler.Metadata.ExcludeFromDescription) + if (metadata.ExcludeFromDescription) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".ExcludeFromDescription()"); } - if (requestHandler.Metadata.Tags is { Count: > 0 }) + if (metadata.Tags is { Count: > 0 }) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".WithTags("); - source.Append(string.Join(", ", requestHandler.Metadata.Tags.Value.Select(StringLiteral))); + source.Append(string.Join(", ", metadata.Tags.Value.Select(StringLiteral))); source.Append(')'); } - if (requestHandler.Metadata.Accepts is { Count: > 0 }) - foreach (var accepts in requestHandler.Metadata.Accepts.Value) + if (metadata.Accepts is { Count: > 0 }) + foreach (var accepts in metadata.Accepts.Value) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".Accepts<"); source.Append(accepts.RequestType); source.Append('>'); @@ -2127,11 +2231,11 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(')'); } - if (requestHandler.Metadata.Produces is { Count: > 0 }) - foreach (var produces in requestHandler.Metadata.Produces.Value) + if (metadata.Produces is { Count: > 0 }) + foreach (var produces in metadata.Produces.Value) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".Produces<"); source.Append(produces.ResponseType); source.Append('>'); @@ -2141,53 +2245,53 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl source.Append(')'); } - if (requestHandler.Metadata.ProducesProblem is { Count: > 0 }) - foreach (var producesProblem in requestHandler.Metadata.ProducesProblem.Value) + if (metadata.ProducesProblem is { Count: > 0 }) + foreach (var producesProblem in metadata.ProducesProblem.Value) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".ProducesProblem("); source.Append(producesProblem.StatusCode); AppendOptionalContentTypes(source, producesProblem.ContentType, producesProblem.AdditionalContentTypes); source.Append(')'); } - if (requestHandler.Metadata.ProducesValidationProblem is { Count: > 0 }) - foreach (var producesValidationProblem in requestHandler.Metadata.ProducesValidationProblem.Value) + if (metadata.ProducesValidationProblem is { Count: > 0 }) + foreach (var producesValidationProblem in metadata.ProducesValidationProblem.Value) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".ProducesValidationProblem("); source.Append(producesValidationProblem.StatusCode); AppendOptionalContentTypes(source, producesValidationProblem.ContentType, producesValidationProblem.AdditionalContentTypes); source.Append(')'); } - if (requestHandler.RequireAuthorization) + if (configuration.RequireAuthorization) { source.AppendLine(); - if (requestHandler.AuthorizationPolicies is { Count: > 0 }) + if (configuration.AuthorizationPolicies is { Count: > 0 }) { - source.Append(continuationIndent); + source.Append(indent); source.Append(".RequireAuthorization("); - source.Append(string.Join(", ", requestHandler.AuthorizationPolicies.Value.Select(StringLiteral))); + source.Append(string.Join(", ", configuration.AuthorizationPolicies.Value.Select(StringLiteral))); source.Append(')'); } else { - source.Append(continuationIndent); + source.Append(indent); source.Append(".RequireAuthorization()"); } } - if (requestHandler.RequireCors) + if (configuration.RequireCors) { source.AppendLine(); - source.Append(continuationIndent); - if (!string.IsNullOrEmpty(requestHandler.CorsPolicyName)) + source.Append(indent); + if (!string.IsNullOrEmpty(configuration.CorsPolicyName)) { source.Append(".RequireCors("); - source.Append(StringLiteral(requestHandler.CorsPolicyName)); + source.Append(StringLiteral(configuration.CorsPolicyName)); source.Append(')'); } else @@ -2196,59 +2300,59 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } } - if (requestHandler.RequiredHosts is { Count: > 0 }) + if (configuration.RequiredHosts is { Count: > 0 }) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".RequireHost("); - source.Append(string.Join(", ", requestHandler.RequiredHosts.Value.Select(StringLiteral))); + source.Append(string.Join(", ", configuration.RequiredHosts.Value.Select(StringLiteral))); source.Append(')'); } - if (requestHandler.RequireRateLimiting && !string.IsNullOrEmpty(requestHandler.RateLimitingPolicyName)) + if (configuration.RequireRateLimiting && !string.IsNullOrEmpty(configuration.RateLimitingPolicyName)) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".RequireRateLimiting("); - source.Append(StringLiteral(requestHandler.RateLimitingPolicyName)); + source.Append(StringLiteral(configuration.RateLimitingPolicyName)); source.Append(')'); } - if (requestHandler.DisableAntiforgery) + if (configuration.DisableAntiforgery) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".DisableAntiforgery()"); } - if (requestHandler.AllowAnonymous) + if (configuration.AllowAnonymous) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".AllowAnonymous()"); } - if (requestHandler.ShortCircuit) + if (configuration.ShortCircuit) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".ShortCircuit()"); } - if (requestHandler.DisableRequestTimeout) + if (configuration.DisableRequestTimeout) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".DisableRequestTimeout()"); } - else if (requestHandler.WithRequestTimeout) + else if (configuration.WithRequestTimeout) { source.AppendLine(); - source.Append(continuationIndent); - if (!string.IsNullOrEmpty(requestHandler.RequestTimeoutPolicyName)) + source.Append(indent); + if (!string.IsNullOrEmpty(configuration.RequestTimeoutPolicyName)) { source.Append(".WithRequestTimeout("); - source.Append(StringLiteral(requestHandler.RequestTimeoutPolicyName)); + source.Append(StringLiteral(configuration.RequestTimeoutPolicyName)); source.Append(')'); } else @@ -2257,33 +2361,91 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl } } - if (requestHandler.EndpointFilterTypes is { Count: > 0 }) - foreach (var filterType in requestHandler.EndpointFilterTypes.Value) + if (configuration.EndpointFilterTypes is { Count: > 0 }) + foreach (var filterType in configuration.EndpointFilterTypes.Value) { source.AppendLine(); - source.Append(continuationIndent); + source.Append(indent); source.Append(".AddEndpointFilter<"); source.Append(filterType); source.Append(">()"); } + } - if (wrapWithConfigure && configureAcceptsServiceProvider) - { - source.AppendLine(","); - source.Append(indent); - source.Append("builder.ServiceProvider"); - } + private static EndpointConfiguration MergeEndpointConfigurations(EndpointConfiguration classConfiguration, EndpointConfiguration methodConfiguration) + { + var metadata = MergeRequestHandlerMetadata(classConfiguration.Metadata, methodConfiguration.Metadata); + var authorizationPolicies = MergeDistinctStrings(classConfiguration.AuthorizationPolicies, methodConfiguration.AuthorizationPolicies); + var requiredHosts = MergeDistinctStrings(classConfiguration.RequiredHosts, methodConfiguration.RequiredHosts); + var endpointFilterTypes = ConcatEquatable(classConfiguration.EndpointFilterTypes, methodConfiguration.EndpointFilterTypes); + var requireAuthorization = classConfiguration.RequireAuthorization || methodConfiguration.RequireAuthorization; + var disableAntiforgery = classConfiguration.DisableAntiforgery || methodConfiguration.DisableAntiforgery; + var allowAnonymous = classConfiguration.AllowAnonymous || methodConfiguration.AllowAnonymous; + var requireCors = classConfiguration.RequireCors || methodConfiguration.RequireCors; + var corsPolicyName = methodConfiguration.CorsPolicyName ?? classConfiguration.CorsPolicyName; + var requireRateLimiting = classConfiguration.RequireRateLimiting || methodConfiguration.RequireRateLimiting; + var rateLimitingPolicyName = methodConfiguration.RateLimitingPolicyName ?? classConfiguration.RateLimitingPolicyName; + var shortCircuit = classConfiguration.ShortCircuit || methodConfiguration.ShortCircuit; + var disableRequestTimeout = classConfiguration.DisableRequestTimeout || methodConfiguration.DisableRequestTimeout; + var withRequestTimeout = classConfiguration.WithRequestTimeout || methodConfiguration.WithRequestTimeout; + string? requestTimeoutPolicyName = null; + if (methodConfiguration.WithRequestTimeout) + requestTimeoutPolicyName = methodConfiguration.RequestTimeoutPolicyName; + else if (classConfiguration.WithRequestTimeout) + requestTimeoutPolicyName = classConfiguration.RequestTimeoutPolicyName; - if (wrapWithConfigure) + if (disableRequestTimeout) { - source.AppendLine(); - source.Append(" );"); - source.AppendLine(); - } - else - { - source.AppendLine(";"); + withRequestTimeout = false; + requestTimeoutPolicyName = null; } + + var order = methodConfiguration.Order ?? classConfiguration.Order; + var endpointGroupName = methodConfiguration.EndpointGroupName ?? classConfiguration.EndpointGroupName; + + return new EndpointConfiguration(metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, + corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, + withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName); + } + + private static RequestHandlerMetadata MergeRequestHandlerMetadata(RequestHandlerMetadata classMetadata, RequestHandlerMetadata methodMetadata) + { + return new RequestHandlerMetadata( + methodMetadata.Name ?? classMetadata.Name, + methodMetadata.DisplayName ?? classMetadata.DisplayName, + methodMetadata.Summary ?? classMetadata.Summary, + methodMetadata.Description ?? classMetadata.Description, + MergeDistinctStrings(classMetadata.Tags, methodMetadata.Tags), + ConcatEquatable(classMetadata.Accepts, methodMetadata.Accepts), + ConcatEquatable(classMetadata.Produces, methodMetadata.Produces), + ConcatEquatable(classMetadata.ProducesProblem, methodMetadata.ProducesProblem), + ConcatEquatable(classMetadata.ProducesValidationProblem, methodMetadata.ProducesValidationProblem), + classMetadata.ExcludeFromDescription || methodMetadata.ExcludeFromDescription + ); + } + + private static EquatableImmutableArray? MergeDistinctStrings(EquatableImmutableArray? first, EquatableImmutableArray? second) + { + if (first is not { Count: > 0 }) + return second; + if (second is not { Count: > 0 }) + return first; + + var merged = MergeUnion(first, second.Value); + return merged.Count > 0 ? merged : null; + } + + private static EquatableImmutableArray? ConcatEquatable(EquatableImmutableArray? first, EquatableImmutableArray? second) + { + if (first is not { Count: > 0 }) + return second; + if (second is not { Count: > 0 }) + return first; + + var builder = ImmutableArray.CreateBuilder(first.Value.Count + second.Value.Count); + builder.AddRange(first.Value); + builder.AddRange(second.Value); + return builder.ToEquatableImmutableArray(); } private static string? GetMapMethodSuffix(string httpMethod) @@ -2482,6 +2644,20 @@ private readonly record struct RequestHandler( RequestHandlerMethod Method, string HttpMethod, string Pattern, + EndpointConfiguration Configuration + ); + + private readonly record struct RequestHandlerClass( + string Name, + bool IsStatic, + bool HasConfigureMethod, + bool ConfigureMethodAcceptsServiceProvider, + string? MapGroupPattern, + string? MapGroupBuilderIdentifier, + EndpointConfiguration Configuration + ); + + private readonly record struct EndpointConfiguration( RequestHandlerMetadata Metadata, bool RequireAuthorization, EquatableImmutableArray? AuthorizationPolicies, @@ -2501,8 +2677,6 @@ private readonly record struct RequestHandler( string? EndpointGroupName ); - private readonly record struct RequestHandlerClass(string Name, bool IsStatic, bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); - private readonly record struct RequestHandlerMethod(string Name, bool IsStatic, bool IsAwaitable, EquatableImmutableArray Parameters); private readonly record struct RequestHandlerMetadata( diff --git a/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs index 7db85c8..45c3fb8 100644 --- a/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs +++ b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs @@ -50,7 +50,8 @@ public static string BuildAuthorizationMatrixSource( bool disableRequestTimeout, int orderValue, string? groupName, - bool excludeFromDescription) + bool excludeFromDescription, + string? mapGroupPattern = null) { var builder = new StringBuilder(); @@ -85,6 +86,11 @@ public static string BuildAuthorizationMatrixSource( builder.AppendLine($"[GroupName(\"{groupName}\")]"); } + if (!string.IsNullOrWhiteSpace(mapGroupPattern)) + { + builder.AppendLine($"[MapGroup(\"{mapGroupPattern}\")]"); + } + if (applyShortCircuit) { builder.AppendLine("[ShortCircuit]"); @@ -150,6 +156,15 @@ public static string BuildAuthorizationMatrixSource( } builder.AppendLine(" public static Ok Handle(int id) => id >= 0 ? TypedResults.Ok() : TypedResults.Ok();"); + + if (!string.IsNullOrWhiteSpace(mapGroupPattern)) + { + builder.AppendLine(); + builder.AppendLine(" [MapDelete(\"/matrix/{id:int}\")]"); + builder.AppendLine(" public static Results Delete(int id)"); + builder.AppendLine(" => id >= 0 ? TypedResults.NoContent() : TypedResults.NotFound();"); + } + builder.AppendLine("}"); return builder.ToString(); } diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..ca2a016 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassMapGroup_MapEndpointHandlers.verified.txt @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + var _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group = builder.MapGroup("/individuals") + .WithGroupName("ClassGroup") + .WithOrder(2) + .ExcludeFromDescription() + .WithTags("Class", "Matrix") + .RequireAuthorization("ClassPolicy") + .RequireCors("ClassCors") + .RequireHost("*.individual.com") + .ShortCircuit() + .WithRequestTimeout("ClassTimeout"); + + _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group.MapDelete("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Delete) + .WithName("Delete"); + + _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithTags("Method", "Matrix") + .AllowAnonymous(); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.cs b/tests/GeneratedEndpoints.Tests/IndividualTests.cs index 40012ef..f514bd5 100644 --- a/tests/GeneratedEndpoints.Tests/IndividualTests.cs +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.cs @@ -165,6 +165,28 @@ public async Task GroupName() await VerifyIndividualAsync(source, nameof(GroupName)); } + [Fact] + public async Task ClassMapGroup() + { + var source = AuthorizationScenario( + classRequireAuthorization: true, + classTags: true, + classHost: "*.individual.com", + classRequireCors: true, + classCorsPolicy: "ClassCors", + applyShortCircuit: true, + applyRequestTimeout: true, + requestTimeoutPolicy: "ClassTimeout", + orderValue: 2, + groupName: "ClassGroup", + excludeFromDescription: true, + methodAllowAnonymous: true, + methodTags: true, + mapGroupPattern: "/individuals" + ); + await VerifyIndividualAsync(source, nameof(ClassMapGroup)); + } + [Fact] public async Task ExcludeFromDescription() { @@ -446,7 +468,8 @@ private static string AuthorizationScenario( bool disableRequestTimeout = false, int orderValue = 0, string? groupName = null, - bool excludeFromDescription = false) + bool excludeFromDescription = false, + string? mapGroupPattern = null) => SourceFactory.BuildAuthorizationMatrixSource( classAllowAnonymous, methodAllowAnonymous, @@ -468,7 +491,8 @@ private static string AuthorizationScenario( disableRequestTimeout, orderValue, groupName, - excludeFromDescription); + excludeFromDescription, + mapGroupPattern); private static string ConfigureScenario( bool configureWithServiceProvider = false, From ea1b061d8f6eb6054c5885b8290ab7e020da3522 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:54:09 -0500 Subject: [PATCH 54/75] Add group name support to MapGroup attribute (#44) --- README.md | 3 +- src/GeneratedEndpoints/MinimalApiGenerator.cs | 58 +++++-------------- .../Common/SourceFactory.cs | 16 +++-- ...E8A8FA30F_MapEndpointHandlers.verified.txt | 13 +++-- ...2AC35B582_MapEndpointHandlers.verified.txt | 14 +++-- ...41534B0BD_MapEndpointHandlers.verified.txt | 11 +++- ...GroupName_MapEndpointHandlers.verified.txt | 6 +- 7 files changed, 55 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 22c14f7..680f189 100644 --- a/README.md +++ b/README.md @@ -150,11 +150,10 @@ applied to all endpoints within the class. * `[EndpointFilter(Type filterType)]` * `[EndpointFilter]` * `[ExcludeFromDescription]` -* `[GroupName(string name)]` * `[MapConnect(string pattern = "", Name = null)]` * `[MapDelete(string pattern = "", Name = null)]` * `[MapFallback(string pattern = "", Name = null)]` -* `[MapGroup(string pattern)]` +* `[MapGroup(string pattern, Name = null)]` * `[MapGet(string pattern = "", Name = null)]` * `[MapHead(string pattern = "", Name = null)]` * `[MapOptions(string pattern = "", Name = null)]` diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index b0a364d..bc4c2ad 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -60,10 +60,6 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string OrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{OrderAttributeName}"; private const string OrderAttributeHint = $"{OrderAttributeFullyQualifiedName}.gs.cs"; - private const string GroupNameAttributeName = "GroupNameAttribute"; - private const string GroupNameAttributeFullyQualifiedName = $"{AttributesNamespace}.{GroupNameAttributeName}"; - private const string GroupNameAttributeHint = $"{GroupNameAttributeFullyQualifiedName}.gs.cs"; - private const string MapGroupAttributeName = "MapGroupAttribute"; private const string MapGroupAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapGroupAttributeName}"; private const string MapGroupAttributeHint = $"{MapGroupAttributeFullyQualifiedName}.gs.cs"; @@ -446,36 +442,6 @@ internal sealed class {{OrderAttributeName}} : global::System.Attribute """; context.AddSource(OrderAttributeHint, SourceText.From(orderSource, Encoding.UTF8)); - // GroupName - var groupNameSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the endpoint group name for the annotated class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - internal sealed class {{GroupNameAttributeName}} : global::System.Attribute - { - /// - /// Gets the endpoint group name. - /// - public string GroupName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The endpoint group name to apply. - public {{GroupNameAttributeName}}(string groupName) - { - GroupName = groupName; - } - } - - """; - context.AddSource(GroupNameAttributeHint, SourceText.From(groupNameSource, Encoding.UTF8)); - // MapGroup var mapGroupSource = $$""" {{FileHeader}} @@ -493,6 +459,11 @@ internal sealed class {{MapGroupAttributeName}} : global::System.Attribute /// public string Pattern { get; } + /// + /// Gets or sets the endpoint group name. + /// + public string? Name { get; set; } + /// /// Initializes a new instance of the class. /// @@ -1110,14 +1081,11 @@ ref string? summary continue; } - if (IsGeneratedAttribute(attributeClass, GroupNameAttributeName)) + if (IsGeneratedAttribute(attributeClass, MapGroupAttributeName)) { - if (attribute.ConstructorArguments.Length > 0) - { - var groupName = NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string); - if (!string.IsNullOrEmpty(groupName)) - endpointGroupName = groupName; - } + var groupName = GetNamedStringValue(attribute, NameAttributeNamedParameter); + if (!string.IsNullOrEmpty(groupName)) + endpointGroupName = groupName; continue; } @@ -1324,9 +1292,9 @@ private static string NormalizeRequiredContentType(string? contentType, string d if (attribute.ConstructorArguments.Length > 0) { - var pattern = NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string); - if (!string.IsNullOrEmpty(pattern)) - return pattern; + var pattern = attribute.ConstructorArguments[0].Value as string; + if (pattern is not null) + return pattern.Trim(); } } @@ -1993,7 +1961,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine(" {"); var groupedClasses = requestHandlers.Select(static handler => handler.Class) - .Where(static handlerClass => !string.IsNullOrEmpty(handlerClass.MapGroupPattern)) + .Where(static handlerClass => handlerClass.MapGroupPattern is not null) .Distinct() .ToArray(); diff --git a/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs index 45c3fb8..dd2c246 100644 --- a/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs +++ b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs @@ -81,14 +81,22 @@ public static string BuildAuthorizationMatrixSource( builder.AppendLine($"[RequireCors{cors}]"); } - if (!string.IsNullOrWhiteSpace(groupName)) + if (!string.IsNullOrWhiteSpace(groupName) && mapGroupPattern is null) { - builder.AppendLine($"[GroupName(\"{groupName}\")]"); + mapGroupPattern = string.Empty; } - if (!string.IsNullOrWhiteSpace(mapGroupPattern)) + if (mapGroupPattern is not null) { - builder.AppendLine($"[MapGroup(\"{mapGroupPattern}\")]"); + var mapGroupAttribute = new StringBuilder(); + mapGroupAttribute.Append($"[MapGroup(\"{mapGroupPattern}\""); + if (!string.IsNullOrWhiteSpace(groupName)) + { + mapGroupAttribute.Append($", Name = \"{groupName}\""); + } + + mapGroupAttribute.Append(")]"); + builder.AppendLine(mapGroupAttribute.ToString()); } if (applyShortCircuit) diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt index b2993f3..31c3fd8 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_257E8A8FA30F_MapEndpointHandlers.verified.txt @@ -23,18 +23,21 @@ internal static class EndpointRouteBuilderExtensions { internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) { - builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) - .WithName("GetMatrix") + var _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group = builder.MapGroup("") .WithGroupName("Docs") .WithOrder(-5) .ExcludeFromDescription() + .RequireCors() + .RequireHost("api.alt.com") + .DisableRequestTimeout(); + + _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") .WithTags("Method", "Matrix") .RequireAuthorization("MethodPolicy") .RequireCors("MethodCors") - .RequireHost("api.alt.com") .RequireRateLimiting("BurstPolicy") - .AllowAnonymous() - .DisableRequestTimeout(); + .AllowAnonymous(); return builder; } diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt index e8bc7b5..4397c69 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_25B2AC35B582_MapEndpointHandlers.verified.txt @@ -23,20 +23,24 @@ internal static class EndpointRouteBuilderExtensions { internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) { - builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) - .WithName("GetMatrix") + var _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group = builder.MapGroup("") .WithGroupName("Reporting") .WithOrder(5) .ExcludeFromDescription() - .WithTags("Class", "Matrix", "Method") + .WithTags("Class", "Matrix") .RequireAuthorization("ClassPolicy") .RequireCors("NamedCorsPolicy") - .RequireHost("*.contoso.com", "api.contoso.com", "contoso.com") - .RequireRateLimiting("RatePolicy") + .RequireHost("*.contoso.com") .AllowAnonymous() .ShortCircuit() .WithRequestTimeout("TimeoutPolicy"); + _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .WithTags("Method", "Matrix") + .RequireHost("api.contoso.com", "contoso.com") + .RequireRateLimiting("RatePolicy"); + return builder; } } diff --git a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt index bb25ed3..ae4b564 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt +++ b/tests/GeneratedEndpoints.Tests/GeneratedSourceTests.AuthorizationAndMetadataMatrix_4F441534B0BD_MapEndpointHandlers.verified.txt @@ -22,17 +22,22 @@ internal static class EndpointRouteBuilderExtensions { internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) { - builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) - .WithName("GetMatrix") + var _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group = builder.MapGroup("") .WithGroupName("Operations") .ExcludeFromDescription() .WithTags("Class", "Matrix") - .RequireAuthorization("ClassPolicy", "MethodPolicy") + .RequireAuthorization("ClassPolicy") .RequireCors() .RequireHost("*.example.com") .AllowAnonymous() .DisableRequestTimeout(); + _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") + .RequireAuthorization("MethodPolicy") + .RequireCors() + .AllowAnonymous(); + return builder; } } diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt index 974c923..32df90b 100644 --- a/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.GroupName_MapEndpointHandlers.verified.txt @@ -22,10 +22,12 @@ internal static class EndpointRouteBuilderExtensions { internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) { - builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) - .WithName("GetMatrix") + var _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group = builder.MapGroup("") .WithGroupName("IndividualGroup"); + _GeneratedEndpointsTests_AuthorizationMatrixEndpoints_Group.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix"); + return builder; } } From 020accfec4b7d969672517fcba020561c587a55e Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:55:02 -0500 Subject: [PATCH 55/75] Cleanup. --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index bc4c2ad..853e383 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -462,7 +462,7 @@ internal sealed class {{MapGroupAttributeName}} : global::System.Attribute /// /// Gets or sets the endpoint group name. /// - public string? Name { get; set; } + public string? Name { get; init; } /// /// Initializes a new instance of the class. From 591d1b107256ec188a62a3ca6de51aa8f1f9e67c Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:12:17 -0500 Subject: [PATCH 56/75] Add DisableValidation attribute support (#45) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 47 +++++++++++++++++-- .../Common/SourceFactory.cs | 14 +++++- ...alidation_AddEndpointHandlers.verified.txt | 24 ++++++++++ ...alidation_MapEndpointHandlers.verified.txt | 34 ++++++++++++++ ...alidation_AddEndpointHandlers.verified.txt | 24 ++++++++++ ...alidation_MapEndpointHandlers.verified.txt | 34 ++++++++++++++ .../IndividualTests.cs | 22 ++++++++- 7 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 853e383..d1e7d38 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -52,6 +52,10 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private const string DisableRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableRequestTimeoutAttributeName}"; private const string DisableRequestTimeoutAttributeHint = $"{DisableRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; + private const string DisableValidationAttributeName = "DisableValidationAttribute"; + private const string DisableValidationAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableValidationAttributeName}"; + private const string DisableValidationAttributeHint = $"{DisableValidationAttributeFullyQualifiedName}.gs.cs"; + private const string RequestTimeoutAttributeName = "RequestTimeoutAttribute"; private const string RequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequestTimeoutAttributeName}"; private const string RequestTimeoutAttributeHint = $"{RequestTimeoutAttributeFullyQualifiedName}.gs.cs"; @@ -375,6 +379,23 @@ internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.At """; context.AddSource(DisableRequestTimeoutAttributeHint, SourceText.From(disableRequestTimeoutSource, Encoding.UTF8)); + // DisableValidation + var disableValidationSource = $$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute + { + } + + """; + context.AddSource(DisableValidationAttributeHint, SourceText.From(disableValidationSource, Encoding.UTF8)); + // RequestTimeout var requestTimeoutSource = $$""" {{FileHeader}} @@ -974,6 +995,7 @@ bool enforceMethodRequireAuthorizationRules string? rateLimitingPolicyName = null; List? endpointFilters = null; bool? shortCircuit = null; + bool? disableValidation = null; bool? disableRequestTimeout = null; bool? withRequestTimeout = null; string? requestTimeoutPolicyName = null; @@ -992,7 +1014,7 @@ bool enforceMethodRequireAuthorizationRules GetAdditionalRequestHandlerAttributeValues(attributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, ref allowAnonymous, ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, ref producesValidationProblem, ref requireCors, ref corsPolicyName, ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, - ref hasAllowAnonymousAttribute, ref hasRequireAuthorizationAttribute, ref shortCircuit, ref disableRequestTimeout, ref withRequestTimeout, + ref hasAllowAnonymousAttribute, ref hasRequireAuthorizationAttribute, ref shortCircuit, ref disableValidation, ref disableRequestTimeout, ref withRequestTimeout, ref requestTimeoutPolicyName, ref order, ref endpointGroupName, ref summary ); @@ -1005,7 +1027,7 @@ bool enforceMethodRequireAuthorizationRules return new EndpointConfiguration(metadata, requireAuthorization ?? false, authorizationPolicies, disableAntiforgery ?? false, allowAnonymous ?? false, requireCors ?? false, corsPolicyName, requiredHosts, requireRateLimiting ?? false, rateLimitingPolicyName, - ToEquatableOrNull(endpointFilters), shortCircuit ?? false, disableRequestTimeout ?? false, withRequestTimeout ?? false, + ToEquatableOrNull(endpointFilters), shortCircuit ?? false, disableValidation ?? false, disableRequestTimeout ?? false, withRequestTimeout ?? false, withRequestTimeout ?? false ? requestTimeoutPolicyName : null, order, endpointGroupName); } @@ -1030,6 +1052,7 @@ private static void GetAdditionalRequestHandlerAttributeValues( ref bool hasAllowAnonymousAttribute, ref bool hasRequireAuthorizationAttribute, ref bool? shortCircuit, + ref bool? disableValidation, ref bool? disableRequestTimeout, ref bool? withRequestTimeout, ref string? requestTimeoutPolicyName, @@ -1050,6 +1073,12 @@ ref string? summary continue; } + if (IsGeneratedAttribute(attributeClass, DisableValidationAttributeName)) + { + disableValidation = true; + continue; + } + if (IsGeneratedAttribute(attributeClass, DisableRequestTimeoutAttributeName)) { disableRequestTimeout = true; @@ -2307,6 +2336,16 @@ private static void AppendEndpointConfiguration(StringBuilder source, string ind source.Append(".ShortCircuit()"); } + if (configuration.DisableValidation) + { + source.AppendLine(); + source.AppendLine("#if NET10_0_OR_GREATER"); + source.Append(indent); + source.Append(".DisableValidation()"); + source.AppendLine(); + source.AppendLine("#endif"); + } + if (configuration.DisableRequestTimeout) { source.AppendLine(); @@ -2354,6 +2393,7 @@ private static EndpointConfiguration MergeEndpointConfigurations(EndpointConfigu var requireRateLimiting = classConfiguration.RequireRateLimiting || methodConfiguration.RequireRateLimiting; var rateLimitingPolicyName = methodConfiguration.RateLimitingPolicyName ?? classConfiguration.RateLimitingPolicyName; var shortCircuit = classConfiguration.ShortCircuit || methodConfiguration.ShortCircuit; + var disableValidation = classConfiguration.DisableValidation || methodConfiguration.DisableValidation; var disableRequestTimeout = classConfiguration.DisableRequestTimeout || methodConfiguration.DisableRequestTimeout; var withRequestTimeout = classConfiguration.WithRequestTimeout || methodConfiguration.WithRequestTimeout; string? requestTimeoutPolicyName = null; @@ -2372,7 +2412,7 @@ private static EndpointConfiguration MergeEndpointConfigurations(EndpointConfigu var endpointGroupName = methodConfiguration.EndpointGroupName ?? classConfiguration.EndpointGroupName; return new EndpointConfiguration(metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, - corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableRequestTimeout, + corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableValidation, disableRequestTimeout, withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName); } @@ -2638,6 +2678,7 @@ private readonly record struct EndpointConfiguration( string? RateLimitingPolicyName, EquatableImmutableArray? EndpointFilterTypes, bool ShortCircuit, + bool DisableValidation, bool DisableRequestTimeout, bool WithRequestTimeout, string? RequestTimeoutPolicyName, diff --git a/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs index dd2c246..ac721c9 100644 --- a/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs +++ b/tests/GeneratedEndpoints.Tests/Common/SourceFactory.cs @@ -51,7 +51,9 @@ public static string BuildAuthorizationMatrixSource( int orderValue, string? groupName, bool excludeFromDescription, - string? mapGroupPattern = null) + string? mapGroupPattern = null, + bool classDisableValidation = false, + bool methodDisableValidation = false) { var builder = new StringBuilder(); @@ -104,6 +106,11 @@ public static string BuildAuthorizationMatrixSource( builder.AppendLine("[ShortCircuit]"); } + if (classDisableValidation) + { + builder.AppendLine("[DisableValidation]"); + } + if (applyRequestTimeout) { var timeoutArgument = string.IsNullOrWhiteSpace(requestTimeoutPolicy) @@ -163,6 +170,11 @@ public static string BuildAuthorizationMatrixSource( builder.AppendLine($" [RequireRateLimiting{rateLimit}]"); } + if (methodDisableValidation) + { + builder.AppendLine(" [DisableValidation]"); + } + builder.AppendLine(" public static Ok Handle(int id) => id >= 0 ? TypedResults.Ok() : TypedResults.Ok();"); if (!string.IsNullOrWhiteSpace(mapGroupPattern)) diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..7dcaad1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") +#if NET10_0_OR_GREATER + .DisableValidation() +#endif +; + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..c6b5484 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..7dcaad1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) + .WithName("GetMatrix") +#if NET10_0_OR_GREATER + .DisableValidation() +#endif +; + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.cs b/tests/GeneratedEndpoints.Tests/IndividualTests.cs index f514bd5..ace1135 100644 --- a/tests/GeneratedEndpoints.Tests/IndividualTests.cs +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.cs @@ -130,6 +130,20 @@ public async Task ShortCircuit() await VerifyIndividualAsync(source, nameof(ShortCircuit)); } + [Fact] + public async Task ClassDisableValidation() + { + var source = AuthorizationScenario(classDisableValidation: true); + await VerifyIndividualAsync(source, nameof(ClassDisableValidation)); + } + + [Fact] + public async Task MethodDisableValidation() + { + var source = AuthorizationScenario(methodDisableValidation: true); + await VerifyIndividualAsync(source, nameof(MethodDisableValidation)); + } + [Fact] public async Task RequestTimeout() { @@ -469,7 +483,9 @@ private static string AuthorizationScenario( int orderValue = 0, string? groupName = null, bool excludeFromDescription = false, - string? mapGroupPattern = null) + string? mapGroupPattern = null, + bool classDisableValidation = false, + bool methodDisableValidation = false) => SourceFactory.BuildAuthorizationMatrixSource( classAllowAnonymous, methodAllowAnonymous, @@ -492,7 +508,9 @@ private static string AuthorizationScenario( orderValue, groupName, excludeFromDescription, - mapGroupPattern); + mapGroupPattern, + classDisableValidation, + methodDisableValidation); private static string ConfigureScenario( bool configureWithServiceProvider = false, From 5692a0edff839b09d85773e071742e6825c7de99 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:17:01 -0500 Subject: [PATCH 57/75] Fixed preprocessor directive. --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 12 ++++-------- tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs | 4 ++-- .../GeneratedEndpoints.Tests.csproj | 4 ++-- ...isableValidation_MapEndpointHandlers.verified.txt | 2 -- ...isableValidation_MapEndpointHandlers.verified.txt | 2 -- 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index d1e7d38..86d8486 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -381,6 +381,7 @@ internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.At // DisableValidation var disableValidationSource = $$""" + #if NET10_0_OR_GREATER {{FileHeader}} namespace {{AttributesNamespace}}; @@ -392,6 +393,7 @@ namespace {{AttributesNamespace}}; internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute { } + #endif """; context.AddSource(DisableValidationAttributeHint, SourceText.From(disableValidationSource, Encoding.UTF8)); @@ -1319,12 +1321,8 @@ private static string NormalizeRequiredContentType(string? contentType, string d if (!IsGeneratedAttribute(attributeClass, MapGroupAttributeName)) continue; - if (attribute.ConstructorArguments.Length > 0) - { - var pattern = attribute.ConstructorArguments[0].Value as string; - if (pattern is not null) - return pattern.Trim(); - } + if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string pattern) + return pattern.Trim(); } return null; @@ -2339,11 +2337,9 @@ private static void AppendEndpointConfiguration(StringBuilder source, string ind if (configuration.DisableValidation) { source.AppendLine(); - source.AppendLine("#if NET10_0_OR_GREATER"); source.Append(indent); source.Append(".DisableValidation()"); source.AppendLine(); - source.AppendLine("#endif"); } if (configuration.DisableRequestTimeout) diff --git a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs index fc95fb5..8771b24 100644 --- a/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs +++ b/tests/GeneratedEndpoints.Tests/Common/TestHelpers.cs @@ -9,9 +9,9 @@ public static class TestHelpers { public static GeneratorDriverRunResult RunGenerator(IEnumerable sources) { - var cSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp11); + var cSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp11).WithPreprocessorSymbols("NET10_0_OR_GREATER"); var cSharpCompilationOptions = new CSharpCompilationOptions(OutputKind.NetModule).WithNullableContextOptions(NullableContextOptions.Enable); - var (_, result) = IncrementalGenerator.RunWithDiagnostics(sources, cSharpParseOptions, AspNet80.References.All, cSharpCompilationOptions); + var (_, result) = IncrementalGenerator.RunWithDiagnostics(sources, cSharpParseOptions, AspNet100.References.All, cSharpCompilationOptions); return result; } diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj index 88ec984..15b79e5 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable latest @@ -14,7 +14,7 @@ - + all diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt index 7dcaad1..e00e76c 100644 --- a/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.ClassDisableValidation_MapEndpointHandlers.verified.txt @@ -24,9 +24,7 @@ internal static class EndpointRouteBuilderExtensions { builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) .WithName("GetMatrix") -#if NET10_0_OR_GREATER .DisableValidation() -#endif ; return builder; diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt index 7dcaad1..e00e76c 100644 --- a/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.MethodDisableValidation_MapEndpointHandlers.verified.txt @@ -24,9 +24,7 @@ internal static class EndpointRouteBuilderExtensions { builder.MapGet("/matrix/{id:int}", global::GeneratedEndpointsTests.AuthorizationMatrixEndpoints.Handle) .WithName("GetMatrix") -#if NET10_0_OR_GREATER .DisableValidation() -#endif ; return builder; From f1e2c098d19574d650da9683a5f5d6ac71b3ccbe Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:17:33 -0500 Subject: [PATCH 58/75] Fix readme. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 680f189..04bc411 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ applied to all endpoints within the class. * `[Description(string description)]` * `[DisableAntiforgery]` * `[DisableRequestTimeout]` +* `[DisableValidation]` * `[DisplayName(string displayName)]` * `[EndpointFilter(Type filterType)]` * `[EndpointFilter]` From 273c7bad45e32215bc6c246103b077fc3612cd30 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:24:38 -0500 Subject: [PATCH 59/75] Docs: reformat attribute reference (#46) --- README.md | 74 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 04bc411..1f6aa08 100644 --- a/README.md +++ b/README.md @@ -140,39 +140,41 @@ applied to all endpoints within the class. ## Attribute Reference -* `[Accepts(string contentType = "application/json", params string[] additionalContentTypes, RequestType = null, IsOptional = false)]` -* `[Accepts(string contentType = "application/json", params string[] additionalContentTypes, IsOptional = false)]` -* `[AllowAnonymous]` -* `[Description(string description)]` -* `[DisableAntiforgery]` -* `[DisableRequestTimeout]` -* `[DisableValidation]` -* `[DisplayName(string displayName)]` -* `[EndpointFilter(Type filterType)]` -* `[EndpointFilter]` -* `[ExcludeFromDescription]` -* `[MapConnect(string pattern = "", Name = null)]` -* `[MapDelete(string pattern = "", Name = null)]` -* `[MapFallback(string pattern = "", Name = null)]` -* `[MapGroup(string pattern, Name = null)]` -* `[MapGet(string pattern = "", Name = null)]` -* `[MapHead(string pattern = "", Name = null)]` -* `[MapOptions(string pattern = "", Name = null)]` -* `[MapPatch(string pattern = "", Name = null)]` -* `[MapPost(string pattern = "", Name = null)]` -* `[MapPut(string pattern = "", Name = null)]` -* `[MapQuery(string pattern = "", Name = null)]` -* `[MapTrace(string pattern = "", Name = null)]` -* `[Order(int order)]` -* `[ProducesProblem(int statusCode = StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes)]` -* `[ProducesResponse(int statusCode = StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes, ResponseType = null)]` -* `[ProducesResponse(int statusCode = StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes)]` -* `[ProducesValidationProblem(int statusCode = StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes)]` -* `[RequestTimeout(string? policyName = null)]` -* `[RequireAuthorization(params string[] policies)]` -* `[RequireCors(string? policyName = null)]` -* `[RequireHost(params string[] hosts)]` -* `[RequireRateLimiting(string policyName)]` -* `[ShortCircuit]` -* `[Summary(string summary)]` -* `[Tags(params string[] tags)]` +| Definition | Usage | Description | +| --- | --- | --- | +| `[Accepts(string contentType = "application/json", params string[] additionalContentTypes, RequestType = null, IsOptional = false)]` | Method | Declares the accepted request body CLR type, optional status, and list of content types for the handler. | +| `[Accepts(string contentType = "application/json", params string[] additionalContentTypes, IsOptional = false)]` | Method | Generic shortcut for specifying the request type and accepted content types for the handler. | +| `[AllowAnonymous]` | Class or Method | Allows the annotated endpoint or class to bypass authorization requirements. | +| `[Description(string description)]` | Method | Sets the OpenAPI description metadata for the generated endpoint. | +| `[DisableAntiforgery]` | Class or Method | Disables antiforgery protection for the annotated endpoint(s). | +| `[DisableRequestTimeout]` | Class or Method | Disables request timeout enforcement for the annotated endpoint(s). | +| `[DisableValidation]` | Class or Method | Disables automatic request validation (when supported) for the annotated endpoint(s). | +| `[DisplayName(string displayName)]` | Method | Overrides the endpoint display name used in diagnostics and metadata. | +| `[EndpointFilter(Type filterType)]` | Class or Method | Adds the specified endpoint filter type to the handler pipeline. | +| `[EndpointFilter]` | Class or Method | Generic form for registering an endpoint filter type on the handler pipeline. | +| `[ExcludeFromDescription]` | Class or Method | Hides the endpoint or class from generated API descriptions (e.g., OpenAPI). | +| `[MapConnect(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP CONNECT endpoint using the supplied route pattern and optional name. | +| `[MapDelete(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP DELETE endpoint using the supplied route pattern and optional name. | +| `[MapFallback(string pattern = "", Name = null)]` | Method | Maps the method as the fallback endpoint invoked when no other route matches. | +| `[MapGroup(string pattern, Name = null)]` | Class | Assigns a route group pattern and optional endpoint group name to every handler in the class. | +| `[MapGet(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP GET endpoint using the supplied route pattern and optional name. | +| `[MapHead(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP HEAD endpoint using the supplied route pattern and optional name. | +| `[MapOptions(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP OPTIONS endpoint using the supplied route pattern and optional name. | +| `[MapPatch(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP PATCH endpoint using the supplied route pattern and optional name. | +| `[MapPost(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP POST endpoint using the supplied route pattern and optional name. | +| `[MapPut(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP PUT endpoint using the supplied route pattern and optional name. | +| `[MapQuery(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP QUERY endpoint using the supplied route pattern and optional name. | +| `[MapTrace(string pattern = "", Name = null)]` | Method | Marks the method as an HTTP TRACE endpoint using the supplied route pattern and optional name. | +| `[Order(int order)]` | Method | Controls the order in which endpoint conventions are applied to the handler. | +| `[ProducesProblem(int statusCode = StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes)]` | Method | Declares that the endpoint emits a problem details payload for the given status code and content types. | +| `[ProducesResponse(int statusCode = StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes, ResponseType = null)]` | Method | Declares response metadata for the handler, including status code, optional CLR type, and content types. | +| `[ProducesResponse(int statusCode = StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes)]` | Method | Generic shorthand for declaring the CLR response type along with status code and content types. | +| `[ProducesValidationProblem(int statusCode = StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes)]` | Method | Declares that the endpoint returns validation problem details for the specified status code and content types. | +| `[RequestTimeout(string? policyName = null)]` | Class or Method | Applies the default or a named request-timeout policy to the handler(s). | +| `[RequireAuthorization(params string[] policies)]` | Class or Method | Enforces authorization on the handler(s), optionally scoping access to specific policies. | +| `[RequireCors(string? policyName = null)]` | Class or Method | Requires the default or a named CORS policy for the annotated handler(s). | +| `[RequireHost(params string[] hosts)]` | Class or Method | Restricts the handler(s) to the specified allowed hostnames. | +| `[RequireRateLimiting(string policyName)]` | Class or Method | Enforces the named rate-limiting policy on the annotated handler(s). | +| `[ShortCircuit]` | Class or Method | Marks the handler(s) to short-circuit the request pipeline when invoked. | +| `[Summary(string summary)]` | Class or Method | Sets the summary metadata applied to the generated endpoint(s). | +| `[Tags(params string[] tags)]` | Class or Method | Assigns OpenAPI tags to the annotated handler(s) for grouping in API docs. | From ab86fae749fa8c539023f25f17e0a5ac96f8ee11 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:27:02 -0500 Subject: [PATCH 60/75] Optimize generator hot paths (#47) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 181 ++++++++++++++---- 1 file changed, 143 insertions(+), 38 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 86d8486..6250324 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -1517,17 +1518,41 @@ private static bool GetNamedBoolValue(AttributeData attribute, string namedParam private static EquatableImmutableArray MergeUnion(EquatableImmutableArray? existing, IEnumerable values) { - var list = new List(); + List? list = null; + HashSet? seen = null; if (existing is { Count: > 0 }) - list.AddRange(existing.Value); + { + list = new List(existing.Value.Count + 4); + seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var item in existing.Value) + { + if (string.IsNullOrWhiteSpace(item)) + continue; + + var trimmed = item.Trim(); + if (trimmed.Length == 0) + continue; + + if (seen.Add(trimmed)) + list.Add(trimmed); + } + } + + seen ??= new HashSet(StringComparer.OrdinalIgnoreCase); + list ??= new List(); foreach (var value in values) { if (string.IsNullOrWhiteSpace(value)) continue; + var trimmed = value.Trim(); - if (!list.Contains(trimmed, StringComparer.OrdinalIgnoreCase)) + if (trimmed.Length == 0) + continue; + + if (seen.Add(trimmed)) list.Add(trimmed); } @@ -1809,18 +1834,23 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM private static void GenerateSource(SourceProductionContext context, ImmutableArray requestHandlers) { - var sorted = requestHandlers.OrderBy(r => r.Class.Name, StringComparer.Ordinal) - .ThenBy(r => r.Method.Name, StringComparer.Ordinal) - .ThenBy(r => r.HttpMethod, StringComparer.Ordinal) - .ThenBy(r => r.Pattern, StringComparer.Ordinal) - .ToImmutableArray(); - + var sorted = SortRequestHandlers(requestHandlers); sorted = EnsureUniqueEndpointNames(sorted); GenerateAddEndpointHandlersClass(context, sorted); GenerateUseEndpointHandlersClass(context, sorted); } + private static ImmutableArray SortRequestHandlers(ImmutableArray requestHandlers) + { + if (requestHandlers.Length <= 1) + return requestHandlers; + + var array = requestHandlers.ToArray(); + Array.Sort(array, RequestHandlerComparer.Instance); + return array.ToImmutableArray(); + } + private static ImmutableArray EnsureUniqueEndpointNames(ImmutableArray requestHandlers) { var collidingHandlers = GetRequestHandlersWithNameCollisions(requestHandlers); @@ -1846,25 +1876,43 @@ private static ImmutableArray EnsureUniqueEndpointNames(Immutabl private static ImmutableHashSet GetRequestHandlersWithNameCollisions(ImmutableArray requestHandlers) { var collidingIndices = ImmutableHashSet.CreateBuilder(); + if (requestHandlers.IsDefaultOrEmpty) + return collidingIndices.ToImmutable(); - var groups = requestHandlers.Select((handler, index) => (handler, index)) - .Where(static tuple => !string.IsNullOrEmpty(tuple.handler.Configuration.Metadata.Name)) - .GroupBy(static tuple => tuple.handler.Configuration.Metadata.Name!, StringComparer.Ordinal); + var nameToMethodMap = new Dictionary>>(StringComparer.Ordinal); - foreach (var group in groups) + for (var index = 0; index < requestHandlers.Length; index++) { - if (group.Count() <= 1) + var handler = requestHandlers[index]; + var name = handler.Configuration.Metadata.Name; + if (string.IsNullOrEmpty(name)) continue; - var collidingMethodGroups = group.GroupBy(static tuple => tuple.handler.Method.Name, StringComparer.Ordinal) - .Where(static methodGroup => methodGroup.Skip(1) - .Any() - ); + if (!nameToMethodMap.TryGetValue(name!, out var methodMap)) + { + methodMap = new Dictionary>(StringComparer.Ordinal); + nameToMethodMap.Add(name!, methodMap); + } + + var methodName = handler.Method.Name; + if (!methodMap.TryGetValue(methodName, out var indices)) + { + indices = new List(); + methodMap.Add(methodName, indices); + } - foreach (var methodGroup in collidingMethodGroups) + indices.Add(index); + } + + foreach (var methodMap in nameToMethodMap.Values) + { + foreach (var indices in methodMap.Values) { - foreach (var entry in methodGroup) - collidingIndices.Add(entry.index); + if (indices.Count <= 1) + continue; + + foreach (var index in indices) + collidingIndices.Add(index); } } @@ -1885,7 +1933,8 @@ private static string GetFullyQualifiedMethodDisplayName(RequestHandler requestH private static void GenerateAddEndpointHandlersClass(SourceProductionContext context, ImmutableArray requestHandlers) { - var source = GetAddEndpointHandlersStringBuilder(requestHandlers); + var nonStaticClassNames = GetDistinctNonStaticClassNames(requestHandlers); + var source = GetAddEndpointHandlersStringBuilder(nonStaticClassNames); source.AppendLine(FileHeader); source.AppendLine(); @@ -1912,9 +1961,7 @@ private static void GenerateAddEndpointHandlersClass(SourceProductionContext con source.AppendLine(" {"); - foreach (var className in requestHandlers.Where(requestHandler => !requestHandler.Class.IsStatic) - .Select(x => x.Class.Name) - .Distinct()) + foreach (var className in nonStaticClassNames) { source.Append(" services.TryAddScoped<"); source.Append(className); @@ -1931,16 +1978,34 @@ private static void GenerateAddEndpointHandlersClass(SourceProductionContext con context.AddSource(AddEndpointHandlersMethodHint, SourceText.From(source.ToString(), Encoding.UTF8)); } - private static StringBuilder GetAddEndpointHandlersStringBuilder(ImmutableArray requestHandlers) + [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", Justification = "Manual loops avoid repeated allocations in the source generator.")] + private static List GetDistinctNonStaticClassNames(ImmutableArray requestHandlers) { - var distinctHandlers = requestHandlers.Select(x => x.Class) - .Where(x => !x.IsStatic) - .Distinct() - .ToArray(); + var classNames = new List(); + if (requestHandlers.IsDefaultOrEmpty) + return classNames; + + var seen = new HashSet(StringComparer.Ordinal); + foreach (var requestHandler in requestHandlers) + { + if (requestHandler.Class.IsStatic) + continue; + + var className = requestHandler.Class.Name; + if (seen.Add(className)) + classNames.Add(className); + } - var estimate = 512 + distinctHandlers.Sum(x => 36 + x.Name.Length); + return classNames; + } + + private static StringBuilder GetAddEndpointHandlersStringBuilder(List nonStaticClassNames) + { + var estimate = 512; + foreach (var className in nonStaticClassNames) + estimate += 36 + className.Length; - estimate += Math.Max(256, distinctHandlers.Length * 12); + estimate += Math.Max(256, nonStaticClassNames.Count * 12); estimate = (int)(estimate * 1.10); estimate = estimate switch @@ -1987,10 +2052,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine(" {"); - var groupedClasses = requestHandlers.Select(static handler => handler.Class) - .Where(static handlerClass => handlerClass.MapGroupPattern is not null) - .Distinct() - .ToArray(); + var groupedClasses = GetClassesWithMapGroups(requestHandlers); foreach (var groupedClass in groupedClasses) { @@ -2003,7 +2065,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine(";"); } - if (groupedClasses.Length > 0) + if (groupedClasses.Count > 0) source.AppendLine(); for (var index = 0; index < requestHandlers.Length; index++) @@ -2026,6 +2088,27 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con context.AddSource(UseEndpointHandlersMethodHint, SourceText.From(source.ToString(), Encoding.UTF8)); } + [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", Justification = "Manual loops avoid repeated allocations in the source generator.")] + private static List GetClassesWithMapGroups(ImmutableArray requestHandlers) + { + var groupedClasses = new List(); + if (requestHandlers.IsDefaultOrEmpty) + return groupedClasses; + + var seen = new HashSet(StringComparer.Ordinal); + foreach (var handler in requestHandlers) + { + var handlerClass = handler.Class; + if (handlerClass.MapGroupPattern is null) + continue; + + if (seen.Add(handlerClass.Name)) + groupedClasses.Add(handlerClass); + } + + return groupedClasses; + } + private static void GenerateMapRequestHandler(StringBuilder source, RequestHandler requestHandler) { var wrapWithConfigure = requestHandler.Class.HasConfigureMethod; @@ -2735,4 +2818,26 @@ private enum BindingSource FromKeyedServices = 7, AsParameters = 8, } + + private sealed class RequestHandlerComparer : IComparer + { + public static RequestHandlerComparer Instance { get; } = new(); + + public int Compare(RequestHandler x, RequestHandler y) + { + var comparison = string.Compare(x.Class.Name, y.Class.Name, StringComparison.Ordinal); + if (comparison != 0) + return comparison; + + comparison = string.Compare(x.Method.Name, y.Method.Name, StringComparison.Ordinal); + if (comparison != 0) + return comparison; + + comparison = string.Compare(x.HttpMethod, y.HttpMethod, StringComparison.Ordinal); + if (comparison != 0) + return comparison; + + return string.Compare(x.Pattern, y.Pattern, StringComparison.Ordinal); + } + } } From c7acecbe91ec01b4c9966ed507aad5a76835ca88 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:32:50 -0500 Subject: [PATCH 61/75] Optimize source generator hot paths (#48) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 223 +++++++++--------- 1 file changed, 111 insertions(+), 112 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 6250324..08f57e0 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -985,85 +985,59 @@ private static EndpointConfiguration GetEndpointConfiguration( bool enforceMethodRequireAuthorizationRules ) { - EquatableImmutableArray? tags = null; - bool? requireAuthorization = null; - EquatableImmutableArray? authorizationPolicies = null; - bool? disableAntiforgery = null; - bool? allowAnonymous = null; - bool? excludeFromDescription = null; - bool? requireCors = null; - string? corsPolicyName = null; - EquatableImmutableArray? requiredHosts = null; - bool? requireRateLimiting = null; - string? rateLimitingPolicyName = null; - List? endpointFilters = null; - bool? shortCircuit = null; - bool? disableValidation = null; - bool? disableRequestTimeout = null; - bool? withRequestTimeout = null; - string? requestTimeoutPolicyName = null; - int? order = null; - string? endpointGroupName = null; - string? summary = null; - - List? accepts = null; - List? produces = null; - List? producesProblem = null; - List? producesValidationProblem = null; - - var hasAllowAnonymousAttribute = false; - var hasRequireAuthorizationAttribute = false; - - GetAdditionalRequestHandlerAttributeValues(attributes, ref tags, ref requireAuthorization, ref authorizationPolicies, ref disableAntiforgery, - ref allowAnonymous, ref excludeFromDescription, ref accepts, ref produces, ref producesProblem, ref producesValidationProblem, ref requireCors, - ref corsPolicyName, ref requiredHosts, ref requireRateLimiting, ref rateLimitingPolicyName, ref endpointFilters, - ref hasAllowAnonymousAttribute, ref hasRequireAuthorizationAttribute, ref shortCircuit, ref disableValidation, ref disableRequestTimeout, ref withRequestTimeout, - ref requestTimeoutPolicyName, ref order, ref endpointGroupName, ref summary - ); + var state = new EndpointAttributeState(); + + GetAdditionalRequestHandlerAttributeValues(attributes, ref state); - if (enforceMethodRequireAuthorizationRules && hasRequireAuthorizationAttribute && !hasAllowAnonymousAttribute) - allowAnonymous = false; + if (enforceMethodRequireAuthorizationRules && state.HasRequireAuthorizationAttribute && !state.HasAllowAnonymousAttribute) + state.AllowAnonymous = false; - var metadata = new RequestHandlerMetadata(name, displayName, summary, description, tags, ToEquatableOrNull(accepts), ToEquatableOrNull(produces), - ToEquatableOrNull(producesProblem), ToEquatableOrNull(producesValidationProblem), excludeFromDescription ?? false + var metadata = new RequestHandlerMetadata(name, displayName, state.Summary, description, state.Tags, ToEquatableOrNull(state.Accepts), + ToEquatableOrNull(state.Produces), ToEquatableOrNull(state.ProducesProblem), ToEquatableOrNull(state.ProducesValidationProblem), + state.ExcludeFromDescription ?? false ); - return new EndpointConfiguration(metadata, requireAuthorization ?? false, authorizationPolicies, disableAntiforgery ?? false, - allowAnonymous ?? false, requireCors ?? false, corsPolicyName, requiredHosts, requireRateLimiting ?? false, rateLimitingPolicyName, - ToEquatableOrNull(endpointFilters), shortCircuit ?? false, disableValidation ?? false, disableRequestTimeout ?? false, withRequestTimeout ?? false, - withRequestTimeout ?? false ? requestTimeoutPolicyName : null, order, endpointGroupName); + var withRequestTimeout = state.WithRequestTimeout ?? false; + var requestTimeoutPolicyName = withRequestTimeout ? state.RequestTimeoutPolicyName : null; + + return new EndpointConfiguration(metadata, state.RequireAuthorization ?? false, state.AuthorizationPolicies, state.DisableAntiforgery ?? false, + state.AllowAnonymous ?? false, state.RequireCors ?? false, state.CorsPolicyName, state.RequiredHosts, state.RequireRateLimiting ?? false, + state.RateLimitingPolicyName, ToEquatableOrNull(state.EndpointFilters), state.ShortCircuit ?? false, state.DisableValidation ?? false, + state.DisableRequestTimeout ?? false, withRequestTimeout, requestTimeoutPolicyName, state.Order, state.EndpointGroupName); } private static void GetAdditionalRequestHandlerAttributeValues( ImmutableArray attributes, - ref EquatableImmutableArray? tags, - ref bool? requireAuthorization, - ref EquatableImmutableArray? authorizationPolicies, - ref bool? disableAntiforgery, - ref bool? allowAnonymous, - ref bool? excludeFromDescription, - ref List? accepts, - ref List? produces, - ref List? producesProblem, - ref List? producesValidationProblem, - ref bool? requireCors, - ref string? corsPolicyName, - ref EquatableImmutableArray? requiredHosts, - ref bool? requireRateLimiting, - ref string? rateLimitingPolicyName, - ref List? endpointFilters, - ref bool hasAllowAnonymousAttribute, - ref bool hasRequireAuthorizationAttribute, - ref bool? shortCircuit, - ref bool? disableValidation, - ref bool? disableRequestTimeout, - ref bool? withRequestTimeout, - ref string? requestTimeoutPolicyName, - ref int? order, - ref string? endpointGroupName, - ref string? summary + ref EndpointAttributeState state ) { + ref var tags = ref state.Tags; + ref var requireAuthorization = ref state.RequireAuthorization; + ref var authorizationPolicies = ref state.AuthorizationPolicies; + ref var disableAntiforgery = ref state.DisableAntiforgery; + ref var allowAnonymous = ref state.AllowAnonymous; + ref var excludeFromDescription = ref state.ExcludeFromDescription; + ref var accepts = ref state.Accepts; + ref var produces = ref state.Produces; + ref var producesProblem = ref state.ProducesProblem; + ref var producesValidationProblem = ref state.ProducesValidationProblem; + ref var requireCors = ref state.RequireCors; + ref var corsPolicyName = ref state.CorsPolicyName; + ref var requiredHosts = ref state.RequiredHosts; + ref var requireRateLimiting = ref state.RequireRateLimiting; + ref var rateLimitingPolicyName = ref state.RateLimitingPolicyName; + ref var endpointFilters = ref state.EndpointFilters; + ref var hasAllowAnonymousAttribute = ref state.HasAllowAnonymousAttribute; + ref var hasRequireAuthorizationAttribute = ref state.HasRequireAuthorizationAttribute; + ref var shortCircuit = ref state.ShortCircuit; + ref var disableValidation = ref state.DisableValidation; + ref var disableRequestTimeout = ref state.DisableRequestTimeout; + ref var withRequestTimeout = ref state.WithRequestTimeout; + ref var requestTimeoutPolicyName = ref state.RequestTimeoutPolicyName; + ref var order = ref state.Order; + ref var endpointGroupName = ref state.EndpointGroupName; + ref var summary = ref state.Summary; + foreach (var attribute in attributes) { var attributeClass = attribute.AttributeClass; @@ -1151,15 +1125,7 @@ ref string? summary if (attribute.ConstructorArguments.Length > 0) { var arg = attribute.ConstructorArguments[0]; - if (arg.Values.Length > 0) - { - var values = arg.Values - .Select(v => v.Value as string) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .Select(s => s!.Trim()); - - MergeInto(ref tags, values); - } + MergeInto(ref tags, arg.Values); } continue; @@ -1172,15 +1138,7 @@ ref string? summary if (attribute.ConstructorArguments.Length == 1) { var arg = attribute.ConstructorArguments[0]; - if (arg.Values.Length > 0) - { - var values = arg.Values - .Select(v => v.Value as string) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .Select(s => s!.Trim()); - - MergeInto(ref authorizationPolicies, values); - } + MergeInto(ref authorizationPolicies, arg.Values); } continue; @@ -1200,16 +1158,11 @@ ref string? summary var arg = attribute.ConstructorArguments[0]; if (arg is { Kind: TypedConstantKind.Array, Values.Length: > 0 }) { - var values = arg.Values - .Select(v => v.Value as string) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .Select(s => s!.Trim()); - - MergeInto(ref requiredHosts, values); + MergeInto(ref requiredHosts, arg.Values); } else if (arg.Value is string singleHost && !string.IsNullOrWhiteSpace(singleHost)) { - MergeInto(ref requiredHosts, [singleHost]); + MergeInto(ref requiredHosts, new[] { singleHost.Trim() }); } } @@ -1291,6 +1244,29 @@ private static void MergeInto(ref EquatableImmutableArray? target, IEnum target = merged.Count > 0 ? merged : null; } + private static void MergeInto(ref EquatableImmutableArray? target, ImmutableArray values) + { + if (values.IsDefaultOrEmpty) + return; + + List? normalized = null; + foreach (var value in values) + { + if (value.Value is not string stringValue) + continue; + + var trimmed = NormalizeOptionalString(stringValue); + if (trimmed is not { Length: > 0 }) + continue; + + normalized ??= new List(values.Length); + normalized.Add(trimmed); + } + + if (normalized is { Count: > 0 }) + MergeInto(ref target, normalized); + } + private static EquatableImmutableArray? ToEquatableOrNull(List? values) { return values is { Count: > 0 } ? values.ToEquatableImmutableArray() : null; @@ -1879,7 +1855,7 @@ private static ImmutableHashSet GetRequestHandlersWithNameCollisions(Immuta if (requestHandlers.IsDefaultOrEmpty) return collidingIndices.ToImmutable(); - var nameToMethodMap = new Dictionary>>(StringComparer.Ordinal); + var nameToMethodMap = new Dictionary>(requestHandlers.Length); for (var index = 0; index < requestHandlers.Length; index++) { @@ -1888,32 +1864,23 @@ private static ImmutableHashSet GetRequestHandlersWithNameCollisions(Immuta if (string.IsNullOrEmpty(name)) continue; - if (!nameToMethodMap.TryGetValue(name!, out var methodMap)) - { - methodMap = new Dictionary>(StringComparer.Ordinal); - nameToMethodMap.Add(name!, methodMap); - } - - var methodName = handler.Method.Name; - if (!methodMap.TryGetValue(methodName, out var indices)) + var key = new HandlerNameKey(name!, handler.Method.Name); + if (!nameToMethodMap.TryGetValue(key, out var indices)) { indices = new List(); - methodMap.Add(methodName, indices); + nameToMethodMap.Add(key, indices); } indices.Add(index); } - foreach (var methodMap in nameToMethodMap.Values) + foreach (var indices in nameToMethodMap.Values) { - foreach (var indices in methodMap.Values) - { - if (indices.Count <= 1) - continue; + if (indices.Count <= 1) + continue; - foreach (var index in indices) - collidingIndices.Add(index); - } + foreach (var index in indices) + collidingIndices.Add(index); } return collidingIndices.ToImmutable(); @@ -2806,6 +2773,38 @@ private readonly record struct ProducesValidationProblemMetadata( private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); + private readonly record struct HandlerNameKey(string Name, string Method); + + private struct EndpointAttributeState + { + public EquatableImmutableArray? Tags; + public bool? RequireAuthorization; + public EquatableImmutableArray? AuthorizationPolicies; + public bool? DisableAntiforgery; + public bool? AllowAnonymous; + public bool? ExcludeFromDescription; + public List? Accepts; + public List? Produces; + public List? ProducesProblem; + public List? ProducesValidationProblem; + public bool? RequireCors; + public string? CorsPolicyName; + public EquatableImmutableArray? RequiredHosts; + public bool? RequireRateLimiting; + public string? RateLimitingPolicyName; + public List? EndpointFilters; + public bool HasAllowAnonymousAttribute; + public bool HasRequireAuthorizationAttribute; + public bool? ShortCircuit; + public bool? DisableValidation; + public bool? DisableRequestTimeout; + public bool? WithRequestTimeout; + public string? RequestTimeoutPolicyName; + public int? Order; + public string? EndpointGroupName; + public string? Summary; + } + private enum BindingSource { None = 0, From 1b00c2d811e2e5336b66422947511e706364bfbd Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:40:17 -0500 Subject: [PATCH 62/75] Optimize generator attribute handling (#49) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 348 +++++++++--------- 1 file changed, 183 insertions(+), 165 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 08f57e0..0fab827 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1044,154 +1044,132 @@ ref EndpointAttributeState state if (attributeClass is null) continue; - if (IsGeneratedAttribute(attributeClass, ShortCircuitAttributeName)) + switch (GetGeneratedAttributeKind(attributeClass)) { - shortCircuit = true; - continue; - } - - if (IsGeneratedAttribute(attributeClass, DisableValidationAttributeName)) - { - disableValidation = true; - continue; - } - - if (IsGeneratedAttribute(attributeClass, DisableRequestTimeoutAttributeName)) - { - disableRequestTimeout = true; - withRequestTimeout = false; - requestTimeoutPolicyName = null; - continue; - } - - if (IsGeneratedAttribute(attributeClass, RequestTimeoutAttributeName)) - { - disableRequestTimeout = false; - withRequestTimeout = true; - - string? policyName = null; - if (attribute.ConstructorArguments.Length > 0) - policyName = attribute.ConstructorArguments[0].Value as string; - - policyName ??= GetNamedStringValue(attribute, PolicyNameAttributeNamedParameter); - - requestTimeoutPolicyName = NormalizeOptionalString(policyName); - continue; - } - - if (IsGeneratedAttribute(attributeClass, OrderAttributeName)) - { - if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int orderValue) - order = orderValue; - - continue; - } - - if (IsGeneratedAttribute(attributeClass, MapGroupAttributeName)) - { - var groupName = GetNamedStringValue(attribute, NameAttributeNamedParameter); - if (!string.IsNullOrEmpty(groupName)) - endpointGroupName = groupName; - - continue; - } - - if (IsGeneratedAttribute(attributeClass, SummaryAttributeName)) - { - if (attribute.ConstructorArguments.Length > 0) + case GeneratedAttributeKind.ShortCircuit: + shortCircuit = true; + continue; + case GeneratedAttributeKind.DisableValidation: + disableValidation = true; + continue; + case GeneratedAttributeKind.DisableRequestTimeout: + disableRequestTimeout = true; + withRequestTimeout = false; + requestTimeoutPolicyName = null; + continue; + case GeneratedAttributeKind.RequestTimeout: { - var summaryValue = NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string); - if (!string.IsNullOrEmpty(summaryValue)) - summary = summaryValue; - } + disableRequestTimeout = false; + withRequestTimeout = true; - continue; - } + string? policyName = null; + if (attribute.ConstructorArguments.Length > 0) + policyName = attribute.ConstructorArguments[0].Value as string; - if (IsGeneratedAttribute(attributeClass, AcceptsAttributeName)) - { - TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); - continue; - } - - if (IsGeneratedAttribute(attributeClass, ProducesResponseAttributeName)) - { - TryAddProducesMetadata(attribute, attributeClass, ref produces); - continue; - } - - if (IsAttribute(attributeClass, "TagsAttribute", AspNetCoreHttpNamespaceParts)) - { - if (attribute.ConstructorArguments.Length > 0) - { - var arg = attribute.ConstructorArguments[0]; - MergeInto(ref tags, arg.Values); + policyName ??= GetNamedStringValue(attribute, PolicyNameAttributeNamedParameter); + requestTimeoutPolicyName = NormalizeOptionalString(policyName); + continue; } - - continue; - } - - if (IsGeneratedAttribute(attributeClass, RequireAuthorizationAttributeName)) - { - requireAuthorization = true; - hasRequireAuthorizationAttribute = true; - if (attribute.ConstructorArguments.Length == 1) + case GeneratedAttributeKind.Order: + if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int orderValue) + order = orderValue; + continue; + case GeneratedAttributeKind.MapGroup: { - var arg = attribute.ConstructorArguments[0]; - MergeInto(ref authorizationPolicies, arg.Values); + var groupName = GetNamedStringValue(attribute, NameAttributeNamedParameter); + if (!string.IsNullOrEmpty(groupName)) + endpointGroupName = groupName; + continue; } - - continue; - } - - if (IsGeneratedAttribute(attributeClass, RequireCorsAttributeName)) - { - requireCors = true; - corsPolicyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; - continue; - } - - if (IsGeneratedAttribute(attributeClass, RequireHostAttributeName)) - { - if (attribute.ConstructorArguments.Length == 1) - { - var arg = attribute.ConstructorArguments[0]; - if (arg is { Kind: TypedConstantKind.Array, Values.Length: > 0 }) + case GeneratedAttributeKind.Summary: + if (attribute.ConstructorArguments.Length > 0) { - MergeInto(ref requiredHosts, arg.Values); + var summaryValue = NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string); + if (!string.IsNullOrEmpty(summaryValue)) + summary = summaryValue; } - else if (arg.Value is string singleHost && !string.IsNullOrWhiteSpace(singleHost)) + continue; + case GeneratedAttributeKind.Accepts: + TryAddAcceptsMetadata(attribute, attributeClass, ref accepts); + continue; + case GeneratedAttributeKind.ProducesResponse: + TryAddProducesMetadata(attribute, attributeClass, ref produces); + continue; + case GeneratedAttributeKind.RequireAuthorization: + requireAuthorization = true; + hasRequireAuthorizationAttribute = true; + if (attribute.ConstructorArguments.Length == 1) { - MergeInto(ref requiredHosts, new[] { singleHost.Trim() }); + var arg = attribute.ConstructorArguments[0]; + MergeInto(ref authorizationPolicies, arg.Values); } - } - continue; - } - - if (IsGeneratedAttribute(attributeClass, RequireRateLimitingAttributeName)) - { - var policyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; + continue; + case GeneratedAttributeKind.RequireCors: + requireCors = true; + corsPolicyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; + continue; + case GeneratedAttributeKind.RequireHost: + if (attribute.ConstructorArguments.Length == 1) + { + var arg = attribute.ConstructorArguments[0]; + if (arg is { Kind: TypedConstantKind.Array, Values.Length: > 0 }) + { + MergeInto(ref requiredHosts, arg.Values); + } + else if (arg.Value is string singleHost && !string.IsNullOrWhiteSpace(singleHost)) + { + MergeInto(ref requiredHosts, new[] { singleHost.Trim() }); + } + } - if (!string.IsNullOrEmpty(policyName)) + continue; + case GeneratedAttributeKind.RequireRateLimiting: { - requireRateLimiting = true; - rateLimitingPolicyName = policyName; - } - - continue; - } + var policyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; - if (IsGeneratedAttribute(attributeClass, EndpointFilterAttributeName)) - { - TryAddEndpointFilter(attribute, attributeClass, ref endpointFilters); - continue; - } + if (!string.IsNullOrEmpty(policyName)) + { + requireRateLimiting = true; + rateLimitingPolicyName = policyName; + } - if (IsGeneratedAttribute(attributeClass, DisableAntiforgeryAttributeName)) - { - disableAntiforgery = true; - continue; + continue; + } + case GeneratedAttributeKind.EndpointFilter: + TryAddEndpointFilter(attribute, attributeClass, ref endpointFilters); + continue; + case GeneratedAttributeKind.DisableAntiforgery: + disableAntiforgery = true; + continue; + case GeneratedAttributeKind.ProducesProblem: + { + var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode + ? producesProblemStatusCode + : 500; + var contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) + : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; + + var producesProblemList = producesProblem ??= []; + producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes)); + continue; + } + case GeneratedAttributeKind.ProducesValidationProblem: + { + var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode + ? producesValidationProblemStatusCode + : 400; + var contentType = attribute.ConstructorArguments.Length > 1 + ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) + : null; + var additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; + + var producesValidationProblemList = producesValidationProblem ??= []; + producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes)); + continue; + } } if (IsAttribute(attributeClass, AllowAnonymousAttributeName, AspNetCoreAuthorizationNamespaceParts)) @@ -1201,39 +1179,21 @@ ref EndpointAttributeState state continue; } - if (IsAttribute(attributeClass, "ExcludeFromDescriptionAttribute", AspNetCoreRoutingNamespaceParts)) + if (IsAttribute(attributeClass, "TagsAttribute", AspNetCoreHttpNamespaceParts)) { - excludeFromDescription = true; - continue; - } + if (attribute.ConstructorArguments.Length > 0) + { + var arg = attribute.ConstructorArguments[0]; + MergeInto(ref tags, arg.Values); + } - if (IsGeneratedAttribute(attributeClass, ProducesProblemAttributeName)) - { - var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesProblemStatusCode - ? producesProblemStatusCode - : 500; - var contentType = attribute.ConstructorArguments.Length > 1 - ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) - : null; - var additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; - - var producesProblemList = producesProblem ??= []; - producesProblemList.Add(new ProducesProblemMetadata(statusCode, contentType, additionalContentTypes)); continue; } - if (IsGeneratedAttribute(attributeClass, ProducesValidationProblemAttributeName)) + if (IsAttribute(attributeClass, "ExcludeFromDescriptionAttribute", AspNetCoreRoutingNamespaceParts)) { - var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode - ? producesValidationProblemStatusCode - : 400; - var contentType = attribute.ConstructorArguments.Length > 1 - ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) - : null; - var additionalContentTypes = attribute.ConstructorArguments.Length > 2 ? GetStringArrayValues(attribute.ConstructorArguments[2]) : null; - - var producesValidationProblemList = producesValidationProblem ??= []; - producesValidationProblemList.Add(new ProducesValidationProblemMetadata(statusCode, contentType, additionalContentTypes)); + excludeFromDescription = true; + continue; } } } @@ -1295,7 +1255,7 @@ private static string NormalizeRequiredContentType(string? contentType, string d if (attributeClass is null) continue; - if (!IsGeneratedAttribute(attributeClass, MapGroupAttributeName)) + if (GetGeneratedAttributeKind(attributeClass) != GeneratedAttributeKind.MapGroup) continue; if (attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string pattern) @@ -1337,10 +1297,33 @@ private static string GetMapGroupIdentifier(string className) return builder.Count > 0 ? builder.ToEquatableImmutable() : null; } - private static bool IsGeneratedAttribute(INamedTypeSymbol attributeClass, string attributeName) + private static GeneratedAttributeKind GetGeneratedAttributeKind(INamedTypeSymbol attributeClass) { var definition = attributeClass.OriginalDefinition; - return definition.Name == attributeName && IsInNamespace(definition.ContainingNamespace, AttributesNamespaceParts); + if (!IsInNamespace(definition.ContainingNamespace, AttributesNamespaceParts)) + return GeneratedAttributeKind.None; + + return definition.Name switch + { + ShortCircuitAttributeName => GeneratedAttributeKind.ShortCircuit, + DisableValidationAttributeName => GeneratedAttributeKind.DisableValidation, + DisableRequestTimeoutAttributeName => GeneratedAttributeKind.DisableRequestTimeout, + RequestTimeoutAttributeName => GeneratedAttributeKind.RequestTimeout, + OrderAttributeName => GeneratedAttributeKind.Order, + MapGroupAttributeName => GeneratedAttributeKind.MapGroup, + SummaryAttributeName => GeneratedAttributeKind.Summary, + AcceptsAttributeName => GeneratedAttributeKind.Accepts, + ProducesResponseAttributeName => GeneratedAttributeKind.ProducesResponse, + RequireAuthorizationAttributeName => GeneratedAttributeKind.RequireAuthorization, + RequireCorsAttributeName => GeneratedAttributeKind.RequireCors, + RequireHostAttributeName => GeneratedAttributeKind.RequireHost, + RequireRateLimitingAttributeName => GeneratedAttributeKind.RequireRateLimiting, + EndpointFilterAttributeName => GeneratedAttributeKind.EndpointFilter, + DisableAntiforgeryAttributeName => GeneratedAttributeKind.DisableAntiforgery, + ProducesProblemAttributeName => GeneratedAttributeKind.ProducesProblem, + ProducesValidationProblemAttributeName => GeneratedAttributeKind.ProducesValidationProblem, + _ => GeneratedAttributeKind.None, + }; } private static bool IsAttribute(INamedTypeSymbol attributeClass, string attributeName, string[] namespaceParts) @@ -2256,7 +2239,7 @@ private static void AppendEndpointConfiguration(StringBuilder source, string ind source.AppendLine(); source.Append(indent); source.Append(".WithTags("); - source.Append(string.Join(", ", metadata.Tags.Value.Select(StringLiteral))); + AppendCommaSeparatedLiterals(source, metadata.Tags.Value); source.Append(')'); } @@ -2319,7 +2302,7 @@ private static void AppendEndpointConfiguration(StringBuilder source, string ind { source.Append(indent); source.Append(".RequireAuthorization("); - source.Append(string.Join(", ", configuration.AuthorizationPolicies.Value.Select(StringLiteral))); + AppendCommaSeparatedLiterals(source, configuration.AuthorizationPolicies.Value); source.Append(')'); } else @@ -2350,7 +2333,7 @@ private static void AppendEndpointConfiguration(StringBuilder source, string ind source.AppendLine(); source.Append(indent); source.Append(".RequireHost("); - source.Append(string.Join(", ", configuration.RequiredHosts.Value.Select(StringLiteral))); + AppendCommaSeparatedLiterals(source, configuration.RequiredHosts.Value); source.Append(')'); } @@ -2666,6 +2649,19 @@ private static void AppendAdditionalContentTypes(StringBuilder source, Equatable } } + private static void AppendCommaSeparatedLiterals(StringBuilder source, EquatableImmutableArray values) + { + if (values.Count == 0) + return; + + source.Append(StringLiteral(values[0])); + for (var i = 1; i < values.Count; i++) + { + source.Append(", "); + source.Append(StringLiteral(values[i])); + } + } + private static void AppendOptionalContentTypes(StringBuilder source, string? contentType, EquatableImmutableArray? additionalContentTypes) { if (string.IsNullOrEmpty(contentType) && additionalContentTypes is not { Count: > 0 }) @@ -2805,6 +2801,28 @@ private struct EndpointAttributeState public string? Summary; } + private enum GeneratedAttributeKind + { + None = 0, + ShortCircuit, + DisableValidation, + DisableRequestTimeout, + RequestTimeout, + Order, + MapGroup, + Summary, + Accepts, + ProducesResponse, + RequireAuthorization, + RequireCors, + RequireHost, + RequireRateLimiting, + EndpointFilter, + DisableAntiforgery, + ProducesProblem, + ProducesValidationProblem, + } + private enum BindingSource { None = 0, From 332867bc11e3cc9eb7fba88cb92125a02115d4d6 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:54:15 -0500 Subject: [PATCH 63/75] Improve generator performance with aggressive caching (#50) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 123 +++++++++++++++--- 1 file changed, 103 insertions(+), 20 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 0fab827..0330e7e 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Runtime.CompilerServices; +using System.Threading; using System.Text; using GeneratedEndpoints.Common; using Microsoft.CodeAnalysis; @@ -115,6 +117,9 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private static readonly string[] AspNetCoreAuthorizationNamespaceParts = ["Microsoft", "AspNetCore", "Authorization"]; private static readonly string[] AspNetCoreRoutingNamespaceParts = ["Microsoft", "AspNetCore", "Routing"]; private static readonly string[] ComponentModelNamespaceParts = ["System", "ComponentModel"]; + private static readonly ConditionalWeakTable CompilationTypeCaches = new(); + private static readonly ConditionalWeakTable RequestHandlerClassCache = new(); + private static readonly ConditionalWeakTable GeneratedAttributeKindCache = new(); private static readonly ImmutableArray HttpAttributeDefinitions = [ @@ -896,12 +901,10 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke return null; var attribute = context.Attributes[0]; - var requestHandlerClassResult = GetRequestHandlerClass(requestHandlerMethodSymbol, context.SemanticModel.Compilation, cancellationToken); - if (requestHandlerClassResult is null) + var requestHandlerClass = GetRequestHandlerClass(requestHandlerMethodSymbol, context.SemanticModel.Compilation, cancellationToken); + if (requestHandlerClass is null) return null; - var (_, requestHandlerClass) = requestHandlerClassResult.Value; - var requestHandlerMethod = GetRequestHandlerMethod(requestHandlerMethodSymbol, cancellationToken); var (httpMethod, pattern, name) = GetRequestHandlerAttribute(attribute, cancellationToken); @@ -912,7 +915,7 @@ private static bool RequestHandlerFilter(SyntaxNode syntaxNode, CancellationToke var methodConfiguration = GetEndpointConfiguration(requestHandlerMethodSymbol.GetAttributes(), name, displayName, description, true); - var requestHandler = new RequestHandler(requestHandlerClass, requestHandlerMethod, httpMethod, pattern, methodConfiguration); + var requestHandler = new RequestHandler(requestHandlerClass.Value, requestHandlerMethod, httpMethod, pattern, methodConfiguration); return requestHandler; } @@ -1247,6 +1250,7 @@ private static string NormalizeRequiredContentType(string? contentType, string d return string.IsNullOrWhiteSpace(value) ? null : value!.Trim(); } + [SuppressMessage("Major Code Smell", "S3398:Move this method into a class of its own", Justification = "Shared helper for multiple caching paths.")] private static string? GetMapGroupPattern(INamedTypeSymbol classSymbol) { foreach (var attribute in classSymbol.GetAttributes()) @@ -1265,6 +1269,7 @@ private static string NormalizeRequiredContentType(string? contentType, string d return null; } + [SuppressMessage("Major Code Smell", "S3398:Move this method into a class of its own", Justification = "Shared helper for multiple caching paths.")] private static string GetMapGroupIdentifier(string className) { if (className.StartsWith(GlobalPrefix, StringComparison.Ordinal)) @@ -1300,6 +1305,15 @@ private static string GetMapGroupIdentifier(string className) private static GeneratedAttributeKind GetGeneratedAttributeKind(INamedTypeSymbol attributeClass) { var definition = attributeClass.OriginalDefinition; + var cacheEntry = GeneratedAttributeKindCache.GetValue(definition, static def => + new GeneratedAttributeKindCacheEntry(GetGeneratedAttributeKindCore(def)) + ); + + return cacheEntry.Kind; + } + + private static GeneratedAttributeKind GetGeneratedAttributeKindCore(INamedTypeSymbol definition) + { if (!IsInNamespace(definition.ContainingNamespace, AttributesNamespaceParts)) return GeneratedAttributeKind.None; @@ -1532,7 +1546,7 @@ private static RequestHandlerMethod GetRequestHandlerMethod(IMethodSymbol method return requestHandlerMethod; } - private static (INamedTypeSymbol RequestHandlerClassSymbol, RequestHandlerClass RequestHandlerClass)? GetRequestHandlerClass( + private static RequestHandlerClass? GetRequestHandlerClass( IMethodSymbol methodSymbol, Compilation compilation, CancellationToken cancellationToken @@ -1544,23 +1558,19 @@ CancellationToken cancellationToken if (classSymbol.TypeKind != TypeKind.Class) return null; - var name = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var isStatic = classSymbol.IsStatic; - var endpointConventionBuilderSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); - var serviceProviderSymbol = compilation.GetTypeByMetadataName("System.IServiceProvider"); - var configureMethodDetails = GetConfigureMethodDetails(classSymbol, endpointConventionBuilderSymbol, serviceProviderSymbol, cancellationToken); - - var mapGroupPattern = GetMapGroupPattern(classSymbol); - var mapGroupIdentifier = mapGroupPattern is null ? null : GetMapGroupIdentifier(name); - var classConfiguration = GetEndpointConfiguration(classSymbol.GetAttributes(), null, null, null, false); - - var requestHandlerClass = new RequestHandlerClass(name, isStatic, configureMethodDetails.HasConfigureMethod, - configureMethodDetails.ConfigureMethodAcceptsServiceProvider, mapGroupPattern, mapGroupIdentifier, classConfiguration - ); + var typeCache = GetCompilationTypeCache(compilation); + var cacheEntry = RequestHandlerClassCache.GetValue(classSymbol, static _ => new RequestHandlerClassCacheEntry()); + var requestHandlerClass = cacheEntry.GetOrCreate(classSymbol, typeCache, cancellationToken); + return requestHandlerClass; + } - return (classSymbol, requestHandlerClass); + private static CompilationTypeCache GetCompilationTypeCache(Compilation compilation) + { + return CompilationTypeCaches.GetValue(compilation, static c => new CompilationTypeCache(c)); } + + [SuppressMessage("Major Code Smell", "S3398:Move this method into a class of its own", Justification = "Shared helper reused by caching infrastructure.")] private static ConfigureMethodDetails GetConfigureMethodDetails( INamedTypeSymbol classSymbol, INamedTypeSymbol? endpointConventionBuilderSymbol, @@ -2857,4 +2867,77 @@ public int Compare(RequestHandler x, RequestHandler y) return string.Compare(x.Pattern, y.Pattern, StringComparison.Ordinal); } } + + private sealed class CompilationTypeCache + { + public CompilationTypeCache(Compilation compilation) + { + EndpointConventionBuilderSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); + ServiceProviderSymbol = compilation.GetTypeByMetadataName("System.IServiceProvider"); + } + + public INamedTypeSymbol? EndpointConventionBuilderSymbol { get; } + + public INamedTypeSymbol? ServiceProviderSymbol { get; } + } + + private sealed class RequestHandlerClassCacheEntry + { + private RequestHandlerClass _value; + private bool _initialized; + private readonly object _lock = new(); + + public RequestHandlerClass GetOrCreate( + INamedTypeSymbol classSymbol, + CompilationTypeCache compilationCache, + CancellationToken cancellationToken + ) + { + if (_initialized) + return _value; + + lock (_lock) + { + if (_initialized) + return _value; + + cancellationToken.ThrowIfCancellationRequested(); + + var name = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var isStatic = classSymbol.IsStatic; + var configureMethodDetails = GetConfigureMethodDetails( + classSymbol, + compilationCache.EndpointConventionBuilderSymbol, + compilationCache.ServiceProviderSymbol, + cancellationToken + ); + + var mapGroupPattern = GetMapGroupPattern(classSymbol); + var mapGroupIdentifier = mapGroupPattern is null ? null : GetMapGroupIdentifier(name); + var classConfiguration = GetEndpointConfiguration(classSymbol.GetAttributes(), null, null, null, false); + + _value = new RequestHandlerClass( + name, + isStatic, + configureMethodDetails.HasConfigureMethod, + configureMethodDetails.ConfigureMethodAcceptsServiceProvider, + mapGroupPattern, + mapGroupIdentifier, + classConfiguration + ); + _initialized = true; + return _value; + } + } + } + + private sealed class GeneratedAttributeKindCacheEntry + { + public GeneratedAttributeKindCacheEntry(GeneratedAttributeKind kind) + { + Kind = kind; + } + + public GeneratedAttributeKind Kind { get; } + } } From 1363b9444557bf4778ff2d45dcb28a8d41a11000 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:04:42 -0500 Subject: [PATCH 64/75] Optimize source generator performance (#51) --- .../Common/StringBuilderPool.cs | 47 + src/GeneratedEndpoints/MinimalApiGenerator.cs | 1294 ++++++++--------- 2 files changed, 687 insertions(+), 654 deletions(-) create mode 100644 src/GeneratedEndpoints/Common/StringBuilderPool.cs diff --git a/src/GeneratedEndpoints/Common/StringBuilderPool.cs b/src/GeneratedEndpoints/Common/StringBuilderPool.cs new file mode 100644 index 0000000..fc8b4bc --- /dev/null +++ b/src/GeneratedEndpoints/Common/StringBuilderPool.cs @@ -0,0 +1,47 @@ +using System; +using System.Text; + +namespace GeneratedEndpoints.Common; + +/// Provides a simple per-thread cache for instances. +internal static class StringBuilderPool +{ + private const int MaxBuilderCapacity = 128 * 1024; + + [ThreadStatic] + private static StringBuilder? _cachedInstance; + + /// Gets a with at least the requested capacity. + public static StringBuilder Get(int capacity = 16) + { + var builder = _cachedInstance; + if (builder is null) + return new StringBuilder(capacity); + + _cachedInstance = null; + builder.Clear(); + if (builder.Capacity < capacity) + builder.EnsureCapacity(capacity); + + return builder; + } + + /// Returns the to the pool if it is below the maximum retention size. + public static void Return(StringBuilder builder) + { + if (builder.Capacity > MaxBuilderCapacity) + return; + + builder.Clear(); + if (_cachedInstance is null) + _cachedInstance = builder; + } + + /// Converts the builder to a string and returns it to the pool. + public static string ToStringAndReturn(StringBuilder builder) + { + var result = builder.ToString(); + Return(builder); + return result; + } +} diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 0330e7e..c70d313 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -153,6 +153,612 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator #nullable enable """; + private static readonly SourceText RequireAuthorizationAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that authorization is required for the annotated endpoint or class. + /// Optionally restricts access to the specified authorization policies. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Attribute + { + /// + /// Gets the policy names that the endpoint or class requires. + /// + public string[] PolicyNames { get; } + + /// + /// Marks the endpoint or class as requiring authorization. + /// + public {{RequireAuthorizationAttributeName}}() + { + PolicyNames = []; + } + + /// + /// Marks the endpoint or class as requiring authorization with one or more policies. + /// + public {{RequireAuthorizationAttributeName}}(params string[] policyNames) + { + PolicyNames = policyNames ?? []; + } + } + """, Encoding.UTF8); + + private static readonly SourceText RequireCorsAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires a configured CORS policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional CORS policy name. + /// + public string? PolicyName { get; } + + /// + /// Marks the endpoint or class as requiring the default CORS policy. + /// + public {{RequireCorsAttributeName}}() + { + } + + /// + /// Marks the endpoint or class as requiring the specified named CORS policy. + /// + public {{RequireCorsAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + """, Encoding.UTF8); + + private static readonly SourceText RequireRateLimitingAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires the provided rate limiting policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The rate limiting policy to apply. + public {{RequireRateLimitingAttributeName}}(string policyName) + { + PolicyName = policyName; + } + + /// + /// Gets the rate limiting policy name. + /// + public string PolicyName { get; } + } + """, Encoding.UTF8); + + private static readonly SourceText RequireHostAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the allowed hosts for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireHostAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The hosts that are allowed to access the endpoint. + public {{RequireHostAttributeName}}(params string[] hosts) + { + Hosts = hosts ?? []; + } + + /// + /// Gets the allowed hosts. + /// + public string[] Hosts { get; } + } + """, Encoding.UTF8); + + private static readonly SourceText DisableAntiforgeryAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Disables antiforgery protection for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attribute + { + } + + """, Encoding.UTF8); + + private static readonly SourceText ShortCircuitAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Marks the annotated endpoint or class to short-circuit the request pipeline. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute + { + } + + """, Encoding.UTF8); + + private static readonly SourceText DisableRequestTimeoutAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Disables the request timeout for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute + { + } + + """, Encoding.UTF8); + + private static readonly SourceText DisableValidationAttributeSourceText = SourceText.From( +$$""" +#if NET10_0_OR_GREATER +{{FileHeader}} + +namespace {{AttributesNamespace}}; + +/// +/// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute +{ +} +#endif + +""", Encoding.UTF8); + + private static readonly SourceText RequestTimeoutAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Applies the request timeout metadata to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional request timeout policy name. + /// + public string? PolicyName { get; init; } + + /// + /// Applies the default request timeout behavior. + /// + public {{RequestTimeoutAttributeName}}() + { + } + + /// + /// Applies the specified request timeout policy. + /// + /// The request timeout policy name. + public {{RequestTimeoutAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + + """, Encoding.UTF8); + + private static readonly SourceText OrderAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the order for the annotated endpoint when building conventions. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{OrderAttributeName}} : global::System.Attribute + { + /// + /// Gets the order that will be applied to the endpoint. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The order value to apply to the endpoint. + public {{OrderAttributeName}}(int order) + { + Order = order; + } + } + + """, Encoding.UTF8); + + private static readonly SourceText MapGroupAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the route group for the annotated class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal sealed class {{MapGroupAttributeName}} : global::System.Attribute + { + /// + /// Gets the route group pattern. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint group name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route group pattern to apply. + public {{MapGroupAttributeName}}(string pattern) + { + Pattern = pattern; + } + } + + """, Encoding.UTF8); + + private static readonly SourceText SummaryAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the summary metadata for the annotated endpoint. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{SummaryAttributeName}} : global::System.Attribute + { + /// + /// Gets the summary value for the endpoint. + /// + public string Summary { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The summary to apply to the endpoint. + public {{SummaryAttributeName}}(string summary) + { + Summary = summary; + } + } + + """, Encoding.UTF8); + + private static readonly SourceText AcceptsAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the request type and content types accepted by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type? RequestType { get; init; } + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. + /// + /// The CLR type of the request body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type RequestType => typeof(TRequest); + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Accepts attribute class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8); + + private static readonly SourceText EndpointFilterAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies an endpoint filter type to apply to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The CLR type of the endpoint filter. + public {{EndpointFilterAttributeName}}(global::System.Type filterType) + { + FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); + } + } + + /// + /// Specifies an endpoint filter type using a generic argument. + /// + /// The CLR type of the endpoint filter. + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType => typeof(TFilter); + } + + """, Encoding.UTF8); + + private static readonly SourceText ProducesResponseAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type? ResponseType { get; init; } + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. + /// + /// The CLR type of the response body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType => typeof(TResponse); + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Produces attribute class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8); + + private static readonly SourceText ProducesProblemAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8); + + private static readonly SourceText ProducesValidationProblemAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a validation problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesValidationProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8); + public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(RegisterAttributes); @@ -177,7 +783,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb, bool allowEmptyPattern = false) { var fullyQualifiedName = $"{AttributesNamespace}.{attributeName}"; - return new HttpAttributeDefinition(attributeName, fullyQualifiedName, $"{fullyQualifiedName}.gs.cs", verb, allowEmptyPattern); + var hint = $"{fullyQualifiedName}.gs.cs"; + var summaryVerb = verb == FallbackHttpMethod ? "fallback" : verb; + var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, attributeName, summaryVerb, allowEmptyPattern); + return new HttpAttributeDefinition(attributeName, fullyQualifiedName, hint, verb, allowEmptyPattern, SourceText.From(source, Encoding.UTF8)); } private static IncrementalValueProvider> CombineRequestHandlers( @@ -198,650 +807,25 @@ ImmutableArray>> handler private static void RegisterAttributes(IncrementalGeneratorPostInitializationContext context) { foreach (var definition in HttpAttributeDefinitions) - { - var summaryVerb = definition.Verb == FallbackHttpMethod ? "fallback" : definition.Verb; - var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, definition.Name, summaryVerb, definition.AllowEmptyPattern); - context.AddSource(definition.Hint, SourceText.From(source, Encoding.UTF8)); - } - - // RequireAuthorization - var requireAuthorizationSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that authorization is required for the annotated endpoint or class. - /// Optionally restricts access to the specified authorization policies. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Attribute - { - /// - /// Gets the policy names that the endpoint or class requires. - /// - public string[] PolicyNames { get; } - - /// - /// Marks the endpoint or class as requiring authorization. - /// - public {{RequireAuthorizationAttributeName}}() - { - PolicyNames = []; - } - - /// - /// Marks the endpoint or class as requiring authorization with one or more policies. - /// - public {{RequireAuthorizationAttributeName}}(params string[] policyNames) - { - PolicyNames = policyNames ?? []; - } - } - """; - context.AddSource(RequireAuthorizationAttributeHint, SourceText.From(requireAuthorizationSource, Encoding.UTF8)); - - // RequireCors - var requireCorsSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the annotated endpoint requires a configured CORS policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional CORS policy name. - /// - public string? PolicyName { get; } - - /// - /// Marks the endpoint or class as requiring the default CORS policy. - /// - public {{RequireCorsAttributeName}}() - { - } - - /// - /// Marks the endpoint or class as requiring the specified named CORS policy. - /// - public {{RequireCorsAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } - """; - context.AddSource(RequireCorsAttributeHint, SourceText.From(requireCorsSource, Encoding.UTF8)); - - // RequireRateLimiting - var requireRateLimitingSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the annotated endpoint requires the provided rate limiting policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The rate limiting policy to apply. - public {{RequireRateLimitingAttributeName}}(string policyName) - { - PolicyName = policyName; - } - - /// - /// Gets the rate limiting policy name. - /// - public string PolicyName { get; } - } - """; - context.AddSource(RequireRateLimitingAttributeHint, SourceText.From(requireRateLimitingSource, Encoding.UTF8)); - - // RequireHost - var requireHostSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the allowed hosts for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireHostAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The hosts that are allowed to access the endpoint. - public {{RequireHostAttributeName}}(params string[] hosts) - { - Hosts = hosts ?? []; - } - - /// - /// Gets the allowed hosts. - /// - public string[] Hosts { get; } - } - """; - context.AddSource(RequireHostAttributeHint, SourceText.From(requireHostSource, Encoding.UTF8)); - - // DisableAntiforgery - var disableAntiforgerySource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Disables antiforgery protection for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attribute - { - } - - """; - context.AddSource(DisableAntiforgeryAttributeHint, SourceText.From(disableAntiforgerySource, Encoding.UTF8)); - - // ShortCircuit - var shortCircuitSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Marks the annotated endpoint or class to short-circuit the request pipeline. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute - { - } - - """; - context.AddSource(ShortCircuitAttributeHint, SourceText.From(shortCircuitSource, Encoding.UTF8)); - - // DisableRequestTimeout - var disableRequestTimeoutSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Disables the request timeout for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute - { - } - - """; - context.AddSource(DisableRequestTimeoutAttributeHint, SourceText.From(disableRequestTimeoutSource, Encoding.UTF8)); - - // DisableValidation - var disableValidationSource = $$""" - #if NET10_0_OR_GREATER - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute - { - } - #endif - - """; - context.AddSource(DisableValidationAttributeHint, SourceText.From(disableValidationSource, Encoding.UTF8)); - - // RequestTimeout - var requestTimeoutSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Applies the request timeout metadata to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional request timeout policy name. - /// - public string? PolicyName { get; init; } - - /// - /// Applies the default request timeout behavior. - /// - public {{RequestTimeoutAttributeName}}() - { - } - - /// - /// Applies the specified request timeout policy. - /// - /// The request timeout policy name. - public {{RequestTimeoutAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } - - """; - context.AddSource(RequestTimeoutAttributeHint, SourceText.From(requestTimeoutSource, Encoding.UTF8)); - - // Order - var orderSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the order for the annotated endpoint when building conventions. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{OrderAttributeName}} : global::System.Attribute - { - /// - /// Gets the order that will be applied to the endpoint. - /// - public int Order { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The order value to apply to the endpoint. - public {{OrderAttributeName}}(int order) - { - Order = order; - } - } - - """; - context.AddSource(OrderAttributeHint, SourceText.From(orderSource, Encoding.UTF8)); - - // MapGroup - var mapGroupSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the route group for the annotated class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - internal sealed class {{MapGroupAttributeName}} : global::System.Attribute - { - /// - /// Gets the route group pattern. - /// - public string Pattern { get; } - - /// - /// Gets or sets the endpoint group name. - /// - public string? Name { get; init; } - - /// - /// Initializes a new instance of the class. - /// - /// The route group pattern to apply. - public {{MapGroupAttributeName}}(string pattern) - { - Pattern = pattern; - } - } - - """; - context.AddSource(MapGroupAttributeHint, SourceText.From(mapGroupSource, Encoding.UTF8)); - - // Summary - var summarySource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the summary metadata for the annotated endpoint. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{SummaryAttributeName}} : global::System.Attribute - { - /// - /// Gets the summary value for the endpoint. - /// - public string Summary { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The summary to apply to the endpoint. - public {{SummaryAttributeName}}(string summary) - { - Summary = summary; - } - } - - """; - context.AddSource(SummaryAttributeHint, SourceText.From(summarySource, Encoding.UTF8)); - - // Accepts - var acceptsSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the request type and content types accepted by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type? RequestType { get; init; } - - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } - - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } - - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - /// - /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. - /// - /// The CLR type of the request body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type RequestType => typeof(TRequest); - - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } - - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } - - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the generic Accepts attribute class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """; - context.AddSource(AcceptsAttributeHint, SourceText.From(acceptsSource, Encoding.UTF8)); - - // EndpointFilter - var endpointFilterSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies an endpoint filter type to apply to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute - { - /// - /// Gets the CLR type of the endpoint filter. - /// - public global::System.Type FilterType { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The CLR type of the endpoint filter. - public {{EndpointFilterAttributeName}}(global::System.Type filterType) - { - FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); - } - } - - /// - /// Specifies an endpoint filter type using a generic argument. - /// - /// The CLR type of the endpoint filter. - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute - { - /// - /// Gets the CLR type of the endpoint filter. - /// - public global::System.Type FilterType => typeof(TFilter); - } - - """; - context.AddSource(EndpointFilterAttributeHint, SourceText.From(endpointFilterSource, Encoding.UTF8)); - - // Produces - var producesSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type? ResponseType { get; init; } - - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - /// - /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. - /// - /// The CLR type of the response body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type ResponseType => typeof(TResponse); - - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the generic Produces attribute class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """; - context.AddSource(ProducesResponseAttributeHint, SourceText.From(producesSource, Encoding.UTF8)); - - // ProducesProblem - var producesProblemSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the endpoint produces a problem details payload. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute - { - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """; - context.AddSource(ProducesProblemAttributeHint, SourceText.From(producesProblemSource, Encoding.UTF8)); - - // ProducesValidationProblem - var producesValidationProblemSource = $$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the endpoint produces a validation problem details payload. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute - { - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesValidationProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """; - context.AddSource(ProducesValidationProblemAttributeHint, SourceText.From(producesValidationProblemSource, Encoding.UTF8)); + context.AddSource(definition.Hint, definition.SourceText); + + context.AddSource(RequireAuthorizationAttributeHint, RequireAuthorizationAttributeSourceText); + context.AddSource(RequireCorsAttributeHint, RequireCorsAttributeSourceText); + context.AddSource(RequireRateLimitingAttributeHint, RequireRateLimitingAttributeSourceText); + context.AddSource(RequireHostAttributeHint, RequireHostAttributeSourceText); + context.AddSource(DisableAntiforgeryAttributeHint, DisableAntiforgeryAttributeSourceText); + context.AddSource(ShortCircuitAttributeHint, ShortCircuitAttributeSourceText); + context.AddSource(DisableRequestTimeoutAttributeHint, DisableRequestTimeoutAttributeSourceText); + context.AddSource(DisableValidationAttributeHint, DisableValidationAttributeSourceText); + context.AddSource(RequestTimeoutAttributeHint, RequestTimeoutAttributeSourceText); + context.AddSource(OrderAttributeHint, OrderAttributeSourceText); + context.AddSource(MapGroupAttributeHint, MapGroupAttributeSourceText); + context.AddSource(SummaryAttributeHint, SummaryAttributeSourceText); + context.AddSource(AcceptsAttributeHint, AcceptsAttributeSourceText); + context.AddSource(EndpointFilterAttributeHint, EndpointFilterAttributeSourceText); + context.AddSource(ProducesResponseAttributeHint, ProducesResponseAttributeSourceText); + context.AddSource(ProducesProblemAttributeHint, ProducesProblemAttributeSourceText); + context.AddSource(ProducesValidationProblemAttributeHint, ProducesValidationProblemAttributeSourceText); } private static string GenerateHttpAttributeSource( @@ -1275,7 +1259,7 @@ private static string GetMapGroupIdentifier(string className) if (className.StartsWith(GlobalPrefix, StringComparison.Ordinal)) className = className.Substring(GlobalPrefix.Length); - var builder = new StringBuilder(className.Length + 8); + var builder = StringBuilderPool.Get(className.Length + 8); builder.Append('_'); foreach (var character in className) @@ -1284,7 +1268,7 @@ private static string GetMapGroupIdentifier(string className) } builder.Append("_Group"); - return builder.ToString(); + return StringBuilderPool.ToStringAndReturn(builder); } private static EquatableImmutableArray? GetStringArrayValues(TypedConstant typedConstant) @@ -1935,7 +1919,8 @@ private static void GenerateAddEndpointHandlersClass(SourceProductionContext con """ ); - context.AddSource(AddEndpointHandlersMethodHint, SourceText.From(source.ToString(), Encoding.UTF8)); + var sourceText = StringBuilderPool.ToStringAndReturn(source); + context.AddSource(AddEndpointHandlersMethodHint, SourceText.From(sourceText, Encoding.UTF8)); } [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", Justification = "Manual loops avoid repeated allocations in the source generator.")] @@ -1975,7 +1960,7 @@ private static StringBuilder GetAddEndpointHandlersStringBuilder(List no _ => estimate, }; - return new StringBuilder(estimate); + return StringBuilderPool.Get(estimate); } private static void GenerateUseEndpointHandlersClass(SourceProductionContext context, ImmutableArray requestHandlers) @@ -2045,7 +2030,8 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con """ ); - context.AddSource(UseEndpointHandlersMethodHint, SourceText.From(source.ToString(), Encoding.UTF8)); + var sourceText = StringBuilderPool.ToStringAndReturn(source); + context.AddSource(UseEndpointHandlersMethodHint, SourceText.From(sourceText, Encoding.UTF8)); } [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", Justification = "Manual loops avoid repeated allocations in the source generator.")] @@ -2543,7 +2529,7 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< if (estimate > 65536) estimate = 65536; - return new StringBuilder(estimate); + return StringBuilderPool.Get(estimate); } [SuppressMessage("Globalization", "CA1308: Normalize strings to uppercase", Justification = "C# boolean literals must be lowercase.")] @@ -2610,7 +2596,7 @@ private static string StringLiteral(string? value) if (value is null) return "null"; - var sb = new StringBuilder(value.Length + 2); + var sb = StringBuilderPool.Get(value.Length + 2); sb.Append('"'); foreach (var c in value) { @@ -2644,7 +2630,7 @@ private static string StringLiteral(string? value) } } sb.Append('"'); - return sb.ToString(); + return StringBuilderPool.ToStringAndReturn(sb); } private static void AppendAdditionalContentTypes(StringBuilder source, EquatableImmutableArray? additionalContentTypes) @@ -2697,7 +2683,7 @@ _ when char.IsControl(c) => "\\u" + ((int)c).ToString("x4", CultureInfo.Invarian }; } - private readonly record struct HttpAttributeDefinition(string Name, string FullyQualifiedName, string Hint, string Verb, bool AllowEmptyPattern); + private readonly record struct HttpAttributeDefinition(string Name, string FullyQualifiedName, string Hint, string Verb, bool AllowEmptyPattern, SourceText SourceText); private readonly record struct RequestHandler( RequestHandlerClass Class, From fc4bc3e5c30030ca68d49fede693819fd30d23a1 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:09:17 -0500 Subject: [PATCH 65/75] Add tests for attribute generation (#52) --- ...erationTests.AcceptsAttribute.verified.txt | 90 ++++++++++++ ...s.DisableAntiforgeryAttribute.verified.txt | 21 +++ ...isableRequestTimeoutAttribute.verified.txt | 21 +++ ...ts.DisableValidationAttribute.verified.txt | 23 +++ ...Tests.EndpointFilterAttribute.verified.txt | 47 +++++++ ...tionTests.MapConnectAttribute.verified.txt | 39 +++++ ...ationTests.MapDeleteAttribute.verified.txt | 39 +++++ ...ionTests.MapFallbackAttribute.verified.txt | 39 +++++ ...nerationTests.MapGetAttribute.verified.txt | 39 +++++ ...rationTests.MapGroupAttribute.verified.txt | 39 +++++ ...erationTests.MapHeadAttribute.verified.txt | 39 +++++ ...tionTests.MapOptionsAttribute.verified.txt | 39 +++++ ...rationTests.MapPatchAttribute.verified.txt | 39 +++++ ...erationTests.MapPostAttribute.verified.txt | 39 +++++ ...nerationTests.MapPutAttribute.verified.txt | 39 +++++ ...rationTests.MapQueryAttribute.verified.txt | 39 +++++ ...rationTests.MapTraceAttribute.verified.txt | 39 +++++ ...enerationTests.OrderAttribute.verified.txt | 34 +++++ ...ests.ProducesProblemAttribute.verified.txt | 48 +++++++ ...sts.ProducesResponseAttribute.verified.txt | 94 +++++++++++++ ...cesValidationProblemAttribute.verified.txt | 48 +++++++ ...Tests.RequestTimeoutAttribute.verified.txt | 41 ++++++ ...RequireAuthorizationAttribute.verified.txt | 42 ++++++ ...ionTests.RequireCorsAttribute.verified.txt | 40 ++++++ ...ionTests.RequireHostAttribute.verified.txt | 34 +++++ ....RequireRateLimitingAttribute.verified.txt | 34 +++++ ...onTests.ShortCircuitAttribute.verified.txt | 21 +++ ...erationTests.SummaryAttribute.verified.txt | 34 +++++ .../AttributeGenerationTests.cs | 133 ++++++++++++++++++ 29 files changed, 1273 insertions(+) create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableAntiforgeryAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableRequestTimeoutAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableValidationAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.EndpointFilterAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapConnectAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapDeleteAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGetAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGroupAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapHeadAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapOptionsAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPatchAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPostAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPutAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapQueryAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapTraceAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.OrderAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesProblemAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesResponseAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesValidationProblemAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireAuthorizationAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireCorsAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireHostAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireRateLimitingAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ShortCircuitAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.SummaryAttribute.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/AttributeGenerationTests.cs diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt new file mode 100644 index 0000000..8c2c4c1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt @@ -0,0 +1,90 @@ + //----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies the request type and content types accepted by the annotated endpoint or class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class AcceptsAttribute : global::System.Attribute +{ + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type? RequestType { get; init; } + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public AcceptsAttribute(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } +} + +/// +/// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. +/// +/// The CLR type of the request body. +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class AcceptsAttribute : global::System.Attribute +{ + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type RequestType => typeof(TRequest); + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Accepts attribute class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public AcceptsAttribute(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableAntiforgeryAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableAntiforgeryAttribute.verified.txt new file mode 100644 index 0000000..4ee8f6c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableAntiforgeryAttribute.verified.txt @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Disables antiforgery protection for the annotated endpoint or class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class DisableAntiforgeryAttribute : global::System.Attribute +{ +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableRequestTimeoutAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableRequestTimeoutAttribute.verified.txt new file mode 100644 index 0000000..1b5b5e6 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableRequestTimeoutAttribute.verified.txt @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Disables the request timeout for the annotated endpoint or class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class DisableRequestTimeoutAttribute : global::System.Attribute +{ +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableValidationAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableValidationAttribute.verified.txt new file mode 100644 index 0000000..d4329f5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.DisableValidationAttribute.verified.txt @@ -0,0 +1,23 @@ +#if NET10_0_OR_GREATER +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class DisableValidationAttribute : global::System.Attribute +{ +} +#endif diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.EndpointFilterAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.EndpointFilterAttribute.verified.txt new file mode 100644 index 0000000..e96674f --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.EndpointFilterAttribute.verified.txt @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies an endpoint filter type to apply to the annotated endpoint or class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class EndpointFilterAttribute : global::System.Attribute +{ + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The CLR type of the endpoint filter. + public EndpointFilterAttribute(global::System.Type filterType) + { + FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); + } +} + +/// +/// Specifies an endpoint filter type using a generic argument. +/// +/// The CLR type of the endpoint filter. +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class EndpointFilterAttribute : global::System.Attribute +{ + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType => typeof(TFilter); +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapConnectAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapConnectAttribute.verified.txt new file mode 100644 index 0000000..0d2d237 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapConnectAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP CONNECT minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapConnectAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapConnectAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapDeleteAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapDeleteAttribute.verified.txt new file mode 100644 index 0000000..e4b8449 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapDeleteAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP DELETE minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapDeleteAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapDeleteAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt new file mode 100644 index 0000000..461f2f9 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP fallback minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapFallbackAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapFallbackAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern = "") + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGetAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGetAttribute.verified.txt new file mode 100644 index 0000000..f769cb7 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGetAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP GET minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapGetAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapGetAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGroupAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGroupAttribute.verified.txt new file mode 100644 index 0000000..b089593 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapGroupAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies the route group for the annotated class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +internal sealed class MapGroupAttribute : global::System.Attribute +{ + /// + /// Gets the route group pattern. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint group name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route group pattern to apply. + public MapGroupAttribute(string pattern) + { + Pattern = pattern; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapHeadAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapHeadAttribute.verified.txt new file mode 100644 index 0000000..6c7e980 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapHeadAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP HEAD minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapHeadAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapHeadAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapOptionsAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapOptionsAttribute.verified.txt new file mode 100644 index 0000000..b873462 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapOptionsAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP OPTIONS minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapOptionsAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapOptionsAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPatchAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPatchAttribute.verified.txt new file mode 100644 index 0000000..8a13fd5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPatchAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP PATCH minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapPatchAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapPatchAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPostAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPostAttribute.verified.txt new file mode 100644 index 0000000..0afbe37 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPostAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP POST minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapPostAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapPostAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPutAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPutAttribute.verified.txt new file mode 100644 index 0000000..92762a0 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapPutAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP PUT minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapPutAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapPutAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapQueryAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapQueryAttribute.verified.txt new file mode 100644 index 0000000..ec47f1f --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapQueryAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP QUERY minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapQueryAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapQueryAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapTraceAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapTraceAttribute.verified.txt new file mode 100644 index 0000000..7d354c1 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapTraceAttribute.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Identifies a method as an HTTP TRACE minimal API endpoint with the specified route pattern. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class MapTraceAttribute : global::System.Attribute +{ + /// + /// Gets the route pattern for the endpoint. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route pattern for the endpoint. + public MapTraceAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern) + { + Pattern = pattern; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.OrderAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.OrderAttribute.verified.txt new file mode 100644 index 0000000..add300d --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.OrderAttribute.verified.txt @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies the order for the annotated endpoint when building conventions. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class OrderAttribute : global::System.Attribute +{ + /// + /// Gets the order that will be applied to the endpoint. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The order value to apply to the endpoint. + public OrderAttribute(int order) + { + Order = order; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesProblemAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesProblemAttribute.verified.txt new file mode 100644 index 0000000..516fc6f --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesProblemAttribute.verified.txt @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies that the endpoint produces a problem details payload. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class ProducesProblemAttribute : global::System.Attribute +{ + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public ProducesProblemAttribute(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesResponseAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesResponseAttribute.verified.txt new file mode 100644 index 0000000..e5d6748 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesResponseAttribute.verified.txt @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies a response type, status code, and content types produced by the annotated endpoint or class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class ProducesResponseAttribute : global::System.Attribute +{ +/// +/// Gets the response type produced by the endpoint. +/// +public global::System.Type? ResponseType { get; init; } + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public ProducesResponseAttribute(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } +} + +/// +/// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. +/// +/// The CLR type of the response body. +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class ProducesResponseAttribute : global::System.Attribute +{ + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType => typeof(TResponse); + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Produces attribute class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public ProducesResponseAttribute(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesValidationProblemAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesValidationProblemAttribute.verified.txt new file mode 100644 index 0000000..9d0ddd6 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ProducesValidationProblemAttribute.verified.txt @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies that the endpoint produces a validation problem details payload. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +internal sealed class ProducesValidationProblemAttribute : global::System.Attribute +{ + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public ProducesValidationProblemAttribute(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt new file mode 100644 index 0000000..746a157 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt @@ -0,0 +1,41 @@ + //----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + + namespace Microsoft.AspNetCore.Generated.Attributes; + + /// + /// Applies the request timeout metadata to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class RequestTimeoutAttribute : global::System.Attribute + { + /// + /// Gets the optional request timeout policy name. + /// + public string? PolicyName { get; init; } + + /// + /// Applies the default request timeout behavior. + /// + public RequestTimeoutAttribute() + { + } + + /// + /// Applies the specified request timeout policy. + /// + /// The request timeout policy name. + public RequestTimeoutAttribute(string policyName) + { + PolicyName = policyName; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireAuthorizationAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireAuthorizationAttribute.verified.txt new file mode 100644 index 0000000..cf3e247 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireAuthorizationAttribute.verified.txt @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies that authorization is required for the annotated endpoint or class. +/// Optionally restricts access to the specified authorization policies. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class RequireAuthorizationAttribute : global::System.Attribute +{ + /// + /// Gets the policy names that the endpoint or class requires. + /// + public string[] PolicyNames { get; } + + /// + /// Marks the endpoint or class as requiring authorization. + /// + public RequireAuthorizationAttribute() + { + PolicyNames = []; + } + + /// + /// Marks the endpoint or class as requiring authorization with one or more policies. + /// + public RequireAuthorizationAttribute(params string[] policyNames) + { + PolicyNames = policyNames ?? []; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireCorsAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireCorsAttribute.verified.txt new file mode 100644 index 0000000..ad8377c --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireCorsAttribute.verified.txt @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies that the annotated endpoint requires a configured CORS policy. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class RequireCorsAttribute : global::System.Attribute +{ + /// + /// Gets the optional CORS policy name. + /// + public string? PolicyName { get; } + + /// + /// Marks the endpoint or class as requiring the default CORS policy. + /// + public RequireCorsAttribute() + { + } + + /// + /// Marks the endpoint or class as requiring the specified named CORS policy. + /// + public RequireCorsAttribute(string policyName) + { + PolicyName = policyName; + } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireHostAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireHostAttribute.verified.txt new file mode 100644 index 0000000..2303b72 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireHostAttribute.verified.txt @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies the allowed hosts for the annotated endpoint or class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class RequireHostAttribute : global::System.Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The hosts that are allowed to access the endpoint. + public RequireHostAttribute(params string[] hosts) + { + Hosts = hosts ?? []; + } + + /// + /// Gets the allowed hosts. + /// + public string[] Hosts { get; } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireRateLimitingAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireRateLimitingAttribute.verified.txt new file mode 100644 index 0000000..678d330 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequireRateLimitingAttribute.verified.txt @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies that the annotated endpoint requires the provided rate limiting policy. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class RequireRateLimitingAttribute : global::System.Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The rate limiting policy to apply. + public RequireRateLimitingAttribute(string policyName) + { + PolicyName = policyName; + } + + /// + /// Gets the rate limiting policy name. + /// + public string PolicyName { get; } +} \ No newline at end of file diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ShortCircuitAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ShortCircuitAttribute.verified.txt new file mode 100644 index 0000000..fd75bc0 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.ShortCircuitAttribute.verified.txt @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Marks the annotated endpoint or class to short-circuit the request pipeline. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class ShortCircuitAttribute : global::System.Attribute +{ +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.SummaryAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.SummaryAttribute.verified.txt new file mode 100644 index 0000000..2fccdce --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.SummaryAttribute.verified.txt @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +namespace Microsoft.AspNetCore.Generated.Attributes; + +/// +/// Specifies the summary metadata for the annotated endpoint. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class SummaryAttribute : global::System.Attribute +{ + /// + /// Gets the summary value for the endpoint. + /// + public string Summary { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The summary to apply to the endpoint. + public SummaryAttribute(string summary) + { + Summary = summary; + } +} diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.cs b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.cs new file mode 100644 index 0000000..1831f55 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.cs @@ -0,0 +1,133 @@ +using GeneratedEndpoints.Tests.Common; +using Microsoft.CodeAnalysis; +using SourceGeneratorTestHelpers.XUnit; + +namespace GeneratedEndpoints.Tests; + +[UsesVerify] +public class AttributeGenerationTests +{ + private const string AttributeTestSource = "internal static class AttributeTestEndpoints { }"; + private static readonly GeneratorDriverRunResult GeneratorResult = + TestHelpers.RunGenerator(TestHelpers.GetSources(AttributeTestSource, withNamespace: true)); + + public AttributeGenerationTests() + { + ModuleInitializer.Initialize(); + } + + [Fact] + public Task MapGetAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapGetAttribute.gs.cs"); + + [Fact] + public Task MapPostAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapPostAttribute.gs.cs"); + + [Fact] + public Task MapPutAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapPutAttribute.gs.cs"); + + [Fact] + public Task MapPatchAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapPatchAttribute.gs.cs"); + + [Fact] + public Task MapDeleteAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapDeleteAttribute.gs.cs"); + + [Fact] + public Task MapOptionsAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapOptionsAttribute.gs.cs"); + + [Fact] + public Task MapHeadAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapHeadAttribute.gs.cs"); + + [Fact] + public Task MapQueryAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapQueryAttribute.gs.cs"); + + [Fact] + public Task MapTraceAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapTraceAttribute.gs.cs"); + + [Fact] + public Task MapConnectAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapConnectAttribute.gs.cs"); + + [Fact] + public Task MapFallbackAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapFallbackAttribute.gs.cs"); + + [Fact] + public Task RequireAuthorizationAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.RequireAuthorizationAttribute.gs.cs"); + + [Fact] + public Task RequireCorsAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.RequireCorsAttribute.gs.cs"); + + [Fact] + public Task RequireRateLimitingAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.RequireRateLimitingAttribute.gs.cs"); + + [Fact] + public Task RequireHostAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.RequireHostAttribute.gs.cs"); + + [Fact] + public Task DisableAntiforgeryAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.DisableAntiforgeryAttribute.gs.cs"); + + [Fact] + public Task ShortCircuitAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.ShortCircuitAttribute.gs.cs"); + + [Fact] + public Task DisableRequestTimeoutAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.DisableRequestTimeoutAttribute.gs.cs"); + + [Fact] + public Task DisableValidationAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.DisableValidationAttribute.gs.cs"); + + [Fact] + public Task RequestTimeoutAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.RequestTimeoutAttribute.gs.cs"); + + [Fact] + public Task OrderAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.OrderAttribute.gs.cs"); + + [Fact] + public Task MapGroupAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.MapGroupAttribute.gs.cs"); + + [Fact] + public Task SummaryAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.SummaryAttribute.gs.cs"); + + [Fact] + public Task AcceptsAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.AcceptsAttribute.gs.cs"); + + [Fact] + public Task EndpointFilterAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.EndpointFilterAttribute.gs.cs"); + + [Fact] + public Task ProducesResponseAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.ProducesResponseAttribute.gs.cs"); + + [Fact] + public Task ProducesProblemAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.ProducesProblemAttribute.gs.cs"); + + [Fact] + public Task ProducesValidationProblemAttribute() + => VerifyAttributeAsync("Microsoft.AspNetCore.Generated.Attributes.ProducesValidationProblemAttribute.gs.cs"); + + private static Task VerifyAttributeAsync(string fileName) + => GeneratorResult.VerifyAsync(fileName); +} From 271a5cdd2d8833609f4e35141b33e673687ee1b0 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:10:06 -0500 Subject: [PATCH 66/75] Add async method variant tests (#53) --- ...dVariants_AddEndpointHandlers.verified.txt | 24 +++++++++++ ...dVariants_MapEndpointHandlers.verified.txt | 39 ++++++++++++++++++ .../IndividualTests.cs | 41 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_AddEndpointHandlers.verified.txt create mode 100644 tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_MapEndpointHandlers.verified.txt diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_AddEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_AddEndpointHandlers.verified.txt new file mode 100644 index 0000000..fe15aef --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_AddEndpointHandlers.verified.txt @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointServicesExtensions +{ + internal static void AddEndpointHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_MapEndpointHandlers.verified.txt b/tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_MapEndpointHandlers.verified.txt new file mode 100644 index 0000000..2d957e5 --- /dev/null +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.AsyncMethodVariants_MapEndpointHandlers.verified.txt @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by MinimalApiGenerator which can be found +// in the GeneratedEndpoints namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Generated.Routing; + +internal static class EndpointRouteBuilderExtensions +{ + internal static IEndpointRouteBuilder MapEndpointHandlers(this IEndpointRouteBuilder builder) + { + builder.MapGet("/task", static ([FromServices] global::GeneratedEndpointsTests.AsyncHandlerEndpoints handler) => handler.TaskOnly()) + .WithName("TaskOnly"); + + builder.MapGet("/task-result", static async ([FromServices] global::GeneratedEndpointsTests.AsyncHandlerEndpoints handler, int id) => await handler.TaskWithResult(id)) + .WithName("TaskWithResult"); + + builder.MapPost("/valuetask", static ([FromServices] global::GeneratedEndpointsTests.AsyncHandlerEndpoints handler) => handler.ValueTaskOnly()) + .WithName("ValueTaskOnly"); + + builder.MapPost("/valuetask-result", static async ([FromServices] global::GeneratedEndpointsTests.AsyncHandlerEndpoints handler, int id) => await handler.ValueTaskWithResult(id)) + .WithName("ValueTaskWithResult"); + + return builder; + } +} diff --git a/tests/GeneratedEndpoints.Tests/IndividualTests.cs b/tests/GeneratedEndpoints.Tests/IndividualTests.cs index ace1135..e6c0265 100644 --- a/tests/GeneratedEndpoints.Tests/IndividualTests.cs +++ b/tests/GeneratedEndpoints.Tests/IndividualTests.cs @@ -446,6 +446,13 @@ public async Task ContractRequireAuthorization() await VerifyIndividualAsync(source, nameof(ContractRequireAuthorization)); } + [Fact] + public async Task AsyncMethodVariants() + { + var source = AsyncHandlerScenario(); + await VerifyIndividualAsync(source, nameof(AsyncMethodVariants)); + } + private static async Task VerifyIndividualAsync(string source, string scenario, bool withNamespace = true) { var sources = TestHelpers.GetSources(source, withNamespace); @@ -594,4 +601,38 @@ private static string ContractScenario( acceptsContentType2, producesContentType1, producesContentType2); + + private static string AsyncHandlerScenario() + => """ + using System.Threading.Tasks; + + internal sealed class AsyncHandlerEndpoints + { + [MapGet("/task")] + public async Task TaskOnly() + { + await Task.Yield(); + } + + [MapGet("/task-result")] + public async Task, NotFound>> TaskWithResult(int id) + { + await Task.Yield(); + return id >= 0 ? TypedResults.Ok("task") : TypedResults.NotFound(); + } + + [MapPost("/valuetask")] + public async ValueTask ValueTaskOnly() + { + await Task.Yield(); + } + + [MapPost("/valuetask-result")] + public async ValueTask, NotFound>> ValueTaskWithResult(int id) + { + await Task.Yield(); + return id >= 0 ? TypedResults.Ok("value") : TypedResults.NotFound(); + } + } + """; } From 3d1ab4426766d3a699124678ca95dd911598f6ec Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:14:59 -0500 Subject: [PATCH 67/75] Optimize generator performance (#54) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 112 ++++++++++++------ 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index c70d313..c3b49ad 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1700,7 +1700,7 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM { cancellationToken.ThrowIfCancellationRequested(); - var methodParameters = new List(); + var methodParameters = new List(methodSymbol.Parameters.Length); foreach (var parameter in methodSymbol.Parameters) { cancellationToken.ThrowIfCancellationRequested(); @@ -1752,7 +1752,8 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM var parameterName = parameter.Name; var parameterType = parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var key = typedKey.HasValue ? ConstLiteral(typedKey.Value) : null; - methodParameters.Add(new Parameter(parameterName, parameterType, source, key, bindingName)); + var bindingPrefix = GetBindingSourceAttribute(source, key, bindingName); + methodParameters.Add(new Parameter(parameterName, parameterType, bindingPrefix)); } return methodParameters.ToEquatableImmutableArray(); @@ -1826,13 +1827,13 @@ private static ImmutableArray EnsureUniqueEndpointNames(Immutabl return builder.MoveToImmutable(); } - private static ImmutableHashSet GetRequestHandlersWithNameCollisions(ImmutableArray requestHandlers) + private static ImmutableArray GetRequestHandlersWithNameCollisions(ImmutableArray requestHandlers) { - var collidingIndices = ImmutableHashSet.CreateBuilder(); if (requestHandlers.IsDefaultOrEmpty) - return collidingIndices.ToImmutable(); + return ImmutableArray.Empty; - var nameToMethodMap = new Dictionary>(requestHandlers.Length); + Dictionary? nameToFirstIndex = null; + HashSet? collidingIndices = null; for (var index = 0; index < requestHandlers.Length; index++) { @@ -1841,26 +1842,28 @@ private static ImmutableHashSet GetRequestHandlersWithNameCollisions(Immuta if (string.IsNullOrEmpty(name)) continue; + nameToFirstIndex ??= new Dictionary(requestHandlers.Length); var key = new HandlerNameKey(name!, handler.Method.Name); - if (!nameToMethodMap.TryGetValue(key, out var indices)) + + if (nameToFirstIndex.TryGetValue(key, out var firstIndex)) { - indices = new List(); - nameToMethodMap.Add(key, indices); + collidingIndices ??= new HashSet(); + collidingIndices.Add(firstIndex); + collidingIndices.Add(index); + } + else + { + nameToFirstIndex.Add(key, index); } - - indices.Add(index); } - foreach (var indices in nameToMethodMap.Values) - { - if (indices.Count <= 1) - continue; + if (collidingIndices is null || collidingIndices.Count == 0) + return ImmutableArray.Empty; - foreach (var index in indices) - collidingIndices.Add(index); - } - - return collidingIndices.ToImmutable(); + var builder = ImmutableArray.CreateBuilder(collidingIndices.Count); + builder.AddRange(collidingIndices); + builder.Sort(); + return builder.MoveToImmutable(); } private static string GetFullyQualifiedMethodDisplayName(RequestHandler requestHandler) @@ -1946,21 +1949,19 @@ private static List GetDistinctNonStaticClassNames(ImmutableArray nonStaticClassNames) { - var estimate = 512; + var estimate = 512L; foreach (var className in nonStaticClassNames) estimate += 36 + className.Length; estimate += Math.Max(256, nonStaticClassNames.Count * 12); - estimate = (int)(estimate * 1.10); + estimate = (long)(estimate * 1.10); - estimate = estimate switch - { - < 512 => 512, - > 8192 => 8192, - _ => estimate, - }; + if (estimate < 512) + estimate = 512; + else if (estimate > int.MaxValue) + estimate = int.MaxValue; - return StringBuilderPool.Get(estimate); + return StringBuilderPool.Get((int)estimate); } private static void GenerateUseEndpointHandlersClass(SourceProductionContext context, ImmutableArray requestHandlers) @@ -1974,7 +1975,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.AppendLine("using Microsoft.AspNetCore.Http;"); source.AppendLine("using Microsoft.AspNetCore.Mvc;"); source.AppendLine("using Microsoft.AspNetCore.Routing;"); - if (requestHandlers.Any(static handler => handler.Configuration.RequireRateLimiting)) + if (HasRateLimitedHandlers(requestHandlers)) source.AppendLine("using Microsoft.AspNetCore.RateLimiting;"); source.AppendLine("using Microsoft.Extensions.DependencyInjection;"); source.AppendLine(); @@ -2034,6 +2035,17 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con context.AddSource(UseEndpointHandlersMethodHint, SourceText.From(sourceText, Encoding.UTF8)); } + private static bool HasRateLimitedHandlers(ImmutableArray requestHandlers) + { + foreach (var handler in requestHandlers) + { + if (handler.Configuration.RequireRateLimiting) + return true; + } + + return false; + } + [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", Justification = "Manual loops avoid repeated allocations in the source generator.")] private static List GetClassesWithMapGroups(ImmutableArray requestHandlers) { @@ -2118,7 +2130,7 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl foreach (var parameter in requestHandler.Method.Parameters) { source.Append(", "); - source.Append(GetBindingSourceAttribute(parameter.Source, parameter.Key, parameter.BindingName)); + source.Append(parameter.BindingPrefix); source.Append(parameter.Type); source.Append(' '); source.Append(parameter.Name); @@ -2524,12 +2536,14 @@ private static StringBuilder GetUseEndpointHandlersStringBuilder(ImmutableArray< const int baseSize = 4096; const int perHandler = 512; - var estimate = baseSize + requestHandlers.Length * perHandler; + var handlerCount = Math.Max(requestHandlers.Length, 0); + var estimate = baseSize + (long)perHandler * handlerCount; + estimate = (long)(estimate * 1.10); - if (estimate > 65536) - estimate = 65536; + if (estimate > int.MaxValue) + estimate = int.MaxValue; - return StringBuilderPool.Get(estimate); + return StringBuilderPool.Get((int)Math.Max(baseSize, estimate)); } [SuppressMessage("Globalization", "CA1308: Normalize strings to uppercase", Justification = "C# boolean literals must be lowercase.")] @@ -2596,10 +2610,28 @@ private static string StringLiteral(string? value) if (value is null) return "null"; + var firstEscapeIndex = -1; + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + if (c == '\"' || c == '\\' || c == '\n' || c == '\r' || c == '\t' || c == '\0' || char.IsControl(c)) + { + firstEscapeIndex = i; + break; + } + } + + if (firstEscapeIndex < 0) + return string.Concat("\"", value, "\""); + var sb = StringBuilderPool.Get(value.Length + 2); sb.Append('"'); - foreach (var c in value) + if (firstEscapeIndex > 0) + sb.Append(value, 0, firstEscapeIndex); + + for (var i = firstEscapeIndex; i < value.Length; i++) { + var c = value[i]; switch (c) { case '\"': @@ -2622,13 +2654,19 @@ private static string StringLiteral(string? value) break; default: if (char.IsControl(c)) + { sb.Append("\\u") .Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); + } else + { sb.Append(c); + } + break; } } + sb.Append('"'); return StringBuilderPool.ToStringAndReturn(sb); } @@ -2761,7 +2799,7 @@ private readonly record struct ProducesValidationProblemMetadata( EquatableImmutableArray? AdditionalContentTypes ); - private readonly record struct Parameter(string Name, string Type, BindingSource Source, string? Key, string? BindingName); + private readonly record struct Parameter(string Name, string Type, string BindingPrefix); private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); From b8e1968decc4b0d87e81e626365578bd6864cdb2 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:31:11 -0500 Subject: [PATCH 68/75] Cleanup. --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 1340 ++++++++--------- ...erationTests.AcceptsAttribute.verified.txt | 2 +- ...Tests.RequestTimeoutAttribute.verified.txt | 50 +- .../GeneratedEndpoints.Tests.csproj | 22 +- 4 files changed, 696 insertions(+), 718 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index c3b49ad..cc127b1 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; -using System.Threading; using System.Text; using GeneratedEndpoints.Common; using Microsoft.CodeAnalysis; @@ -121,6 +119,20 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private static readonly ConditionalWeakTable RequestHandlerClassCache = new(); private static readonly ConditionalWeakTable GeneratedAttributeKindCache = new(); + private static readonly string FileHeader = $""" + //----------------------------------------------------------------------------- + // + // This code was generated by {nameof(MinimalApiGenerator)} which can be found + // in the {typeof(MinimalApiGenerator).Namespace} namespace. + // + // Changes to this file may cause incorrect behavior + // and will be lost if the code is regenerated. + // + //----------------------------------------------------------------------------- + + #nullable enable + """; + private static readonly ImmutableArray HttpAttributeDefinitions = [ CreateHttpAttributeDefinition("MapGetAttribute", "GET"), @@ -139,625 +151,627 @@ public sealed class MinimalApiGenerator : IIncrementalGenerator private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); - private static readonly string FileHeader = $""" - //----------------------------------------------------------------------------- - // - // This code was generated by {nameof(MinimalApiGenerator)} which can be found - // in the {typeof(MinimalApiGenerator).Namespace} namespace. - // - // Changes to this file may cause incorrect behavior - // and will be lost if the code is regenerated. - // - //----------------------------------------------------------------------------- - - #nullable enable - """; - private static readonly SourceText RequireAuthorizationAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that authorization is required for the annotated endpoint or class. - /// Optionally restricts access to the specified authorization policies. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Attribute - { - /// - /// Gets the policy names that the endpoint or class requires. - /// - public string[] PolicyNames { get; } - - /// - /// Marks the endpoint or class as requiring authorization. - /// - public {{RequireAuthorizationAttributeName}}() - { - PolicyNames = []; - } - - /// - /// Marks the endpoint or class as requiring authorization with one or more policies. - /// - public {{RequireAuthorizationAttributeName}}(params string[] policyNames) - { - PolicyNames = policyNames ?? []; - } - } - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that authorization is required for the annotated endpoint or class. + /// Optionally restricts access to the specified authorization policies. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Attribute + { + /// + /// Gets the policy names that the endpoint or class requires. + /// + public string[] PolicyNames { get; } + + /// + /// Marks the endpoint or class as requiring authorization. + /// + public {{RequireAuthorizationAttributeName}}() + { + PolicyNames = []; + } + + /// + /// Marks the endpoint or class as requiring authorization with one or more policies. + /// + public {{RequireAuthorizationAttributeName}}(params string[] policyNames) + { + PolicyNames = policyNames ?? []; + } + } + """, Encoding.UTF8 + ); private static readonly SourceText RequireCorsAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the annotated endpoint requires a configured CORS policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional CORS policy name. - /// - public string? PolicyName { get; } - - /// - /// Marks the endpoint or class as requiring the default CORS policy. - /// - public {{RequireCorsAttributeName}}() - { - } - - /// - /// Marks the endpoint or class as requiring the specified named CORS policy. - /// - public {{RequireCorsAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires a configured CORS policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional CORS policy name. + /// + public string? PolicyName { get; } + + /// + /// Marks the endpoint or class as requiring the default CORS policy. + /// + public {{RequireCorsAttributeName}}() + { + } + + /// + /// Marks the endpoint or class as requiring the specified named CORS policy. + /// + public {{RequireCorsAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + """, Encoding.UTF8 + ); private static readonly SourceText RequireRateLimitingAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the annotated endpoint requires the provided rate limiting policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The rate limiting policy to apply. - public {{RequireRateLimitingAttributeName}}(string policyName) - { - PolicyName = policyName; - } - - /// - /// Gets the rate limiting policy name. - /// - public string PolicyName { get; } - } - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires the provided rate limiting policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The rate limiting policy to apply. + public {{RequireRateLimitingAttributeName}}(string policyName) + { + PolicyName = policyName; + } + + /// + /// Gets the rate limiting policy name. + /// + public string PolicyName { get; } + } + """, Encoding.UTF8 + ); private static readonly SourceText RequireHostAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the allowed hosts for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireHostAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The hosts that are allowed to access the endpoint. - public {{RequireHostAttributeName}}(params string[] hosts) - { - Hosts = hosts ?? []; - } - - /// - /// Gets the allowed hosts. - /// - public string[] Hosts { get; } - } - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the allowed hosts for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireHostAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The hosts that are allowed to access the endpoint. + public {{RequireHostAttributeName}}(params string[] hosts) + { + Hosts = hosts ?? []; + } + + /// + /// Gets the allowed hosts. + /// + public string[] Hosts { get; } + } + """, Encoding.UTF8 + ); private static readonly SourceText DisableAntiforgeryAttributeSourceText = SourceText.From($$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Disables antiforgery protection for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attribute - { - } + /// + /// Disables antiforgery protection for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attribute + { + } - """, Encoding.UTF8); + """, Encoding.UTF8 + ); private static readonly SourceText ShortCircuitAttributeSourceText = SourceText.From($$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Marks the annotated endpoint or class to short-circuit the request pipeline. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute - { - } + /// + /// Marks the annotated endpoint or class to short-circuit the request pipeline. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute + { + } - """, Encoding.UTF8); + """, Encoding.UTF8 + ); private static readonly SourceText DisableRequestTimeoutAttributeSourceText = SourceText.From($$""" - {{FileHeader}} + {{FileHeader}} - namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; - /// - /// Disables the request timeout for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute - { - } + /// + /// Disables the request timeout for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute + { + } - """, Encoding.UTF8); + """, Encoding.UTF8 + ); - private static readonly SourceText DisableValidationAttributeSourceText = SourceText.From( -$$""" -#if NET10_0_OR_GREATER -{{FileHeader}} + private static readonly SourceText DisableValidationAttributeSourceText = SourceText.From($$""" + #if NET10_0_OR_GREATER + {{FileHeader}} -namespace {{AttributesNamespace}}; + namespace {{AttributesNamespace}}; -/// -/// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. -/// -[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] -internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute -{ -} -#endif + /// + /// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute + { + } + #endif -""", Encoding.UTF8); + """, Encoding.UTF8 + ); private static readonly SourceText RequestTimeoutAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Applies the request timeout metadata to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional request timeout policy name. - /// - public string? PolicyName { get; init; } - - /// - /// Applies the default request timeout behavior. - /// - public {{RequestTimeoutAttributeName}}() - { - } - - /// - /// Applies the specified request timeout policy. - /// - /// The request timeout policy name. - public {{RequestTimeoutAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Applies the request timeout metadata to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional request timeout policy name. + /// + public string? PolicyName { get; init; } + + /// + /// Applies the default request timeout behavior. + /// + public {{RequestTimeoutAttributeName}}() + { + } + + /// + /// Applies the specified request timeout policy. + /// + /// The request timeout policy name. + public {{RequestTimeoutAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + + """, Encoding.UTF8 + ); private static readonly SourceText OrderAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the order for the annotated endpoint when building conventions. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{OrderAttributeName}} : global::System.Attribute - { - /// - /// Gets the order that will be applied to the endpoint. - /// - public int Order { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The order value to apply to the endpoint. - public {{OrderAttributeName}}(int order) - { - Order = order; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the order for the annotated endpoint when building conventions. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{OrderAttributeName}} : global::System.Attribute + { + /// + /// Gets the order that will be applied to the endpoint. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The order value to apply to the endpoint. + public {{OrderAttributeName}}(int order) + { + Order = order; + } + } + + """, Encoding.UTF8 + ); private static readonly SourceText MapGroupAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the route group for the annotated class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - internal sealed class {{MapGroupAttributeName}} : global::System.Attribute - { - /// - /// Gets the route group pattern. - /// - public string Pattern { get; } - - /// - /// Gets or sets the endpoint group name. - /// - public string? Name { get; init; } - - /// - /// Initializes a new instance of the class. - /// - /// The route group pattern to apply. - public {{MapGroupAttributeName}}(string pattern) - { - Pattern = pattern; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the route group for the annotated class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal sealed class {{MapGroupAttributeName}} : global::System.Attribute + { + /// + /// Gets the route group pattern. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint group name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route group pattern to apply. + public {{MapGroupAttributeName}}(string pattern) + { + Pattern = pattern; + } + } + + """, Encoding.UTF8 + ); private static readonly SourceText SummaryAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the summary metadata for the annotated endpoint. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{SummaryAttributeName}} : global::System.Attribute - { - /// - /// Gets the summary value for the endpoint. - /// - public string Summary { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The summary to apply to the endpoint. - public {{SummaryAttributeName}}(string summary) - { - Summary = summary; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the summary metadata for the annotated endpoint. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{SummaryAttributeName}} : global::System.Attribute + { + /// + /// Gets the summary value for the endpoint. + /// + public string Summary { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The summary to apply to the endpoint. + public {{SummaryAttributeName}}(string summary) + { + Summary = summary; + } + } + + """, Encoding.UTF8 + ); private static readonly SourceText AcceptsAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the request type and content types accepted by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type? RequestType { get; init; } - - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } - - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } - - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - /// - /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. - /// - /// The CLR type of the request body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type RequestType => typeof(TRequest); - - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } - - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } - - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the generic Accepts attribute class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the request type and content types accepted by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type? RequestType { get; init; } + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. + /// + /// The CLR type of the request body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type RequestType => typeof(TRequest); + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Accepts attribute class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); private static readonly SourceText EndpointFilterAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies an endpoint filter type to apply to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute - { - /// - /// Gets the CLR type of the endpoint filter. - /// - public global::System.Type FilterType { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The CLR type of the endpoint filter. - public {{EndpointFilterAttributeName}}(global::System.Type filterType) - { - FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); - } - } - - /// - /// Specifies an endpoint filter type using a generic argument. - /// - /// The CLR type of the endpoint filter. - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute - { - /// - /// Gets the CLR type of the endpoint filter. - /// - public global::System.Type FilterType => typeof(TFilter); - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies an endpoint filter type to apply to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The CLR type of the endpoint filter. + public {{EndpointFilterAttributeName}}(global::System.Type filterType) + { + FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); + } + } + + /// + /// Specifies an endpoint filter type using a generic argument. + /// + /// The CLR type of the endpoint filter. + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType => typeof(TFilter); + } + + """, Encoding.UTF8 + ); private static readonly SourceText ProducesResponseAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type? ResponseType { get; init; } - - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - /// - /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. - /// - /// The CLR type of the response body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type ResponseType => typeof(TResponse); - - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the generic Produces attribute class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type? ResponseType { get; init; } + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. + /// + /// The CLR type of the response body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType => typeof(TResponse); + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Produces attribute class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); private static readonly SourceText ProducesProblemAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the endpoint produces a problem details payload. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute - { - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); private static readonly SourceText ProducesValidationProblemAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the endpoint produces a validation problem details payload. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute - { - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesValidationProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8); + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a validation problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesValidationProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -785,7 +799,7 @@ private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attr var fullyQualifiedName = $"{AttributesNamespace}.{attributeName}"; var hint = $"{fullyQualifiedName}.gs.cs"; var summaryVerb = verb == FallbackHttpMethod ? "fallback" : verb; - var source = GenerateHttpAttributeSource(FileHeader, AttributesNamespace, attributeName, summaryVerb, allowEmptyPattern); + var source = GenerateHttpAttributeSource(AttributesNamespace, attributeName, summaryVerb, allowEmptyPattern); return new HttpAttributeDefinition(attributeName, fullyQualifiedName, hint, verb, allowEmptyPattern, SourceText.From(source, Encoding.UTF8)); } @@ -798,8 +812,10 @@ ImmutableArray>> handler var combined = handlerProviders[0]; for (var i = 1; i < handlerProviders.Length; i++) + { combined = combined.Combine(handlerProviders[i]) .Select(static (x, _) => x.Left.AddRange(x.Right)); + } return combined; } @@ -828,17 +844,11 @@ private static void RegisterAttributes(IncrementalGeneratorPostInitializationCon context.AddSource(ProducesValidationProblemAttributeHint, ProducesValidationProblemAttributeSourceText); } - private static string GenerateHttpAttributeSource( - string fileHeader, - string attributesNamespace, - string attributeName, - string summaryVerb, - bool allowEmptyPattern - ) + private static string GenerateHttpAttributeSource(string attributesNamespace, string attributeName, string summaryVerb, bool allowEmptyPattern) { var patternDefaultValue = allowEmptyPattern ? " = \"\"" : string.Empty; return $$""" - {{fileHeader}} + {{FileHeader}} namespace {{attributesNamespace}}; @@ -976,7 +986,7 @@ bool enforceMethodRequireAuthorizationRules GetAdditionalRequestHandlerAttributeValues(attributes, ref state); - if (enforceMethodRequireAuthorizationRules && state.HasRequireAuthorizationAttribute && !state.HasAllowAnonymousAttribute) + if (enforceMethodRequireAuthorizationRules && state is { HasRequireAuthorizationAttribute: true, HasAllowAnonymousAttribute: false }) state.AllowAnonymous = false; var metadata = new RequestHandlerMetadata(name, displayName, state.Summary, description, state.Tags, ToEquatableOrNull(state.Accepts), @@ -990,13 +1000,11 @@ bool enforceMethodRequireAuthorizationRules return new EndpointConfiguration(metadata, state.RequireAuthorization ?? false, state.AuthorizationPolicies, state.DisableAntiforgery ?? false, state.AllowAnonymous ?? false, state.RequireCors ?? false, state.CorsPolicyName, state.RequiredHosts, state.RequireRateLimiting ?? false, state.RateLimitingPolicyName, ToEquatableOrNull(state.EndpointFilters), state.ShortCircuit ?? false, state.DisableValidation ?? false, - state.DisableRequestTimeout ?? false, withRequestTimeout, requestTimeoutPolicyName, state.Order, state.EndpointGroupName); + state.DisableRequestTimeout ?? false, withRequestTimeout, requestTimeoutPolicyName, state.Order, state.EndpointGroupName + ); } - private static void GetAdditionalRequestHandlerAttributeValues( - ImmutableArray attributes, - ref EndpointAttributeState state - ) + private static void GetAdditionalRequestHandlerAttributeValues(ImmutableArray attributes, ref EndpointAttributeState state) { ref var tags = ref state.Tags; ref var requireAuthorization = ref state.RequireAuthorization; @@ -1094,26 +1102,26 @@ ref EndpointAttributeState state continue; case GeneratedAttributeKind.RequireCors: requireCors = true; - corsPolicyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; + corsPolicyName = attribute.ConstructorArguments.Length > 0 + ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) + : null; continue; case GeneratedAttributeKind.RequireHost: if (attribute.ConstructorArguments.Length == 1) { var arg = attribute.ConstructorArguments[0]; if (arg is { Kind: TypedConstantKind.Array, Values.Length: > 0 }) - { MergeInto(ref requiredHosts, arg.Values); - } else if (arg.Value is string singleHost && !string.IsNullOrWhiteSpace(singleHost)) - { - MergeInto(ref requiredHosts, new[] { singleHost.Trim() }); - } + MergeInto(ref requiredHosts, [singleHost.Trim()]); } continue; case GeneratedAttributeKind.RequireRateLimiting: { - var policyName = attribute.ConstructorArguments.Length > 0 ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) : null; + var policyName = attribute.ConstructorArguments.Length > 0 + ? NormalizeOptionalString(attribute.ConstructorArguments[0].Value as string) + : null; if (!string.IsNullOrEmpty(policyName)) { @@ -1145,9 +1153,10 @@ ref EndpointAttributeState state } case GeneratedAttributeKind.ProducesValidationProblem: { - var statusCode = attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode - ? producesValidationProblemStatusCode - : 400; + var statusCode = + attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is int producesValidationProblemStatusCode + ? producesValidationProblemStatusCode + : 400; var contentType = attribute.ConstructorArguments.Length > 1 ? NormalizeOptionalContentType(attribute.ConstructorArguments[1].Value as string) : null; @@ -1178,10 +1187,7 @@ ref EndpointAttributeState state } if (IsAttribute(attributeClass, "ExcludeFromDescriptionAttribute", AspNetCoreRoutingNamespaceParts)) - { excludeFromDescription = true; - continue; - } } } @@ -1263,9 +1269,7 @@ private static string GetMapGroupIdentifier(string className) builder.Append('_'); foreach (var character in className) - { builder.Append(char.IsLetterOrDigit(character) ? character : '_'); - } builder.Append("_Group"); return StringBuilderPool.ToStringAndReturn(builder); @@ -1289,9 +1293,8 @@ private static string GetMapGroupIdentifier(string className) private static GeneratedAttributeKind GetGeneratedAttributeKind(INamedTypeSymbol attributeClass) { var definition = attributeClass.OriginalDefinition; - var cacheEntry = GeneratedAttributeKindCache.GetValue(definition, static def => - new GeneratedAttributeKindCacheEntry(GetGeneratedAttributeKindCore(def)) - ); + var cacheEntry = + GeneratedAttributeKindCache.GetValue(definition, static def => new GeneratedAttributeKindCacheEntry(GetGeneratedAttributeKindCore(def))); return cacheEntry.Kind; } @@ -1498,7 +1501,7 @@ private static EquatableImmutableArray MergeUnion(EquatableImmutableArra } seen ??= new HashSet(StringComparer.OrdinalIgnoreCase); - list ??= new List(); + list ??= []; foreach (var value in values) { @@ -1530,11 +1533,7 @@ private static RequestHandlerMethod GetRequestHandlerMethod(IMethodSymbol method return requestHandlerMethod; } - private static RequestHandlerClass? GetRequestHandlerClass( - IMethodSymbol methodSymbol, - Compilation compilation, - CancellationToken cancellationToken - ) + private static RequestHandlerClass? GetRequestHandlerClass(IMethodSymbol methodSymbol, Compilation compilation, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -1553,7 +1552,6 @@ private static CompilationTypeCache GetCompilationTypeCache(Compilation compilat return CompilationTypeCaches.GetValue(compilation, static c => new CompilationTypeCache(c)); } - [SuppressMessage("Major Code Smell", "S3398:Move this method into a class of its own", Justification = "Shared helper reused by caching infrastructure.")] private static ConfigureMethodDetails GetConfigureMethodDetails( INamedTypeSymbol classSymbol, @@ -1802,7 +1800,7 @@ private static ImmutableArray SortRequestHandlers(ImmutableArray var array = requestHandlers.ToArray(); Array.Sort(array, RequestHandlerComparer.Instance); - return array.ToImmutableArray(); + return [..array]; } private static ImmutableArray EnsureUniqueEndpointNames(ImmutableArray requestHandlers) @@ -1820,8 +1818,14 @@ private static ImmutableArray EnsureUniqueEndpointNames(Immutabl { Name = GetFullyQualifiedMethodDisplayName(handler), }; - configuration = configuration with { Metadata = metadata }; - builder[index] = handler with { Configuration = configuration }; + configuration = configuration with + { + Metadata = metadata, + }; + builder[index] = handler with + { + Configuration = configuration, + }; } return builder.MoveToImmutable(); @@ -1847,7 +1851,7 @@ private static ImmutableArray GetRequestHandlersWithNameCollisions(Immutabl if (nameToFirstIndex.TryGetValue(key, out var firstIndex)) { - collidingIndices ??= new HashSet(); + collidingIndices ??= []; collidingIndices.Add(firstIndex); collidingIndices.Add(index); } @@ -1926,7 +1930,9 @@ private static void GenerateAddEndpointHandlersClass(SourceProductionContext con context.AddSource(AddEndpointHandlersMethodHint, SourceText.From(sourceText, Encoding.UTF8)); } - [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", Justification = "Manual loops avoid repeated allocations in the source generator.")] + [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", + Justification = "Manual loops avoid repeated allocations in the source generator." + )] private static List GetDistinctNonStaticClassNames(ImmutableArray requestHandlers) { var classNames = new List(); @@ -2007,7 +2013,7 @@ private static void GenerateUseEndpointHandlersClass(SourceProductionContext con source.Append(" = builder.MapGroup("); source.Append(StringLiteral(groupedClass.MapGroupPattern!)); source.Append(')'); - AppendEndpointConfiguration(source, " ", groupedClass.Configuration, includeNameAndDisplayName: false); + AppendEndpointConfiguration(source, " ", groupedClass.Configuration, false); source.AppendLine(";"); } @@ -2046,7 +2052,9 @@ private static bool HasRateLimitedHandlers(ImmutableArray reques return false; } - [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", Justification = "Manual loops avoid repeated allocations in the source generator.")] + [SuppressMessage("Major Code Smell", "S3267:Loops should be simplified by calling the \"Select\" LINQ method", + Justification = "Manual loops avoid repeated allocations in the source generator." + )] private static List GetClassesWithMapGroups(ImmutableArray requestHandlers) { var groupedClasses = new List(); @@ -2156,7 +2164,7 @@ private static void GenerateMapRequestHandler(StringBuilder source, RequestHandl if (requestHandler.Class.MapGroupPattern is null) configuration = MergeEndpointConfigurations(requestHandler.Class.Configuration, configuration); - AppendEndpointConfiguration(source, continuationIndent, configuration, includeNameAndDisplayName: true); + AppendEndpointConfiguration(source, continuationIndent, configuration, true); if (wrapWithConfigure && configureAcceptsServiceProvider) { @@ -2448,22 +2456,18 @@ private static EndpointConfiguration MergeEndpointConfigurations(EndpointConfigu var order = methodConfiguration.Order ?? classConfiguration.Order; var endpointGroupName = methodConfiguration.EndpointGroupName ?? classConfiguration.EndpointGroupName; - return new EndpointConfiguration(metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, - corsPolicyName, requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableValidation, disableRequestTimeout, - withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName); + return new EndpointConfiguration(metadata, requireAuthorization, authorizationPolicies, disableAntiforgery, allowAnonymous, requireCors, corsPolicyName, + requiredHosts, requireRateLimiting, rateLimitingPolicyName, endpointFilterTypes, shortCircuit, disableValidation, disableRequestTimeout, + withRequestTimeout, requestTimeoutPolicyName, order, endpointGroupName + ); } private static RequestHandlerMetadata MergeRequestHandlerMetadata(RequestHandlerMetadata classMetadata, RequestHandlerMetadata methodMetadata) { - return new RequestHandlerMetadata( - methodMetadata.Name ?? classMetadata.Name, - methodMetadata.DisplayName ?? classMetadata.DisplayName, - methodMetadata.Summary ?? classMetadata.Summary, - methodMetadata.Description ?? classMetadata.Description, - MergeDistinctStrings(classMetadata.Tags, methodMetadata.Tags), - ConcatEquatable(classMetadata.Accepts, methodMetadata.Accepts), - ConcatEquatable(classMetadata.Produces, methodMetadata.Produces), - ConcatEquatable(classMetadata.ProducesProblem, methodMetadata.ProducesProblem), + return new RequestHandlerMetadata(methodMetadata.Name ?? classMetadata.Name, methodMetadata.DisplayName ?? classMetadata.DisplayName, + methodMetadata.Summary ?? classMetadata.Summary, methodMetadata.Description ?? classMetadata.Description, + MergeDistinctStrings(classMetadata.Tags, methodMetadata.Tags), ConcatEquatable(classMetadata.Accepts, methodMetadata.Accepts), + ConcatEquatable(classMetadata.Produces, methodMetadata.Produces), ConcatEquatable(classMetadata.ProducesProblem, methodMetadata.ProducesProblem), ConcatEquatable(classMetadata.ProducesValidationProblem, methodMetadata.ProducesValidationProblem), classMetadata.ExcludeFromDescription || methodMetadata.ExcludeFromDescription ); @@ -2654,14 +2658,10 @@ private static string StringLiteral(string? value) break; default: if (char.IsControl(c)) - { sb.Append("\\u") .Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); - } else - { sb.Append(c); - } break; } @@ -2721,7 +2721,14 @@ _ when char.IsControl(c) => "\\u" + ((int)c).ToString("x4", CultureInfo.Invarian }; } - private readonly record struct HttpAttributeDefinition(string Name, string FullyQualifiedName, string Hint, string Verb, bool AllowEmptyPattern, SourceText SourceText); + private readonly record struct HttpAttributeDefinition( + string Name, + string FullyQualifiedName, + string Hint, + string Verb, + bool AllowEmptyPattern, + SourceText SourceText + ); private readonly record struct RequestHandler( RequestHandlerClass Class, @@ -2892,30 +2899,21 @@ public int Compare(RequestHandler x, RequestHandler y) } } - private sealed class CompilationTypeCache + private sealed class CompilationTypeCache(Compilation compilation) { - public CompilationTypeCache(Compilation compilation) - { - EndpointConventionBuilderSymbol = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); - ServiceProviderSymbol = compilation.GetTypeByMetadataName("System.IServiceProvider"); - } - - public INamedTypeSymbol? EndpointConventionBuilderSymbol { get; } + public INamedTypeSymbol? EndpointConventionBuilderSymbol { get; } = + compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); - public INamedTypeSymbol? ServiceProviderSymbol { get; } + public INamedTypeSymbol? ServiceProviderSymbol { get; } = compilation.GetTypeByMetadataName("System.IServiceProvider"); } private sealed class RequestHandlerClassCacheEntry { + private readonly object _lock = new(); private RequestHandlerClass _value; private bool _initialized; - private readonly object _lock = new(); - public RequestHandlerClass GetOrCreate( - INamedTypeSymbol classSymbol, - CompilationTypeCache compilationCache, - CancellationToken cancellationToken - ) + public RequestHandlerClass GetOrCreate(INamedTypeSymbol classSymbol, CompilationTypeCache compilationCache, CancellationToken cancellationToken) { if (_initialized) return _value; @@ -2929,25 +2927,16 @@ CancellationToken cancellationToken var name = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var isStatic = classSymbol.IsStatic; - var configureMethodDetails = GetConfigureMethodDetails( - classSymbol, - compilationCache.EndpointConventionBuilderSymbol, - compilationCache.ServiceProviderSymbol, - cancellationToken + var configureMethodDetails = GetConfigureMethodDetails(classSymbol, compilationCache.EndpointConventionBuilderSymbol, + compilationCache.ServiceProviderSymbol, cancellationToken ); var mapGroupPattern = GetMapGroupPattern(classSymbol); var mapGroupIdentifier = mapGroupPattern is null ? null : GetMapGroupIdentifier(name); var classConfiguration = GetEndpointConfiguration(classSymbol.GetAttributes(), null, null, null, false); - _value = new RequestHandlerClass( - name, - isStatic, - configureMethodDetails.HasConfigureMethod, - configureMethodDetails.ConfigureMethodAcceptsServiceProvider, - mapGroupPattern, - mapGroupIdentifier, - classConfiguration + _value = new RequestHandlerClass(name, isStatic, configureMethodDetails.HasConfigureMethod, + configureMethodDetails.ConfigureMethodAcceptsServiceProvider, mapGroupPattern, mapGroupIdentifier, classConfiguration ); _initialized = true; return _value; @@ -2955,13 +2944,8 @@ CancellationToken cancellationToken } } - private sealed class GeneratedAttributeKindCacheEntry + private sealed class GeneratedAttributeKindCacheEntry(GeneratedAttributeKind kind) { - public GeneratedAttributeKindCacheEntry(GeneratedAttributeKind kind) - { - Kind = kind; - } - - public GeneratedAttributeKind Kind { get; } + public GeneratedAttributeKind Kind { get; } = kind; } } diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt index 8c2c4c1..d4d0ac4 100644 --- a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.AcceptsAttribute.verified.txt @@ -1,4 +1,4 @@ - //----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // This code was generated by MinimalApiGenerator which can be found // in the GeneratedEndpoints namespace. diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt index 746a157..dee02d0 100644 --- a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.RequestTimeoutAttribute.verified.txt @@ -1,4 +1,4 @@ - //----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // This code was generated by MinimalApiGenerator which can be found // in the GeneratedEndpoints namespace. @@ -10,32 +10,32 @@ #nullable enable - namespace Microsoft.AspNetCore.Generated.Attributes; +namespace Microsoft.AspNetCore.Generated.Attributes; - /// - /// Applies the request timeout metadata to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class RequestTimeoutAttribute : global::System.Attribute - { - /// - /// Gets the optional request timeout policy name. - /// - public string? PolicyName { get; init; } +/// +/// Applies the request timeout metadata to the annotated endpoint or class. +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] +internal sealed class RequestTimeoutAttribute : global::System.Attribute +{ + /// + /// Gets the optional request timeout policy name. + /// + public string? PolicyName { get; init; } - /// - /// Applies the default request timeout behavior. - /// - public RequestTimeoutAttribute() - { - } + /// + /// Applies the default request timeout behavior. + /// + public RequestTimeoutAttribute() + { + } - /// - /// Applies the specified request timeout policy. - /// - /// The request timeout policy name. - public RequestTimeoutAttribute(string policyName) - { - PolicyName = policyName; + /// + /// Applies the specified request timeout policy. + /// + /// The request timeout policy name. + public RequestTimeoutAttribute(string policyName) + { + PolicyName = policyName; } } diff --git a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj index 15b79e5..1f74d48 100644 --- a/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj +++ b/tests/GeneratedEndpoints.Tests/GeneratedEndpoints.Tests.csproj @@ -10,24 +10,24 @@ - + - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + @@ -38,10 +38,4 @@ - - - GeneratedSourceTests.cs - - - From e3e8f3a831dad1264c078538d5772d7add43e0e4 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:39:21 -0500 Subject: [PATCH 69/75] Optimize source generator performance (#55) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 134 ++++++++++-------- 1 file changed, 77 insertions(+), 57 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index cc127b1..0ec261f 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System.Buffers; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -1022,6 +1023,7 @@ private static void GetAdditionalRequestHandlerAttributeValues(ImmutableArray? endpointFilters) + private static void TryAddEndpointFilter( + AttributeData attribute, + INamedTypeSymbol attributeClass, + ref List? endpointFilters, + ref HashSet? endpointFilterSet) { if (attributeClass is { IsGenericType: true, TypeArguments.Length: 1 }) { - TryAddEndpointFilterType(attributeClass.TypeArguments[0], ref endpointFilters); + TryAddEndpointFilterType(attributeClass.TypeArguments[0], ref endpointFilters, ref endpointFilterSet); return; } @@ -1426,10 +1432,13 @@ private static void TryAddEndpointFilter(AttributeData attribute, INamedTypeSymb return; if (attribute.ConstructorArguments[0].Value is ITypeSymbol filterTypeSymbol) - TryAddEndpointFilterType(filterTypeSymbol, ref endpointFilters); + TryAddEndpointFilterType(filterTypeSymbol, ref endpointFilters, ref endpointFilterSet); } - private static void TryAddEndpointFilterType(ITypeSymbol? typeSymbol, ref List? endpointFilters) + private static void TryAddEndpointFilterType( + ITypeSymbol? typeSymbol, + ref List? endpointFilters, + ref HashSet? endpointFilterSet) { if (typeSymbol is null or ITypeParameterSymbol or IErrorTypeSymbol) return; @@ -1438,9 +1447,12 @@ private static void TryAddEndpointFilterType(ITypeSymbol? typeSymbol, ref List(StringComparer.Ordinal); + if (!endpointFilterSet.Add(displayString)) + return; + + endpointFilters ??= []; + endpointFilters.Add(displayString); } private static ITypeSymbol? GetNamedTypeSymbol(AttributeData attribute, string namedParameter) @@ -1483,40 +1495,29 @@ private static EquatableImmutableArray MergeUnion(EquatableImmutableArra if (existing is { Count: > 0 }) { - list = new List(existing.Value.Count + 4); - seen = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var item in existing.Value) - { - if (string.IsNullOrWhiteSpace(item)) - continue; - - var trimmed = item.Trim(); - if (trimmed.Length == 0) - continue; - - if (seen.Add(trimmed)) - list.Add(trimmed); - } + var count = existing.Value.Count; + list = new List(count + 4); + list.AddRange(existing.Value); + seen = new HashSet(existing.Value, StringComparer.OrdinalIgnoreCase); } - seen ??= new HashSet(StringComparer.OrdinalIgnoreCase); - list ??= []; - foreach (var value in values) { - if (string.IsNullOrWhiteSpace(value)) + var normalized = NormalizeOptionalString(value); + if (normalized is not { Length: > 0 }) continue; - var trimmed = value.Trim(); - if (trimmed.Length == 0) + seen ??= new HashSet(StringComparer.OrdinalIgnoreCase); + if (!seen.Add(normalized)) continue; - if (seen.Add(trimmed)) - list.Add(trimmed); + if (list is null) + list = []; + + list.Add(normalized); } - return list.ToEquatableImmutableArray(); + return list?.ToEquatableImmutableArray() ?? EquatableImmutableArray.Empty; } private static RequestHandlerMethod GetRequestHandlerMethod(IMethodSymbol methodSymbol, CancellationToken cancellationToken) @@ -1836,38 +1837,56 @@ private static ImmutableArray GetRequestHandlersWithNameCollisions(Immutabl if (requestHandlers.IsDefaultOrEmpty) return ImmutableArray.Empty; - Dictionary? nameToFirstIndex = null; - HashSet? collidingIndices = null; + var handlerCount = requestHandlers.Length; + var nameToFirstIndex = new Dictionary(handlerCount); + var collisionFlags = ArrayPool.Shared.Rent(handlerCount); + Array.Clear(collisionFlags, 0, handlerCount); + List? collidingIndices = null; - for (var index = 0; index < requestHandlers.Length; index++) + try { - var handler = requestHandlers[index]; - var name = handler.Configuration.Metadata.Name; - if (string.IsNullOrEmpty(name)) - continue; + for (var index = 0; index < handlerCount; index++) + { + var handler = requestHandlers[index]; + var name = handler.Configuration.Metadata.Name; + if (string.IsNullOrEmpty(name)) + continue; - nameToFirstIndex ??= new Dictionary(requestHandlers.Length); - var key = new HandlerNameKey(name!, handler.Method.Name); + var key = new HandlerNameKey(name!, handler.Method.Name); - if (nameToFirstIndex.TryGetValue(key, out var firstIndex)) - { - collidingIndices ??= []; - collidingIndices.Add(firstIndex); - collidingIndices.Add(index); - } - else - { - nameToFirstIndex.Add(key, index); + if (nameToFirstIndex.TryGetValue(key, out var firstIndex)) + { + MarkCollision(firstIndex); + MarkCollision(index); + } + else + { + nameToFirstIndex.Add(key, index); + } } + + if (collidingIndices is null || collidingIndices.Count == 0) + return ImmutableArray.Empty; + + collidingIndices.Sort(); + var builder = ImmutableArray.CreateBuilder(collidingIndices.Count); + builder.AddRange(collidingIndices); + return builder.MoveToImmutable(); + } + finally + { + ArrayPool.Shared.Return(collisionFlags); } - if (collidingIndices is null || collidingIndices.Count == 0) - return ImmutableArray.Empty; + void MarkCollision(int handlerIndex) + { + if (collisionFlags[handlerIndex]) + return; - var builder = ImmutableArray.CreateBuilder(collidingIndices.Count); - builder.AddRange(collidingIndices); - builder.Sort(); - return builder.MoveToImmutable(); + collisionFlags[handlerIndex] = true; + collidingIndices ??= new List(); + collidingIndices.Add(handlerIndex); + } } private static string GetFullyQualifiedMethodDisplayName(RequestHandler requestHandler) @@ -2830,6 +2849,7 @@ private struct EndpointAttributeState public bool? RequireRateLimiting; public string? RateLimitingPolicyName; public List? EndpointFilters; + public HashSet? EndpointFilterSet; public bool HasAllowAnonymousAttribute; public bool HasRequireAuthorizationAttribute; public bool? ShortCircuit; From 1a4cbc7c2fa0a178210bf59642ca22c13132f297 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:39:47 -0500 Subject: [PATCH 70/75] Cleanup. --- src/GeneratedEndpoints/Common/StringBuilderPool.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/GeneratedEndpoints/Common/StringBuilderPool.cs b/src/GeneratedEndpoints/Common/StringBuilderPool.cs index fc8b4bc..92ab132 100644 --- a/src/GeneratedEndpoints/Common/StringBuilderPool.cs +++ b/src/GeneratedEndpoints/Common/StringBuilderPool.cs @@ -1,4 +1,3 @@ -using System; using System.Text; namespace GeneratedEndpoints.Common; From 14418cb316ea07d975ac712e7248018cc5d51bf6 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:46:13 -0500 Subject: [PATCH 71/75] Refactor MinimalApiGenerator into partial files (#56) --- .../MinimalApiGenerator.Constants.cs | 773 ++++++++++++++ .../MinimalApiGenerator.Types.cs | 241 +++++ src/GeneratedEndpoints/MinimalApiGenerator.cs | 990 +----------------- 3 files changed, 1015 insertions(+), 989 deletions(-) create mode 100644 src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs create mode 100644 src/GeneratedEndpoints/MinimalApiGenerator.Types.cs diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs new file mode 100644 index 0000000..0cb231a --- /dev/null +++ b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs @@ -0,0 +1,773 @@ +using System.Buffers; +using System.Collections.Immutable; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using GeneratedEndpoints.Common; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace GeneratedEndpoints; + +public sealed partial class MinimalApiGenerator +{ + private const string BaseNamespace = "Microsoft.AspNetCore.Generated"; + private const string AttributesNamespace = $"{BaseNamespace}.Attributes"; + + private const string FallbackHttpMethod = "__FALLBACK__"; + + private const string NameAttributeNamedParameter = "Name"; + private const string ResponseTypeAttributeNamedParameter = "ResponseType"; + private const string RequestTypeAttributeNamedParameter = "RequestType"; + private const string IsOptionalAttributeNamedParameter = "IsOptional"; + private const string PolicyNameAttributeNamedParameter = "PolicyName"; + + private const string RequireAuthorizationAttributeName = "RequireAuthorizationAttribute"; + private const string RequireAuthorizationAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireAuthorizationAttributeName}"; + private const string RequireAuthorizationAttributeHint = $"{RequireAuthorizationAttributeFullyQualifiedName}.gs.cs"; + + private const string RequireCorsAttributeName = "RequireCorsAttribute"; + private const string RequireCorsAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireCorsAttributeName}"; + private const string RequireCorsAttributeHint = $"{RequireCorsAttributeFullyQualifiedName}.gs.cs"; + + private const string RequireRateLimitingAttributeName = "RequireRateLimitingAttribute"; + private const string RequireRateLimitingAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireRateLimitingAttributeName}"; + private const string RequireRateLimitingAttributeHint = $"{RequireRateLimitingAttributeFullyQualifiedName}.gs.cs"; + + private const string RequireHostAttributeName = "RequireHostAttribute"; + private const string RequireHostAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireHostAttributeName}"; + private const string RequireHostAttributeHint = $"{RequireHostAttributeFullyQualifiedName}.gs.cs"; + + private const string DisableAntiforgeryAttributeName = "DisableAntiforgeryAttribute"; + private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; + private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; + + private const string ShortCircuitAttributeName = "ShortCircuitAttribute"; + private const string ShortCircuitAttributeFullyQualifiedName = $"{AttributesNamespace}.{ShortCircuitAttributeName}"; + private const string ShortCircuitAttributeHint = $"{ShortCircuitAttributeFullyQualifiedName}.gs.cs"; + + private const string DisableRequestTimeoutAttributeName = "DisableRequestTimeoutAttribute"; + private const string DisableRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableRequestTimeoutAttributeName}"; + private const string DisableRequestTimeoutAttributeHint = $"{DisableRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; + + private const string DisableValidationAttributeName = "DisableValidationAttribute"; + private const string DisableValidationAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableValidationAttributeName}"; + private const string DisableValidationAttributeHint = $"{DisableValidationAttributeFullyQualifiedName}.gs.cs"; + + private const string RequestTimeoutAttributeName = "RequestTimeoutAttribute"; + private const string RequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequestTimeoutAttributeName}"; + private const string RequestTimeoutAttributeHint = $"{RequestTimeoutAttributeFullyQualifiedName}.gs.cs"; + + private const string OrderAttributeName = "OrderAttribute"; + private const string OrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{OrderAttributeName}"; + private const string OrderAttributeHint = $"{OrderAttributeFullyQualifiedName}.gs.cs"; + + private const string MapGroupAttributeName = "MapGroupAttribute"; + private const string MapGroupAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapGroupAttributeName}"; + private const string MapGroupAttributeHint = $"{MapGroupAttributeFullyQualifiedName}.gs.cs"; + + private const string SummaryAttributeName = "SummaryAttribute"; + private const string SummaryAttributeFullyQualifiedName = $"{AttributesNamespace}.{SummaryAttributeName}"; + private const string SummaryAttributeHint = $"{SummaryAttributeFullyQualifiedName}.gs.cs"; + + private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; + + private const string EndpointFilterAttributeName = "EndpointFilterAttribute"; + private const string EndpointFilterAttributeFullyQualifiedName = $"{AttributesNamespace}.{EndpointFilterAttributeName}"; + private const string EndpointFilterAttributeHint = $"{EndpointFilterAttributeFullyQualifiedName}.gs.cs"; + + private const string AcceptsAttributeName = "AcceptsAttribute"; + private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}"; + private const string AcceptsAttributeHint = $"{AcceptsAttributeFullyQualifiedName}.gs.cs"; + + private const string ProducesResponseAttributeName = "ProducesResponseAttribute"; + private const string ProducesResponseAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesResponseAttributeName}"; + private const string ProducesResponseAttributeHint = $"{ProducesResponseAttributeFullyQualifiedName}.gs.cs"; + + private const string ProducesProblemAttributeName = "ProducesProblemAttribute"; + private const string ProducesProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesProblemAttributeName}"; + private const string ProducesProblemAttributeHint = $"{ProducesProblemAttributeFullyQualifiedName}.gs.cs"; + + private const string ProducesValidationProblemAttributeName = "ProducesValidationProblemAttribute"; + + private const string ProducesValidationProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesValidationProblemAttributeName}"; + + private const string ProducesValidationProblemAttributeHint = $"{ProducesValidationProblemAttributeFullyQualifiedName}.gs.cs"; + + private const string RoutingNamespace = $"{BaseNamespace}.Routing"; + + private const string AddEndpointHandlersClassName = "EndpointServicesExtensions"; + private const string AddEndpointHandlersMethodName = "AddEndpointHandlers"; + private const string AddEndpointHandlersMethodHint = $"{RoutingNamespace}.{AddEndpointHandlersMethodName}.g.cs"; + + private const string UseEndpointHandlersClassName = "EndpointRouteBuilderExtensions"; + private const string UseEndpointHandlersMethodName = "MapEndpointHandlers"; + private const string UseEndpointHandlersMethodHint = $"{RoutingNamespace}.{UseEndpointHandlersMethodName}.g.cs"; + + private const string ConfigureMethodName = "Configure"; + private const string AsyncSuffix = "Async"; + private const string GlobalPrefix = "global::"; + private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.'); + private static readonly string[] AspNetCoreHttpNamespaceParts = ["Microsoft", "AspNetCore", "Http"]; + private static readonly string[] AspNetCoreAuthorizationNamespaceParts = ["Microsoft", "AspNetCore", "Authorization"]; + private static readonly string[] AspNetCoreRoutingNamespaceParts = ["Microsoft", "AspNetCore", "Routing"]; + private static readonly string[] ComponentModelNamespaceParts = ["System", "ComponentModel"]; + private static readonly ConditionalWeakTable CompilationTypeCaches = new(); + private static readonly ConditionalWeakTable RequestHandlerClassCache = new(); + private static readonly ConditionalWeakTable GeneratedAttributeKindCache = new(); + + private static readonly string FileHeader = $""" + //----------------------------------------------------------------------------- + // + // This code was generated by {nameof(MinimalApiGenerator)} which can be found + // in the {typeof(MinimalApiGenerator).Namespace} namespace. + // + // Changes to this file may cause incorrect behavior + // and will be lost if the code is regenerated. + // + //----------------------------------------------------------------------------- + + #nullable enable + """; + + private static readonly ImmutableArray HttpAttributeDefinitions = + [ + CreateHttpAttributeDefinition("MapGetAttribute", "GET"), + CreateHttpAttributeDefinition("MapPostAttribute", "POST"), + CreateHttpAttributeDefinition("MapPutAttribute", "PUT"), + CreateHttpAttributeDefinition("MapPatchAttribute", "PATCH"), + CreateHttpAttributeDefinition("MapDeleteAttribute", "DELETE"), + CreateHttpAttributeDefinition("MapOptionsAttribute", "OPTIONS"), + CreateHttpAttributeDefinition("MapHeadAttribute", "HEAD"), + CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), + CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), + CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), + CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod, true), + ]; + + private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = + HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); + + private static readonly SourceText RequireAuthorizationAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that authorization is required for the annotated endpoint or class. + /// Optionally restricts access to the specified authorization policies. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Attribute + { + /// + /// Gets the policy names that the endpoint or class requires. + /// + public string[] PolicyNames { get; } + + /// + /// Marks the endpoint or class as requiring authorization. + /// + public {{RequireAuthorizationAttributeName}}() + { + PolicyNames = []; + } + + /// + /// Marks the endpoint or class as requiring authorization with one or more policies. + /// + public {{RequireAuthorizationAttributeName}}(params string[] policyNames) + { + PolicyNames = policyNames ?? []; + } + } + """, Encoding.UTF8 + ); + + private static readonly SourceText RequireCorsAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires a configured CORS policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional CORS policy name. + /// + public string? PolicyName { get; } + + /// + /// Marks the endpoint or class as requiring the default CORS policy. + /// + public {{RequireCorsAttributeName}}() + { + } + + /// + /// Marks the endpoint or class as requiring the specified named CORS policy. + /// + public {{RequireCorsAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + """, Encoding.UTF8 + ); + + private static readonly SourceText RequireRateLimitingAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the annotated endpoint requires the provided rate limiting policy. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The rate limiting policy to apply. + public {{RequireRateLimitingAttributeName}}(string policyName) + { + PolicyName = policyName; + } + + /// + /// Gets the rate limiting policy name. + /// + public string PolicyName { get; } + } + """, Encoding.UTF8 + ); + + private static readonly SourceText RequireHostAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the allowed hosts for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequireHostAttributeName}} : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The hosts that are allowed to access the endpoint. + public {{RequireHostAttributeName}}(params string[] hosts) + { + Hosts = hosts ?? []; + } + + /// + /// Gets the allowed hosts. + /// + public string[] Hosts { get; } + } + """, Encoding.UTF8 + ); + + private static readonly SourceText DisableAntiforgeryAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Disables antiforgery protection for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attribute + { + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText ShortCircuitAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Marks the annotated endpoint or class to short-circuit the request pipeline. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute + { + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText DisableRequestTimeoutAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Disables the request timeout for the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute + { + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText DisableValidationAttributeSourceText = SourceText.From($$""" + #if NET10_0_OR_GREATER + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute + { + } + #endif + + """, Encoding.UTF8 + ); + + private static readonly SourceText RequestTimeoutAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Applies the request timeout metadata to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute + { + /// + /// Gets the optional request timeout policy name. + /// + public string? PolicyName { get; init; } + + /// + /// Applies the default request timeout behavior. + /// + public {{RequestTimeoutAttributeName}}() + { + } + + /// + /// Applies the specified request timeout policy. + /// + /// The request timeout policy name. + public {{RequestTimeoutAttributeName}}(string policyName) + { + PolicyName = policyName; + } + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText OrderAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the order for the annotated endpoint when building conventions. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{OrderAttributeName}} : global::System.Attribute + { + /// + /// Gets the order that will be applied to the endpoint. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The order value to apply to the endpoint. + public {{OrderAttributeName}}(int order) + { + Order = order; + } + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText MapGroupAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the route group for the annotated class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + internal sealed class {{MapGroupAttributeName}} : global::System.Attribute + { + /// + /// Gets the route group pattern. + /// + public string Pattern { get; } + + /// + /// Gets or sets the endpoint group name. + /// + public string? Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The route group pattern to apply. + public {{MapGroupAttributeName}}(string pattern) + { + Pattern = pattern; + } + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText SummaryAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the summary metadata for the annotated endpoint. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal sealed class {{SummaryAttributeName}} : global::System.Attribute + { + /// + /// Gets the summary value for the endpoint. + /// + public string Summary { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The summary to apply to the endpoint. + public {{SummaryAttributeName}}(string summary) + { + Summary = summary; + } + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText AcceptsAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies the request type and content types accepted by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type? RequestType { get; init; } + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. + /// + /// The CLR type of the request body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{AcceptsAttributeName}} : global::System.Attribute + { + /// + /// Gets the request type accepted by the endpoint. + /// + public global::System.Type RequestType => typeof(TRequest); + + /// + /// Gets a value indicating whether the request body is optional. + /// + public bool IsOptional { get; init; } + + /// + /// Gets the primary content type accepted by the endpoint. + /// + public string ContentType { get; } + + /// + /// Gets the additional content types accepted by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Accepts attribute class. + /// + /// The primary content type accepted by the endpoint. + /// Additional content types accepted by the endpoint. + public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) + { + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText EndpointFilterAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies an endpoint filter type to apply to the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The CLR type of the endpoint filter. + public {{EndpointFilterAttributeName}}(global::System.Type filterType) + { + FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); + } + } + + /// + /// Specifies an endpoint filter type using a generic argument. + /// + /// The CLR type of the endpoint filter. + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute + { + /// + /// Gets the CLR type of the endpoint filter. + /// + public global::System.Type FilterType => typeof(TFilter); + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText ProducesResponseAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type? ResponseType { get; init; } + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + /// + /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. + /// + /// The CLR type of the response body. + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute + { + /// + /// Gets the response type produced by the endpoint. + /// + public global::System.Type ResponseType => typeof(TResponse); + + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the generic Produces attribute class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText ProducesProblemAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); + + private static readonly SourceText ProducesValidationProblemAttributeSourceText = SourceText.From($$""" + {{FileHeader}} + + namespace {{AttributesNamespace}}; + + /// + /// Specifies that the endpoint produces a validation problem details payload. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute + { + /// + /// Gets the HTTP status code returned by the endpoint. + /// + public int StatusCode { get; } + + /// + /// Gets the primary content type produced by the endpoint. + /// + public string? ContentType { get; } + + /// + /// Gets the additional content types produced by the endpoint. + /// + public string[] AdditionalContentTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP status code returned by the endpoint. + /// The primary content type produced by the endpoint. + /// Additional content types produced by the endpoint. + public {{ProducesValidationProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) + { + StatusCode = statusCode; + ContentType = contentType; + AdditionalContentTypes = additionalContentTypes ?? []; + } + } + + """, Encoding.UTF8 + ); + +} diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.Types.cs b/src/GeneratedEndpoints/MinimalApiGenerator.Types.cs new file mode 100644 index 0000000..006c52e --- /dev/null +++ b/src/GeneratedEndpoints/MinimalApiGenerator.Types.cs @@ -0,0 +1,241 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using GeneratedEndpoints.Common; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace GeneratedEndpoints; + +public sealed partial class MinimalApiGenerator +{ + private readonly record struct HttpAttributeDefinition( + string Name, + string FullyQualifiedName, + string Hint, + string Verb, + bool AllowEmptyPattern, + SourceText SourceText + ); + + private readonly record struct RequestHandler( + RequestHandlerClass Class, + RequestHandlerMethod Method, + string HttpMethod, + string Pattern, + EndpointConfiguration Configuration + ); + + private readonly record struct RequestHandlerClass( + string Name, + bool IsStatic, + bool HasConfigureMethod, + bool ConfigureMethodAcceptsServiceProvider, + string? MapGroupPattern, + string? MapGroupBuilderIdentifier, + EndpointConfiguration Configuration + ); + + private readonly record struct EndpointConfiguration( + RequestHandlerMetadata Metadata, + bool RequireAuthorization, + EquatableImmutableArray? AuthorizationPolicies, + bool DisableAntiforgery, + bool AllowAnonymous, + bool RequireCors, + string? CorsPolicyName, + EquatableImmutableArray? RequiredHosts, + bool RequireRateLimiting, + string? RateLimitingPolicyName, + EquatableImmutableArray? EndpointFilterTypes, + bool ShortCircuit, + bool DisableValidation, + bool DisableRequestTimeout, + bool WithRequestTimeout, + string? RequestTimeoutPolicyName, + int? Order, + string? EndpointGroupName + ); + + private readonly record struct RequestHandlerMethod(string Name, bool IsStatic, bool IsAwaitable, EquatableImmutableArray Parameters); + + private readonly record struct RequestHandlerMetadata( + string? Name, + string? DisplayName, + string? Summary, + string? Description, + EquatableImmutableArray? Tags, + EquatableImmutableArray? Accepts, + EquatableImmutableArray? Produces, + EquatableImmutableArray? ProducesProblem, + EquatableImmutableArray? ProducesValidationProblem, + bool ExcludeFromDescription + ); + + private readonly record struct AcceptsMetadata( + string RequestType, + string ContentType, + EquatableImmutableArray? AdditionalContentTypes, + bool IsOptional + ); + + private readonly record struct ProducesMetadata( + string ResponseType, + int StatusCode, + string? ContentType, + EquatableImmutableArray? AdditionalContentTypes + ); + + private readonly record struct ProducesProblemMetadata(int StatusCode, string? ContentType, EquatableImmutableArray? AdditionalContentTypes); + + private readonly record struct ProducesValidationProblemMetadata( + int StatusCode, + string? ContentType, + EquatableImmutableArray? AdditionalContentTypes + ); + + private readonly record struct Parameter(string Name, string Type, string BindingPrefix); + + private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); + + private readonly record struct HandlerNameKey(string Name, string Method); + + private struct EndpointAttributeState + { + public EquatableImmutableArray? Tags; + public bool? RequireAuthorization; + public EquatableImmutableArray? AuthorizationPolicies; + public bool? DisableAntiforgery; + public bool? AllowAnonymous; + public bool? ExcludeFromDescription; + public List? Accepts; + public List? Produces; + public List? ProducesProblem; + public List? ProducesValidationProblem; + public bool? RequireCors; + public string? CorsPolicyName; + public EquatableImmutableArray? RequiredHosts; + public bool? RequireRateLimiting; + public string? RateLimitingPolicyName; + public List? EndpointFilters; + public HashSet? EndpointFilterSet; + public bool HasAllowAnonymousAttribute; + public bool HasRequireAuthorizationAttribute; + public bool? ShortCircuit; + public bool? DisableValidation; + public bool? DisableRequestTimeout; + public bool? WithRequestTimeout; + public string? RequestTimeoutPolicyName; + public int? Order; + public string? EndpointGroupName; + public string? Summary; + } + + private enum GeneratedAttributeKind + { + None = 0, + ShortCircuit, + DisableValidation, + DisableRequestTimeout, + RequestTimeout, + Order, + MapGroup, + Summary, + Accepts, + ProducesResponse, + RequireAuthorization, + RequireCors, + RequireHost, + RequireRateLimiting, + EndpointFilter, + DisableAntiforgery, + ProducesProblem, + ProducesValidationProblem, + } + + private enum BindingSource + { + None = 0, + FromRoute = 1, + FromQuery = 2, + FromHeader = 3, + FromBody = 4, + FromForm = 5, + FromServices = 6, + FromKeyedServices = 7, + AsParameters = 8, + } + + private sealed class RequestHandlerComparer : IComparer + { + public static RequestHandlerComparer Instance { get; } = new(); + + public int Compare(RequestHandler x, RequestHandler y) + { + var comparison = string.Compare(x.Class.Name, y.Class.Name, StringComparison.Ordinal); + if (comparison != 0) + return comparison; + + comparison = string.Compare(x.Method.Name, y.Method.Name, StringComparison.Ordinal); + if (comparison != 0) + return comparison; + + comparison = string.Compare(x.HttpMethod, y.HttpMethod, StringComparison.Ordinal); + if (comparison != 0) + return comparison; + + return string.Compare(x.Pattern, y.Pattern, StringComparison.Ordinal); + } + } + + private sealed class CompilationTypeCache(Compilation compilation) + { + public INamedTypeSymbol? EndpointConventionBuilderSymbol { get; } = + compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); + + public INamedTypeSymbol? ServiceProviderSymbol { get; } = compilation.GetTypeByMetadataName("System.IServiceProvider"); + } + + private sealed class RequestHandlerClassCacheEntry + { + private readonly object _lock = new(); + private RequestHandlerClass _value; + private bool _initialized; + + public RequestHandlerClass GetOrCreate(INamedTypeSymbol classSymbol, CompilationTypeCache compilationCache, CancellationToken cancellationToken) + { + if (_initialized) + return _value; + + lock (_lock) + { + if (_initialized) + return _value; + + cancellationToken.ThrowIfCancellationRequested(); + + var name = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var isStatic = classSymbol.IsStatic; + var configureMethodDetails = GetConfigureMethodDetails(classSymbol, compilationCache.EndpointConventionBuilderSymbol, + compilationCache.ServiceProviderSymbol, cancellationToken + ); + + var mapGroupPattern = GetMapGroupPattern(classSymbol); + var mapGroupIdentifier = mapGroupPattern is null ? null : GetMapGroupIdentifier(name); + var classConfiguration = GetEndpointConfiguration(classSymbol.GetAttributes(), null, null, null, false); + + _value = new RequestHandlerClass(name, isStatic, configureMethodDetails.HasConfigureMethod, + configureMethodDetails.ConfigureMethodAcceptsServiceProvider, mapGroupPattern, mapGroupIdentifier, classConfiguration + ); + _initialized = true; + return _value; + } + } + } + + private sealed class GeneratedAttributeKindCacheEntry(GeneratedAttributeKind kind) + { + public GeneratedAttributeKind Kind { get; } = kind; + } +} diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 0ec261f..81a0b0a 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -13,767 +13,8 @@ namespace GeneratedEndpoints; [Generator] -public sealed class MinimalApiGenerator : IIncrementalGenerator +public sealed partial class MinimalApiGenerator : IIncrementalGenerator { - private const string BaseNamespace = "Microsoft.AspNetCore.Generated"; - private const string AttributesNamespace = $"{BaseNamespace}.Attributes"; - - private const string FallbackHttpMethod = "__FALLBACK__"; - - private const string NameAttributeNamedParameter = "Name"; - private const string ResponseTypeAttributeNamedParameter = "ResponseType"; - private const string RequestTypeAttributeNamedParameter = "RequestType"; - private const string IsOptionalAttributeNamedParameter = "IsOptional"; - private const string PolicyNameAttributeNamedParameter = "PolicyName"; - - private const string RequireAuthorizationAttributeName = "RequireAuthorizationAttribute"; - private const string RequireAuthorizationAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireAuthorizationAttributeName}"; - private const string RequireAuthorizationAttributeHint = $"{RequireAuthorizationAttributeFullyQualifiedName}.gs.cs"; - - private const string RequireCorsAttributeName = "RequireCorsAttribute"; - private const string RequireCorsAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireCorsAttributeName}"; - private const string RequireCorsAttributeHint = $"{RequireCorsAttributeFullyQualifiedName}.gs.cs"; - - private const string RequireRateLimitingAttributeName = "RequireRateLimitingAttribute"; - private const string RequireRateLimitingAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireRateLimitingAttributeName}"; - private const string RequireRateLimitingAttributeHint = $"{RequireRateLimitingAttributeFullyQualifiedName}.gs.cs"; - - private const string RequireHostAttributeName = "RequireHostAttribute"; - private const string RequireHostAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequireHostAttributeName}"; - private const string RequireHostAttributeHint = $"{RequireHostAttributeFullyQualifiedName}.gs.cs"; - - private const string DisableAntiforgeryAttributeName = "DisableAntiforgeryAttribute"; - private const string DisableAntiforgeryAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableAntiforgeryAttributeName}"; - private const string DisableAntiforgeryAttributeHint = $"{DisableAntiforgeryAttributeFullyQualifiedName}.gs.cs"; - - private const string ShortCircuitAttributeName = "ShortCircuitAttribute"; - private const string ShortCircuitAttributeFullyQualifiedName = $"{AttributesNamespace}.{ShortCircuitAttributeName}"; - private const string ShortCircuitAttributeHint = $"{ShortCircuitAttributeFullyQualifiedName}.gs.cs"; - - private const string DisableRequestTimeoutAttributeName = "DisableRequestTimeoutAttribute"; - private const string DisableRequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableRequestTimeoutAttributeName}"; - private const string DisableRequestTimeoutAttributeHint = $"{DisableRequestTimeoutAttributeFullyQualifiedName}.gs.cs"; - - private const string DisableValidationAttributeName = "DisableValidationAttribute"; - private const string DisableValidationAttributeFullyQualifiedName = $"{AttributesNamespace}.{DisableValidationAttributeName}"; - private const string DisableValidationAttributeHint = $"{DisableValidationAttributeFullyQualifiedName}.gs.cs"; - - private const string RequestTimeoutAttributeName = "RequestTimeoutAttribute"; - private const string RequestTimeoutAttributeFullyQualifiedName = $"{AttributesNamespace}.{RequestTimeoutAttributeName}"; - private const string RequestTimeoutAttributeHint = $"{RequestTimeoutAttributeFullyQualifiedName}.gs.cs"; - - private const string OrderAttributeName = "OrderAttribute"; - private const string OrderAttributeFullyQualifiedName = $"{AttributesNamespace}.{OrderAttributeName}"; - private const string OrderAttributeHint = $"{OrderAttributeFullyQualifiedName}.gs.cs"; - - private const string MapGroupAttributeName = "MapGroupAttribute"; - private const string MapGroupAttributeFullyQualifiedName = $"{AttributesNamespace}.{MapGroupAttributeName}"; - private const string MapGroupAttributeHint = $"{MapGroupAttributeFullyQualifiedName}.gs.cs"; - - private const string SummaryAttributeName = "SummaryAttribute"; - private const string SummaryAttributeFullyQualifiedName = $"{AttributesNamespace}.{SummaryAttributeName}"; - private const string SummaryAttributeHint = $"{SummaryAttributeFullyQualifiedName}.gs.cs"; - - private const string AllowAnonymousAttributeName = "AllowAnonymousAttribute"; - - private const string EndpointFilterAttributeName = "EndpointFilterAttribute"; - private const string EndpointFilterAttributeFullyQualifiedName = $"{AttributesNamespace}.{EndpointFilterAttributeName}"; - private const string EndpointFilterAttributeHint = $"{EndpointFilterAttributeFullyQualifiedName}.gs.cs"; - - private const string AcceptsAttributeName = "AcceptsAttribute"; - private const string AcceptsAttributeFullyQualifiedName = $"{AttributesNamespace}.{AcceptsAttributeName}"; - private const string AcceptsAttributeHint = $"{AcceptsAttributeFullyQualifiedName}.gs.cs"; - - private const string ProducesResponseAttributeName = "ProducesResponseAttribute"; - private const string ProducesResponseAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesResponseAttributeName}"; - private const string ProducesResponseAttributeHint = $"{ProducesResponseAttributeFullyQualifiedName}.gs.cs"; - - private const string ProducesProblemAttributeName = "ProducesProblemAttribute"; - private const string ProducesProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesProblemAttributeName}"; - private const string ProducesProblemAttributeHint = $"{ProducesProblemAttributeFullyQualifiedName}.gs.cs"; - - private const string ProducesValidationProblemAttributeName = "ProducesValidationProblemAttribute"; - - private const string ProducesValidationProblemAttributeFullyQualifiedName = $"{AttributesNamespace}.{ProducesValidationProblemAttributeName}"; - - private const string ProducesValidationProblemAttributeHint = $"{ProducesValidationProblemAttributeFullyQualifiedName}.gs.cs"; - - private const string RoutingNamespace = $"{BaseNamespace}.Routing"; - - private const string AddEndpointHandlersClassName = "EndpointServicesExtensions"; - private const string AddEndpointHandlersMethodName = "AddEndpointHandlers"; - private const string AddEndpointHandlersMethodHint = $"{RoutingNamespace}.{AddEndpointHandlersMethodName}.g.cs"; - - private const string UseEndpointHandlersClassName = "EndpointRouteBuilderExtensions"; - private const string UseEndpointHandlersMethodName = "MapEndpointHandlers"; - private const string UseEndpointHandlersMethodHint = $"{RoutingNamespace}.{UseEndpointHandlersMethodName}.g.cs"; - - private const string ConfigureMethodName = "Configure"; - private const string AsyncSuffix = "Async"; - private const string GlobalPrefix = "global::"; - private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.'); - private static readonly string[] AspNetCoreHttpNamespaceParts = ["Microsoft", "AspNetCore", "Http"]; - private static readonly string[] AspNetCoreAuthorizationNamespaceParts = ["Microsoft", "AspNetCore", "Authorization"]; - private static readonly string[] AspNetCoreRoutingNamespaceParts = ["Microsoft", "AspNetCore", "Routing"]; - private static readonly string[] ComponentModelNamespaceParts = ["System", "ComponentModel"]; - private static readonly ConditionalWeakTable CompilationTypeCaches = new(); - private static readonly ConditionalWeakTable RequestHandlerClassCache = new(); - private static readonly ConditionalWeakTable GeneratedAttributeKindCache = new(); - - private static readonly string FileHeader = $""" - //----------------------------------------------------------------------------- - // - // This code was generated by {nameof(MinimalApiGenerator)} which can be found - // in the {typeof(MinimalApiGenerator).Namespace} namespace. - // - // Changes to this file may cause incorrect behavior - // and will be lost if the code is regenerated. - // - //----------------------------------------------------------------------------- - - #nullable enable - """; - - private static readonly ImmutableArray HttpAttributeDefinitions = - [ - CreateHttpAttributeDefinition("MapGetAttribute", "GET"), - CreateHttpAttributeDefinition("MapPostAttribute", "POST"), - CreateHttpAttributeDefinition("MapPutAttribute", "PUT"), - CreateHttpAttributeDefinition("MapPatchAttribute", "PATCH"), - CreateHttpAttributeDefinition("MapDeleteAttribute", "DELETE"), - CreateHttpAttributeDefinition("MapOptionsAttribute", "OPTIONS"), - CreateHttpAttributeDefinition("MapHeadAttribute", "HEAD"), - CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), - CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), - CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), - CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod, true), - ]; - - private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = - HttpAttributeDefinitions.ToImmutableDictionary(static definition => definition.Name); - - private static readonly SourceText RequireAuthorizationAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that authorization is required for the annotated endpoint or class. - /// Optionally restricts access to the specified authorization policies. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireAuthorizationAttributeName}} : global::System.Attribute - { - /// - /// Gets the policy names that the endpoint or class requires. - /// - public string[] PolicyNames { get; } - - /// - /// Marks the endpoint or class as requiring authorization. - /// - public {{RequireAuthorizationAttributeName}}() - { - PolicyNames = []; - } - - /// - /// Marks the endpoint or class as requiring authorization with one or more policies. - /// - public {{RequireAuthorizationAttributeName}}(params string[] policyNames) - { - PolicyNames = policyNames ?? []; - } - } - """, Encoding.UTF8 - ); - - private static readonly SourceText RequireCorsAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the annotated endpoint requires a configured CORS policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireCorsAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional CORS policy name. - /// - public string? PolicyName { get; } - - /// - /// Marks the endpoint or class as requiring the default CORS policy. - /// - public {{RequireCorsAttributeName}}() - { - } - - /// - /// Marks the endpoint or class as requiring the specified named CORS policy. - /// - public {{RequireCorsAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } - """, Encoding.UTF8 - ); - - private static readonly SourceText RequireRateLimitingAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the annotated endpoint requires the provided rate limiting policy. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireRateLimitingAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The rate limiting policy to apply. - public {{RequireRateLimitingAttributeName}}(string policyName) - { - PolicyName = policyName; - } - - /// - /// Gets the rate limiting policy name. - /// - public string PolicyName { get; } - } - """, Encoding.UTF8 - ); - - private static readonly SourceText RequireHostAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the allowed hosts for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequireHostAttributeName}} : global::System.Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// The hosts that are allowed to access the endpoint. - public {{RequireHostAttributeName}}(params string[] hosts) - { - Hosts = hosts ?? []; - } - - /// - /// Gets the allowed hosts. - /// - public string[] Hosts { get; } - } - """, Encoding.UTF8 - ); - - private static readonly SourceText DisableAntiforgeryAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Disables antiforgery protection for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableAntiforgeryAttributeName}} : global::System.Attribute - { - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText ShortCircuitAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Marks the annotated endpoint or class to short-circuit the request pipeline. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{ShortCircuitAttributeName}} : global::System.Attribute - { - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText DisableRequestTimeoutAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Disables the request timeout for the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableRequestTimeoutAttributeName}} : global::System.Attribute - { - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText DisableValidationAttributeSourceText = SourceText.From($$""" - #if NET10_0_OR_GREATER - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Disables request validation for the annotated endpoint or class when targeting .NET 10 or later. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{DisableValidationAttributeName}} : global::System.Attribute - { - } - #endif - - """, Encoding.UTF8 - ); - - private static readonly SourceText RequestTimeoutAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Applies the request timeout metadata to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{RequestTimeoutAttributeName}} : global::System.Attribute - { - /// - /// Gets the optional request timeout policy name. - /// - public string? PolicyName { get; init; } - - /// - /// Applies the default request timeout behavior. - /// - public {{RequestTimeoutAttributeName}}() - { - } - - /// - /// Applies the specified request timeout policy. - /// - /// The request timeout policy name. - public {{RequestTimeoutAttributeName}}(string policyName) - { - PolicyName = policyName; - } - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText OrderAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the order for the annotated endpoint when building conventions. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{OrderAttributeName}} : global::System.Attribute - { - /// - /// Gets the order that will be applied to the endpoint. - /// - public int Order { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The order value to apply to the endpoint. - public {{OrderAttributeName}}(int order) - { - Order = order; - } - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText MapGroupAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the route group for the annotated class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - internal sealed class {{MapGroupAttributeName}} : global::System.Attribute - { - /// - /// Gets the route group pattern. - /// - public string Pattern { get; } - - /// - /// Gets or sets the endpoint group name. - /// - public string? Name { get; init; } - - /// - /// Initializes a new instance of the class. - /// - /// The route group pattern to apply. - public {{MapGroupAttributeName}}(string pattern) - { - Pattern = pattern; - } - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText SummaryAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the summary metadata for the annotated endpoint. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = false)] - internal sealed class {{SummaryAttributeName}} : global::System.Attribute - { - /// - /// Gets the summary value for the endpoint. - /// - public string Summary { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The summary to apply to the endpoint. - public {{SummaryAttributeName}}(string summary) - { - Summary = summary; - } - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText AcceptsAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies the request type and content types accepted by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type? RequestType { get; init; } - - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } - - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } - - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - /// - /// Specifies the request type using a generic argument and the content types accepted by the annotated endpoint or class. - /// - /// The CLR type of the request body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{AcceptsAttributeName}} : global::System.Attribute - { - /// - /// Gets the request type accepted by the endpoint. - /// - public global::System.Type RequestType => typeof(TRequest); - - /// - /// Gets a value indicating whether the request body is optional. - /// - public bool IsOptional { get; init; } - - /// - /// Gets the primary content type accepted by the endpoint. - /// - public string ContentType { get; } - - /// - /// Gets the additional content types accepted by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the generic Accepts attribute class. - /// - /// The primary content type accepted by the endpoint. - /// Additional content types accepted by the endpoint. - public {{AcceptsAttributeName}}(string contentType = "application/json", params string[] additionalContentTypes) - { - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/json" : contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText EndpointFilterAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies an endpoint filter type to apply to the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute - { - /// - /// Gets the CLR type of the endpoint filter. - /// - public global::System.Type FilterType { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The CLR type of the endpoint filter. - public {{EndpointFilterAttributeName}}(global::System.Type filterType) - { - FilterType = filterType ?? throw new global::System.ArgumentNullException(nameof(filterType)); - } - } - - /// - /// Specifies an endpoint filter type using a generic argument. - /// - /// The CLR type of the endpoint filter. - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{EndpointFilterAttributeName}} : global::System.Attribute - { - /// - /// Gets the CLR type of the endpoint filter. - /// - public global::System.Type FilterType => typeof(TFilter); - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText ProducesResponseAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies a response type, status code, and content types produced by the annotated endpoint or class. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type? ResponseType { get; init; } - - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - /// - /// Specifies a response type using a generic argument along with status code and content types produced by the annotated endpoint or class. - /// - /// The CLR type of the response body. - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesResponseAttributeName}} : global::System.Attribute - { - /// - /// Gets the response type produced by the endpoint. - /// - public global::System.Type ResponseType => typeof(TResponse); - - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the generic Produces attribute class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesResponseAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status200OK, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText ProducesProblemAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the endpoint produces a problem details payload. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesProblemAttributeName}} : global::System.Attribute - { - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8 - ); - - private static readonly SourceText ProducesValidationProblemAttributeSourceText = SourceText.From($$""" - {{FileHeader}} - - namespace {{AttributesNamespace}}; - - /// - /// Specifies that the endpoint produces a validation problem details payload. - /// - [global::System.AttributeUsage(global::System.AttributeTargets.Method, Inherited = false, AllowMultiple = true)] - internal sealed class {{ProducesValidationProblemAttributeName}} : global::System.Attribute - { - /// - /// Gets the HTTP status code returned by the endpoint. - /// - public int StatusCode { get; } - - /// - /// Gets the primary content type produced by the endpoint. - /// - public string? ContentType { get; } - - /// - /// Gets the additional content types produced by the endpoint. - /// - public string[] AdditionalContentTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP status code returned by the endpoint. - /// The primary content type produced by the endpoint. - /// Additional content types produced by the endpoint. - public {{ProducesValidationProblemAttributeName}}(int statusCode = global::Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest, string? contentType = null, params string[] additionalContentTypes) - { - StatusCode = statusCode; - ContentType = contentType; - AdditionalContentTypes = additionalContentTypes ?? []; - } - } - - """, Encoding.UTF8 - ); - public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(RegisterAttributes); @@ -2739,233 +1980,4 @@ _ when char.IsControl(c) => "\\u" + ((int)c).ToString("x4", CultureInfo.Invarian _ => c.ToString(), }; } - - private readonly record struct HttpAttributeDefinition( - string Name, - string FullyQualifiedName, - string Hint, - string Verb, - bool AllowEmptyPattern, - SourceText SourceText - ); - - private readonly record struct RequestHandler( - RequestHandlerClass Class, - RequestHandlerMethod Method, - string HttpMethod, - string Pattern, - EndpointConfiguration Configuration - ); - - private readonly record struct RequestHandlerClass( - string Name, - bool IsStatic, - bool HasConfigureMethod, - bool ConfigureMethodAcceptsServiceProvider, - string? MapGroupPattern, - string? MapGroupBuilderIdentifier, - EndpointConfiguration Configuration - ); - - private readonly record struct EndpointConfiguration( - RequestHandlerMetadata Metadata, - bool RequireAuthorization, - EquatableImmutableArray? AuthorizationPolicies, - bool DisableAntiforgery, - bool AllowAnonymous, - bool RequireCors, - string? CorsPolicyName, - EquatableImmutableArray? RequiredHosts, - bool RequireRateLimiting, - string? RateLimitingPolicyName, - EquatableImmutableArray? EndpointFilterTypes, - bool ShortCircuit, - bool DisableValidation, - bool DisableRequestTimeout, - bool WithRequestTimeout, - string? RequestTimeoutPolicyName, - int? Order, - string? EndpointGroupName - ); - - private readonly record struct RequestHandlerMethod(string Name, bool IsStatic, bool IsAwaitable, EquatableImmutableArray Parameters); - - private readonly record struct RequestHandlerMetadata( - string? Name, - string? DisplayName, - string? Summary, - string? Description, - EquatableImmutableArray? Tags, - EquatableImmutableArray? Accepts, - EquatableImmutableArray? Produces, - EquatableImmutableArray? ProducesProblem, - EquatableImmutableArray? ProducesValidationProblem, - bool ExcludeFromDescription - ); - - private readonly record struct AcceptsMetadata( - string RequestType, - string ContentType, - EquatableImmutableArray? AdditionalContentTypes, - bool IsOptional - ); - - private readonly record struct ProducesMetadata( - string ResponseType, - int StatusCode, - string? ContentType, - EquatableImmutableArray? AdditionalContentTypes - ); - - private readonly record struct ProducesProblemMetadata(int StatusCode, string? ContentType, EquatableImmutableArray? AdditionalContentTypes); - - private readonly record struct ProducesValidationProblemMetadata( - int StatusCode, - string? ContentType, - EquatableImmutableArray? AdditionalContentTypes - ); - - private readonly record struct Parameter(string Name, string Type, string BindingPrefix); - - private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); - - private readonly record struct HandlerNameKey(string Name, string Method); - - private struct EndpointAttributeState - { - public EquatableImmutableArray? Tags; - public bool? RequireAuthorization; - public EquatableImmutableArray? AuthorizationPolicies; - public bool? DisableAntiforgery; - public bool? AllowAnonymous; - public bool? ExcludeFromDescription; - public List? Accepts; - public List? Produces; - public List? ProducesProblem; - public List? ProducesValidationProblem; - public bool? RequireCors; - public string? CorsPolicyName; - public EquatableImmutableArray? RequiredHosts; - public bool? RequireRateLimiting; - public string? RateLimitingPolicyName; - public List? EndpointFilters; - public HashSet? EndpointFilterSet; - public bool HasAllowAnonymousAttribute; - public bool HasRequireAuthorizationAttribute; - public bool? ShortCircuit; - public bool? DisableValidation; - public bool? DisableRequestTimeout; - public bool? WithRequestTimeout; - public string? RequestTimeoutPolicyName; - public int? Order; - public string? EndpointGroupName; - public string? Summary; - } - - private enum GeneratedAttributeKind - { - None = 0, - ShortCircuit, - DisableValidation, - DisableRequestTimeout, - RequestTimeout, - Order, - MapGroup, - Summary, - Accepts, - ProducesResponse, - RequireAuthorization, - RequireCors, - RequireHost, - RequireRateLimiting, - EndpointFilter, - DisableAntiforgery, - ProducesProblem, - ProducesValidationProblem, - } - - private enum BindingSource - { - None = 0, - FromRoute = 1, - FromQuery = 2, - FromHeader = 3, - FromBody = 4, - FromForm = 5, - FromServices = 6, - FromKeyedServices = 7, - AsParameters = 8, - } - - private sealed class RequestHandlerComparer : IComparer - { - public static RequestHandlerComparer Instance { get; } = new(); - - public int Compare(RequestHandler x, RequestHandler y) - { - var comparison = string.Compare(x.Class.Name, y.Class.Name, StringComparison.Ordinal); - if (comparison != 0) - return comparison; - - comparison = string.Compare(x.Method.Name, y.Method.Name, StringComparison.Ordinal); - if (comparison != 0) - return comparison; - - comparison = string.Compare(x.HttpMethod, y.HttpMethod, StringComparison.Ordinal); - if (comparison != 0) - return comparison; - - return string.Compare(x.Pattern, y.Pattern, StringComparison.Ordinal); - } - } - - private sealed class CompilationTypeCache(Compilation compilation) - { - public INamedTypeSymbol? EndpointConventionBuilderSymbol { get; } = - compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"); - - public INamedTypeSymbol? ServiceProviderSymbol { get; } = compilation.GetTypeByMetadataName("System.IServiceProvider"); - } - - private sealed class RequestHandlerClassCacheEntry - { - private readonly object _lock = new(); - private RequestHandlerClass _value; - private bool _initialized; - - public RequestHandlerClass GetOrCreate(INamedTypeSymbol classSymbol, CompilationTypeCache compilationCache, CancellationToken cancellationToken) - { - if (_initialized) - return _value; - - lock (_lock) - { - if (_initialized) - return _value; - - cancellationToken.ThrowIfCancellationRequested(); - - var name = classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var isStatic = classSymbol.IsStatic; - var configureMethodDetails = GetConfigureMethodDetails(classSymbol, compilationCache.EndpointConventionBuilderSymbol, - compilationCache.ServiceProviderSymbol, cancellationToken - ); - - var mapGroupPattern = GetMapGroupPattern(classSymbol); - var mapGroupIdentifier = mapGroupPattern is null ? null : GetMapGroupIdentifier(name); - var classConfiguration = GetEndpointConfiguration(classSymbol.GetAttributes(), null, null, null, false); - - _value = new RequestHandlerClass(name, isStatic, configureMethodDetails.HasConfigureMethod, - configureMethodDetails.ConfigureMethodAcceptsServiceProvider, mapGroupPattern, mapGroupIdentifier, classConfiguration - ); - _initialized = true; - return _value; - } - } - } - - private sealed class GeneratedAttributeKindCacheEntry(GeneratedAttributeKind kind) - { - public GeneratedAttributeKind Kind { get; } = kind; - } } From e04d2ae8e5aa15d9c6f471a2e9427b9171b7c237 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:50:41 -0500 Subject: [PATCH 72/75] Cleanup. --- src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs | 5 +---- src/GeneratedEndpoints/MinimalApiGenerator.Types.cs | 7 ------- src/GeneratedEndpoints/MinimalApiGenerator.cs | 7 +++---- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs index 0cb231a..3ba0dbb 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs @@ -1,9 +1,6 @@ -using System.Buffers; using System.Collections.Immutable; -using System.Globalization; using System.Runtime.CompilerServices; using System.Text; -using GeneratedEndpoints.Common; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; @@ -142,7 +139,7 @@ public sealed partial class MinimalApiGenerator CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), - CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod, true), + CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod), ]; private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.Types.cs b/src/GeneratedEndpoints/MinimalApiGenerator.Types.cs index 006c52e..8ec7a24 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.Types.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.Types.cs @@ -1,7 +1,3 @@ -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text; using GeneratedEndpoints.Common; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; @@ -15,7 +11,6 @@ private readonly record struct HttpAttributeDefinition( string FullyQualifiedName, string Hint, string Verb, - bool AllowEmptyPattern, SourceText SourceText ); @@ -99,8 +94,6 @@ private readonly record struct ProducesValidationProblemMetadata( private readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider); - private readonly record struct HandlerNameKey(string Name, string Method); - private struct EndpointAttributeState { public EquatableImmutableArray? Tags; diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 81a0b0a..c51a70b 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Runtime.CompilerServices; using System.Text; using GeneratedEndpoints.Common; using Microsoft.CodeAnalysis; @@ -42,7 +41,7 @@ private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attr var hint = $"{fullyQualifiedName}.gs.cs"; var summaryVerb = verb == FallbackHttpMethod ? "fallback" : verb; var source = GenerateHttpAttributeSource(AttributesNamespace, attributeName, summaryVerb, allowEmptyPattern); - return new HttpAttributeDefinition(attributeName, fullyQualifiedName, hint, verb, allowEmptyPattern, SourceText.From(source, Encoding.UTF8)); + return new HttpAttributeDefinition(attributeName, fullyQualifiedName, hint, verb, SourceText.From(source, Encoding.UTF8)); } private static IncrementalValueProvider> CombineRequestHandlers( @@ -1079,7 +1078,7 @@ private static ImmutableArray GetRequestHandlersWithNameCollisions(Immutabl return ImmutableArray.Empty; var handlerCount = requestHandlers.Length; - var nameToFirstIndex = new Dictionary(handlerCount); + var nameToFirstIndex = new Dictionary<(string Name, string Method), int>(handlerCount); var collisionFlags = ArrayPool.Shared.Rent(handlerCount); Array.Clear(collisionFlags, 0, handlerCount); List? collidingIndices = null; @@ -1093,7 +1092,7 @@ private static ImmutableArray GetRequestHandlersWithNameCollisions(Immutabl if (string.IsNullOrEmpty(name)) continue; - var key = new HandlerNameKey(name!, handler.Method.Name); + var key = (name!, handler.Method.Name); if (nameToFirstIndex.TryGetValue(key, out var firstIndex)) { From 23e7dd3c094f44058bc51f3321b18e62bd85cf1e Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:57:55 -0500 Subject: [PATCH 73/75] Cleanup. --- .../MinimalApiGenerator.Constants.cs | 2 +- src/GeneratedEndpoints/MinimalApiGenerator.cs | 14 ++++++-------- ...nerationTests.MapFallbackAttribute.verified.txt | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs index 3ba0dbb..a854312 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs @@ -139,7 +139,7 @@ public sealed partial class MinimalApiGenerator CreateHttpAttributeDefinition("MapQueryAttribute", "QUERY"), CreateHttpAttributeDefinition("MapTraceAttribute", "TRACE"), CreateHttpAttributeDefinition("MapConnectAttribute", "CONNECT"), - CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod), + CreateHttpAttributeDefinition("MapFallbackAttribute", FallbackHttpMethod, true), ]; private static readonly ImmutableDictionary HttpAttributeDefinitionsByName = diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index c51a70b..af6aa98 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -35,12 +35,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(requestHandlers, GenerateSource); } - private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb, bool allowEmptyPattern = false) + private static HttpAttributeDefinition CreateHttpAttributeDefinition(string attributeName, string verb, bool allowOptionalPattern = false) { var fullyQualifiedName = $"{AttributesNamespace}.{attributeName}"; var hint = $"{fullyQualifiedName}.gs.cs"; var summaryVerb = verb == FallbackHttpMethod ? "fallback" : verb; - var source = GenerateHttpAttributeSource(AttributesNamespace, attributeName, summaryVerb, allowEmptyPattern); + var source = GenerateHttpAttributeSource(AttributesNamespace, attributeName, summaryVerb, allowOptionalPattern); return new HttpAttributeDefinition(attributeName, fullyQualifiedName, hint, verb, SourceText.From(source, Encoding.UTF8)); } @@ -85,9 +85,8 @@ private static void RegisterAttributes(IncrementalGeneratorPostInitializationCon context.AddSource(ProducesValidationProblemAttributeHint, ProducesValidationProblemAttributeSourceText); } - private static string GenerateHttpAttributeSource(string attributesNamespace, string attributeName, string summaryVerb, bool allowEmptyPattern) + private static string GenerateHttpAttributeSource(string attributesNamespace, string attributeName, string summaryVerb, bool allowOptionalPattern = false) { - var patternDefaultValue = allowEmptyPattern ? " = \"\"" : string.Empty; return $$""" {{FileHeader}} @@ -102,7 +101,7 @@ internal sealed class {{attributeName}} : global::System.Attribute /// /// Gets the route pattern for the endpoint. /// - public string Pattern { get; } + public string{{(allowOptionalPattern ? "?" : "")}} Pattern { get; } /// /// Gets or sets the endpoint name. @@ -113,7 +112,7 @@ internal sealed class {{attributeName}} : global::System.Attribute /// Initializes a new instance of the class. /// /// The route pattern for the endpoint. - public {{attributeName}}([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern{{patternDefaultValue}}) + public {{attributeName}}([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string{{(allowOptionalPattern ? "?" : "")}} pattern{{(allowOptionalPattern ? " = null" : "")}}) { Pattern = pattern; } @@ -751,8 +750,7 @@ private static EquatableImmutableArray MergeUnion(EquatableImmutableArra if (!seen.Add(normalized)) continue; - if (list is null) - list = []; + list ??= []; list.Add(normalized); } diff --git a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt index 461f2f9..560ee77 100644 --- a/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt +++ b/tests/GeneratedEndpoints.Tests/AttributeGenerationTests.MapFallbackAttribute.verified.txt @@ -21,7 +21,7 @@ internal sealed class MapFallbackAttribute : global::System.Attribute /// /// Gets the route pattern for the endpoint. /// - public string Pattern { get; } + public string? Pattern { get; } /// /// Gets or sets the endpoint name. @@ -32,7 +32,7 @@ internal sealed class MapFallbackAttribute : global::System.Attribute /// Initializes a new instance of the class. /// /// The route pattern for the endpoint. - public MapFallbackAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern = "") + public MapFallbackAttribute([global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string? pattern = null) { Pattern = pattern; } From d1a19854aee30fff1e208c4cba4e4be059d8ea20 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 01:06:26 -0500 Subject: [PATCH 74/75] Optimize parameter binding detection (#57) --- .../Common/TypeSymbolExtensions.cs | 186 ------------------ .../MinimalApiGenerator.Constants.cs | 3 + src/GeneratedEndpoints/MinimalApiGenerator.cs | 71 +++---- 3 files changed, 41 insertions(+), 219 deletions(-) diff --git a/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs b/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs index 4548136..1047514 100644 --- a/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs +++ b/src/GeneratedEndpoints/Common/TypeSymbolExtensions.cs @@ -2,194 +2,8 @@ namespace GeneratedEndpoints.Common; -/// Provides extension methods for working with type symbols. internal static class TypeSymbolExtensions { - public static bool IsFromRouteAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "FromRouteAttribute", - ContainingNamespace: - { - Name: "Mvc", - ContainingNamespace: - { - Name: "AspNetCore", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsFromQueryAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "FromQueryAttribute", - ContainingNamespace: - { - Name: "Mvc", - ContainingNamespace: - { - Name: "AspNetCore", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsFromHeaderAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "FromHeaderAttribute", - ContainingNamespace: - { - Name: "Mvc", - ContainingNamespace: - { - Name: "AspNetCore", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsFromBodyAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "FromBodyAttribute", - ContainingNamespace: - { - Name: "Mvc", - ContainingNamespace: - { - Name: "AspNetCore", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsFromFormAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "FromFormAttribute", - ContainingNamespace: - { - Name: "Mvc", - ContainingNamespace: - { - Name: "AspNetCore", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsFromServicesAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "FromServicesAttribute", - ContainingNamespace: - { - Name: "Mvc", - ContainingNamespace: - { - Name: "AspNetCore", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsFromKeyedServicesAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "FromKeyedServicesAttribute", - ContainingNamespace: - { - Name: "DependencyInjection", - ContainingNamespace: - { - Name: "Extensions", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsAsParametersAttribute(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "AsParametersAttribute", - ContainingNamespace: - { - Name: "Http", - ContainingNamespace: - { - Name: "AspNetCore", - ContainingNamespace: - { - Name: "Microsoft", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }, - }; - } - - public static bool IsCancellationToken(this ITypeSymbol symbol) - { - return symbol is INamedTypeSymbol - { - MetadataName: "CancellationToken", - ContainingNamespace: - { - Name: "Threading", - ContainingNamespace: - { - Name: "System", - ContainingNamespace.IsGlobalNamespace: true, - }, - }, - }; - } - public static bool IsValueTask(this ITypeSymbol symbol, out INamedTypeSymbol valueTaskSymbol) { if (symbol is INamedTypeSymbol diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs index a854312..0f51419 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.Constants.cs @@ -106,8 +106,11 @@ public sealed partial class MinimalApiGenerator private const string GlobalPrefix = "global::"; private static readonly string[] AttributesNamespaceParts = AttributesNamespace.Split('.'); private static readonly string[] AspNetCoreHttpNamespaceParts = ["Microsoft", "AspNetCore", "Http"]; + private static readonly string[] AspNetCoreMvcNamespaceParts = ["Microsoft", "AspNetCore", "Mvc"]; private static readonly string[] AspNetCoreAuthorizationNamespaceParts = ["Microsoft", "AspNetCore", "Authorization"]; private static readonly string[] AspNetCoreRoutingNamespaceParts = ["Microsoft", "AspNetCore", "Routing"]; + private static readonly string[] ExtensionsDependencyInjectionNamespaceParts = + ["Microsoft", "Extensions", "DependencyInjection"]; private static readonly string[] ComponentModelNamespaceParts = ["System", "ComponentModel"]; private static readonly ConditionalWeakTable CompilationTypeCaches = new(); private static readonly ConditionalWeakTable RequestHandlerClassCache = new(); diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index af6aa98..3e56c7b 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -574,6 +574,26 @@ private static bool IsAttribute(INamedTypeSymbol attributeClass, string attribut return definition.Name == attributeName && IsInNamespace(definition.ContainingNamespace, namespaceParts); } + private static BindingSource GetBindingSourceFromAttributeClass(INamedTypeSymbol attributeClass) + { + var definition = attributeClass.OriginalDefinition; + var namespaceSymbol = definition.ContainingNamespace; + + return definition.Name switch + { + "FromRouteAttribute" when IsInNamespace(namespaceSymbol, AspNetCoreMvcNamespaceParts) => BindingSource.FromRoute, + "FromQueryAttribute" when IsInNamespace(namespaceSymbol, AspNetCoreMvcNamespaceParts) => BindingSource.FromQuery, + "FromHeaderAttribute" when IsInNamespace(namespaceSymbol, AspNetCoreMvcNamespaceParts) => BindingSource.FromHeader, + "FromBodyAttribute" when IsInNamespace(namespaceSymbol, AspNetCoreMvcNamespaceParts) => BindingSource.FromBody, + "FromFormAttribute" when IsInNamespace(namespaceSymbol, AspNetCoreMvcNamespaceParts) => BindingSource.FromForm, + "FromServicesAttribute" when IsInNamespace(namespaceSymbol, AspNetCoreMvcNamespaceParts) => BindingSource.FromServices, + "FromKeyedServicesAttribute" when IsInNamespace(namespaceSymbol, ExtensionsDependencyInjectionNamespaceParts) + => BindingSource.FromKeyedServices, + "AsParametersAttribute" when IsInNamespace(namespaceSymbol, AspNetCoreHttpNamespaceParts) => BindingSource.AsParameters, + _ => BindingSource.None, + }; + } + private static bool IsInNamespace(INamespaceSymbol? namespaceSymbol, string[] namespaceParts) { for (var i = namespaceParts.Length - 1; i >= 0; i--) @@ -937,7 +957,7 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM { cancellationToken.ThrowIfCancellationRequested(); - var methodParameters = new List(methodSymbol.Parameters.Length); + var methodParameters = ImmutableArray.CreateBuilder(methodSymbol.Parameters.Length); foreach (var parameter in methodSymbol.Parameters) { cancellationToken.ThrowIfCancellationRequested(); @@ -946,44 +966,29 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM TypedConstant? typedKey = null; string? bindingName = null; - var attributes = parameter.GetAttributes(); - foreach (var attribute in attributes) + foreach (var attribute in parameter.GetAttributes()) { var attributeClass = attribute.AttributeClass; if (attributeClass is null) continue; - if (attributeClass.IsFromRouteAttribute()) - { - source = BindingSource.FromRoute; - bindingName = GetBindingAttributeName(attribute) ?? bindingName; - } - if (attributeClass.IsFromQueryAttribute()) - { - source = BindingSource.FromQuery; - bindingName = GetBindingAttributeName(attribute) ?? bindingName; - } - if (attributeClass.IsFromHeaderAttribute()) - { - source = BindingSource.FromHeader; - bindingName = GetBindingAttributeName(attribute) ?? bindingName; - } - if (attributeClass.IsFromBodyAttribute()) - source = BindingSource.FromBody; - if (attributeClass.IsFromFormAttribute()) - { - source = BindingSource.FromForm; - bindingName = GetBindingAttributeName(attribute) ?? bindingName; - } - if (attributeClass.IsFromServicesAttribute()) - source = BindingSource.FromServices; - if (attributeClass.IsFromKeyedServicesAttribute()) + var attributeSource = GetBindingSourceFromAttributeClass(attributeClass); + if (attributeSource == BindingSource.None) + continue; + + source = attributeSource; + switch (attributeSource) { - source = BindingSource.FromKeyedServices; - typedKey = attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0] : null; + case BindingSource.FromRoute: + case BindingSource.FromQuery: + case BindingSource.FromHeader: + case BindingSource.FromForm: + bindingName = GetBindingAttributeName(attribute) ?? bindingName; + break; + case BindingSource.FromKeyedServices: + typedKey = attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0] : null; + break; } - if (attributeClass.IsAsParametersAttribute()) - source = BindingSource.AsParameters; } var parameterName = parameter.Name; @@ -993,7 +998,7 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM methodParameters.Add(new Parameter(parameterName, parameterType, bindingPrefix)); } - return methodParameters.ToEquatableImmutableArray(); + return methodParameters.ToEquatableImmutable(); } private static string? GetBindingAttributeName(AttributeData attribute) From b916f583a850416be3966b8d5ec013f6e8ddc4d9 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Sun, 16 Nov 2025 01:08:46 -0500 Subject: [PATCH 75/75] Add cancellation checks to source generation context (#58) --- src/GeneratedEndpoints/MinimalApiGenerator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/GeneratedEndpoints/MinimalApiGenerator.cs b/src/GeneratedEndpoints/MinimalApiGenerator.cs index 3e56c7b..c5ebad8 100644 --- a/src/GeneratedEndpoints/MinimalApiGenerator.cs +++ b/src/GeneratedEndpoints/MinimalApiGenerator.cs @@ -1030,6 +1030,8 @@ private static EquatableImmutableArray GetRequestHandlerParameters(IM private static void GenerateSource(SourceProductionContext context, ImmutableArray requestHandlers) { + context.CancellationToken.ThrowIfCancellationRequested(); + var sorted = SortRequestHandlers(requestHandlers); sorted = EnsureUniqueEndpointNames(sorted); @@ -1146,6 +1148,8 @@ private static string GetFullyQualifiedMethodDisplayName(RequestHandler requestH private static void GenerateAddEndpointHandlersClass(SourceProductionContext context, ImmutableArray requestHandlers) { + context.CancellationToken.ThrowIfCancellationRequested(); + var nonStaticClassNames = GetDistinctNonStaticClassNames(requestHandlers); var source = GetAddEndpointHandlersStringBuilder(nonStaticClassNames); source.AppendLine(FileHeader); @@ -1234,6 +1238,8 @@ private static StringBuilder GetAddEndpointHandlersStringBuilder(List no private static void GenerateUseEndpointHandlersClass(SourceProductionContext context, ImmutableArray requestHandlers) { + context.CancellationToken.ThrowIfCancellationRequested(); + var source = GetUseEndpointHandlersStringBuilder(requestHandlers); source.AppendLine(FileHeader);