diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index bcbe36c63524..22e82eb81590 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; @@ -152,10 +150,7 @@ public static IEndpointConventionBuilder MapMethods( { ArgumentNullException.ThrowIfNull(httpMethods); - var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate); - builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}"); - builder.WithMetadata(new HttpMethodMetadata(httpMethods)); - return builder; + return endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate, httpMethods); } /// @@ -186,41 +181,21 @@ public static IEndpointConventionBuilder Map( this IEndpointRouteBuilder endpoints, RoutePattern pattern, RequestDelegate requestDelegate) + { + return Map(endpoints, pattern, requestDelegate, httpMethods: null); + } + + private static IEndpointConventionBuilder Map( + this IEndpointRouteBuilder endpoints, + RoutePattern pattern, + RequestDelegate requestDelegate, + IEnumerable? httpMethods) { ArgumentNullException.ThrowIfNull(endpoints); ArgumentNullException.ThrowIfNull(pattern); ArgumentNullException.ThrowIfNull(requestDelegate); - const int defaultOrder = 0; - - var builder = new RouteEndpointBuilder( - requestDelegate, - pattern, - defaultOrder) - { - DisplayName = pattern.RawText ?? pattern.DebuggerToString(), - }; - - // Add delegate attributes as metadata - var attributes = requestDelegate.Method.GetCustomAttributes(); - - // This can be null if the delegate is a dynamic method or compiled from an expression tree - if (attributes != null) - { - foreach (var attribute in attributes) - { - builder.Metadata.Add(attribute); - } - } - - var dataSource = endpoints.DataSources.OfType().FirstOrDefault(); - if (dataSource == null) - { - dataSource = new ModelEndpointDataSource(); - endpoints.DataSources.Add(dataSource); - } - - return dataSource.AddEndpointBuilder(builder); + return endpoints.GetOrAddRouteEndpointDataSource().AddRequestDelegate(pattern, requestDelegate, httpMethods); } /// @@ -429,18 +404,38 @@ private static RouteHandlerBuilder Map( ArgumentNullException.ThrowIfNull(pattern); ArgumentNullException.ThrowIfNull(handler); - var dataSource = endpoints.DataSources.OfType().FirstOrDefault(); - if (dataSource is null) + return endpoints.GetOrAddRouteEndpointDataSource().AddRouteHandler(pattern, handler, httpMethods, isFallback); + } + + private static RouteEndpointDataSource GetOrAddRouteEndpointDataSource(this IEndpointRouteBuilder endpoints) + { + RouteEndpointDataSource? routeEndpointDataSource = null; + + foreach (var dataSource in endpoints.DataSources) { - var routeHandlerOptions = endpoints.ServiceProvider.GetService>(); + if (dataSource is RouteEndpointDataSource foundDataSource) + { + routeEndpointDataSource = foundDataSource; + break; + } + } + + if (routeEndpointDataSource is null) + { + // ServiceProvider isn't nullable, but it is being called by methods that historically did not access this property, so we null check anyway. + var routeHandlerOptions = endpoints.ServiceProvider?.GetService>(); var throwOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false; - dataSource = new RouteEndpointDataSource(endpoints.ServiceProvider, throwOnBadRequest); - endpoints.DataSources.Add(dataSource); + routeEndpointDataSource = new RouteEndpointDataSource(endpoints.ServiceProvider ?? EmptyServiceProvider.Instance, throwOnBadRequest); + endpoints.DataSources.Add(routeEndpointDataSource); } - var conventions = dataSource.AddEndpoint(pattern, handler, httpMethods, isFallback); + return routeEndpointDataSource; + } - return new RouteHandlerBuilder(conventions); + private sealed class EmptyServiceProvider : IServiceProvider + { + public static EmptyServiceProvider Instance { get; } = new EmptyServiceProvider(); + public object? GetService(Type serviceType) => null; } } diff --git a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs index 3910e4a2bd64..3000b1ae8a46 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs @@ -15,7 +15,7 @@ public sealed class RouteHandlerBuilder : IEndpointConventionBuilder /// /// Instantiates a new given a ThrowOnAddAfterEndpointBuiltConventionCollection from - /// . + /// . /// /// The convention list returned from . internal RouteHandlerBuilder(ICollection> conventions) diff --git a/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs b/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs index 1341c9bb5f1a..5b38606d58e7 100644 --- a/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs +++ b/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs @@ -8,16 +8,16 @@ namespace Microsoft.AspNetCore.Routing; internal sealed class ConfigureRouteHandlerOptions : IConfigureOptions { - private readonly IHostEnvironment _environment; + private readonly IHostEnvironment? _environment; - public ConfigureRouteHandlerOptions(IHostEnvironment environment) + public ConfigureRouteHandlerOptions(IHostEnvironment? environment = null) { _environment = environment; } public void Configure(RouteHandlerOptions options) { - if (_environment.IsDevelopment()) + if (_environment?.IsDevelopment() ?? false) { options.ThrowOnBadRequest = true; } diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 0e1f47d2ab67..1351c5a7cc8a 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -27,7 +27,6 @@ - diff --git a/src/Http/Routing/src/ModelEndpointDataSource.cs b/src/Http/Routing/src/ModelEndpointDataSource.cs deleted file mode 100644 index df36e6756bda..000000000000 --- a/src/Http/Routing/src/ModelEndpointDataSource.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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 Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Routing; - -internal sealed class ModelEndpointDataSource : EndpointDataSource -{ - private readonly List _endpointConventionBuilders; - - public ModelEndpointDataSource() - { - _endpointConventionBuilders = new List(); - } - - public IEndpointConventionBuilder AddEndpointBuilder(EndpointBuilder endpointBuilder) - { - var builder = new DefaultEndpointConventionBuilder(endpointBuilder); - _endpointConventionBuilders.Add(builder); - - return builder; - } - - public override IChangeToken GetChangeToken() - { - return NullChangeToken.Singleton; - } - - public override IReadOnlyList Endpoints => _endpointConventionBuilders.Select(e => e.Build()).ToArray(); - - // for testing - internal IEnumerable EndpointBuilders => _endpointConventionBuilders.Select(b => b.EndpointBuilder); -} diff --git a/src/Http/Routing/src/RouteEndpointBuilder.cs b/src/Http/Routing/src/RouteEndpointBuilder.cs index b4c3e1e03468..2660c24bb261 100644 --- a/src/Http/Routing/src/RouteEndpointBuilder.cs +++ b/src/Http/Routing/src/RouteEndpointBuilder.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; @@ -44,8 +43,6 @@ public RouteEndpointBuilder( } /// - [UnconditionalSuppressMessage("Trimmer", "IL2026", - Justification = "We surface a RequireUnreferencedCode in AddEndpointFilter which is required to call unreferenced code here. The trimmer is unable to infer this.")] public override Endpoint Build() { if (RequestDelegate is null) @@ -53,35 +50,11 @@ public override Endpoint Build() throw new InvalidOperationException($"{nameof(RequestDelegate)} must be specified to construct a {nameof(RouteEndpoint)}."); } - var requestDelegate = RequestDelegate; - - // Only replace the RequestDelegate if filters have been applied to this builder and they were not already handled by RouteEndpointDataSource. - // This affects other data sources like DefaultEndpointDataSource (this is people manually newing up a data source with a list of Endpoints), - // ModelEndpointDataSource (Map(RoutePattern, RequestDelegate) and by extension MapHub, MapHealthChecks, etc...), - // ActionEndpointDataSourceBase (MapControllers, MapRazorPages, etc...) and people with custom data sources or otherwise manually building endpoints - // using this type. At the moment this class is sealed, so at the moment we do not need to concern ourselves with what derived types may be doing. - if (EndpointFilterFactories is { Count: > 0 }) - { - // Even with filters applied, RDF.Create() will return back the exact same RequestDelegate instance we pass in if filters decide not to modify the - // invocation pipeline. We're just passing in a RequestDelegate so none of the fancy options pertaining to how the Delegate parameters are handled - // do not matter. - RequestDelegateFactoryOptions rdfOptions = new() - { - EndpointFilterFactories = EndpointFilterFactories, - EndpointMetadata = Metadata, - }; - - // We ignore the returned EndpointMetadata has been already populated since we passed in non-null EndpointMetadata. - requestDelegate = RequestDelegateFactory.Create(requestDelegate, rdfOptions).RequestDelegate; - } - - var routeEndpoint = new RouteEndpoint( - requestDelegate, + return new RouteEndpoint( + RequestDelegate, RoutePattern, Order, new EndpointMetadataCollection(Metadata), DisplayName); - - return routeEndpoint; } } diff --git a/src/Http/Routing/src/RouteEndpointDataSource.cs b/src/Http/Routing/src/RouteEndpointDataSource.cs index e15e2da25943..476b032fbf2d 100644 --- a/src/Http/Routing/src/RouteEndpointDataSource.cs +++ b/src/Http/Routing/src/RouteEndpointDataSource.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; @@ -25,24 +24,50 @@ public RouteEndpointDataSource(IServiceProvider applicationServices, bool throwO _throwOnBadRequest = throwOnBadRequest; } - public ICollection> AddEndpoint( + public RouteHandlerBuilder AddRequestDelegate( + RoutePattern pattern, + RequestDelegate requestDelegate, + IEnumerable? httpMethods) + { + + var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); + + _routeEntries.Add(new() + { + RoutePattern = pattern, + RouteHandler = requestDelegate, + HttpMethods = httpMethods, + RouteAttributes = RouteAttributes.None, + Conventions = conventions, + }); + + return new RouteHandlerBuilder(conventions); + } + + public RouteHandlerBuilder AddRouteHandler( RoutePattern pattern, Delegate routeHandler, IEnumerable? httpMethods, bool isFallback) { - RouteEntry entry = new() + var conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(); + + var routeAttributes = RouteAttributes.RouteHandler; + if (isFallback) + { + routeAttributes |= RouteAttributes.Fallback; + } + + _routeEntries.Add(new() { RoutePattern = pattern, RouteHandler = routeHandler, HttpMethods = httpMethods, - IsFallback = isFallback, - Conventions = new ThrowOnAddAfterEndpointBuiltConventionCollection(), - }; + RouteAttributes = routeAttributes, + Conventions = conventions, + }); - _routeEntries.Add(entry); - - return entry.Conventions; + return new RouteHandlerBuilder(conventions); } public override IReadOnlyList Endpoints @@ -88,15 +113,21 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( { 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; + + // 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.RawText ?? pattern.DebuggerToString(); - // Methods defined in a top-level program are generated as statics so the delegate target will be null. - // Inline lambdas are compiler generated method so they be filtered that way. - if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName) - || !TypeHelper.IsCompilerGeneratedMethod(handler.Method)) + // 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(handler.Method, out var methodName)) { - endpointName ??= handler.Method.Name; - displayName = $"{displayName} => {endpointName}"; + displayName = $"{displayName} => {methodName}"; } if (entry.HttpMethods is not null) @@ -105,13 +136,17 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( displayName = $"HTTP: {string.Join(", ", entry.HttpMethods)} {displayName}"; } - if (entry.IsFallback) + if (isFallback) { displayName = $"Fallback {displayName}"; } - RequestDelegate? factoryCreatedRequestDelegate = null; - RequestDelegate redirectedRequestDelegate = context => + // 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) { @@ -121,21 +156,16 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( return factoryCreatedRequestDelegate(context); }; - // 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 = entry.IsFallback ? int.MaxValue : 0; - - RouteEndpointBuilder builder = new(redirectedRequestDelegate, pattern, order) + // 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, + Metadata = { handler.Method }, }; - // 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. - builder.Metadata.Add(handler.Method); - if (entry.HttpMethods is not null) { builder.Metadata.Add(new HttpMethodMetadata(entry.HttpMethods)); @@ -166,29 +196,29 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder( entrySpecificConvention(builder); } - var routeParamNames = new List(pattern.Parameters.Count); - foreach (var parameter in pattern.Parameters) - { - routeParamNames.Add(parameter.Name); - } - - RequestDelegateFactoryOptions factoryOptions = new() + if (isRouteHandler || builder.EndpointFilterFactories is { Count: > 0}) { - ServiceProvider = _applicationServices, - RouteParameterNames = routeParamNames, - ThrowOnBadRequest = _throwOnBadRequest, - DisableInferBodyFromParameters = ShouldDisableInferredBodyParameters(entry.HttpMethods), - EndpointMetadata = builder.Metadata, - EndpointFilterFactories = builder.EndpointFilterFactories, - }; - - // We ignore the returned EndpointMetadata has been already populated since we passed in non-null EndpointMetadata. - factoryCreatedRequestDelegate = RequestDelegateFactory.Create(entry.RouteHandler, factoryOptions).RequestDelegate; + var routeParamNames = new List(pattern.Parameters.Count); + foreach (var parameter in pattern.Parameters) + { + routeParamNames.Add(parameter.Name); + } - // Clear out any filters so they don't get rerun in Build(). We can rethink how we do this later when exposed as public API. - builder.EndpointFilterFactories = null; + RequestDelegateFactoryOptions factoryOptions = new() + { + ServiceProvider = _applicationServices, + RouteParameterNames = routeParamNames, + ThrowOnBadRequest = _throwOnBadRequest, + DisableInferBodyFromParameters = ShouldDisableInferredBodyParameters(entry.HttpMethods), + EndpointMetadata = builder.Metadata, + EndpointFilterFactories = builder.EndpointFilterFactories, + }; + + // We ignore the returned EndpointMetadata has been already populated since we passed in non-null EndpointMetadata. + factoryCreatedRequestDelegate = RequestDelegateFactory.Create(entry.RouteHandler, factoryOptions).RequestDelegate; + } - if (ReferenceEquals(builder.RequestDelegate, redirectedRequestDelegate)) + if (ReferenceEquals(builder.RequestDelegate, redirectRequestDelegate)) { // No convention has changed builder.RequestDelegate, so we can just replace it with the final version as an optimization. // We still set factoryRequestDelegate in case something is still referencing the redirected version of the RequestDelegate. @@ -233,10 +263,21 @@ private struct RouteEntry public RoutePattern RoutePattern { get; init; } public Delegate RouteHandler { get; init; } public IEnumerable? HttpMethods { get; init; } - public bool IsFallback { get; init; } + public RouteAttributes RouteAttributes { get; init; } public ThrowOnAddAfterEndpointBuiltConventionCollection Conventions { get; init; } } + [Flags] + private enum RouteAttributes + { + // The endpoint was defined by a RequestDelegate, RequestDelegateFactory.Create() should be skipped unless there are endpoint filters. + None = 0, + // This was added as Delegate route handler, so RequestDelegateFactory.Create() should always be called. + RouteHandler = 1, + // This was added by MapFallback. + Fallback = 2, + } + // This private class is only exposed to internal code via ICollection> in RouteEndpointBuilder where only Add is called. private sealed class ThrowOnAddAfterEndpointBuiltConventionCollection : List>, ICollection> { diff --git a/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs index 9caac8465e86..bd352464d104 100644 --- a/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Moq; @@ -359,6 +360,7 @@ private IServiceProvider CreateServices(MatcherFactory matcherFactory) var listener = new DiagnosticListener("Microsoft.AspNetCore"); services.AddSingleton(listener); services.AddSingleton(listener); + services.AddSingleton(Mock.Of()); var serviceProvder = services.BuildServiceProvider(); diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index f7ab3518642b..b43a63604f0f 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -3,18 +3,13 @@ #nullable enable -using System.IO.Pipelines; using System.Linq.Expressions; using System.Reflection; using System.Text; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Moq; namespace Microsoft.AspNetCore.Builder; @@ -27,7 +22,6 @@ private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpo GetBuilderEndpointDataSource(endpointRouteBuilder) switch { RouteEndpointDataSource routeDataSource => routeDataSource.GetSingleRouteEndpointBuilder(), - ModelEndpointDataSource modelDataSource => Assert.IsType(Assert.Single(modelDataSource.EndpointBuilders)), _ => throw new InvalidOperationException($"Unknown EndointDataSource type!"), }; @@ -198,9 +192,10 @@ public void MapEndpoint_AttributesCollectedAsMetadata() // Assert var endpointBuilder1 = GetRouteEndpointBuilder(builder); Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); - Assert.Equal(2, endpointBuilder1.Metadata.Count); - Assert.IsType(endpointBuilder1.Metadata[0]); - Assert.IsType(endpointBuilder1.Metadata[1]); + Assert.Equal(3, endpointBuilder1.Metadata.Count); + Assert.Equal(((RequestDelegate)Handle).Method, endpointBuilder1.Metadata[0]); + Assert.IsType(endpointBuilder1.Metadata[1]); + Assert.IsType(endpointBuilder1.Metadata[2]); } [Fact] @@ -233,10 +228,13 @@ public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned() var dataSource = Assert.Single(builder.DataSources); var endpoint = Assert.Single(dataSource.Endpoints); - Assert.Equal(3, endpoint.Metadata.Count); - Assert.Equal("ATTRIBUTE", GetMethod(endpoint.Metadata[0])); + // As with the Delegate Map method overloads for route handlers, the attributes on the RequestDelegate + // can override the HttpMethodMetadata. Extension methods could already do this. + Assert.Equal(4, endpoint.Metadata.Count); + Assert.Equal(((RequestDelegate)HandleHttpMetdata).Method, endpoint.Metadata[0]); Assert.Equal("METHOD", GetMethod(endpoint.Metadata[1])); - Assert.Equal("BUILDER", GetMethod(endpoint.Metadata[2])); + Assert.Equal("ATTRIBUTE", GetMethod(endpoint.Metadata[2])); + Assert.Equal("BUILDER", GetMethod(endpoint.Metadata[3])); Assert.Equal("BUILDER", endpoint.Metadata.GetMetadata()?.HttpMethods.Single()); diff --git a/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs b/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs index ed6f408337cb..1bfc5ecc724c 100644 --- a/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs @@ -28,4 +28,42 @@ public void Build_AllValuesSet_EndpointCreated() Assert.Equal("/", endpoint.RoutePattern.RawText); Assert.Equal(metadata, Assert.Single(endpoint.Metadata)); } + + [Fact] + public async void Build_DoesNot_RunFilters() + { + var endpointFilterCallCount = 0; + var invocationFilterCallCount = 0; + var invocationCallCount = 0; + + const int defaultOrder = 0; + RequestDelegate requestDelegate = (d) => + { + invocationCallCount++; + return Task.CompletedTask; + }; + + var builder = new RouteEndpointBuilder(requestDelegate, RoutePatternFactory.Parse("/"), defaultOrder); + + builder.EndpointFilterFactories = new List>(); + builder.EndpointFilterFactories.Add((endopintContext, next) => + { + endpointFilterCallCount++; + + return invocationContext => + { + invocationFilterCallCount++; + + return next(invocationContext); + }; + }); + + var endpoint = Assert.IsType(builder.Build()); + + await endpoint.RequestDelegate(new DefaultHttpContext()); + + Assert.Equal(0, endpointFilterCallCount); + Assert.Equal(0, invocationFilterCallCount); + Assert.Equal(1, invocationCallCount); + } } diff --git a/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs b/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs index 247fc36674f8..96025ce57446 100644 --- a/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs +++ b/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs @@ -55,14 +55,15 @@ public void ThrowOnBadRequestIsNotOverwrittenIfNotInDevelopmentEnvironment() } [Fact] - public void RouteHandlerOptionsFailsToResolveWithoutHostEnvironment() + public void RouteHandlerOptionsCanResolveWithoutHostEnvironment() { var services = new ServiceCollection(); services.AddOptions(); services.AddRouting(); var serviceProvider = services.BuildServiceProvider(); - Assert.Throws(() => serviceProvider.GetRequiredService>()); + var options = serviceProvider.GetRequiredService>(); + Assert.False(options.Value.ThrowOnBadRequest); } private class HostEnvironment : IHostEnvironment diff --git a/src/Middleware/Rewrite/test/MiddlewareTests.cs b/src/Middleware/Rewrite/test/MiddlewareTests.cs index eedf71436506..b6078a1c5aef 100644 --- a/src/Middleware/Rewrite/test/MiddlewareTests.cs +++ b/src/Middleware/Rewrite/test/MiddlewareTests.cs @@ -668,7 +668,7 @@ public async Task RewriteAfterUseRoutingHitsOriginalEndpoint() var response = await server.CreateClient().GetStringAsync("foo"); - Assert.Equal("/foo HTTP: GET from /foos", response); + Assert.Equal("HTTP: GET /foo from /foos", response); } [Fact] diff --git a/src/Shared/RoslynUtils/GeneratedNameParser.cs b/src/Shared/RoslynUtils/GeneratedNameParser.cs deleted file mode 100644 index 82d6c2da1fa5..000000000000 --- a/src/Shared/RoslynUtils/GeneratedNameParser.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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.CodeAnalysis; - -// This code is a stop-gap and exists to address the issues with extracting -// original method names from generated local functions. See https://github.com/dotnet/roslyn/issues/55651 -// for more info. -namespace Microsoft.CodeAnalysis.CSharp.Symbols; - -internal static class GeneratedNameParser -{ - /// - /// Parses generated local function name out of a generated method name. - /// - internal static bool TryParseLocalFunctionName(string generatedName, [NotNullWhen(true)] out string? originalName) - { - originalName = null; - - var startIndex = generatedName.LastIndexOf(">g__", StringComparison.Ordinal); - var endIndex = generatedName.LastIndexOf("|", StringComparison.Ordinal); - if (startIndex >= 0 && endIndex >= 0 && endIndex - startIndex > 4) - { - originalName = generatedName.Substring(startIndex + 4, endIndex - startIndex - 4); - return true; - } - - return false; - } -} diff --git a/src/Shared/RoslynUtils/TypeHelper.cs b/src/Shared/RoslynUtils/TypeHelper.cs index b7f8a20ab851..51ca29d9c925 100644 --- a/src/Shared/RoslynUtils/TypeHelper.cs +++ b/src/Shared/RoslynUtils/TypeHelper.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace System.Runtime.CompilerServices; @@ -32,8 +33,44 @@ internal static bool IsCompilerGeneratedType(Type? type = null) /// /// The method to evaluate. /// if is compiler generated. - internal static bool IsCompilerGeneratedMethod(MethodInfo method) + private static bool IsCompilerGeneratedMethod(MethodInfo method) { return Attribute.IsDefined(method, typeof(CompilerGeneratedAttribute)) || IsCompilerGeneratedType(method.DeclaringType); } + + /// + /// Parses generated local function name out of a generated method name. This code is a stop-gap and exists to address the issues with extracting + /// original method names from generated local functions. See https://github.com/dotnet/roslyn/issues/55651 for more info. + /// + private static bool TryParseLocalFunctionName(string generatedName, [NotNullWhen(true)] out string? originalName) + { + originalName = null; + + var startIndex = generatedName.LastIndexOf(">g__", StringComparison.Ordinal); + var endIndex = generatedName.LastIndexOf("|", StringComparison.Ordinal); + if (startIndex >= 0 && endIndex >= 0 && endIndex - startIndex > 4) + { + originalName = generatedName.Substring(startIndex + 4, endIndex - startIndex - 4); + return true; + } + + return false; + } + + /// + /// Tries to get non-compiler-generated name of function. This parses generated local function names out of a generated method name if possible. + /// + internal static bool TryGetNonCompilerGeneratedMethodName(MethodInfo method, [NotNullWhen(true)] out string? originalName) + { + var methodName = method.Name; + + if (!IsCompilerGeneratedMethod(method)) + { + originalName = methodName; + return true; + } + + return TryParseLocalFunctionName(methodName, out originalName); + } } +