diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 2b3c14d62cdc..529e7e6f4b4d 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -20,7 +20,6 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator "MapPut", "MapDelete", "MapPatch", - "Map", }; public void Initialize(IncrementalGeneratorInitializationContext context) @@ -46,48 +45,54 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .WithTrackingName("EndpointModel"); var thunks = endpoints.Select((endpoint, _) => $$""" - [{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( - (del, builder) => +[{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( + (methodInfo, options) => { - builder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); + if (options == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, - (del, builder) => + (del, options, inferredMetadataResult) => { var handler = ({{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}})del; EndpointFilterDelegate? filteredInvocation = null; - if (builder.FilterFactories.Count > 0) + if (options.EndpointBuilder.FilterFactories.Count > 0) { filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => { if (ic.HttpContext.Response.StatusCode == 400) { - return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + return ValueTask.FromResult(Results.Empty); } {{StaticRouteHandlerModelEmitter.EmitFilteredInvocation()}} }, - builder, + options.EndpointBuilder, handler.Method); } {{StaticRouteHandlerModelEmitter.EmitRequestHandler()}} {{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} - return filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); }), """); 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, - {{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, - [System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) - { - return GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); - } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::{{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, {{StaticRouteHandlerModelEmitter.EmitVerb(endpoint)}}, filePath, lineNumber); + } """); var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect()); diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index 800e4c21e761..6cb436e4d9e9 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -15,26 +15,9 @@ internal static class RequestDelegateGeneratorSources // //------------------------------------------------------------------------------ #nullable enable -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 GeneratedCodeAttribute => $@"[System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(RequestDelegateGeneratorSources).Assembly.FullName}"", ""{typeof(RequestDelegateGeneratorSources).Assembly.GetName().Version}"")]"; public static string GetGeneratedRouteBuilderExtensionsSource(string genericThunks, string thunks, string endpoints) => $$""" {{SourceHeader}} @@ -53,360 +36,140 @@ public SourceKey(string path, int line) Line = line; } } -} - -{{GeneratedCodeAttribute}} -// This class needs to be internal so that the compiled application -// has access to the strongly-typed endpoint definitions that are -// generated by the compiler so that they will be favored by -// overload resolution and opt the runtime in to the code generated -// implementation produced here. -internal static class GenerateRouteBuilderEndpoints -{ - 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 }; - {{endpoints}} +{{GetEndpoints(endpoints)}} } -{{GeneratedCodeAttribute}} -file static class GeneratedRouteBuilderExtensionsCore +namespace Microsoft.AspNetCore.Http.Generated { - internal static class GenericThunks - { - public static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { - {{genericThunks}} - }; - } - - internal static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { - {{thunks}} - }; - - internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( - this global::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); - } - - internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore( - this global::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); - } - - internal static SourceGeneratedRouteEndpointDataSource GetOrAddRouteEndpointDataSource(IEndpointRouteBuilder endpoints) - { - SourceGeneratedRouteEndpointDataSource? routeEndpointDataSource = null; - foreach (var dataSource in endpoints.DataSources) - { - if (dataSource is SourceGeneratedRouteEndpointDataSource foundDataSource) + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + 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.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Primitives; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + file static class GeneratedRouteBuilderExtensionsCore + { +{{GetGenericThunks(genericThunks)}} +{{GetThunks(thunks)}} + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, 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--) { - routeEndpointDataSource = foundDataSource; - break; + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); } + return filteredInvocation; } - if (routeEndpointDataSource is null) - { - routeEndpointDataSource = new SourceGeneratedRouteEndpointDataSource(endpoints.ServiceProvider); - endpoints.DataSources.Add(routeEndpointDataSource); - } - return routeEndpointDataSource; - } - internal static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, global::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--) + private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider { - var filterFactory = routeHandlerFilters[i]; - filteredInvocation = filterFactory(context0, filteredInvocation); + T.PopulateMetadata(method, builder); } - return filteredInvocation; - } - - internal static void PopulateMetadata(System.Reflection.MethodInfo method, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider - { - T.PopulateMetadata(method, builder); - } - internal static void PopulateMetadata(System.Reflection.ParameterInfo parameter, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameter, builder); - } - - internal 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); - } - } -} - -{{GeneratedCodeAttribute}} -file 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) + private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : IEndpointParameterMetadataProvider { - routeAttributes |= RouteAttributes.Fallback; + T.PopulateMetadata(parameter, builder); } - _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 + private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) { - var endpoints = new RouteEndpoint[_routeEntries.Count]; - for (int i = 0; i < _routeEntries.Count; i++) + if (obj is IResult r) { - endpoints[i] = (RouteEndpoint)CreateRouteEndpointBuilder(_routeEntries[i]).Build(); + return r.ExecuteAsync(httpContext); } - 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) + else if (obj is string s) { - throw new InvalidOperationException("Resources.RouteEndpointDataSource_RequestDelegateCannotBeCalledBeforeBuild"); + return httpContext.Response.WriteAsync(s); } - 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) + else { - groupConvention(builder); + return httpContext.Response.WriteAsJsonAsync(obj); } } - // 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) + } +} +"""; + private static string GetGenericThunks(string genericThunks) => genericThunks != string.Empty ? $$""" + private static class GenericThunks { - 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); + public static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + {{genericThunks}} + }; } - 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) + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) { - entryFinallyConvention(builder); + var (populateMetadata, createRequestDelegate) = GenericThunks.map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); } - 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 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; } - } +""" : string.Empty; - [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 file class is only exposed to internal code via ICollection> in RouteEndpointBuilder where only Add is called. -{{GeneratedCodeAttribute}} -file 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) + private static string GetThunks(string thunks) => thunks != string.Empty ? $$""" + private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { - throw new InvalidOperationException("Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild"); - } - Add(convention); - } -} + {{thunks}} + }; -{{GeneratedCodeAttribute}} -file 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) + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); + } +""" : string.Empty; + + private static string GetEndpoints(string endpoints) => endpoints != string.Empty ? $$""" + // This class needs to be internal so that the compiled application + // has access to the strongly-typed endpoint definitions that are + // generated by the compiler so that they will be favored by + // overload resolution and opt the runtime in to the code generated + // implementation produced here. + {{GeneratedCodeAttribute}} + internal static class GenerateRouteBuilderEndpoints { - _finallyConventions.Add(finalConvention); + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch }; + +{{endpoints}} } -} -"""; +""" : string.Empty; } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 2e1e9249e635..996cec75e731 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; + namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; internal static class StaticRouteHandlerModelEmitter @@ -28,6 +30,19 @@ public static string EmitSourceKey(Endpoint endpoint) return $@"(@""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; } + public static string EmitVerb(Endpoint endpoint) + { + return endpoint.HttpMethod switch + { + "MapGet" => "GetVerb", + "MapPut" => "PutVerb", + "MapPost" => "PostVerb", + "MapDelete" => "DeleteVerb", + "MapPatch" => "PatchVerb", + _ => throw new ArgumentException($"Received unexpected HTTP method: {endpoint.HttpMethod}") + }; + } + /* * TODO: Emit invocation to the request handler. The structure * involved here consists of a call to bind parameters, check @@ -37,7 +52,7 @@ public static string EmitSourceKey(Endpoint endpoint) public static string EmitRequestHandler() { return """ -System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext httpContext) +Task RequestHandler(HttpContext httpContext) { var result = handler(); return httpContext.Response.WriteAsync(result); @@ -55,7 +70,7 @@ System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext public static string EmitFilteredRequestHandler() { return """ -async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Http.HttpContext httpContext) +async Task RequestHandlerFiltered(HttpContext httpContext) { var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); @@ -81,6 +96,6 @@ async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Ht */ public static string EmitFilteredInvocation() { - return "return System.Threading.Tasks.ValueTask.FromResult(handler());"; + return "return ValueTask.FromResult(handler());"; } } 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 7d43a3aa61bb..95942fd9ed66 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 @@ -24,4 +24,10 @@ + + + + PreserveNewest + + diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt new file mode 100644 index 000000000000..8309dc890cee --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt @@ -0,0 +1,183 @@ +//------------------------------------------------------------------------------ +// +// 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 + +namespace Microsoft.AspNetCore.Builder +{ + %GENERATEDCODEATTRIBUTE% + internal class SourceKey + { + public string Path { get; init; } + public int Line { get; init; } + + public SourceKey(string path, int line) + { + Path = path; + Line = line; + } + } + + // This class needs to be internal so that the compiled application + // has access to the strongly-typed endpoint definitions that are + // generated by the compiler so that they will be favored by + // overload resolution and opt the runtime in to the code generated + // implementation produced here. + %GENERATEDCODEATTRIBUTE% + internal static class GenerateRouteBuilderEndpoints + { + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch }; + + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + } + + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + 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.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Primitives; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + file static class GeneratedRouteBuilderExtensionsCore + { + + private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + [(@"TestMapActions.cs", 15)] = ( + (methodInfo, options) => + { + if (options == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + var result = handler(); + return httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + + }; + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, 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(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider + { + T.PopulateMetadata(method, builder); + } + + private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : 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); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 80ea89c6b40f..87fe135e619b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Text; using Microsoft.AspNetCore.Builder; @@ -36,7 +37,7 @@ internal static (ImmutableArray, Compilation) RunGenerator(s driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var _); var diagnostics = updatedCompilation.GetDiagnostics(); - Assert.Empty(diagnostics.Where(d => d.Severity > DiagnosticSeverity.Warning)); + Assert.Empty(diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning)); var runResult = driver.GetRunResult(); return (runResult.Results, updatedCompilation); @@ -172,6 +173,48 @@ public static IEndpointRouteBuilder MapTestEndpoints(this IEndpointRouteBuilder return compilation; } + internal async Task VerifyAgainstBaselineUsingFile(Compilation compilation, [CallerMemberName] string callerName = "") + { + var baselineFilePath = Path.Combine("RequestDelegateGenerator", "Baselines", $"{callerName}.generated.txt"); + var generatedCode = compilation.SyntaxTrees.Last(); + var baseline = await File.ReadAllTextAsync(baselineFilePath); + var expectedLines = baseline + .TrimEnd() // Trim newlines added by autoformat + .Replace("%GENERATEDCODEATTRIBUTE%", RequestDelegateGeneratorSources.GeneratedCodeAttribute) + .Split(Environment.NewLine); + + Assert.True(CompareLines(expectedLines, generatedCode.GetText(), out var errorMessage), errorMessage); + } + + private bool CompareLines(string[] expectedLines, SourceText sourceText, out string message) + { + if (expectedLines.Length != sourceText.Lines.Count) + { + message = $"Line numbers do not match. Expected: {expectedLines.Length} lines, but generated {sourceText.Lines.Count}"; + return false; + } + var index = 0; + foreach (var textLine in sourceText.Lines) + { + var expectedLine = expectedLines[index].Trim().ReplaceLineEndings(); + var actualLine = textLine.ToString().Trim().ReplaceLineEndings(); + if (!expectedLine.Equals(actualLine, StringComparison.Ordinal)) + { + message = $""" +Line {textLine.LineNumber} does not match. +Expected Line: +{expectedLine} +Actual Line: +{textLine} +"""; + return false; + } + index++; + } + message = string.Empty; + return true; + } + private sealed class AppLocalResolver : ICompilationAssemblyResolver { public bool TryResolveAssemblyPaths(CompilationLibrary library, List assemblies) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index 606624f589db..18db6f48d6bf 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -39,6 +39,42 @@ public async Task MapAction_NoParam_StringReturn(string source, string expectedB Assert.Equal(expectedBody, body); } + [Fact] + public async Task MapGet_NoParam_StringReturn_WithFilter() + { + var source = """ +app.MapGet("/hello", () => "Hello world!") + .AddEndpointFilter(async (context, next) => { + var result = await next(context); + return $"Filtered: {result}"; + }); +"""; + var expectedBody = "Filtered: Hello world!"; + var (results, compilation) = RunGenerator(source); + + await VerifyAgainstBaselineUsingFile(compilation); + + 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); + } + [Theory] [InlineData("""app.MapGet("/hello", () => 2);""")] [InlineData("""app.MapGet("/hello", () => new System.DateTime());""")] diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index 648356d1a304..a05c8006992a 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -439,7 +439,7 @@ private static RouteHandlerBuilder Map( .AddRouteHandler(pattern, handler, httpMethods, isFallback, RequestDelegateFactory.InferMetadata, RequestDelegateFactory.Create); } - private static RouteEndpointDataSource GetOrAddRouteEndpointDataSource(this IEndpointRouteBuilder endpoints) + internal static RouteEndpointDataSource GetOrAddRouteEndpointDataSource(this IEndpointRouteBuilder endpoints) { RouteEndpointDataSource? routeEndpointDataSource = null; diff --git a/src/Http/Routing/src/Builder/RouteHandlerServices.cs b/src/Http/Routing/src/Builder/RouteHandlerServices.cs new file mode 100644 index 000000000000..e3b700a70a6b --- /dev/null +++ b/src/Http/Routing/src/Builder/RouteHandlerServices.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; + +namespace Microsoft.AspNetCore.Routing; + +/// +/// Provides methods used for invoking the route endpoint +/// infrastructure with custom funcs for populating metadata +/// and creating request delegates. Intended to be consumed from +/// the RequestDeleatgeGenerator only. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class RouteHandlerServices +{ + /// + /// Registers an endpoint with custom functions for constructing + /// a request delegate for its handler and populating metadata for + /// the endpoint. Intended for consumption in the RequestDelegateGenerator. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// The set of supported HTTP methods. May not be null. + /// A delegate for populating endpoint metadata. + /// A delegate for constructing a RequestDelegate. + /// + public static RouteHandlerBuilder Map( + IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler, + IEnumerable httpMethods, + Func populateMetadata, + Func createRequestDelegate) + { + ArgumentNullException.ThrowIfNull(endpoints); + ArgumentNullException.ThrowIfNull(pattern); + ArgumentNullException.ThrowIfNull(handler); + ArgumentNullException.ThrowIfNull(populateMetadata); + ArgumentNullException.ThrowIfNull(createRequestDelegate); + + return endpoints + .GetOrAddRouteEndpointDataSource() + .AddRouteHandler(RoutePatternFactory.Parse(pattern), + handler, + httpMethods, + isFallback: false, + populateMetadata, + createRequestDelegate); + } +} diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..a552550b5e1c 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Routing.RouteHandlerServices +static Microsoft.AspNetCore.Routing.RouteHandlerServices.Map(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! handler, System.Collections.Generic.IEnumerable! httpMethods, System.Func! populateMetadata, System.Func! createRequestDelegate) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder!