File: RouteEndpointDataSource.cs
Web Access
Project: src\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.AspNetCore.Routing;
 
internal sealed class RouteEndpointDataSource : EndpointDataSource
{
    private readonly List<RouteEntry> _routeEntries = new();
    private readonly IServiceProvider _applicationServices;
    private readonly bool _throwOnBadRequest;
 
    public RouteEndpointDataSource(IServiceProvider applicationServices, bool throwOnBadRequest)
    {
        _applicationServices = applicationServices;
        _throwOnBadRequest = throwOnBadRequest;
    }
 
    public RouteHandlerBuilder AddRequestDelegate(
        RoutePattern pattern,
        RequestDelegate requestDelegate,
        IEnumerable<string>? httpMethods,
        Func<Delegate, RequestDelegateFactoryOptions, RequestDelegateMetadataResult?, RequestDelegateResult> createHandlerRequestDelegateFunc,
        MethodInfo methodInfo)
    {
        var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection();
        var finallyConventions = new ThrowOnAddAfterEndpointBuiltConventionCollection();
 
        _routeEntries.Add(new()
        {
            RoutePattern = pattern,
            RouteHandler = requestDelegate,
            HttpMethods = httpMethods,
            RouteAttributes = RouteAttributes.None,
            Conventions = conventions,
            FinallyConventions = finallyConventions,
            InferMetadataFunc = null, // Metadata isn't infered from RequestDelegate endpoints
            CreateHandlerRequestDelegateFunc = createHandlerRequestDelegateFunc,
            Method = methodInfo // MethodInfo needed to resolve attributes for RequestDelegate endpoints
        });
 
        return new RouteHandlerBuilder(conventions, finallyConventions);
    }
 
