diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index d002acd3a5e8..88f9ca11ef49 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -20,6 +20,7 @@ public sealed class WebApplicationBuilder private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder"; private const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet"; private const string AuthorizationMiddlewareSetKey = "__AuthorizationMiddlewareSet"; + private const string UseRoutingKey = "__UseRouting"; private readonly HostApplicationBuilder _hostApplicationBuilder; private readonly ServiceDescriptor _genericWebHostServiceDescriptor; @@ -162,6 +163,8 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder)) { app.UseRouting(); + // Middleware the needs to re-route will use this property to call UseRouting() + _builtApplication.Properties[UseRoutingKey] = app.Properties[UseRoutingKey]; } else { diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs index b4e908854cc2..254f88211cbe 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder.Extensions; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Builder; @@ -31,6 +32,16 @@ public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, Path return app; } + // Only use this path if there's a global router (in the 'WebApplication' case). + if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + { + return app.Use(next => + { + var newNext = RerouteHelper.Reroute(app, routeBuilder, next); + return new UsePathBaseMiddleware(newNext, pathBase).Invoke; + }); + } + return app.UseMiddleware(pathBase); } } diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index bcd4d403f322..167bc9455f5f 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -28,6 +28,7 @@ Microsoft.AspNetCore.Http.HttpResponse + diff --git a/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj b/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj index 563e3cd7585b..36c3a0c54c7a 100644 --- a/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj +++ b/src/Http/Http.Abstractions/test/Microsoft.AspNetCore.Http.Abstractions.Tests.csproj @@ -10,8 +10,10 @@ + + diff --git a/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs index c01b6a735601..448dfdf23009 100644 --- a/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs +++ b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; namespace Microsoft.AspNetCore.Builder.Extensions; @@ -138,6 +139,73 @@ public Task PathBaseCanHavePercentCharacters(string registeredPathBase, string p return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); } + [Fact] + public async Task PathBaseWorksAfterUseRoutingIfGlobalRouteBuilderUsed() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.UseRouting(); + + app.UsePathBase("/base"); + + app.UseEndpoints(endpoints => + { + endpoints.Map("/path", context => context.Response.WriteAsync("Response")); + }); + + await app.StartAsync(); + + using var server = app.GetTestServer(); + + var response = await server.CreateClient().GetStringAsync("/base/path"); + + Assert.Equal("Response", response); + } + + [Fact] + public async Task PathBaseWorksBeforeUseRoutingIfGlobalRouteBuilderUsed() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.UsePathBase("/base"); + + app.UseRouting(); + + app.MapGet("/path", context => context.Response.WriteAsync("Response")); + + await app.StartAsync(); + + using var server = app.GetTestServer(); + + var response = await server.CreateClient().GetStringAsync("/base/path"); + + Assert.Equal("Response", response); + } + + [Fact] + public async Task PathBaseWorksWithoutUseRoutingWithWebApplication() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.UsePathBase("/base"); + + app.MapGet("/path", context => context.Response.WriteAsync("Response")); + + await app.StartAsync(); + + using var server = app.GetTestServer(); + + var response = await server.CreateClient().GetStringAsync("/base/path"); + + Assert.Equal("Response", response); + } + private static async Task TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) { HttpContext requestContext = CreateRequest(pathBase, requestPath); diff --git a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs index 6d14b6bd980e..29598aea3412 100644 --- a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs @@ -15,6 +15,7 @@ public static class EndpointRoutingApplicationBuilderExtensions { private const string EndpointRouteBuilder = "__EndpointRouteBuilder"; private const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + private const string UseRoutingKey = "__UseRouting"; /// /// Adds a middleware to the specified . @@ -54,6 +55,10 @@ public static IApplicationBuilder UseRouting(this IApplicationBuilder builder) builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder; } + // Add UseRouting function to properties so that middleware that can't reference UseRouting directly can call UseRouting via this property + // This is part of the global endpoint route builder concept + builder.Properties.TryAdd(UseRoutingKey, (object)UseRouting); + return builder.UseMiddleware(endpointRouteBuilder); } diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs index b84e60b0c7e3..a0e77bd337dc 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -103,13 +104,12 @@ public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder a private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBuilder app, IOptions? options) { - const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; var problemDetailsService = app.ApplicationServices.GetService(); app.Properties["analysis.NextMiddlewareName"] = "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware"; // Only use this path if there's a global router (in the 'WebApplication' case). - if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) { return app.Use(next => { @@ -123,16 +123,9 @@ private static IApplicationBuilder SetExceptionHandlerMiddleware(IApplicationBui if (!string.IsNullOrEmpty(options.Value.ExceptionHandlingPath) && options.Value.ExceptionHandler is null) { - // start a new middleware pipeline - var builder = app.New(); - // use the old routing pipeline if it exists so we preserve all the routes and matching logic - // ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties. - builder.Properties[globalRouteBuilderKey] = routeBuilder; - builder.UseRouting(); - // apply the next middleware - builder.Run(next); + var newNext = RerouteHelper.Reroute(app, routeBuilder, next); // store the pipeline for the error case - options.Value.ExceptionHandler = builder.Build(); + options.Value.ExceptionHandler = newNext; } return new ExceptionHandlerMiddlewareImpl(next, loggerFactory, options, diagnosticListener, problemDetailsService).Invoke; diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index ebf506b0a0ae..97805388d9c8 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index 745834c359c9..f615da9112da 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder; @@ -172,23 +173,12 @@ public static IApplicationBuilder UseStatusCodePagesWithReExecute( throw new ArgumentNullException(nameof(app)); } - const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; // Only use this path if there's a global router (in the 'WebApplication' case). - if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) { return app.Use(next => { - RequestDelegate? newNext = null; - // start a new middleware pipeline - var builder = app.New(); - // use the old routing pipeline if it exists so we preserve all the routes and matching logic - // ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties. - builder.Properties[globalRouteBuilderKey] = routeBuilder; - builder.UseRouting(); - // apply the next middleware - builder.Run(next); - newNext = builder.Build(); - + var newNext = RerouteHelper.Reroute(app, routeBuilder, next); return new StatusCodePagesMiddleware(next, Options.Create(new StatusCodePagesOptions() { HandleAsync = CreateHandler(pathFormat, queryFormat, newNext) })).Invoke; }); diff --git a/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj b/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj index 330bcf53be7a..8150ba5cfd38 100644 --- a/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj +++ b/src/Middleware/Rewrite/src/Microsoft.AspNetCore.Rewrite.csproj @@ -23,4 +23,8 @@ + + + + diff --git a/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs b/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs index 1902bfbc42ab..f56d7ee850f8 100644 --- a/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs +++ b/src/Middleware/Rewrite/src/RewriteBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -53,9 +54,8 @@ public static IApplicationBuilder UseRewriter(this IApplicationBuilder app, Rewr private static IApplicationBuilder AddRewriteMiddleware(IApplicationBuilder app, IOptions? options) { - const string globalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; // Only use this path if there's a global router (in the 'WebApplication' case). - if (app.Properties.TryGetValue(globalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) + if (app.Properties.TryGetValue(RerouteHelper.GlobalRouteBuilderKey, out var routeBuilder) && routeBuilder is not null) { return app.Use(next => { @@ -67,15 +67,8 @@ private static IApplicationBuilder AddRewriteMiddleware(IApplicationBuilder app, var webHostEnv = app.ApplicationServices.GetRequiredService(); var loggerFactory = app.ApplicationServices.GetRequiredService(); - // start a new middleware pipeline - var builder = app.New(); - // use the old routing pipeline if it exists so we preserve all the routes and matching logic - // ((IApplicationBuilder)WebApplication).New() does not copy globalRouteBuilderKey automatically like it does for all other properties. - builder.Properties[globalRouteBuilderKey] = routeBuilder; - builder.UseRouting(); - // apply the next middleware - builder.Run(next); - options.Value.BranchedNext = builder.Build(); + var newNext = RerouteHelper.Reroute(app, routeBuilder, next); + options.Value.BranchedNext = newNext; return new RewriteMiddleware(next, webHostEnv, loggerFactory, options).Invoke; }); diff --git a/src/Shared/Reroute.cs b/src/Shared/Reroute.cs new file mode 100644 index 000000000000..d44e10320510 --- /dev/null +++ b/src/Shared/Reroute.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing; + +internal static class RerouteHelper +{ + internal const string GlobalRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + internal const string UseRoutingKey = "__UseRouting"; + + internal static RequestDelegate Reroute(IApplicationBuilder app, object routeBuilder, RequestDelegate next) + { + if (app.Properties.TryGetValue(UseRoutingKey, out var useRouting) && useRouting is Func useRoutingFunc) + { + var builder = app.New(); + // use the old routing pipeline if it exists so we preserve all the routes and matching logic + // ((IApplicationBuilder)WebApplication).New() does not copy GlobalRouteBuilderKey automatically like it does for all other properties. + builder.Properties[GlobalRouteBuilderKey] = routeBuilder; + + // UseRouting() + useRoutingFunc(builder); + + // apply the next middleware + builder.Run(next); + + return builder.Build(); + } + + return next; + } +}