2
0
Эх сурвалжийг харах

Add infrastructure for RequestDelegateGenerator (#45924)

* Add infrastructure for RequestDelegateGenerator

* Opt for disabling generator via MSBuild

* Address feedback from review

* Don't emit sources if no endpoints found

* Address feedback from review

* Address feedback and consolidate emitted source

* Support named MapAction method parameters

* Fix up indentation

* Rename project and add GeneratedCodeAttributes

* Implement IEquatable on static model elements

* Use records for model

* Remove RoutePatternParameter list
Safia Abdalla 3 жил өмнө
parent
commit
b110c980c0

+ 19 - 0
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.Generators", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.Generators.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}

+ 5 - 0
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" />
 
+    <ProjectReference Include="$(RepoRoot)src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.Generators.csproj"
+      Private="false"
+      ReferenceOutputAssembly="false" />
+
     <!-- Enforce build order. Targeting pack needs to bundle analyzers and information about the runtime. -->
     <ProjectReference Include="..\..\App.Runtime\src\Microsoft.AspNetCore.App.Runtime.csproj"
       Private="false"
@@ -176,6 +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.Generators\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Http.Generators.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
 
       <_InitialRefPackContent Include="@(AspNetCoreReferenceAssemblyPath)" PackagePath="$(RefAssemblyPackagePath)" />
       <_InitialRefPackContent Include="@(AspNetCoreReferenceDocXml)" PackagePath="$(RefAssemblyPackagePath)" />

+ 28 - 0
src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj

@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+    <IsPackable>false</IsPackable>
+    <IsAnalyzersProject>true</IsAnalyzersProject>
+    <AddPublicApiAnalyzers>false</AddPublicApiAnalyzers>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
+    <Reference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <InternalsVisibleTo Include="Microsoft.AspNetCore.Http.Extensions.Tests" />
+  </ItemGroup>
+  
+  <ItemGroup>
+    <Compile Include="$(SharedSourceRoot)IsExternalInit.cs" LinkBase="Shared" />
+  </ItemGroup>
+
+</Project>

+ 117 - 0
src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs

@@ -0,0 +1,117 @@
+// 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.Generators.StaticRouteHandlerModel;
+
+namespace Microsoft.AspNetCore.Http.Generators;
+
+[Generator]
+public sealed class RequestDelegateGenerator : IIncrementalGenerator
+{
+    private static readonly string[] _knownMethods =
+    {
+        "MapGet",
+        "MapPost",
+        "MapPut",
+        "MapDelete",
+        "MapPatch",
+        "Map",
+    };
+
+    public void Initialize(IncrementalGeneratorInitializationContext context)
+    {
+        var endpoints = 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) =>
+            {
+                var operation = context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation;
+                return StaticRouteHandlerModelParser.GetEndpointFromOperation(operation);
+            })
+            .Where(endpoint => endpoint.Response.ResponseType == "string")
+            .WithTrackingName("EndpointModel");
+
+        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<object?>(Results.Empty);
+                        }
+                        {{StaticRouteHandlerModelEmitter.EmitFilteredInvocation()}}
+                    },
+                    builder,
+                    handler.Method);
+                }
+
+                {{StaticRouteHandlerModelEmitter.EmitRequestHandler()}}
+                {{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}}
+
+                return filteredInvocation is null ? RequestHandler : RequestHandlerFiltered;
+            }),
+""");
+
+        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 MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber);
+    }
+""");
+
+        var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect());
+
+        context.RegisterSourceOutput(thunksAndEndpoints, (context, sources) =>
+        {
+            var (thunks, endpoints) = sources;
+
+            var endpointsCode = new StringBuilder();
+            var thunksCode = new StringBuilder();
+            foreach (var endpoint in endpoints)
+            {
+                endpointsCode.AppendLine(endpoint);
+            }
+            foreach (var thunk in thunks)
+            {
+                thunksCode.AppendLine(thunk);
+            }
+
+            var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource(
+                genericThunks: string.Empty,
+                thunks: thunksCode.ToString(),
+                endpoints: endpointsCode.ToString());
+            context.AddSource("GeneratedRouteBuilderExtensions.g.cs", code);
+        });
+    }
+}

+ 401 - 0
src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs

@@ -0,0 +1,401 @@
+// 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.Generators;
+
+internal static class RequestDelegateGeneratorSources
+{
+    private const string SourceHeader = """
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+#nullable enable
+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<System.Delegate, Microsoft.AspNetCore.Builder.EndpointBuilder>;
+using RequestDelegateFactoryFunc = System.Func<System.Delegate, Microsoft.AspNetCore.Builder.EndpointBuilder, Microsoft.AspNetCore.Http.RequestDelegate>;
+""";
+
+    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; }
+        public int Line { get; init; }
+
+        public SourceKey(string path, int line)
+        {
+            Path = path;
+            Line = line;
+        }
+    }
+}
+
+{{GeneratedCodeAttribute}}
+internal static 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 class GenericThunks<T>
+    {
+        public static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
+        {
+            {{genericThunks}}
+        };
+    }
+
+    private static readonly global::System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
+    {
+        {{thunks}}
+    };
+
+    {{endpoints}}
+
+    private static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore<T>(
+        this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes,
+        string pattern,
+        System.Delegate handler,
+        IEnumerable<string> httpMethods,
+        string filePath,
+        int lineNumber)
+    {
+        var (populate, factory) = GenericThunks<T>.map[(filePath, lineNumber)];
+        return GetOrAddRouteEndpointDataSource(routes).AddRouteHandler(RoutePatternFactory.Parse(pattern), handler, httpMethods, isFallback: false, populate, factory);
+    }
+
+    private static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore(
+        this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder routes,
+        string pattern,
+        System.Delegate handler,
+        IEnumerable<string> 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, 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--)
+        {
+            var filterFactory = routeHandlerFilters[i];
+            filteredInvocation = filterFactory(context0, filteredInvocation);
+        }
+        return filteredInvocation;
+    }
+
+    private static void PopulateMetadata<T>(System.Reflection.MethodInfo method, EndpointBuilder builder) where T : Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider
+    {
+        T.PopulateMetadata(method, builder);
+    }
+
+    private static void PopulateMetadata<T>(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);
+        }
+    }
+
+    {{GeneratedCodeAttribute}}
+    private sealed class SourceGeneratedRouteEndpointDataSource : EndpointDataSource
+    {
+        private readonly List<RouteEntry> _routeEntries = new();
+        private readonly IServiceProvider _applicationServices;
+
+        public SourceGeneratedRouteEndpointDataSource(IServiceProvider applicationServices)
+        {
+            _applicationServices = applicationServices;
+        }
+
+        public RouteHandlerBuilder AddRouteHandler(
+            RoutePattern pattern,
+            Delegate routeHandler,
+            IEnumerable<string> 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<RouteEndpoint> 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<RouteEndpoint> 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<Action<EndpointBuilder>>? groupConventions = null, IReadOnlyList<Action<EndpointBuilder>>? 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 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<string> 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<Action<EndpointBuilder>> in RouteEndpointBuilder where only Add is called.
+        private sealed class ThrowOnAddAfterEndpointBuiltConventionCollection : List<Action<EndpointBuilder>>, ICollection<Action<EndpointBuilder>>
+        {
+            // 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<Action<EndpointBuilder>>.Add(Action<EndpointBuilder> convention)
+            {
+                if (IsReadOnly)
+                {
+                    throw new InvalidOperationException("Resources.RouteEndpointDataSource_ConventionsCannotBeModifiedAfterBuild");
+                }
+                Add(convention);
+            }
+        }
+
+        private sealed class ConventionBuilder : IEndpointConventionBuilder
+        {
+            private readonly ICollection<Action<EndpointBuilder>> _conventions;
+            private readonly ICollection<Action<EndpointBuilder>> _finallyConventions;
+            public ConventionBuilder(ICollection<Action<EndpointBuilder>> conventions, ICollection<Action<EndpointBuilder>> finallyConventions)
+            {
+                _conventions = conventions;
+                _finallyConventions = finallyConventions;
+            }
+            /// <summary>
+            /// Adds the specified convention to the builder. Conventions are used to customize <see cref="EndpointBuilder"/> instances.
+            /// </summary>
+            /// <param name="convention">The convention to add to the builder.</param>
+            public void Add(Action<EndpointBuilder> convention)
+            {
+                _conventions.Add(convention);
+            }
+            public void Finally(Action<EndpointBuilder> finalConvention)
+            {
+                _finallyConventions.Add(finalConvention);
+            }
+        }
+    }
+}
+""";
+}

+ 86 - 0
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.Generators.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<string, int>
+     * ```
+     *
+     * Where `string` and `int` represent parameter types. For handlers
+     * that do return a value, `System.Func<string, int, string>` 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<object?>(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<object?>(Results.Empty);
+     * ```
+     */
+    public static string EmitFilteredInvocation()
+    {
+        return "return System.Threading.Tasks.ValueTask.FromResult<object?>(handler());";
+    }
+}

+ 121 - 0
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs

@@ -0,0 +1,121 @@
+// 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.Generators.StaticRouteHandlerModel;
+
+internal static class StaticRouteHandlerModelParser
+{
+    private const int RoutePatternArgumentOrdinal = 1;
+    private const int RouteHandlerArgumentOrdinal = 2;
+
+    private static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern)
+    {
+        return new EndpointRoute(routePattern.ValueText);
+    }
+
+    private static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method)
+    {
+        return new EndpointResponse(method.ReturnType.ToString(), "plain/text");
+    }
+
+    public static Endpoint GetEndpointFromOperation(IInvocationOperation operation)
+    {
+        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);
+
+        var invocationExpression = (InvocationExpressionSyntax)operation.Syntax;
+        var httpMethod = ((IdentifierNameSyntax)((MemberAccessExpressionSyntax)invocationExpression.Expression).Name).Identifier.ValueText;
+
+        return new Endpoint(httpMethod,
+            GetEndpointRouteFromArgument(routeToken),
+            GetEndpointResponseFromMethod(method),
+            (filePath, span.EndLinePosition.Line + 1));
+    }
+
+    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)
+    {
+        foreach (var argument in invocation.Arguments)
+        {
+            if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal)
+            {
+                method = ResolveMethodFromOperation(argument);
+                return true;
+            }
+        }
+        method = null;
+        return false;
+    }
+
+    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,
+        IParenthesizedOperation parenthesized => ResolveMethodFromOperation(parenthesized.Operand),
+        _ => 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;
+    }
+}

+ 20 - 0
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs

@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel;
+
+internal enum RequestParameterSource
+{
+    Query,
+    Route,
+    Header,
+    Form,
+    Service,
+    BodyOrService,
+}
+
+internal record RequestParameter(string Name, string Type, RequestParameterSource Source, bool IsOptional, object? DefaultValue);
+internal record EndpointRoute(string RoutePattern);
+internal record EndpointResponse(string ResponseType, string ContentType);
+internal record Endpoint(string HttpMethod, EndpointRoute Route, EndpointResponse Response, (string, int) Location);

+ 8 - 1
src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj

@@ -4,6 +4,7 @@
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
     <!-- This is needed to support codepaths that use NullabilityInfoContext. -->
     <Features>$(Features.Replace('nullablePublicOnly', '')</Features>
+    <PreserveCompilationContext>true</PreserveCompilationContext>
   </PropertyGroup>
 
   <ItemGroup>
@@ -11,10 +12,16 @@
   </ItemGroup>
 
   <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore" />
     <Reference Include="Microsoft.AspNetCore.Http" />
     <Reference Include="Microsoft.AspNetCore.Http.Results" />
     <Reference Include="Microsoft.AspNetCore.Http.Extensions" />
+    <Reference Include="Microsoft.CodeAnalysis.CSharp" />
     <Reference Include="Microsoft.Extensions.DependencyInjection" />
+    <Reference Include="Microsoft.Extensions.DependencyModel" />
+  </ItemGroup>
+  
+  <ItemGroup>
+    <ProjectReference Include="..\gen\Microsoft.AspNetCore.Http.Generators.csproj" />
   </ItemGroup>
-
 </Project>

+ 235 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs

@@ -0,0 +1,235 @@
+// 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.Reflection;
+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;
+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.Generators.Tests;
+
+public class RequestDelegateGeneratorTestBase
+{
+    internal static (ImmutableArray<GeneratorRunResult>, Compilation) RunGenerator(string sources)
+    {
+        var compilation = CreateCompilation(sources);
+        var generator = new RequestDelegateGenerator().AsSourceGenerator();
+
+        // Enable the source generator in tests
+        GeneratorDriver driver = CSharpGeneratorDriver.Create(generators: new[]
+            {
+                generator
+            },
+            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<GeneratorRunResult> results, string stepName)
+    {
+        // 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<StaticRouteHandlerModel.Endpoint>(staticEndpoint);
+            return endpoint;
+        }
+        return null;
+    }
+
+    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<EmbeddedText>();
+
+        // 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<Func<IEndpointRouteBuilder, IEndpointRouteBuilder>>();
+        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 = $$"""
+#nullable enable
+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<PortableExecutableReference>();
+        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<string> 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 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<EndpointDataSource>();
+        }
+
+        private IApplicationBuilder ApplicationBuilder { get; }
+
+        public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New();
+
+        public ICollection<EndpointDataSource> DataSources { get; }
+
+        public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices;
+    }
+}

+ 52 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.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.
+
+namespace Microsoft.AspNetCore.Http.Generators.Tests;
+
+public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase
+{
+    [Theory]
+    [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "Hello world!")]
+    [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);
+
+        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());""")]
+    public void MapGet_UnsupportedSignature_DoesNotEmit(string source)
+    {
+        var (results, compilation) = RunGenerator(source);
+
+        var endpointModel = GetStaticEndpoint(results, "EndpointModel");
+        Assert.Null(endpointModel);
+    }
+}

+ 5 - 4
src/Http/HttpAbstractions.slnf

@@ -20,6 +20,7 @@
       "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\\test\\Microsoft.AspNetCore.Http.Extensions.Tests.csproj",
       "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
@@ -37,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",
@@ -61,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",