    public RouteHandlerBuilder AddRouteHandler(
        RoutePattern pattern,
        Delegate routeHandler,
        IEnumerable<string>? httpMethods,
        bool isFallback,
        Func<MethodInfo, RequestDelegateFactoryOptions?, RequestDelegateMetadataResult>? inferMetadataFunc,
        Func<Delegate, RequestDelegateFactoryOptions, RequestDelegateMetadataResult?, RequestDelegateResult> createHandlerRequestDelegateFunc,
        MethodInfo methodInfo)
    {
        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,
            InferMetadataFunc = inferMetadataFunc,
            CreateHandlerRequestDelegateFunc = createHandlerRequestDelegateFunc,
            Method = methodInfo
        });
 
        return new RouteHandlerBuilder(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;
 
    // For testing
    internal RouteEndpointBuilder GetSingleRouteEndpointBuilder()
    {
        if (_routeEntries.Count is not 1)
        {
            throw new InvalidOperationException($"There are {_routeEntries.Count} endpoints defined! This can only be called for a single endpoint.");
        }
 
        return CreateRouteEndpointBuilder(_routeEntries[0]);
    }
 
    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 methodInfo = entry.Method;
        var isRouteHandler = (entry.RouteAttributes & RouteAttributes.RouteHandler) == RouteAttributes.RouteHandler;
        var isFallback = (entry.RouteAttributes & RouteAttributes.Fallback) == RouteAttributes.Fallback;
 
        // The Map methods don't support customizing the order apart from using int.MaxValue to give MapFallback the lowest priority.
        // Otherwise, we always use the default of 0 unless a convention changes it later.
        var order = isFallback ? int.MaxValue : 0;
        var displayName = pattern.DebuggerToString();
 
        // Don't include the method name for non-route-handlers because the name is just "Invoke" when built from
        // ApplicationBuilder.Build(). This was observed in MapSignalRTests and is not very useful. Maybe if we come up
        // with a better heuristic for what a useful method name is, we could use it for everything. Inline lambdas are
        // compiler generated methods so they are filtered out even for route handlers.
        if (isRouteHandler && TypeHelper.TryGetNonCompilerGeneratedMethodName(methodInfo, out var methodName))
        {
            displayName = $"{displayName} => {methodName}";
        }
 
        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 = isRouteHandler ? null : (RequestDelegate)entry.RouteHandler;
 
        // 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 (isFallback)
        {
            builder.Metadata.Add(FallbackMetadata.Instance);
        }
 
        if (isRouteHandler)
        {
            builder.Metadata.Add(methodInfo);
        }
 
        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);
            }
        }
 
        RequestDelegateFactoryOptions? rdfOptions = null;
        RequestDelegateMetadataResult? rdfMetadataResult = null;
 
        // 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)
        {
            Debug.Assert(entry.InferMetadataFunc != null, "A func to infer metadata must be provided for route handlers.");
 
            rdfOptions = CreateRdfOptions(entry, pattern, builder);
            rdfMetadataResult = entry.InferMetadataFunc(methodInfo, rdfOptions);
        }
 
        // Add delegate attributes as metadata before entry-specific conventions but after group conventions.
        var attributes = entry.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)
        {
            rdfOptions ??= CreateRdfOptions(entry, pattern, builder);
 
            // We ignore the returned EndpointMetadata has been already populated since we passed in non-null EndpointMetadata.
            // We always set factoryRequestDelegate in case something is still referencing the redirected version of the RequestDelegate.
            factoryCreatedRequestDelegate = entry.CreateHandlerRequestDelegateFunc(entry.RouteHandler, rdfOptions, rdfMetadataResult).RequestDelegate;
        }
 
        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 RequestDelegateFactoryOptions CreateRdfOptions(RouteEntry entry, RoutePattern pattern, RouteEndpointBuilder builder)
    {
        return new()
        {
            ServiceProvider = _applicationServices,
            RouteParameterNames = ProduceRouteParamNames(),
            ThrowOnBadRequest = _throwOnBadRequest,
            DisableInferBodyFromParameters = ShouldDisableInferredBodyParameters(entry.HttpMethods),
            EndpointBuilder = builder,
        };
 
        IEnumerable<string> ProduceRouteParamNames()
        {
            foreach (var routePatternPart in pattern.Parameters)
            {
                yield return routePatternPart.Name;
            }
        }
    }
 
    private static bool ShouldDisableInferredBodyParameters(IEnumerable<string>? httpMethods)
    {
        static bool ShouldDisableInferredBodyForMethod(string method) =>
            // GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies
            method.Equals(HttpMethods.Get, StringComparison.Ordinal) ||
            method.Equals(HttpMethods.Delete, StringComparison.Ordinal) ||
            method.Equals(HttpMethods.Head, StringComparison.Ordinal) ||
            method.Equals(HttpMethods.Options, StringComparison.Ordinal) ||
            method.Equals(HttpMethods.Trace, StringComparison.Ordinal) ||
            method.Equals(HttpMethods.Connect, StringComparison.Ordinal);
 
        // If the endpoint accepts any kind of request, we should still infer parameters can come from the body.
        if (httpMethods is null)
        {
            return false;
        }
 
        foreach (var method in httpMethods)
        {
            if (ShouldDisableInferredBodyForMethod(method))
            {
                // If the route handler was mapped explicitly to handle an HTTP method that does not normally have a request body,
                // we assume any invocation of the handler will not have a request body no matter what other HTTP methods it may support.
                return true;
            }
        }
 
        return false;
    }
 
    private readonly struct RouteEntry
    {
        public required RoutePattern RoutePattern { get; init; }
        public required Delegate RouteHandler { get; init; }
        public required IEnumerable<string>? HttpMethods { get; init; }
        public required RouteAttributes RouteAttributes { get; init; }
        public required ThrowOnAddAfterEndpointBuiltConventionCollection Conventions { get; init; }
        public required ThrowOnAddAfterEndpointBuiltConventionCollection FinallyConventions { get; init; }
        public required Func<MethodInfo, RequestDelegateFactoryOptions?, RequestDelegateMetadataResult>? InferMetadataFunc { get; init; }
        public required Func<Delegate, RequestDelegateFactoryOptions, RequestDelegateMetadataResult?, RequestDelegateResult> CreateHandlerRequestDelegateFunc { get; init; }
        public required MethodInfo Method { 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);
        }
    }
}