Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate route handler-endpoints at compile time via source generator #45524

Closed
5 of 8 tasks
captainsafia opened this issue Dec 9, 2022 · 9 comments · Fixed by #46291
Closed
5 of 8 tasks

Generate route handler-endpoints at compile time via source generator #45524

captainsafia opened this issue Dec 9, 2022 · 9 comments · Fixed by #46291
Assignees
Labels
area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Milestone

Comments

@captainsafia
Copy link
Member

captainsafia commented Dec 9, 2022

Currently, RequestDelegates for associated route handler-based endpoints are generated at runtime using LINQ expressions. This is a process that requires a fair amount of reflection. We can leverage source generation to generate these endpoints via static analysis.

Some of this work has been prototyped in uController and will need to be formalized as part of the official project.

Generator Basics

  • Add RequestDelegateGenerator scaffold to aspnetcore repo
  • Respect EnableRequestDelegateGenerator flag in Web SDK
  • Refactor code to support marking GeneratedRouteBuilderExtensions as file class
  • Set up code and tests for emitting diagnostics from the generator
  • Support for a RouteEndpointDataSource implementation that supports a func for populating metadata
  • Support for an RouteEndpointDataSource implementation that allows customizing what RDF is called to produce filtered delegate calls

Invoking Handlers

  • Support generating correct delegate type signature for a given handler
  • Support emitting correct generic-EndpointFilterInvocationContext for a given filtered invocation
@captainsafia captainsafia added the area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Dec 9, 2022
@captainsafia captainsafia self-assigned this Dec 9, 2022
@captainsafia captainsafia added this to the .NET 8 Planning milestone Dec 13, 2022
@ghost
Copy link

ghost commented Dec 13, 2022

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@captainsafia
Copy link
Member Author

For the curious, here's an example of what the code emitted by the generator looks like:

Generated Code
//------------------------------------------------------------------------------
// <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 System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.IO;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using MetadataPopulator = System.Action<System.Delegate, Microsoft.AspNetCore.Builder.EndpointBuilder>;
using RequestDelegateFactoryFunc = System.Func<System.Delegate, Microsoft.AspNetCore.Builder.EndpointBuilder, Microsoft.AspNetCore.Http.RequestDelegate>;

namespace Microsoft.AspNetCore.Builder
{
    internal class SourceKey
    {
        public string Path { get; init; }
        public int Line { get; init; }

        public SourceKey(string path, int line)
        {
            Path = path;
            Line = line;
        }
    }
}


internal static 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 System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
        {
            
        };
    }

    private static readonly System.Collections.Generic.Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new()
    {
            [("TestMapActions.cs", 15)] = (
           (del, builder) =>
            {
                builder.Metadata.Add(new SourceKey("TestMapActions.cs", 15));
            },
            (del, builder) =>
            {
                var handler = (System.Func<string>)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);
                        }
                        return System.Threading.Tasks.ValueTask.FromResult<object?>(handler());
                    },
                    builder,
                    handler.Method);
                }

                System.Threading.Tasks.Task RequestHandler(Microsoft.AspNetCore.Http.HttpContext httpContext)
                {
                        var result = handler();
                        return httpContext.Response.WriteAsync(result);
                }
                async System.Threading.Tasks.Task RequestHandlerFiltered(Microsoft.AspNetCore.Http.HttpContext httpContext)
                {
                    var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext));
                    await ExecuteObjectResult(result, httpContext);
                }

                return filteredInvocation is null ? RequestHandler : RequestHandlerFiltered;
            }),

    };

    internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapPut(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
        [System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, 
        System.Func<string> handler,
        [System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
        [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)
    {
    return MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber);
    }

    private static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore<T>(
        this 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 Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapCore(
        this 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, 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);
        }
    }

    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);
            }
        }
    }
}

