From 182f02d0773f621e72e4f6126fa8c156e12bf258 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 4 Jan 2023 12:55:12 -0800 Subject: [PATCH 01/12] Add infrastructure for RequestDelegateGenerator --- AspNetCore.sln | 19 + .../App.Ref/src/CompatibilitySuppressions.xml | 6 +- .../src/Microsoft.AspNetCore.App.Ref.csproj | 5 + .../src/CompatibilitySuppressions.xml | 6 +- ...ft.AspNetCore.Http.SourceGeneration.csproj | 25 ++ .../gen/RequestDelegateGenerator.cs | 146 +++++++ .../gen/RequestDelegateGeneratorSources.cs | 412 ++++++++++++++++++ .../StaticRouteHandlerModel.Emitter.cs | 86 ++++ .../StaticRouteHandlerModel.Parser.cs | 88 ++++ .../StaticRouteHandlerModel.cs | 52 +++ ...ft.AspNetCore.Http.Extensions.Tests.csproj | 11 + .../RequestDelegateGeneratorTestBase.cs | 287 ++++++++++++ .../RequestDelegateGeneratorTests.cs | 36 ++ src/Http/HttpAbstractions.slnf | 1 + 14 files changed, 1178 insertions(+), 2 deletions(-) create mode 100644 src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.SourceGeneration.csproj create mode 100644 src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs create mode 100644 src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 8395a01cb39f..25221de7cc8a 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1762,6 +1762,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests", "src\Servers\Kestrel\Transport.NamedPipes\test\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj", "{97C7D2A4-87E5-4A4A-A170-D736427D5C21}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.SourceGeneration", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.SourceGeneration.csproj", "{4730F56D-24EF-4BB2-AA75-862E31205F3A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10571,6 +10573,22 @@ Global {97C7D2A4-87E5-4A4A-A170-D736427D5C21}.Release|x64.Build.0 = Release|Any CPU {97C7D2A4-87E5-4A4A-A170-D736427D5C21}.Release|x86.ActiveCfg = Release|Any CPU {97C7D2A4-87E5-4A4A-A170-D736427D5C21}.Release|x86.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|arm64.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|arm64.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x64.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Debug|x86.Build.0 = Debug|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|Any CPU.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|arm64.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|arm64.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x64.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x64.Build.0 = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x86.ActiveCfg = Release|Any CPU + {4730F56D-24EF-4BB2-AA75-862E31205F3A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11441,6 +11459,7 @@ Global {F057512B-55BF-4A8B-A027-A0505F8BA10C} = {4FDDC525-4E60-4CAF-83A3-261C5B43721F} {10173568-A65E-44E5-8C6F-4AA49D0577A1} = {F057512B-55BF-4A8B-A027-A0505F8BA10C} {97C7D2A4-87E5-4A4A-A170-D736427D5C21} = {F057512B-55BF-4A8B-A027-A0505F8BA10C} + {4730F56D-24EF-4BB2-AA75-862E31205F3A} = {225AEDCF-7162-4A86-AC74-06B84660B379} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml index e2ec12ef3b32..2d1a36843ca4 100644 --- a/src/Framework/App.Ref/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Ref/src/CompatibilitySuppressions.xml @@ -1,7 +1,11 @@  - + PKV004 net7.0 + + PKV004 + net8.0 + \ No newline at end of file diff --git a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj index 9c1fc5cd35c8..6f5175e68fe5 100644 --- a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj @@ -77,6 +77,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant Private="false" ReferenceOutputAssembly="false" /> + + <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Components.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.CodeFixes\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.CodeFixes.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> + <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Http.SourceGeneration\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Http.SourceGeneration.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> <_InitialRefPackContent Include="@(AspNetCoreReferenceAssemblyPath)" PackagePath="$(RefAssemblyPackagePath)" /> <_InitialRefPackContent Include="@(AspNetCoreReferenceDocXml)" PackagePath="$(RefAssemblyPackagePath)" /> diff --git a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml index 056469f78b07..5beb41b8eec0 100644 --- a/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml +++ b/src/Framework/App.Runtime/src/CompatibilitySuppressions.xml @@ -1,7 +1,11 @@  - + PKV0001 net7.0 + + PKV0001 + net8.0 + \ No newline at end of file diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.SourceGeneration.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.SourceGeneration.csproj new file mode 100644 index 000000000000..e2c1374c3615 --- /dev/null +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.SourceGeneration.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + false + true + false + true + false + + + + + + + + + + + + + + + + diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs new file mode 100644 index 000000000000..319e6d8f7c85 --- /dev/null +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; + +namespace Microsoft.AspNetCore.Http.SourceGeneration; + +[Generator] +public class RequestDelegateGenerator : IIncrementalGenerator +{ + private static readonly string[] _knownMethods = + { + "MapGet", + "MapPost", + "MapPut", + "MapDelete", + "MapPatch", + "Map", + }; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var isGeneratorEnabled = context.AnalyzerConfigOptionsProvider.Select((provider, _) => + provider.GlobalOptions.TryGetValue("build_property.EnableRequestDelegateGenerator", out var enableRequestDelegateGenerator) + && enableRequestDelegateGenerator == "true"); + + var mapActionOperations = context.SyntaxProvider.CreateSyntaxProvider( + predicate: (node, _) => node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: IdentifierNameSyntax + { + Identifier: { ValueText: var method } + } + }, + ArgumentList: { Arguments: { Count: 2 } args } + } && _knownMethods.Contains(method), + transform: (context, token) => context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation); + + // Filter out any map actions if the generator is not enabled + // via config + var conditionalMapActionOperations = mapActionOperations.Combine(isGeneratorEnabled) + .Where(pair => pair.Right) + .Select((pair, _) => pair.Left); + + var endpoints = conditionalMapActionOperations + .Select((operation, _) => StaticRouteHandlerModelParser.GetEndpointFromOperation(operation)) + .WithTrackingName("EndpointModel"); + + var genericThunks = endpoints.Select((endpoint, token) => + { + var code = RequestDelegateGeneratorSources.GetGenericThunks(string.Empty); + return code; + }); + + var thunks = endpoints.Select((endpoint, _) => $@" [{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}] = ( + (del, builder) => + {{ +builder.Metadata.Add(new SourceKey{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}); + }}, + (del, builder) => + {{ + var handler = ({StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)})del; + EndpointFilterDelegate? filteredInvocation = null; + + if (builder.FilterFactories.Count > 0) + {{ + filteredInvocation = BuildFilterDelegate(ic => + {{ + if (ic.HttpContext.Response.StatusCode == 400) + {{ + return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + }} + {StaticRouteHandlerModelEmitter.EmitFilteredInvocation()} + }}, + builder, + handler.Method); + }} + + {StaticRouteHandlerModelEmitter.EmitRequestHandler()} + {StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()} + + return filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + }}), +"); + + var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => + { + var code = new StringBuilder(); + code.AppendLine($"internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder {endpoint.HttpMethod}"); + code.Append("(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, "); + code.Append(StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)); + code.Append(@" handler, [System.Runtime.CompilerServices.CallerFilePath] string filePath = """", [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)"); + code.AppendLine("{"); + code.AppendLine("return MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber);"); + code.AppendLine("}"); + return code.ToString(); + }); + + context.RegisterSourceOutput(genericThunks.Collect(), (context, sources) => + { + var code = new StringBuilder(); + foreach (var source in sources) + { + code.AppendLine(source); + } + context.AddSource("GeneratedRouteBuilderExtensions.GenericThunks.g.cs", code.ToString()); + }); + + context.RegisterSourceOutput(thunks.Collect(), (context, sources) => + { + var thunks = new StringBuilder(); + foreach (var source in sources) + { + thunks.AppendLine(source); + } + var code = RequestDelegateGeneratorSources.GetThunks(thunks.ToString()); + context.AddSource("GeneratedRouteBuilderExtensions.Thunks.g.cs", code); + }); + + context.RegisterSourceOutput(stronglyTypedEndpointDefinitions.Collect(), (context, sources) => + { + var endpoints = new StringBuilder(); + foreach (var source in sources) + { + endpoints.AppendLine(source); + } + var code = RequestDelegateGeneratorSources.GetEndpoints(endpoints.ToString()); + context.AddSource("GeneratedRouteBuilderExtensions.Endpoints.g.cs", code); + }); + + context.RegisterSourceOutput(isGeneratorEnabled, (context, isGeneratorEnabled) => + { + if (isGeneratorEnabled) + { + context.AddSource("GeneratedRouteBuilderExtensions.Helpers.g.cs", RequestDelegateGeneratorSources.GeneratedRouteBuilderExtensionsSource); + } + }); + } +} diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs new file mode 100644 index 000000000000..555a40dc6e3c --- /dev/null +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -0,0 +1,412 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.SourceGeneration; + +internal static class RequestDelegateGeneratorSources +{ + private const string SourceHeader = $@" +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using System.IO; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; +using MetadataPopulator = System.Action; +using RequestDelegateFactoryFunc = System.Func;"; + + internal const string GeneratedRouteBuilderExtensionsSource = $@" +{SourceHeader} + +namespace Microsoft.AspNetCore.Builder +{{ + internal record SourceKey(string Path, int Line); +}} + + +internal static partial class GeneratedRouteBuilderExtensions +{{ + private static readonly string[] GetVerb = new[] {{ HttpMethods.Get }}; + private static readonly string[] PostVerb = new[] {{ HttpMethods.Post }}; + private static readonly string[] PutVerb = new[] {{ HttpMethods.Put }}; + private static readonly string[] DeleteVerb = new[] {{ HttpMethods.Delete }}; + private static readonly string[] PatchVerb = new[] {{ HttpMethods.Patch }}; + + private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( + this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, + string pattern, + System.Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + {{ + var (populate, factory) = GenericThunks.map[(filePath, lineNumber)]; + return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); + }} + + private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( + this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, + string pattern, + System.Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + {{ + var (populate, factory) = map[(filePath, lineNumber)]; + return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); + }} + + private static SourceGeneratedRouteEndpointDataSource GetOrAddRouteEndpointDataSource(IEndpointRouteBuilder endpoints) + {{ + SourceGeneratedRouteEndpointDataSource? routeEndpointDataSource = null; + foreach (var dataSource in endpoints.DataSources) + {{ + if (dataSource is SourceGeneratedRouteEndpointDataSource foundDataSource) + {{ + routeEndpointDataSource = foundDataSource; + break; + }} + }} + if (routeEndpointDataSource is null) + {{ + routeEndpointDataSource = new SourceGeneratedRouteEndpointDataSource(endpoints.ServiceProvider); + endpoints.DataSources.Add(routeEndpointDataSource); + }} + return routeEndpointDataSource; + }} + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, System.Reflection.MethodInfo mi) + {{ + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + {{ + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }}; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + {{ + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + }} + return filteredInvocation; + }} + + private static void PopulateMetadata(System.Reflection.MethodInfo method, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider + {{ + T.PopulateMetadata(method, builder); + }} + private static void PopulateMetadata(System.Reflection.ParameterInfo parameter, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider + {{ + T.PopulateMetadata(parameter, builder); + }} + + private static Task ExecuteObjectResult(object obj, HttpContext httpContext) + {{ + if (obj is IResult r) + {{ + return r.ExecuteAsync(httpContext); + }} + else if (obj is string s) + {{ + return httpContext.Response.WriteAsync(s); + }} + else + {{ + return httpContext.Response.WriteAsJsonAsync(obj); + }} + }} + + private sealed class SourceGeneratedRouteEndpointDataSource : EndpointDataSource + {{ + private readonly List _routeEntries = new(); + private readonly IServiceProvider _applicationServices; + + public SourceGeneratedRouteEndpointDataSource(IServiceProvider applicationServices) + {{ + _applicationServices = applicationServices; + }} + + public RouteHandlerBuilder AddRouteHandler( + RoutePattern pattern, + Delegate routeHandler, + IEnumerable httpMethods, + bool isFallback, + MetadataPopulator metadataPopulator, + RequestDelegateFactoryFunc requestDelegateFactoryFunc) + {{ + var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); + var finallyConventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); + var routeAttributes = RouteAttributes.RouteHandler; + + if (isFallback) + {{ + routeAttributes |= RouteAttributes.Fallback; + }} + _routeEntries.Add(new() + {{ + RoutePattern = pattern, + RouteHandler = routeHandler, + HttpMethods = httpMethods, + RouteAttributes = routeAttributes, + Conventions = conventions, + FinallyConventions = finallyConventions, + RequestDelegateFactory = requestDelegateFactoryFunc, + MetadataPopulator = metadataPopulator, + }}); + return new RouteHandlerBuilder(new[] {{ new ConventionBuilder(conventions, finallyConventions) }}); + }} + + public override IReadOnlyList Endpoints + {{ + get + {{ + var endpoints = new RouteEndpoint[_routeEntries.Count]; + for (int i = 0; i < _routeEntries.Count; i++) + {{ + endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i]).Build(); + }} + return endpoints; + }} + }} + + public override IReadOnlyList GetGroupedEndpoints(RouteGroupContext context) + {{ + var endpoints = new RouteEndpoint[_routeEntries.Count]; + for (int i = 0; i < _routeEntries.Count; i++) + {{ + endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i], context.Prefix, context.Conventions, context.FinallyConventions).Build(); + }} + return endpoints; + }} + + public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; + + private RouteEndpointBuilder CreateRouteEndpointBuilder( + RouteEntry entry, RoutePattern? groupPrefix = null, IReadOnlyList>? groupConventions = null, IReadOnlyList>? groupFinallyConventions = null) + {{ + var pattern = RoutePatternFactory.Combine(groupPrefix, entry.RoutePattern); + var handler = entry.RouteHandler; + var isRouteHandler = (entry.RouteAttributes & RouteAttributes.RouteHandler) == RouteAttributes.RouteHandler; + var isFallback = (entry.RouteAttributes & RouteAttributes.Fallback) == RouteAttributes.Fallback; + var order = isFallback ? int.MaxValue : 0; + var displayName = pattern.RawText ?? pattern.ToString(); + if (entry.HttpMethods is not null) + {{ + // Prepends the HTTP method to the DisplayName produced with pattern + method name + displayName = $""HTTP: {{string.Join("", "", entry.HttpMethods)}} {{displayName}}""; + }} + if (isFallback) + {{ + displayName = $""Fallback {{displayName}}""; + }} + // If we're not a route handler, we started with a fully realized (although unfiltered) RequestDelegate, so we can just redirect to that + // while running any conventions. We'll put the original back if it remains unfiltered right before building the endpoint. + RequestDelegate? factoryCreatedRequestDelegate = null; + // Let existing conventions capture and call into builder.RequestDelegate as long as they do so after it has been created. + RequestDelegate redirectRequestDelegate = context => + {{ + if (factoryCreatedRequestDelegate is null) + {{ + throw new InvalidOperationException(""Resources.RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild""); + }} + return factoryCreatedRequestDelegate(context); + }}; + // Add MethodInfo and HttpMethodMetadata (if any) as first metadata items as they are intrinsic to the route much like + // the pattern or default display name. This gives visibility to conventions like WithOpenApi() to intrinsic route details + // (namely the MethodInfo) even when applied early as group conventions. + RouteEndpointBuilder builder = new(redirectRequestDelegate, pattern, order) + {{ + DisplayName = displayName, + ApplicationServices = _applicationServices, + }}; + if (isRouteHandler) + {{ + builder.Metadata.Add(handler.Method); + }} + if (entry.HttpMethods is not null) + {{ + builder.Metadata.Add(new HttpMethodMetadata(entry.HttpMethods)); + }} + // Apply group conventions before entry-specific conventions added to the RouteHandlerBuilder. + if (groupConventions is not null) + {{ + foreach (var groupConvention in groupConventions) + {{ + groupConvention(builder); + }} + }} + // Any metadata inferred directly inferred by RDF or indirectly inferred via IEndpoint(Parameter)MetadataProviders are + // considered less specific than method-level attributes and conventions but more specific than group conventions + // so inferred metadata gets added in between these. If group conventions need to override inferred metadata, + // they can do so via IEndpointConventionBuilder.Finally like the do to override any other entry-specific metadata. + if (isRouteHandler) + {{ + entry.MetadataPopulator(entry.RouteHandler, builder); + }} + // Add delegate attributes as metadata before entry-specific conventions but after group conventions. + var attributes = handler.Method.GetCustomAttributes(); + if (attributes is not null) + {{ + foreach (var attribute in attributes) + {{ + builder.Metadata.Add(attribute); + }} + }} + entry.Conventions.IsReadOnly = true; + foreach (var entrySpecificConvention in entry.Conventions) + {{ + entrySpecificConvention(builder); + }} + // If no convention has modified builder.RequestDelegate, we can use the RequestDelegate returned by the RequestDelegateFactory directly. + var conventionOverriddenRequestDelegate = ReferenceEquals(builder.RequestDelegate, redirectRequestDelegate) ? null : builder.RequestDelegate; + if (isRouteHandler || builder.FilterFactories.Count > 0) + {{ + factoryCreatedRequestDelegate = entry.RequestDelegateFactory(entry.RouteHandler, builder); + }} + Debug.Assert(factoryCreatedRequestDelegate is not null); + // Use the overridden RequestDelegate if it exists. If the overridden RequestDelegate is merely wrapping the final RequestDelegate, + // it will still work because of the redirectRequestDelegate. + builder.RequestDelegate = conventionOverriddenRequestDelegate ?? factoryCreatedRequestDelegate; + entry.FinallyConventions.IsReadOnly = true; + foreach (var entryFinallyConvention in entry.FinallyConventions) + {{ + entryFinallyConvention(builder); + }} + if (groupFinallyConventions is not null) + {{ + // Group conventions are ordered by the RouteGroupBuilder before + // being provided here. + foreach (var groupFinallyConvention in groupFinallyConventions) + {{ + groupFinallyConvention(builder); + }} + }} + return builder; + }} + + private struct RouteEntry + {{ + public MetadataPopulator MetadataPopulator {{ get; init; }} + public RequestDelegateFactoryFunc RequestDelegateFactory {{ get; init; }} + public RoutePattern RoutePattern {{ get; init; }} + public Delegate RouteHandler {{ get; init; }} + public IEnumerable HttpMethods {{ get; init; }} + public RouteAttributes RouteAttributes {{ get; init; }} + public ThrowOnAddAfterEndpointBuiltConventionCollection Conventions {{ get; init; }} + public ThrowOnAddAfterEndpointBuiltConventionCollection FinallyConventions {{ get; init; }} + }} + + [Flags] + private enum RouteAttributes + {{ + // The endpoint was defined by a RequestDelegate, RequestDelegateFactory.Create() should be skipped unless there are endpoint filters. + None = 0, + // This was added as Delegate route handler, so RequestDelegateFactory.Create() should always be called. + RouteHandler = 1, + // This was added by MapFallback. + Fallback = 2, + }} + + // This private class is only exposed to internal code via ICollection> in RouteEndpointBuilder where only Add is called. + private sealed class ThrowOnAddAfterEndpointBuiltConventionCollection : List>, ICollection> + {{ + // We throw if someone tries to add conventions to the RouteEntry after endpoints have already been resolved meaning the conventions + // will not be observed given RouteEndpointDataSource is not meant to be dynamic and uses NullChangeToken.Singleton. + public bool IsReadOnly {{ get; set; }} + void ICollection>.Add(Action convention) + {{ + if (IsReadOnly) + {{ + throw new InvalidOperationException(""Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild""); + }} + Add(convention); + }} + }} + + private class ConventionBuilder : IEndpointConventionBuilder + {{ + private readonly ICollection> _conventions; + private readonly ICollection> _finallyConventions; + public ConventionBuilder(ICollection> conventions, ICollection> finallyConventions) + {{ + _conventions = conventions; + _finallyConventions = finallyConventions; + }} + /// + /// Adds the specified convention to the builder. Conventions are used to customize instances. + /// + /// The convention to add to the builder. + public void Add(Action convention) + {{ + _conventions.Add(convention); + }} + public void Finally(Action finalConvention) + {{ + _finallyConventions.Add(finalConvention); + }} + }} + }} +}} +"; + internal static string GetGenericThunks(string genericThunks) + { + return $@" +{SourceHeader} + +internal static partial class GeneratedRouteBuilderExtensions +{{ + internal class GenericThunks + {{ + public static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + {{ +{genericThunks} + }}; + }} +}} +"; + } + + internal static string GetThunks(string thunks) + { + return $@" +{SourceHeader} + +internal static partial class GeneratedRouteBuilderExtensions +{{ + internal static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + {{ + {thunks} + }}; +}} +"; + } + + internal static string GetEndpoints(string endpoints) + { + return $@" +internal static partial class GeneratedRouteBuilderExtensions +{{ + {endpoints} +}} +"; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs new file mode 100644 index 000000000000..f09e5379d287 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; + +internal static class StaticRouteHandlerModelEmitter +{ + /* + * TODO: Emit code that represents the signature of the delegate + * represented by the handler. When the handler does not return a value + * but consumes parameters the following will be emitted: + * + * ``` + * System.Action + * ``` + * + * Where `string` and `int` represent parameter types. For handlers + * that do return a value, `System.Func` will + * be emitted to indicate a `string`return type. + */ + public static string EmitHandlerDelegateType(Endpoint endpoint) + { + return $"System.Func<{endpoint.Response.ResponseType}>"; + } + + public static string EmitSourceKey(Endpoint endpoint) + { + return $@"(""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; + } + + /* + * TODO: Emit invocation to the request handler. The structure + * involved here consists of a call to bind parameters, check + * their validity (optionality), invoke the underlying handler with + * the arguments bound from HTTP context, and write out the response. + */ + public static string EmitRequestHandler() + { + return $@" +System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext httpContext) +{{ + var result = handler(); + return httpContext.Response.WriteAsync(result); +}} +"; + } + + /* + * TODO: Emit invocation to the `filteredInvocation` pipeline by constructing + * the `EndpointFilterInvocationContext` using the bound arguments for the handler. + * In the source generator context, the generic overloads for `EndpointFilterInvocationContext` + * can be used to reduce the boxing that happens at runtime when constructing + * the context object. + */ + public static string EmitFilteredRequestHandler() + { + return $@" +async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Http.HttpContext httpContext) +{{ + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await ExecuteObjectResult(result, httpContext); +}} +"; + } + + /* + * TODO: Emit code that will call the `handler` with + * the appropriate arguments processed via the parameter binding. + * + * ``` + * return System.Threading.Tasks.ValueTask.FromResult(handler(name, age)); + * ``` + * + * If the handler returns void, it will be invoked and an `EmptyHttpResult` + * will be returned to the user. + * + * ``` + * handler(name, age); + * return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + * ``` + */ + public static string EmitFilteredInvocation() + { + return $@"return System.Threading.Tasks.ValueTask.FromResult(handler());"; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs new file mode 100644 index 000000000000..eaf0a1bb6e2f --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; + +internal static class StaticRouteHandlerModelParser +{ + public static EndpointRoute GetEndpointRouteFromArgument(IArgumentOperation argumentOperation) + { + var syntax = argumentOperation.Syntax as ArgumentSyntax; + var expression = syntax.Expression as LiteralExpressionSyntax; + return new EndpointRoute + { + RoutePattern = expression.Token.ValueText + }; + } + + public static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method) + { + return new EndpointResponse + { + ContentType = "plain/text", + ResponseType = method.ReturnType.ToString(), + }; + } + + public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) + { + var routePatternArgument = operation.Arguments[1]; + var method = ResolveMethodFromOperation(operation.Arguments[2]); + var filePath = operation.Syntax.SyntaxTree.FilePath; + var span = operation.Syntax.SyntaxTree.GetLineSpan(operation.Syntax.Span); + + var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; + var httpMethod = ((IdentifierNameSyntax)((MemberAccessExpressionSyntax)invocationExpression.Expression).Name).Identifier.ValueText; + + return new Endpoint + { + Route = GetEndpointRouteFromArgument(routePatternArgument), + Response = GetEndpointResponseFromMethod(method), + Location = (filePath, span.EndLinePosition.Line + 1), + HttpMethod = httpMethod, + }; + } + + private static IMethodSymbol ResolveMethodFromOperation(IOperation operation) => operation switch + { + IArgumentOperation argument => ResolveMethodFromOperation(argument.Value), + IConversionOperation conv => ResolveMethodFromOperation(conv.Operand), + IDelegateCreationOperation del => ResolveMethodFromOperation(del.Target), + IFieldReferenceOperation { Field.IsReadOnly: true } f when ResolveDeclarationOperation(f.Field, operation.SemanticModel) is IOperation op => + ResolveMethodFromOperation(op), + IAnonymousFunctionOperation anon => anon.Symbol, + ILocalFunctionOperation local => local.Symbol, + IMethodReferenceOperation method => method.Method, + _ => null + }; + + private static IOperation ResolveDeclarationOperation(ISymbol symbol, SemanticModel semanticModel) + { + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + var syn = syntaxReference.GetSyntax(); + + if (syn is VariableDeclaratorSyntax + { + Initializer: + { + Value: var expr + } + }) + { + // Use the correct semantic model based on the syntax tree + var operation = semanticModel.GetOperation(expr); + + if (operation is not null) + { + return operation; + } + } + } + + return null; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs new file mode 100644 index 000000000000..27a90ba0101a --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; + +internal enum RequestParameterSource +{ + Query, + Route, + Header, + Form, + Service, + QueryOrService +} + +internal class RequestParameter +{ + public string Name { get; } + public string Type { get; } + public RequestParameterSource Source { get; set; } + public bool IsOptional { get; set; } + public object? DefaultValue { get; set; } +} + +internal class EndpointRoute +{ + public string RoutePattern { get; set; } + + public List RouteParameters { get; set; } +} + +internal class EndpointResponse +{ + public string ResponseType { get; set; } + public string ContentType { get; set; } +} + +internal class EndpointRequest +{ + public List RequestParameters { get; set; } +} + +internal sealed class Endpoint +{ + public string HttpMethod { get; set; } + public EndpointRoute Route { get; set; } + public EndpointRequest Request { get; set; } + public EndpointResponse Response { get; set; } + public (string, int) Location { get; set; } +} diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index 546bc7f16fb5..fe3f44ce4d0e 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -4,6 +4,7 @@ $(DefaultNetCoreTargetFramework) $(Features.Replace('nullablePublicOnly', '') + true @@ -11,10 +12,20 @@ + + + + + + + + + + diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs new file mode 100644 index 000000000000..4351711699a7 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -0,0 +1,287 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.AspNetCore.Http.SourceGeneration; +using Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; +using Microsoft.AspNetCore.Routing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyModel.Resolution; + +namespace Microsoft.AspNetCore.Http.SourceGeneration.Tests; + +public class RequestDelegateGeneratorTestBase +{ + internal static (ImmutableArray, Compilation) RunGenerator(string sources) + { + var compilation = CreateCompilation(sources); + var generator = new RequestDelegateGenerator().AsSourceGenerator(); + + // Enable the source generator in tests + var optionsProvider = new TestAnalyzerConfigOptionsProvider(); + optionsProvider.TestGlobalOptions["build_property.EnableRequestDelegateGenerator"] = "true"; + GeneratorDriver driver = CSharpGeneratorDriver.Create(generators: new[] + { + generator + }, + optionsProvider: optionsProvider, + driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); + + // Run the source generator + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, + out var _); + var diagnostics = updatedCompilation.GetDiagnostics(); + Assert.Empty(diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); + var runResult = driver.GetRunResult(); + + return (runResult.Results, updatedCompilation); + } + + internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArray results, string stepName) + { + var StaticEndpointStep = results[0].TrackedSteps[stepName].Single(); + var StaticEndpointOutput = StaticEndpointStep.Outputs.Single(); + var (StaticEndpoint, _) = StaticEndpointOutput; + var endpoint = Assert.IsType(StaticEndpoint); + return endpoint; + } + + internal static Endpoint GetEndpointFromCompilation(Compilation compilation) + { + var assemblyName = compilation.AssemblyName!; + var symbolsName = Path.ChangeExtension(assemblyName, "pdb"); + + var output = new MemoryStream(); + var pdb = new MemoryStream(); + + var emitOptions = new EmitOptions( + debugInformationFormat: DebugInformationFormat.PortablePdb, + pdbFilePath: symbolsName); + + var embeddedTexts = new List(); + + // Make sure we embed the sources in pdb for easy debugging + foreach (var syntaxTree in compilation.SyntaxTrees) + { + var text = syntaxTree.GetText(); + var encoding = text.Encoding ?? Encoding.UTF8; + var buffer = encoding.GetBytes(text.ToString()); + var sourceText = SourceText.From(buffer, buffer.Length, encoding, canBeEmbedded: true); + + var syntaxRootNode = (CSharpSyntaxNode)syntaxTree.GetRoot(); + var newSyntaxTree = CSharpSyntaxTree.Create(syntaxRootNode, options: null, encoding: encoding, path: syntaxTree.FilePath); + + compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree); + + embeddedTexts.Add(EmbeddedText.FromSource(syntaxTree.FilePath, sourceText)); + } + + var result = compilation.Emit(output, pdb, options: emitOptions, embeddedTexts: embeddedTexts); + + Assert.Empty(result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); + Assert.True(result.Success); + + output.Position = 0; + pdb.Position = 0; + + var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb); + var handler = assembly?.GetType("TestMapActions") + ?.GetMethod("MapTestEndpoints", BindingFlags.Public | BindingFlags.Static) + ?.CreateDelegate>(); + var sourceKeyType = assembly.GetType("Microsoft.AspNetCore.Builder.SourceKey"); + + Assert.NotNull(handler); + + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = handler(builder); + + var dataSource = Assert.Single(builder.DataSources); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var sourceKeyMetadata = endpoint.Metadata.Single(metadata => metadata.GetType() == sourceKeyType); + Assert.NotNull(sourceKeyMetadata); + + return endpoint; + } + + private static Compilation CreateCompilation(string sources) + { + var source = $$""" +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +public static class TestMapActions +{ + public static IEndpointRouteBuilder MapTestEndpoints(this IEndpointRouteBuilder app) + { + {{sources}} + return app; + } +} +"""; + + var syntaxTrees = new[] + { + CSharpSyntaxTree.ParseText(source, path: $"TestMapActions.cs") + }; + + // Add in required metadata references + var resolver = new AppLocalResolver(); + var references = new List(); + var dependencyContext = DependencyContext.Load(typeof(RequestDelegateGeneratorTestBase).Assembly); + + Assert.NotNull(dependencyContext); + + foreach (var defaultCompileLibrary in dependencyContext.CompileLibraries) + { + foreach (var resolveReferencePath in defaultCompileLibrary.ResolveReferencePaths(resolver)) + { + // Skip the source generator itself + if (resolveReferencePath.Equals(typeof(RequestDelegateGenerator).Assembly.Location, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + references.Add(MetadataReference.CreateFromFile(resolveReferencePath)); + } + } + + // Create a Roslyn compilation for the syntax tree. + var compilation = CSharpCompilation.Create(assemblyName: Guid.NewGuid().ToString(), + syntaxTrees: syntaxTrees, + references: references, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + return compilation; + } + + private sealed class AppLocalResolver : ICompilationAssemblyResolver + { + public bool TryResolveAssemblyPaths(CompilationLibrary library, List assemblies) + { + foreach (var assembly in library.Assemblies) + { + var dll = Path.Combine(Directory.GetCurrentDirectory(), "refs", Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies ??= new(); + assemblies.Add(dll); + return true; + } + + dll = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(assembly)); + if (File.Exists(dll)) + { + assemblies ??= new(); + assemblies.Add(dll); + return true; + } + } + + return false; + } + } + + private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory + { + public IServiceProvider ServiceProvider => this; + + public RouteHandlerOptions RouteHandlerOptions { get; set; } = new RouteHandlerOptions(); + + public IServiceScope CreateScope() + { + return this; + } + + public void Dispose() { } + + public object GetService(Type serviceType) + { + return null; + } + } + + private class DefaultEndpointRouteBuilder : IEndpointRouteBuilder + { + public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder) + { + ApplicationBuilder = applicationBuilder ?? throw new ArgumentNullException(nameof(applicationBuilder)); + DataSources = new List(); + } + + public IApplicationBuilder ApplicationBuilder { get; } + + public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); + + public ICollection DataSources { get; } + + public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; + } + + private class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider + { + public override AnalyzerConfigOptions GlobalOptions => TestGlobalOptions; + + public TestAnalyzerConfigOptions TestGlobalOptions { get; } = new TestAnalyzerConfigOptions(); + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); + + public Dictionary AdditionalTextOptions { get; } = new(); + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) + { + return AdditionalTextOptions.TryGetValue(textFile.Path, out var options) ? options : new TestAnalyzerConfigOptions(); + } + + public TestAnalyzerConfigOptionsProvider Clone() + { + var provider = new TestAnalyzerConfigOptionsProvider(); + foreach (var option in this.TestGlobalOptions.Options) + { + provider.TestGlobalOptions[option.Key] = option.Value; + } + foreach (var option in this.AdditionalTextOptions) + { + var newOptions = new TestAnalyzerConfigOptions(); + foreach (var subOption in option.Value.Options) + { + newOptions[subOption.Key] = subOption.Value; + } + provider.AdditionalTextOptions[option.Key] = newOptions; + + } + return provider; + } + } + + private class TestAnalyzerConfigOptions : AnalyzerConfigOptions + { + public Dictionary Options { get; } = new(); + + public string this[string name] + { + get => Options[name]; + set => Options[name] = value; + } + + public override bool TryGetValue(string key, out string value) + => Options.TryGetValue(key, out value); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs new file mode 100644 index 000000000000..abebc24f4fad --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.SourceGeneration; + +namespace Microsoft.AspNetCore.Http.SourceGeneration.Tests; + +public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase +{ + [Theory] + [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "Hello world!")] + public async Task MapGet_NoParam_SimpleReturn(string source, string expectedBody) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/hello", endpointModel.Route.RoutePattern); + + var httpContext = new DefaultHttpContext(); + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } +} diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 29d4f41c4e69..158ac35073dc 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -21,6 +21,7 @@ "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Abstractions\\test\\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", + "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.SourceGeneration.csproj", "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", From 2d8a5092f2ccc56527d782f36b987f6c19cb3ce0 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 Jan 2023 13:28:07 -0800 Subject: [PATCH 02/12] Opt for disabling generator via MSBuild --- .../gen/RequestDelegateGenerator.cs | 19 +---- .../RequestDelegateGeneratorTestBase.cs | 72 ++----------------- .../RequestDelegateGeneratorTests.cs | 2 - 3 files changed, 9 insertions(+), 84 deletions(-) diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 319e6d8f7c85..63e4d372b3a1 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -25,10 +25,6 @@ public class RequestDelegateGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { - var isGeneratorEnabled = context.AnalyzerConfigOptionsProvider.Select((provider, _) => - provider.GlobalOptions.TryGetValue("build_property.EnableRequestDelegateGenerator", out var enableRequestDelegateGenerator) - && enableRequestDelegateGenerator == "true"); - var mapActionOperations = context.SyntaxProvider.CreateSyntaxProvider( predicate: (node, _) => node is InvocationExpressionSyntax { @@ -43,13 +39,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } && _knownMethods.Contains(method), transform: (context, token) => context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation); - // Filter out any map actions if the generator is not enabled - // via config - var conditionalMapActionOperations = mapActionOperations.Combine(isGeneratorEnabled) - .Where(pair => pair.Right) - .Select((pair, _) => pair.Left); - - var endpoints = conditionalMapActionOperations + var endpoints = mapActionOperations .Select((operation, _) => StaticRouteHandlerModelParser.GetEndpointFromOperation(operation)) .WithTrackingName("EndpointModel"); @@ -135,12 +125,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.AddSource("GeneratedRouteBuilderExtensions.Endpoints.g.cs", code); }); - context.RegisterSourceOutput(isGeneratorEnabled, (context, isGeneratorEnabled) => + context.RegisterSourceOutput(endpoints.Collect(), (context, isGeneratorEnabled) => { - if (isGeneratorEnabled) - { - context.AddSource("GeneratedRouteBuilderExtensions.Helpers.g.cs", RequestDelegateGeneratorSources.GeneratedRouteBuilderExtensionsSource); - } + context.AddSource("GeneratedRouteBuilderExtensions.Helpers.g.cs", RequestDelegateGeneratorSources.GeneratedRouteBuilderExtensionsSource); }); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 4351711699a7..7dfce3b8d33d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -2,18 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using System.Linq; using System.Reflection; using System.Runtime.Loader; using System.Text; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.AspNetCore.Http.SourceGeneration; -using Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; using Microsoft.AspNetCore.Routing; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; @@ -30,13 +25,10 @@ internal static (ImmutableArray, Compilation) RunGenerator(s var generator = new RequestDelegateGenerator().AsSourceGenerator(); // Enable the source generator in tests - var optionsProvider = new TestAnalyzerConfigOptionsProvider(); - optionsProvider.TestGlobalOptions["build_property.EnableRequestDelegateGenerator"] = "true"; GeneratorDriver driver = CSharpGeneratorDriver.Create(generators: new[] { generator }, - optionsProvider: optionsProvider, driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); // Run the source generator @@ -51,10 +43,10 @@ internal static (ImmutableArray, Compilation) RunGenerator(s internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArray results, string stepName) { - var StaticEndpointStep = results[0].TrackedSteps[stepName].Single(); - var StaticEndpointOutput = StaticEndpointStep.Outputs.Single(); - var (StaticEndpoint, _) = StaticEndpointOutput; - var endpoint = Assert.IsType(StaticEndpoint); + var staticEndpointStep = results[0].TrackedSteps[stepName].Single(); + var staticEndpointOutput = staticEndpointStep.Outputs.Single(); + var (staticEndpoint, _) = staticEndpointOutput; + var endpoint = Assert.IsType(staticEndpoint); return endpoint; } @@ -97,7 +89,7 @@ internal static Endpoint GetEndpointFromCompilation(Compilation compilation) pdb.Position = 0; var assembly = AssemblyLoadContext.Default.LoadFromStream(output, pdb); - var handler = assembly?.GetType("TestMapActions") + var handler = assembly.GetType("TestMapActions") ?.GetMethod("MapTestEndpoints", BindingFlags.Public | BindingFlags.Static) ?.CreateDelegate>(); var sourceKeyType = assembly.GetType("Microsoft.AspNetCore.Builder.SourceKey"); @@ -203,8 +195,6 @@ private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceSc { public IServiceProvider ServiceProvider => this; - public RouteHandlerOptions RouteHandlerOptions { get; set; } = new RouteHandlerOptions(); - public IServiceScope CreateScope() { return this; @@ -226,7 +216,7 @@ public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder) DataSources = new List(); } - public IApplicationBuilder ApplicationBuilder { get; } + private IApplicationBuilder ApplicationBuilder { get; } public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); @@ -234,54 +224,4 @@ public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder) public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; } - - private class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider - { - public override AnalyzerConfigOptions GlobalOptions => TestGlobalOptions; - - public TestAnalyzerConfigOptions TestGlobalOptions { get; } = new TestAnalyzerConfigOptions(); - - public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); - - public Dictionary AdditionalTextOptions { get; } = new(); - - public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) - { - return AdditionalTextOptions.TryGetValue(textFile.Path, out var options) ? options : new TestAnalyzerConfigOptions(); - } - - public TestAnalyzerConfigOptionsProvider Clone() - { - var provider = new TestAnalyzerConfigOptionsProvider(); - foreach (var option in this.TestGlobalOptions.Options) - { - provider.TestGlobalOptions[option.Key] = option.Value; - } - foreach (var option in this.AdditionalTextOptions) - { - var newOptions = new TestAnalyzerConfigOptions(); - foreach (var subOption in option.Value.Options) - { - newOptions[subOption.Key] = subOption.Value; - } - provider.AdditionalTextOptions[option.Key] = newOptions; - - } - return provider; - } - } - - private class TestAnalyzerConfigOptions : AnalyzerConfigOptions - { - public Dictionary Options { get; } = new(); - - public string this[string name] - { - get => Options[name]; - set => Options[name] = value; - } - - public override bool TryGetValue(string key, out string value) - => Options.TryGetValue(key, out value); - } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index abebc24f4fad..ffd4f9ded2ab 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http.SourceGeneration; - namespace Microsoft.AspNetCore.Http.SourceGeneration.Tests; public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase From 25dc43a6e09cfa1c4d62a82b98c6a110ea9c8080 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 Jan 2023 13:35:19 -0800 Subject: [PATCH 03/12] Address feedback from review --- src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj | 4 ++-- .../gen/Microsoft.AspNetCore.Http.SourceGeneration.csproj | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj index 6f5175e68fe5..579c5a589bab 100644 --- a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj @@ -78,8 +78,8 @@ This package is an internal implementation of the .NET Core SDK and is not meant ReferenceOutputAssembly="false" /> + Private="false" + ReferenceOutputAssembly="false" /> netstandard2.0 - false true false true From b0e147cda53caf9ebee2f895887530d6b9a21e90 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 Jan 2023 14:12:33 -0800 Subject: [PATCH 04/12] Don't emit sources if no endpoints found --- .../gen/RequestDelegateGenerator.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 63e4d372b3a1..ca4da747508e 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -95,6 +95,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(genericThunks.Collect(), (context, sources) => { + if (sources.IsDefaultOrEmpty) + { + return; + } var code = new StringBuilder(); foreach (var source in sources) { @@ -105,6 +109,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(thunks.Collect(), (context, sources) => { + if (sources.IsDefaultOrEmpty) + { + return; + } var thunks = new StringBuilder(); foreach (var source in sources) { @@ -116,6 +124,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(stronglyTypedEndpointDefinitions.Collect(), (context, sources) => { + if (sources.IsDefaultOrEmpty) + { + return; + } var endpoints = new StringBuilder(); foreach (var source in sources) { @@ -125,9 +137,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.AddSource("GeneratedRouteBuilderExtensions.Endpoints.g.cs", code); }); - context.RegisterSourceOutput(endpoints.Collect(), (context, isGeneratorEnabled) => + context.RegisterSourceOutput(endpoints.Collect(), (context, endpoints) => { - context.AddSource("GeneratedRouteBuilderExtensions.Helpers.g.cs", RequestDelegateGeneratorSources.GeneratedRouteBuilderExtensionsSource); + if (!endpoints.IsDefaultOrEmpty) + { + context.AddSource("GeneratedRouteBuilderExtensions.Helpers.g.cs", RequestDelegateGeneratorSources.GeneratedRouteBuilderExtensionsSource); + } }); } } From e5bf644c2fa898a9a9fd2bfecc0dedb9c4c23e17 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 6 Jan 2023 17:43:01 -0800 Subject: [PATCH 05/12] Address feedback from review --- .../gen/RequestDelegateGenerator.cs | 1 + .../gen/RequestDelegateGeneratorSources.cs | 333 +++++++++--------- .../StaticRouteHandlerModel.cs | 8 +- ...ft.AspNetCore.Http.Extensions.Tests.csproj | 6 +- .../RequestDelegateGeneratorTestBase.cs | 16 +- .../RequestDelegateGeneratorTests.cs | 16 +- 6 files changed, 204 insertions(+), 176 deletions(-) diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index ca4da747508e..6421bda286e7 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -41,6 +41,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var endpoints = mapActionOperations .Select((operation, _) => StaticRouteHandlerModelParser.GetEndpointFromOperation(operation)) + .Where(endpoint => endpoint.Response.ResponseType == "string") // Only emit for signatures we support .WithTrackingName("EndpointModel"); var genericThunks = endpoints.Select((endpoint, token) => diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index 555a40dc6e3c..f69530f61eca 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Http.SourceGeneration; internal static class RequestDelegateGeneratorSources { - private const string SourceHeader = $@" + private const string SourceHeader = """ //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -31,24 +31,35 @@ internal static class RequestDelegateGeneratorSources using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; using MetadataPopulator = System.Action; -using RequestDelegateFactoryFunc = System.Func;"; +using RequestDelegateFactoryFunc = System.Func; +"""; - internal const string GeneratedRouteBuilderExtensionsSource = $@" -{SourceHeader} + internal const string GeneratedRouteBuilderExtensionsSource = $$""" +{{SourceHeader}} namespace Microsoft.AspNetCore.Builder -{{ - internal record SourceKey(string Path, int Line); -}} +{ + internal class SourceKey + { + public string Path { get; init; } + public int Line { get; init; } + + public SourceKey(string path, int line) + { + Path = path; + Line = line; + } + } +} internal static partial class GeneratedRouteBuilderExtensions -{{ - private static readonly string[] GetVerb = new[] {{ HttpMethods.Get }}; - private static readonly string[] PostVerb = new[] {{ HttpMethods.Post }}; - private static readonly string[] PutVerb = new[] {{ HttpMethods.Put }}; - private static readonly string[] DeleteVerb = new[] {{ HttpMethods.Delete }}; - private static readonly string[] PatchVerb = new[] {{ HttpMethods.Patch }}; +{ + private static readonly string[] GetVerb = new[] { HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { HttpMethods.Patch }; private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, @@ -57,10 +68,10 @@ private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( IEnumerable httpMethods, string filePath, int lineNumber) - {{ + { var (populate, factory) = GenericThunks.map[(filePath, lineNumber)]; return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); - }} + } private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, @@ -69,81 +80,81 @@ private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( IEnumerable httpMethods, string filePath, int lineNumber) - {{ + { var (populate, factory) = map[(filePath, lineNumber)]; return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); - }} + } private static SourceGeneratedRouteEndpointDataSource GetOrAddRouteEndpointDataSource(IEndpointRouteBuilder endpoints) - {{ + { SourceGeneratedRouteEndpointDataSource? routeEndpointDataSource = null; foreach (var dataSource in endpoints.DataSources) - {{ + { if (dataSource is SourceGeneratedRouteEndpointDataSource foundDataSource) - {{ + { routeEndpointDataSource = foundDataSource; break; - }} - }} + } + } if (routeEndpointDataSource is null) - {{ + { routeEndpointDataSource = new SourceGeneratedRouteEndpointDataSource(endpoints.ServiceProvider); endpoints.DataSources.Add(routeEndpointDataSource); - }} + } return routeEndpointDataSource; - }} + } private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, System.Reflection.MethodInfo mi) - {{ + { var routeHandlerFilters = builder.FilterFactories; var context0 = new EndpointFilterFactoryContext - {{ + { MethodInfo = mi, ApplicationServices = builder.ApplicationServices, - }}; + }; var initialFilteredInvocation = filteredInvocation; for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) - {{ + { var filterFactory = routeHandlerFilters[i]; filteredInvocation = filterFactory(context0, filteredInvocation); - }} + } return filteredInvocation; - }} + } private static void PopulateMetadata(System.Reflection.MethodInfo method, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider - {{ + { T.PopulateMetadata(method, builder); - }} + } private static void PopulateMetadata(System.Reflection.ParameterInfo parameter, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider - {{ + { T.PopulateMetadata(parameter, builder); - }} + } private static Task ExecuteObjectResult(object obj, HttpContext httpContext) - {{ + { if (obj is IResult r) - {{ + { return r.ExecuteAsync(httpContext); - }} + } else if (obj is string s) - {{ + { return httpContext.Response.WriteAsync(s); - }} + } else - {{ + { return httpContext.Response.WriteAsJsonAsync(obj); - }} - }} + } + } private sealed class SourceGeneratedRouteEndpointDataSource : EndpointDataSource - {{ + { private readonly List _routeEntries = new(); private readonly IServiceProvider _applicationServices; public SourceGeneratedRouteEndpointDataSource(IServiceProvider applicationServices) - {{ + { _applicationServices = applicationServices; - }} + } public RouteHandlerBuilder AddRouteHandler( RoutePattern pattern, @@ -152,17 +163,17 @@ public RouteHandlerBuilder AddRouteHandler( bool isFallback, MetadataPopulator metadataPopulator, RequestDelegateFactoryFunc requestDelegateFactoryFunc) - {{ + { var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); var finallyConventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); var routeAttributes = RouteAttributes.RouteHandler; if (isFallback) - {{ + { routeAttributes |= RouteAttributes.Fallback; - }} + } _routeEntries.Add(new() - {{ + { RoutePattern = pattern, RouteHandler = routeHandler, HttpMethods = httpMethods, @@ -171,38 +182,38 @@ public RouteHandlerBuilder AddRouteHandler( FinallyConventions = finallyConventions, RequestDelegateFactory = requestDelegateFactoryFunc, MetadataPopulator = metadataPopulator, - }}); - return new RouteHandlerBuilder(new[] {{ new ConventionBuilder(conventions, finallyConventions) }}); - }} + }); + return new RouteHandlerBuilder(new[] { new ConventionBuilder(conventions, finallyConventions) }); + } public override IReadOnlyList Endpoints - {{ + { get - {{ + { var endpoints = new RouteEndpoint[_routeEntries.Count]; for (int i = 0; i < _routeEntries.Count; i++) - {{ + { endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i]).Build(); - }} + } return endpoints; - }} - }} + } + } public override IReadOnlyList GetGroupedEndpoints(RouteGroupContext context) - {{ + { var endpoints = new RouteEndpoint[_routeEntries.Count]; for (int i = 0; i < _routeEntries.Count; i++) - {{ + { endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i], context.Prefix, context.Conventions, context.FinallyConventions).Build(); - }} + } return endpoints; - }} + } public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; private RouteEndpointBuilder CreateRouteEndpointBuilder( RouteEntry entry, RoutePattern? groupPrefix = null, IReadOnlyList>? groupConventions = null, IReadOnlyList>? groupFinallyConventions = null) - {{ + { var pattern = RoutePatternFactory.Combine(groupPrefix, entry.RoutePattern); var handler = entry.RouteHandler; var isRouteHandler = (entry.RouteAttributes & RouteAttributes.RouteHandler) == RouteAttributes.RouteHandler; @@ -210,203 +221,203 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( var order = isFallback ? int.MaxValue : 0; var displayName = pattern.RawText ?? pattern.ToString(); if (entry.HttpMethods is not null) - {{ + { // Prepends the HTTP method to the DisplayName produced with pattern + method name - displayName = $""HTTP: {{string.Join("", "", entry.HttpMethods)}} {{displayName}}""; - }} + displayName = $"HTTP: {string.Join("", "", entry.HttpMethods)} {displayName}"; + } if (isFallback) - {{ - displayName = $""Fallback {{displayName}}""; - }} + { + displayName = $"Fallback {displayName}"; + } // If we're not a route handler, we started with a fully realized (although unfiltered) RequestDelegate, so we can just redirect to that // while running any conventions. We'll put the original back if it remains unfiltered right before building the endpoint. RequestDelegate? factoryCreatedRequestDelegate = null; // Let existing conventions capture and call into builder.RequestDelegate as long as they do so after it has been created. RequestDelegate redirectRequestDelegate = context => - {{ + { if (factoryCreatedRequestDelegate is null) - {{ - throw new InvalidOperationException(""Resources.RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild""); - }} + { + throw new InvalidOperationException("Resources.RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild"); + } return factoryCreatedRequestDelegate(context); - }}; + }; // Add MethodInfo and HttpMethodMetadata (if any) as first metadata items as they are intrinsic to the route much like // the pattern or default display name. This gives visibility to conventions like WithOpenApi() to intrinsic route details // (namely the MethodInfo) even when applied early as group conventions. RouteEndpointBuilder builder = new(redirectRequestDelegate, pattern, order) - {{ + { DisplayName = displayName, ApplicationServices = _applicationServices, - }}; + }; if (isRouteHandler) - {{ + { builder.Metadata.Add(handler.Method); - }} + } if (entry.HttpMethods is not null) - {{ + { builder.Metadata.Add(new HttpMethodMetadata(entry.HttpMethods)); - }} + } // Apply group conventions before entry-specific conventions added to the RouteHandlerBuilder. if (groupConventions is not null) - {{ + { foreach (var groupConvention in groupConventions) - {{ + { groupConvention(builder); - }} - }} + } + } // Any metadata inferred directly inferred by RDF or indirectly inferred via IEndpoint(Parameter)MetadataProviders are // considered less specific than method-level attributes and conventions but more specific than group conventions // so inferred metadata gets added in between these. If group conventions need to override inferred metadata, // they can do so via IEndpointConventionBuilder.Finally like the do to override any other entry-specific metadata. if (isRouteHandler) - {{ + { entry.MetadataPopulator(entry.RouteHandler, builder); - }} + } // Add delegate attributes as metadata before entry-specific conventions but after group conventions. var attributes = handler.Method.GetCustomAttributes(); if (attributes is not null) - {{ + { foreach (var attribute in attributes) - {{ + { builder.Metadata.Add(attribute); - }} - }} + } + } entry.Conventions.IsReadOnly = true; foreach (var entrySpecificConvention in entry.Conventions) - {{ + { entrySpecificConvention(builder); - }} + } // If no convention has modified builder.RequestDelegate, we can use the RequestDelegate returned by the RequestDelegateFactory directly. var conventionOverriddenRequestDelegate = ReferenceEquals(builder.RequestDelegate, redirectRequestDelegate) ? null : builder.RequestDelegate; if (isRouteHandler || builder.FilterFactories.Count > 0) - {{ + { factoryCreatedRequestDelegate = entry.RequestDelegateFactory(entry.RouteHandler, builder); - }} + } Debug.Assert(factoryCreatedRequestDelegate is not null); // Use the overridden RequestDelegate if it exists. If the overridden RequestDelegate is merely wrapping the final RequestDelegate, // it will still work because of the redirectRequestDelegate. builder.RequestDelegate = conventionOverriddenRequestDelegate ?? factoryCreatedRequestDelegate; entry.FinallyConventions.IsReadOnly = true; foreach (var entryFinallyConvention in entry.FinallyConventions) - {{ + { entryFinallyConvention(builder); - }} + } if (groupFinallyConventions is not null) - {{ + { // Group conventions are ordered by the RouteGroupBuilder before // being provided here. foreach (var groupFinallyConvention in groupFinallyConventions) - {{ + { groupFinallyConvention(builder); - }} - }} + } + } return builder; - }} + } - private struct RouteEntry - {{ - public MetadataPopulator MetadataPopulator {{ get; init; }} - public RequestDelegateFactoryFunc RequestDelegateFactory {{ get; init; }} - public RoutePattern RoutePattern {{ get; init; }} - public Delegate RouteHandler {{ get; init; }} - public IEnumerable HttpMethods {{ get; init; }} - public RouteAttributes RouteAttributes {{ get; init; }} - public ThrowOnAddAfterEndpointBuiltConventionCollection Conventions {{ get; init; }} - public ThrowOnAddAfterEndpointBuiltConventionCollection FinallyConventions {{ get; init; }} - }} + private readonly struct RouteEntry + { + public MetadataPopulator MetadataPopulator { get; init; } + public RequestDelegateFactoryFunc RequestDelegateFactory { get; init; } + public RoutePattern RoutePattern { get; init; } + public Delegate RouteHandler { get; init; } + public IEnumerable HttpMethods { get; init; } + public RouteAttributes RouteAttributes { get; init; } + public ThrowOnAddAfterEndpointBuiltConventionCollection Conventions { get; init; } + public ThrowOnAddAfterEndpointBuiltConventionCollection FinallyConventions { get; init; } + } [Flags] private enum RouteAttributes - {{ + { // The endpoint was defined by a RequestDelegate, RequestDelegateFactory.Create() should be skipped unless there are endpoint filters. None = 0, // This was added as Delegate route handler, so RequestDelegateFactory.Create() should always be called. RouteHandler = 1, // This was added by MapFallback. Fallback = 2, - }} + } // This private class is only exposed to internal code via ICollection> in RouteEndpointBuilder where only Add is called. private sealed class ThrowOnAddAfterEndpointBuiltConventionCollection : List>, ICollection> - {{ + { // We throw if someone tries to add conventions to the RouteEntry after endpoints have already been resolved meaning the conventions // will not be observed given RouteEndpointDataSource is not meant to be dynamic and uses NullChangeToken.Singleton. - public bool IsReadOnly {{ get; set; }} + public bool IsReadOnly { get; set; } void ICollection>.Add(Action convention) - {{ + { if (IsReadOnly) - {{ - throw new InvalidOperationException(""Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild""); - }} + { + throw new InvalidOperationException("Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild"); + } Add(convention); - }} - }} + } + } - private class ConventionBuilder : IEndpointConventionBuilder - {{ + private sealed class ConventionBuilder : IEndpointConventionBuilder + { private readonly ICollection> _conventions; private readonly ICollection> _finallyConventions; public ConventionBuilder(ICollection> conventions, ICollection> finallyConventions) - {{ + { _conventions = conventions; _finallyConventions = finallyConventions; - }} + } /// - /// Adds the specified convention to the builder. Conventions are used to customize instances. + /// Adds the specified convention to the builder. Conventions are used to customize instances. /// - /// The convention to add to the builder. + /// The convention to add to the builder. public void Add(Action convention) - {{ + { _conventions.Add(convention); - }} + } public void Finally(Action finalConvention) - {{ + { _finallyConventions.Add(finalConvention); - }} - }} - }} -}} -"; + } + } + } +} +"""; internal static string GetGenericThunks(string genericThunks) { - return $@" -{SourceHeader} + return $$""" +{{SourceHeader}} internal static partial class GeneratedRouteBuilderExtensions -{{ - internal class GenericThunks - {{ +{ + internal static class GenericThunks + { public static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - {{ -{genericThunks} - }}; - }} -}} -"; + { +{{genericThunks}} + }; + } +} +"""; } internal static string GetThunks(string thunks) { - return $@" -{SourceHeader} + return $$""" +{{SourceHeader}} internal static partial class GeneratedRouteBuilderExtensions -{{ +{ internal static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - {{ - {thunks} - }}; -}} -"; + { + {{thunks}} + }; +} +"""; } internal static string GetEndpoints(string endpoints) { - return $@" + return $$""" internal static partial class GeneratedRouteBuilderExtensions -{{ - {endpoints} -}} -"; +{ + {{endpoints}} +} +"""; } } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs index 27a90ba0101a..4454f48373b5 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -15,7 +15,7 @@ internal enum RequestParameterSource QueryOrService } -internal class RequestParameter +internal sealed class RequestParameter { public string Name { get; } public string Type { get; } @@ -24,20 +24,20 @@ internal class RequestParameter public object? DefaultValue { get; set; } } -internal class EndpointRoute +internal sealed class EndpointRoute { public string RoutePattern { get; set; } public List RouteParameters { get; set; } } -internal class EndpointResponse +internal sealed class EndpointResponse { public string ResponseType { get; set; } public string ContentType { get; set; } } -internal class EndpointRequest +internal sealed class EndpointRequest { public List RequestParameters { get; set; } } diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index fe3f44ce4d0e..24047f4dd90c 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -16,6 +16,7 @@ + @@ -23,9 +24,4 @@ - - - - - diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 7dfce3b8d33d..43b1dc6a93d5 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -43,11 +43,17 @@ internal static (ImmutableArray, Compilation) RunGenerator(s internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArray results, string stepName) { - var staticEndpointStep = results[0].TrackedSteps[stepName].Single(); - var staticEndpointOutput = staticEndpointStep.Outputs.Single(); - var (staticEndpoint, _) = staticEndpointOutput; - var endpoint = Assert.IsType(staticEndpoint); - return endpoint; + // We only invoke the generator once in our test scenarios + var firstGeneratorPass = results[0]; + if (firstGeneratorPass.TrackedSteps.TryGetValue(stepName, out var staticEndpointSteps)) + { + var staticEndpointStep = staticEndpointSteps.Single(); + var staticEndpointOutput = staticEndpointStep.Outputs.Single(); + var (staticEndpoint, _) = staticEndpointOutput; + var endpoint = Assert.IsType(staticEndpoint); + return endpoint; + } + return null; } internal static Endpoint GetEndpointFromCompilation(Compilation compilation) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index ffd4f9ded2ab..a917b3bc650d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -7,7 +7,10 @@ public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase { [Theory] [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "Hello world!")] - public async Task MapGet_NoParam_SimpleReturn(string source, string expectedBody) + [InlineData(@"app.MapPost(""/hello"", () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapDelete(""/hello"", () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapPut(""/hello"", () => ""Hello world!"");", "Hello world!")] + public async Task MapAction_NoParam_StringReturn(string source, string expectedBody) { var (results, compilation) = RunGenerator(source); @@ -31,4 +34,15 @@ public async Task MapGet_NoParam_SimpleReturn(string source, string expectedBody Assert.Equal(200, httpContext.Response.StatusCode); Assert.Equal(expectedBody, body); } + + [Theory] + [InlineData("""app.MapGet("/hello", () => 2);""")] + [InlineData("""app.MapGet("/hello", () => new System.DateTime());""")] + public void MapGet_UnsupportedSignature_DoesNotEmit(string source) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + Assert.Null(endpointModel); + } } From b9b8cec4d0ba3f6df1b77a620805e849682ee387 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sat, 7 Jan 2023 12:26:40 -0800 Subject: [PATCH 06/12] Address feedback and consolidate emitted source --- .../gen/RequestDelegateGenerator.cs | 123 +++++++----------- .../gen/RequestDelegateGeneratorSources.cs | 63 +++------ .../StaticRouteHandlerModel.Emitter.cs | 18 +-- .../StaticRouteHandlerModel.Parser.cs | 1 + .../RequestDelegateGeneratorTestBase.cs | 1 + 5 files changed, 75 insertions(+), 131 deletions(-) diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 6421bda286e7..f35e1a1324ae 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Http.SourceGeneration; [Generator] -public class RequestDelegateGenerator : IIncrementalGenerator +public sealed class RequestDelegateGenerator : IIncrementalGenerator { private static readonly string[] _knownMethods = { @@ -25,7 +25,7 @@ public class RequestDelegateGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { - var mapActionOperations = context.SyntaxProvider.CreateSyntaxProvider( + var endpoints = context.SyntaxProvider.CreateSyntaxProvider( predicate: (node, _) => node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax @@ -37,113 +37,82 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }, ArgumentList: { Arguments: { Count: 2 } args } } && _knownMethods.Contains(method), - transform: (context, token) => context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation); - - var endpoints = mapActionOperations - .Select((operation, _) => StaticRouteHandlerModelParser.GetEndpointFromOperation(operation)) - .Where(endpoint => endpoint.Response.ResponseType == "string") // Only emit for signatures we support + transform: (context, token) => + { + var operation = context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation; + return StaticRouteHandlerModelParser.GetEndpointFromOperation(operation); + }) + .Where(endpoint => endpoint.Response.ResponseType == "string") .WithTrackingName("EndpointModel"); - var genericThunks = endpoints.Select((endpoint, token) => - { - var code = RequestDelegateGeneratorSources.GetGenericThunks(string.Empty); - return code; - }); - - var thunks = endpoints.Select((endpoint, _) => $@" [{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}] = ( - (del, builder) => - {{ -builder.Metadata.Add(new SourceKey{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}); - }}, + var thunks = endpoints.Select((endpoint, _) => $$""" + [{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( (del, builder) => - {{ - var handler = ({StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)})del; + { + builder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); + }, + (del, builder) => + { + var handler = ({{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}})del; EndpointFilterDelegate? filteredInvocation = null; if (builder.FilterFactories.Count > 0) - {{ + { filteredInvocation = BuildFilterDelegate(ic => - {{ + { if (ic.HttpContext.Response.StatusCode == 400) - {{ + { return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); - }} - {StaticRouteHandlerModelEmitter.EmitFilteredInvocation()} - }}, + } + {{StaticRouteHandlerModelEmitter.EmitFilteredInvocation()}} + }, builder, handler.Method); - }} + } - {StaticRouteHandlerModelEmitter.EmitRequestHandler()} - {StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()} + {{StaticRouteHandlerModelEmitter.EmitRequestHandler()}} + {{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} return filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; - }}), -"); + }), +"""); var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => { var code = new StringBuilder(); code.AppendLine($"internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder {endpoint.HttpMethod}"); - code.Append("(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string pattern, "); - code.Append(StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)); - code.Append(@" handler, [System.Runtime.CompilerServices.CallerFilePath] string filePath = """", [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)"); + code.Append("(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,"); + code.AppendLine(@"[System.Diagnostics.CodeAnalysis.StringSyntax(""Route"")] string pattern, "); + code.AppendLine($"{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)} handler,"); + code.AppendLine(@"[System.Runtime.CompilerServices.CallerFilePath] string filePath = """", [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)"); code.AppendLine("{"); - code.AppendLine("return MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber);"); + code.AppendLine(" return MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber);"); code.AppendLine("}"); return code.ToString(); }); - context.RegisterSourceOutput(genericThunks.Collect(), (context, sources) => - { - if (sources.IsDefaultOrEmpty) - { - return; - } - var code = new StringBuilder(); - foreach (var source in sources) - { - code.AppendLine(source); - } - context.AddSource("GeneratedRouteBuilderExtensions.GenericThunks.g.cs", code.ToString()); - }); + var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect()); - context.RegisterSourceOutput(thunks.Collect(), (context, sources) => + context.RegisterSourceOutput(thunksAndEndpoints, (context, sources) => { - if (sources.IsDefaultOrEmpty) - { - return; - } - var thunks = new StringBuilder(); - foreach (var source in sources) - { - thunks.AppendLine(source); - } - var code = RequestDelegateGeneratorSources.GetThunks(thunks.ToString()); - context.AddSource("GeneratedRouteBuilderExtensions.Thunks.g.cs", code); - }); + var (thunks, endpoints) = sources; - context.RegisterSourceOutput(stronglyTypedEndpointDefinitions.Collect(), (context, sources) => - { - if (sources.IsDefaultOrEmpty) + var endpointsCode = new StringBuilder(); + var thunksCode = new StringBuilder(); + foreach (var endpoint in endpoints) { - return; + endpointsCode.AppendLine(endpoint); } - var endpoints = new StringBuilder(); - foreach (var source in sources) + foreach (var thunk in thunks) { - endpoints.AppendLine(source); + thunksCode.AppendLine(thunk); } - var code = RequestDelegateGeneratorSources.GetEndpoints(endpoints.ToString()); - context.AddSource("GeneratedRouteBuilderExtensions.Endpoints.g.cs", code); - }); - context.RegisterSourceOutput(endpoints.Collect(), (context, endpoints) => - { - if (!endpoints.IsDefaultOrEmpty) - { - context.AddSource("GeneratedRouteBuilderExtensions.Helpers.g.cs", RequestDelegateGeneratorSources.GeneratedRouteBuilderExtensionsSource); - } + var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource( + genericThunks: string.Empty, + thunks: thunksCode.ToString(), + endpoints: endpointsCode.ToString()); + context.AddSource("GeneratedRouteBuilderExtensions.g.cs", code); }); } } diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index f69530f61eca..fbb01e5d4ee3 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -34,7 +34,7 @@ internal static class RequestDelegateGeneratorSources using RequestDelegateFactoryFunc = System.Func; """; - internal const string GeneratedRouteBuilderExtensionsSource = $$""" + public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints) => $$""" {{SourceHeader}} namespace Microsoft.AspNetCore.Builder @@ -53,7 +53,7 @@ public SourceKey(string path, int line) } -internal static partial class GeneratedRouteBuilderExtensions +internal static class GeneratedRouteBuilderExtensions { private static readonly string[] GetVerb = new[] { HttpMethods.Get }; private static readonly string[] PostVerb = new[] { HttpMethods.Post }; @@ -61,6 +61,21 @@ internal static partial class GeneratedRouteBuilderExtensions private static readonly string[] DeleteVerb = new[] { HttpMethods.Delete }; private static readonly string[] PatchVerb = new[] { HttpMethods.Patch }; + private static class GenericThunks + { + public static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + {{genericThunks}} + }; + } + + private static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + {{thunks}} + }; + + {{endpoints}} + private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string pattern, @@ -130,7 +145,7 @@ private static void PopulateMetadata(System.Reflection.ParameterInfo paramete T.PopulateMetadata(parameter, builder); } - private static Task ExecuteObjectResult(object obj, HttpContext httpContext) + private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) { if (obj is IResult r) { @@ -378,46 +393,4 @@ public void Finally(Action finalConvention) } } """; - internal static string GetGenericThunks(string genericThunks) - { - return $$""" -{{SourceHeader}} - -internal static partial class GeneratedRouteBuilderExtensions -{ - internal static class GenericThunks - { - public static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { -{{genericThunks}} - }; - } -} -"""; - } - - internal static string GetThunks(string thunks) - { - return $$""" -{{SourceHeader}} - -internal static partial class GeneratedRouteBuilderExtensions -{ - internal static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { - {{thunks}} - }; -} -"""; - } - - internal static string GetEndpoints(string endpoints) - { - return $$""" -internal static partial class GeneratedRouteBuilderExtensions -{ - {{endpoints}} -} -"""; - } } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index f09e5379d287..289b8d8a53de 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -36,13 +36,13 @@ public static string EmitSourceKey(Endpoint endpoint) */ public static string EmitRequestHandler() { - return $@" + return """ System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext httpContext) -{{ +{ var result = handler(); return httpContext.Response.WriteAsync(result); -}} -"; +} +"""; } /* @@ -54,13 +54,13 @@ System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext */ public static string EmitFilteredRequestHandler() { - return $@" + return """ async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Http.HttpContext httpContext) -{{ +{ var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); await ExecuteObjectResult(result, httpContext); -}} -"; +} +"""; } /* @@ -81,6 +81,6 @@ async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Ht */ public static string EmitFilteredInvocation() { - return $@"return System.Threading.Tasks.ValueTask.FromResult(handler());"; + return "return System.Threading.Tasks.ValueTask.FromResult(handler());"; } } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs index eaf0a1bb6e2f..b07fbee65d0b 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -56,6 +56,7 @@ public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) IAnonymousFunctionOperation anon => anon.Symbol, ILocalFunctionOperation local => local.Symbol, IMethodReferenceOperation method => method.Method, + IParenthesizedOperation parenthesized => ResolveMethodFromOperation(parenthesized.Operand), _ => null }; diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 43b1dc6a93d5..05970a5fd573 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -118,6 +118,7 @@ internal static Endpoint GetEndpointFromCompilation(Compilation compilation) private static Compilation CreateCompilation(string sources) { var source = $$""" +#nullable enable using System; using System.Collections.Generic; using System.Linq; From f4af25d2034f70db5dab98734f67ab97b18ea495 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sat, 7 Jan 2023 12:57:19 -0800 Subject: [PATCH 07/12] Support named MapAction method parameters --- .../StaticRouteHandlerModel.Parser.cs | 62 ++++++++++++++++--- .../StaticRouteHandlerModel.cs | 2 +- .../RequestDelegateGeneratorTests.cs | 4 ++ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs index b07fbee65d0b..30a9a531c560 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -8,13 +8,14 @@ namespace Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; internal static class StaticRouteHandlerModelParser { - public static EndpointRoute GetEndpointRouteFromArgument(IArgumentOperation argumentOperation) + private const int _routePatternArgumentOrdinal = 1; + private const int _routeHandlerArgumentOrdinal = 2; + + public static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern) { - var syntax = argumentOperation.Syntax as ArgumentSyntax; - var expression = syntax.Expression as LiteralExpressionSyntax; return new EndpointRoute { - RoutePattern = expression.Token.ValueText + RoutePattern = routePattern.ValueText }; } @@ -29,8 +30,14 @@ public static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol metho public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) { - var routePatternArgument = operation.Arguments[1]; - var method = ResolveMethodFromOperation(operation.Arguments[2]); + if (!TryGetRouteHandlerPattern(operation, out var routeToken)) + { + return null; + } + if (!TryGetRouteHandlerMethod(operation, out var method)) + { + return null; + } var filePath = operation.Syntax.SyntaxTree.FilePath; var span = operation.Syntax.SyntaxTree.GetLineSpan(operation.Syntax.Span); @@ -39,13 +46,54 @@ public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) return new Endpoint { - Route = GetEndpointRouteFromArgument(routePatternArgument), + Route = GetEndpointRouteFromArgument(routeToken), Response = GetEndpointResponseFromMethod(method), Location = (filePath, span.EndLinePosition.Line + 1), HttpMethod = httpMethod, }; } + private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, out SyntaxToken token) + { + IArgumentOperation? argumentOperation = null; + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == _routePatternArgumentOrdinal) + { + argumentOperation = argument; + } + } + if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax || + routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax) + { + token = default; + return false; + } + token = routePatternArgumentLiteralSyntax.Token; + return true; + } + + private static bool TryGetRouteHandlerMethod(IInvocationOperation invocation, out IMethodSymbol method) + { + IArgumentOperation? argumentOperation = null; + method = null; + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == _routeHandlerArgumentOrdinal) + { + argumentOperation = argument; + } + } + + if (argumentOperation is not null) + { + method = ResolveMethodFromOperation(argumentOperation); + return true; + } + + return false; + } + private static IMethodSymbol ResolveMethodFromOperation(IOperation operation) => operation switch { IArgumentOperation argument => ResolveMethodFromOperation(argument.Value), diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs index 4454f48373b5..9026a1d2464a 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -12,7 +12,7 @@ internal enum RequestParameterSource Header, Form, Service, - QueryOrService + BodyOrService } internal sealed class RequestParameter diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index a917b3bc650d..81ab07baaab2 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -10,6 +10,10 @@ public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase [InlineData(@"app.MapPost(""/hello"", () => ""Hello world!"");", "Hello world!")] [InlineData(@"app.MapDelete(""/hello"", () => ""Hello world!"");", "Hello world!")] [InlineData(@"app.MapPut(""/hello"", () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapGet(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapPost(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] + [InlineData(@"app.MapDelete(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] + [InlineData(@"app.MapPut(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] public async Task MapAction_NoParam_StringReturn(string source, string expectedBody) { var (results, compilation) = RunGenerator(source); From 96db4dbe676fb341360ad8d352b89290f9a8e960 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sat, 7 Jan 2023 19:53:16 -0800 Subject: [PATCH 08/12] Fix up indentation --- .../gen/RequestDelegateGenerator.cs | 24 +++++++++---------- .../StaticRouteHandlerModel.Emitter.cs | 18 +++++++------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index f35e1a1324ae..c34708bbfe04 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -77,19 +77,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }), """); - var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => - { - var code = new StringBuilder(); - code.AppendLine($"internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder {endpoint.HttpMethod}"); - code.Append("(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,"); - code.AppendLine(@"[System.Diagnostics.CodeAnalysis.StringSyntax(""Route"")] string pattern, "); - code.AppendLine($"{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)} handler,"); - code.AppendLine(@"[System.Runtime.CompilerServices.CallerFilePath] string filePath = """", [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)"); - code.AppendLine("{"); - code.AppendLine(" return MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber);"); - code.AppendLine("}"); - return code.ToString(); - }); + var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => $$""" +internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( + this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + {{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, + [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + } +"""); var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect()); diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 289b8d8a53de..bb622d71e0a6 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -25,7 +25,7 @@ public static string EmitHandlerDelegateType(Endpoint endpoint) public static string EmitSourceKey(Endpoint endpoint) { - return $@"(""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; + return $@"(@""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; } /* @@ -38,10 +38,10 @@ public static string EmitRequestHandler() { return """ System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext httpContext) -{ - var result = handler(); - return httpContext.Response.WriteAsync(result); -} + { + var result = handler(); + return httpContext.Response.WriteAsync(result); + } """; } @@ -56,10 +56,10 @@ public static string EmitFilteredRequestHandler() { return """ async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Http.HttpContext httpContext) -{ - var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); - await ExecuteObjectResult(result, httpContext); -} + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await ExecuteObjectResult(result, httpContext); + } """; } From 66659344c49f72cacdad7b8e31a509873f73c6df Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sat, 7 Jan 2023 21:36:46 -0800 Subject: [PATCH 09/12] Rename project and add GeneratedCodeAttributes --- AspNetCore.sln | 2 +- .../src/Microsoft.AspNetCore.App.Ref.csproj | 4 +- ...crosoft.AspNetCore.Http.Generators.csproj} | 0 .../gen/RequestDelegateGenerator.cs | 5 +- .../gen/RequestDelegateGeneratorSources.cs | 53 ++++++++++--------- .../StaticRouteHandlerModel.Emitter.cs | 2 +- .../StaticRouteHandlerModel.Parser.cs | 2 +- .../StaticRouteHandlerModel.cs | 2 +- ...ft.AspNetCore.Http.Extensions.Tests.csproj | 2 +- .../RequestDelegateGeneratorTestBase.cs | 3 +- .../RequestDelegateGeneratorTests.cs | 2 +- src/Http/HttpAbstractions.slnf | 10 ++-- 12 files changed, 47 insertions(+), 40 deletions(-) rename src/Http/Http.Extensions/gen/{Microsoft.AspNetCore.Http.SourceGeneration.csproj => Microsoft.AspNetCore.Http.Generators.csproj} (100%) diff --git a/AspNetCore.sln b/AspNetCore.sln index 25221de7cc8a..8320ce409030 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1762,7 +1762,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests", "src\Servers\Kestrel\Transport.NamedPipes\test\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj", "{97C7D2A4-87E5-4A4A-A170-D736427D5C21}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.SourceGeneration", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.SourceGeneration.csproj", "{4730F56D-24EF-4BB2-AA75-862E31205F3A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Generators", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.Generators.csproj", "{4730F56D-24EF-4BB2-AA75-862E31205F3A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj index 579c5a589bab..c852d1016a13 100644 --- a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj @@ -77,7 +77,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant Private="false" ReferenceOutputAssembly="false" /> - @@ -180,7 +180,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Components.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.CodeFixes\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.CodeFixes.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> - <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Http.SourceGeneration\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Http.SourceGeneration.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> + <_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Http.Generators\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Http.Generators.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" /> <_InitialRefPackContent Include="@(AspNetCoreReferenceAssemblyPath)" PackagePath="$(RefAssemblyPackagePath)" /> <_InitialRefPackContent Include="@(AspNetCoreReferenceDocXml)" PackagePath="$(RefAssemblyPackagePath)" /> diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.SourceGeneration.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj similarity index 100% rename from src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.SourceGeneration.csproj rename to src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index c34708bbfe04..6aed91ce80b5 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -6,9 +6,9 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; -using Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; +using Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; -namespace Microsoft.AspNetCore.Http.SourceGeneration; +namespace Microsoft.AspNetCore.Http.Generators; [Generator] public sealed class RequestDelegateGenerator : IIncrementalGenerator @@ -78,6 +78,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) """); var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => $$""" +{{RequestDelegateGeneratorSources.GeneratedCodeAttribute}} internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, [System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index fbb01e5d4ee3..a92584bfbe68 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.SourceGeneration; +namespace Microsoft.AspNetCore.Http.Generators; internal static class RequestDelegateGeneratorSources { @@ -15,30 +15,33 @@ internal static class RequestDelegateGeneratorSources // //------------------------------------------------------------------------------ #nullable enable -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using System.IO; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; +using global::System; +using global::System.Collections; +using global::System.Collections.Generic; +using global::System.Diagnostics; +using global::System.Linq; +using global::System.Reflection; +using global::System.Threading.Tasks; +using global::System.IO; +using global::Microsoft.AspNetCore.Routing; +using global::Microsoft.AspNetCore.Routing.Patterns; +using global::Microsoft.AspNetCore.Builder; +using global::Microsoft.AspNetCore.Http; +using global::Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.FileProviders; +using global::Microsoft.Extensions.Primitives; using MetadataPopulator = System.Action; using RequestDelegateFactoryFunc = System.Func; """; + public static string GeneratedCodeAttribute => $@"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]"; + public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints) => $$""" {{SourceHeader}} namespace Microsoft.AspNetCore.Builder { + {{GeneratedCodeAttribute}} internal class SourceKey { public string Path { get; init; } @@ -52,7 +55,7 @@ public SourceKey(string path, int line) } } - +{{GeneratedCodeAttribute}} internal static class GeneratedRouteBuilderExtensions { private static readonly string[] GetVerb = new[] { HttpMethods.Get }; @@ -63,21 +66,21 @@ internal static class GeneratedRouteBuilderExtensions private static class GenericThunks { - public static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + public static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { {{genericThunks}} }; } - private static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + private static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { {{thunks}} }; {{endpoints}} - private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( - this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, + private static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string pattern, System.Delegate handler, IEnumerable httpMethods, @@ -88,8 +91,8 @@ private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory); } - private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( - this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, + private static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes, string pattern, System.Delegate handler, IEnumerable httpMethods, @@ -119,7 +122,7 @@ private static SourceGeneratedRouteEndpointDataSource GetOrAddRouteEndpointDataS return routeEndpointDataSource; } - private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, System.Reflection.MethodInfo mi) + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, global::System.Reflection.MethodInfo mi) { var routeHandlerFilters = builder.FilterFactories; var context0 = new EndpointFilterFactoryContext @@ -140,6 +143,7 @@ private static void PopulateMetadata(System.Reflection.MethodInfo method, End { T.PopulateMetadata(method, builder); } + private static void PopulateMetadata(System.Reflection.ParameterInfo parameter, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider { T.PopulateMetadata(parameter, builder); @@ -161,6 +165,7 @@ private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) } } + {{GeneratedCodeAttribute}} private sealed class SourceGeneratedRouteEndpointDataSource : EndpointDataSource { private readonly List _routeEntries = new(); diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index bb622d71e0a6..3d27d0e7b34c 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; internal static class StaticRouteHandlerModelEmitter { diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs index 30a9a531c560..38e6924b5633 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; -namespace Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; internal static class StaticRouteHandlerModelParser { diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs index 9026a1d2464a..99e29d282065 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http.SourceGeneration.StaticRouteHandlerModel; +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; internal enum RequestParameterSource { diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index 24047f4dd90c..7d43a3aa61bb 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -22,6 +22,6 @@ - + diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 05970a5fd573..80ea89c6b40f 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -6,6 +6,7 @@ using System.Runtime.Loader; using System.Text; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http.Generators; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.AspNetCore.Routing; @@ -15,7 +16,7 @@ using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.DependencyModel.Resolution; -namespace Microsoft.AspNetCore.Http.SourceGeneration.Tests; +namespace Microsoft.AspNetCore.Http.Generators.Tests; public class RequestDelegateGeneratorTestBase { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index 81ab07baaab2..606624f589db 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.SourceGeneration.Tests; +namespace Microsoft.AspNetCore.Http.Generators.Tests; public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase { diff --git a/src/Http/HttpAbstractions.slnf b/src/Http/HttpAbstractions.slnf index 158ac35073dc..e0117d7cbeeb 100644 --- a/src/Http/HttpAbstractions.slnf +++ b/src/Http/HttpAbstractions.slnf @@ -20,8 +20,8 @@ "src\\Http\\Http.Abstractions\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks.csproj", "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Abstractions\\test\\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", + "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.Generators.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", - "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.SourceGeneration.csproj", "src\\Http\\Http.Extensions\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", @@ -38,15 +38,15 @@ "src\\Http\\Routing\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Routing.Microbenchmarks.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing\\test\\FunctionalTests\\Microsoft.AspNetCore.Routing.FunctionalTests.csproj", - "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj", "src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj", "src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj", "src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj", + "src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj", + "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", + "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj", "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", "src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj", - "src\\Http\\samples\\MinimalSample\\MinimalSample.csproj", - "src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", @@ -62,8 +62,8 @@ "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", - "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj", + "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj", "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", "src\\Servers\\Kestrel\\Transport.Quic\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj", From bb9243d3b097236f6a094ccc7e24ab05fcc60bc0 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Sun, 8 Jan 2023 20:32:19 -0800 Subject: [PATCH 10/12] Implement IEquatable on static model elements --- .../StaticRouteHandlerModel.Parser.cs | 13 +--- .../StaticRouteHandlerModel.cs | 65 +++++++++++++++++-- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs index 38e6924b5633..4a43e3aab0ff 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -75,22 +75,15 @@ private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, o private static bool TryGetRouteHandlerMethod(IInvocationOperation invocation, out IMethodSymbol method) { - IArgumentOperation? argumentOperation = null; - method = null; foreach (var argument in invocation.Arguments) { if (argument.Parameter?.Ordinal == _routeHandlerArgumentOrdinal) { - argumentOperation = argument; + method = ResolveMethodFromOperation(argument); + return true; } } - - if (argumentOperation is not null) - { - method = ResolveMethodFromOperation(argumentOperation); - return true; - } - + method = null; return false; } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs index 99e29d282065..8ea595f82635 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; @@ -15,38 +16,92 @@ internal enum RequestParameterSource BodyOrService } -internal sealed class RequestParameter +internal sealed class RequestParameter : IEquatable { public string Name { get; } public string Type { get; } public RequestParameterSource Source { get; set; } public bool IsOptional { get; set; } public object? DefaultValue { get; set; } + + public override bool Equals(object? obj) + => obj is RequestParameter requestParameter && Equals(requestParameter); + + public bool Equals(RequestParameter other) + => Name.Equals(other.Name, StringComparison.Ordinal) && + Type.Equals(other.Type, StringComparison.Ordinal) && + Source == other.Source && + IsOptional == other.IsOptional && + DefaultValue.Equals(DefaultValue); + + public override int GetHashCode() + => (Name, Type, Source, IsOptional, DefaultValue).GetHashCode(); } -internal sealed class EndpointRoute +internal sealed class EndpointRoute : IEquatable { public string RoutePattern { get; set; } public List RouteParameters { get; set; } + + public override bool Equals(object? obj) + => obj is EndpointRoute route && Equals(route); + + public bool Equals(EndpointRoute other) + => RoutePattern.Equals(other.RoutePattern, StringComparison.Ordinal) && + RouteParameters.Equals(other.RouteParameters); + + public override int GetHashCode() + => (RoutePattern, RouteParameters).GetHashCode(); } -internal sealed class EndpointResponse +internal sealed class EndpointResponse : IEquatable { public string ResponseType { get; set; } public string ContentType { get; set; } + public override bool Equals(object? obj) + => obj is EndpointResponse endpointResponse && Equals(endpointResponse); + + public bool Equals(EndpointResponse other) + => ResponseType == other.ResponseType + && ContentType == other.ContentType; + + public override int GetHashCode() + => (ResponseType, ContentType).GetHashCode(); } -internal sealed class EndpointRequest +internal sealed class EndpointRequest : IEquatable { public List RequestParameters { get; set; } + + public override bool Equals(object? obj) + => obj is EndpointRequest endpointRequest && Equals(endpointRequest); + + public bool Equals(EndpointRequest other) + => RequestParameters == other.RequestParameters; + + public override int GetHashCode() + => RequestParameters.GetHashCode(); } -internal sealed class Endpoint +internal sealed class Endpoint : IEquatable { public string HttpMethod { get; set; } public EndpointRoute Route { get; set; } public EndpointRequest Request { get; set; } public EndpointResponse Response { get; set; } public (string, int) Location { get; set; } + + public override bool Equals(object? obj) + => obj is Endpoint endpoint && Equals(endpoint); + + public bool Equals(Endpoint other) + => HttpMethod == other.HttpMethod && + Route.Equals(other.Route) && + Request.Equals(other.Request) && + Response.Equals(other.Response) && + Location.Equals(other.Location); + + public override int GetHashCode() + => (HttpMethod, Route, Request, Response, Location).GetHashCode(); } From 2d78789db15041139fa41f3113158ea1df9a16f3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 9 Jan 2023 08:50:47 -0800 Subject: [PATCH 11/12] Use records for model --- ...icrosoft.AspNetCore.Http.Generators.csproj | 4 + .../StaticRouteHandlerModel.Parser.cs | 35 +++---- .../StaticRouteHandlerModel.cs | 97 +------------------ 3 files changed, 22 insertions(+), 114 deletions(-) diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj index 69e6b3ca9d60..171d742689ab 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj @@ -20,5 +20,9 @@ + + + + diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs index 4a43e3aab0ff..30c80fac6e05 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -1,5 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; @@ -8,24 +9,17 @@ namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; internal static class StaticRouteHandlerModelParser { - private const int _routePatternArgumentOrdinal = 1; - private const int _routeHandlerArgumentOrdinal = 2; + private const int RoutePatternArgumentOrdinal = 1; + private const int RouteHandlerArgumentOrdinal = 2; - public static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern) + private static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern) { - return new EndpointRoute - { - RoutePattern = routePattern.ValueText - }; + return new EndpointRoute(routePattern.ValueText, new List()); } - public static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method) + private static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method) { - return new EndpointResponse - { - ContentType = "plain/text", - ResponseType = method.ReturnType.ToString(), - }; + return new EndpointResponse(method.ReturnType.ToString(), "plain/text"); } public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) @@ -44,13 +38,10 @@ public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; var httpMethod = ((IdentifierNameSyntax)((MemberAccessExpressionSyntax)invocationExpression.Expression).Name).Identifier.ValueText; - return new Endpoint - { - Route = GetEndpointRouteFromArgument(routeToken), - Response = GetEndpointResponseFromMethod(method), - Location = (filePath, span.EndLinePosition.Line + 1), - HttpMethod = httpMethod, - }; + return new Endpoint(httpMethod, + GetEndpointRouteFromArgument(routeToken), + GetEndpointResponseFromMethod(method), + (filePath, span.EndLinePosition.Line + 1)); } private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, out SyntaxToken token) @@ -58,7 +49,7 @@ private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, o IArgumentOperation? argumentOperation = null; foreach (var argument in invocation.Arguments) { - if (argument.Parameter?.Ordinal == _routePatternArgumentOrdinal) + if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal) { argumentOperation = argument; } @@ -77,7 +68,7 @@ private static bool TryGetRouteHandlerMethod(IInvocationOperation invocation, ou { foreach (var argument in invocation.Arguments) { - if (argument.Parameter?.Ordinal == _routeHandlerArgumentOrdinal) + if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal) { method = ResolveMethodFromOperation(argument); return true; diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs index 8ea595f82635..6ebaac956321 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; - namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; internal enum RequestParameterSource @@ -13,95 +11,10 @@ internal enum RequestParameterSource Header, Form, Service, - BodyOrService -} - -internal sealed class RequestParameter : IEquatable -{ - public string Name { get; } - public string Type { get; } - public RequestParameterSource Source { get; set; } - public bool IsOptional { get; set; } - public object? DefaultValue { get; set; } - - public override bool Equals(object? obj) - => obj is RequestParameter requestParameter && Equals(requestParameter); - - public bool Equals(RequestParameter other) - => Name.Equals(other.Name, StringComparison.Ordinal) && - Type.Equals(other.Type, StringComparison.Ordinal) && - Source == other.Source && - IsOptional == other.IsOptional && - DefaultValue.Equals(DefaultValue); - - public override int GetHashCode() - => (Name, Type, Source, IsOptional, DefaultValue).GetHashCode(); -} - -internal sealed class EndpointRoute : IEquatable -{ - public string RoutePattern { get; set; } - - public List RouteParameters { get; set; } - - public override bool Equals(object? obj) - => obj is EndpointRoute route && Equals(route); - - public bool Equals(EndpointRoute other) - => RoutePattern.Equals(other.RoutePattern, StringComparison.Ordinal) && - RouteParameters.Equals(other.RouteParameters); - - public override int GetHashCode() - => (RoutePattern, RouteParameters).GetHashCode(); -} - -internal sealed class EndpointResponse : IEquatable -{ - public string ResponseType { get; set; } - public string ContentType { get; set; } - public override bool Equals(object? obj) - => obj is EndpointResponse endpointResponse && Equals(endpointResponse); - - public bool Equals(EndpointResponse other) - => ResponseType == other.ResponseType - && ContentType == other.ContentType; - - public override int GetHashCode() - => (ResponseType, ContentType).GetHashCode(); + BodyOrService, } -internal sealed class EndpointRequest : IEquatable -{ - public List RequestParameters { get; set; } - - public override bool Equals(object? obj) - => obj is EndpointRequest endpointRequest && Equals(endpointRequest); - - public bool Equals(EndpointRequest other) - => RequestParameters == other.RequestParameters; - - public override int GetHashCode() - => RequestParameters.GetHashCode(); -} - -internal sealed class Endpoint : IEquatable -{ - public string HttpMethod { get; set; } - public EndpointRoute Route { get; set; } - public EndpointRequest Request { get; set; } - public EndpointResponse Response { get; set; } - public (string, int) Location { get; set; } - - public override bool Equals(object? obj) - => obj is Endpoint endpoint && Equals(endpoint); - - public bool Equals(Endpoint other) - => HttpMethod == other.HttpMethod && - Route.Equals(other.Route) && - Request.Equals(other.Request) && - Response.Equals(other.Response) && - Location.Equals(other.Location); - - public override int GetHashCode() - => (HttpMethod, Route, Request, Response, Location).GetHashCode(); -} +internal record RequestParameter(string Name, string Type, RequestParameterSource Source, bool IsOptional, object? DefaultValue); +internal record EndpointRoute(string RoutePattern, List RouteParameters); +internal record EndpointResponse(string ResponseType, string ContentType); +internal record Endpoint(string HttpMethod, EndpointRoute Route, EndpointResponse Response, (string, int) Location); From 5e88875f79f8b46477c637737d69582fab35d93b Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 9 Jan 2023 09:46:01 -0800 Subject: [PATCH 12/12] Remove RoutePatternParameter list --- .../StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs | 4 ++-- .../gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs index 30c80fac6e05..b7a53d418fca 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; @@ -14,7 +14,7 @@ internal static class StaticRouteHandlerModelParser private static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern) { - return new EndpointRoute(routePattern.ValueText, new List()); + return new EndpointRoute(routePattern.ValueText); } private static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs index 6ebaac956321..fb3a60c63bbf 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs @@ -15,6 +15,6 @@ internal enum RequestParameterSource } internal record RequestParameter(string Name, string Type, RequestParameterSource Source, bool IsOptional, object? DefaultValue); -internal record EndpointRoute(string RoutePattern, List RouteParameters); +internal record EndpointRoute(string RoutePattern); internal record EndpointResponse(string ResponseType, string ContentType); internal record Endpoint(string HttpMethod, EndpointRoute Route, EndpointResponse Response, (string, int) Location);