The entrypoint from user code is the implementation of the strongly-typed MapAction overloads.

    internal static Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapPut(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
        [System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, 
        System.Func<string> handler,
        [System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
        [System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)

These are emitted by the source generator when analyzing each MapAction invocation in the source code. Since the handler type is more strongly-typed to the handler provided by the user, this MapPut overload will be selected over the MapPut overload defined in the framework which expects a Delegate handler.

The method body of these strongly-typed MapAction overloads does a lookup into a Dictionary generated by the source generator that includes two pieces of information:

  1. A action that takes an EndpointBuilder and populates it with metadata
  2. A func that takes an EndpointBuilder and produces either a filtered or unfiltered handler invocation depending on whether or not the build has FilterFactories registered

In 2, we also emit a RequestHandler which implements the RequestDelegate interface and incapsulates the logic of parameter binding, response writing, etc. We also emit a FilteredRequestHandler which invokes the constructed filter pipeline.

Both the functionalities above are supported by the SourceGeneratedRouteEndpointDataSource exists to provide the functionality that is mentioned here:

There's also some changes that need to be made in-framework to support the functionality of the API:

Support for a RouteEndpointDataSource implementation that supports a func for populating metadata
Support for an RouteEndpointDataSource implementation that allows customizing what RDF is called to produce filtered delegate calls

It takes the delegates defined in #1 and #2 and plugs them into the endpoint construction that happens as part of the RouteEndpointDataSource. If we include these features in framework, we can remove this custom implementation from the emitted source code and rely on that.

@Youssef1313
Copy link
Member

  • Fix up references to Microsoft.CodeAnalysis.Analyzers to avoid extra project flag

Note: EnforceExtendedAnalyzerRules isn't extra. I think the pulled Microsoft.CodeAnalysis.Analyzers isn't the latest version. That's why you might not be encountering dotnet/roslyn-analyzers#6229 yet. That will also mean that EnforceExtendedAnalyzerRules has no effect currently. That's mostly a guess though.

@Youssef1313
Copy link
Member

Fix for dotnet/roslyn-analyzers#6229 is merged now. You can still include an explicit reference if you want to use latest previews, but otherwise, it should be good to have the transitive dependency.

@captainsafia
Copy link
Member Author

A scenario to watch out for emerged in our benchmark tests:

C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\qlx1l4on.3jn\sdk\8.0.100-alpha.1.23068.3\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(287,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\MapAction.csproj]
C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\Microsoft.AspNetCore.Http.Generators\Microsoft.AspNetCore.Http.Generators.RequestDelegateGenerator\GeneratedRouteBuilderExtensions.g.cs(69,66): error CS0111: Type 'GenerateRouteBuilderEndpoints' already defines a member called 'MapGet' with the same parameter types [C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\MapAction.csproj]
C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\Microsoft.AspNetCore.Http.Generators\Microsoft.AspNetCore.Http.Generators.RequestDelegateGenerator\GeneratedRouteBuilderExtensions.g.cs(79,66): error CS0111: Type 'GenerateRouteBuilderEndpoints' already defines a member called 'MapGet' with the same parameter types [C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\MapAction.csproj]
C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\Microsoft.AspNetCore.Http.Generators\Microsoft.AspNetCore.Http.Generators.RequestDelegateGenerator\GeneratedRouteBuilderExtensions.g.cs(89,66): error CS0111: Type 'GenerateRouteBuilderEndpoints' already defines a member called 'MapGet' with the same parameter types [C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\MapAction.csproj]
C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\Microsoft.AspNetCore.Http.Generators\Microsoft.AspNetCore.Http.Generators.RequestDelegateGenerator\GeneratedRouteBuilderExtensions.g.cs(99,66): error CS0111: Type 'GenerateRouteBuilderEndpoints' already defines a member called 'MapGet' with the same parameter types [C:\Users\Administrator\AppData\Local\Temp\benchmarks-agent\benchmarks-server-7380\fsz2qhvz.c2h\Benchmarks\src\BenchmarksApps\MapAction\MapAction.csproj]

We should de-dupe endpoints that evaluate to the same delegate signature.

@voroninp
Copy link
Contributor

voroninp commented Jan 22, 2023

Would be nice if SG could generate some static class containing paths to all endpoints.

For example, this

[Route("user")]
public class UserController
{
    [HttpPost("sign-in")]
    public Task<IResult> SignIn(...) { ... }
}

should lead to something like:

public static class Endpoints
{
    public static class User
    {
        public const string Path = "/user";
        public const string SignIn = "sign-in";
        public const string SignInPath = $"Path/{SignIn}";
    }
}

@captainsafia
Copy link
Member Author

Would be nice if SG could generate some static class containing paths to all endpoints.

What's the motivation for having this? Largely as a debugging tool to inspect all the endpoints in an app or are you wanting to use these strings somewhere?

@voroninp
Copy link
Contributor

For integration tests with Refit, or for redirects when developing (no reverse proxy at this point)

@DamianEdwards
Copy link
Member

That's an idea we've discussed in some other contexts recently but deserves its own issue. We'd likely want it to discover more than just routes created via minimal APIs (e.g. Razor Pages, MVC, etc.).

@captainsafia captainsafia moved this to In Progress in [.NET 8] Web Frameworks Jan 30, 2023
@captainsafia captainsafia moved this from In Progress to Done in [.NET 8] Web Frameworks Feb 3, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Mar 5, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Projects
No open projects
Status: Done
Development

Successfully merging a pull request may close this issue.

5 participants