From b88bc699bd7aea6d8aa7369a3ea472d8825c0d97 Mon Sep 17 00:00:00 2001
From: Brennan <brecon@microsoft.com>
Date: Tue, 17 Aug 2021 13:07:55 -0700
Subject: [PATCH] Add ability to modify UseRouting/UseEndpoint behavior for
 WebApplicationBuilder  (#35336)

---
 src/DefaultBuilder/src/WebApplication.cs      |  14 +-
 .../src/WebApplicationBuilder.cs              |  74 ++---
 .../WebApplicationTests.cs                    | 275 +++++++++++++++++-
 ...ointRoutingApplicationBuilderExtensions.cs |  27 +-
 ...RoutingApplicationBuilderExtensionsTest.cs |  48 +++
 5 files changed, 385 insertions(+), 53 deletions(-)

diff --git a/src/DefaultBuilder/src/WebApplication.cs b/src/DefaultBuilder/src/WebApplication.cs
index 4d2de4df64f..d6c8ca5c609 100644
--- a/src/DefaultBuilder/src/WebApplication.cs
+++ b/src/DefaultBuilder/src/WebApplication.cs
@@ -23,11 +23,15 @@ namespace Microsoft.AspNetCore.Builder
         private readonly IHost _host;
         private readonly List<EndpointDataSource> _dataSources = new();
 
+        internal static string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
+
         internal WebApplication(IHost host)
         {
             _host = host;
             ApplicationBuilder = new ApplicationBuilder(host.Services);
             Logger = host.Services.GetRequiredService<ILoggerFactory>().CreateLogger(Environment.ApplicationName);
+
+            Properties[GlobalEndpointRouteBuilderKey] = this;
         }
 
         /// <summary>
@@ -170,7 +174,13 @@ namespace Microsoft.AspNetCore.Builder
         RequestDelegate IApplicationBuilder.Build() => BuildRequestDelegate();
 
         // REVIEW: Should this be wrapping another type?
-        IApplicationBuilder IApplicationBuilder.New() => ApplicationBuilder.New();
+        IApplicationBuilder IApplicationBuilder.New()
+        {
+            var newBuilder = ApplicationBuilder.New();
+            // Remove the route builder so branched pipelines have their own routing world
+            newBuilder.Properties.Remove(GlobalEndpointRouteBuilderKey);
+            return newBuilder;
+        }
 
         IApplicationBuilder IApplicationBuilder.Use(Func<RequestDelegate, RequestDelegate> middleware)
         {
@@ -178,7 +188,7 @@ namespace Microsoft.AspNetCore.Builder
             return this;
         }
 
-        IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => ApplicationBuilder.New();
+        IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => ((IApplicationBuilder)this).New();
 
         private void Listen(string? url)
         {
diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs
index 09d59c6d43f..07b17731f2e 100644
--- a/src/DefaultBuilder/src/WebApplicationBuilder.cs
+++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs
@@ -4,7 +4,6 @@
 using System.Diagnostics;
 using System.Reflection;
 using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
@@ -17,11 +16,10 @@ namespace Microsoft.AspNetCore.Builder
     /// </summary>
     public sealed class WebApplicationBuilder
     {
-        private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
-
         private readonly HostBuilder _hostBuilder = new();
         private readonly BootstrapHostBuilder _bootstrapHostBuilder;
         private readonly WebApplicationServiceCollection _services = new();
+        private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
 
         private WebApplication? _builtApplication;
 
@@ -187,48 +185,39 @@ namespace Microsoft.AspNetCore.Builder
         {
             Debug.Assert(_builtApplication is not null);
 
+            // UseRouting called before WebApplication such as in a StartupFilter
+            // lets remove the property and reset it at the end so we don't mess with the routes in the filter
+            if (app.Properties.TryGetValue(EndpointRouteBuilderKey, out var priorRouteBuilder))
+            {
+                app.Properties.Remove(EndpointRouteBuilderKey);
+            }
+
             if (context.HostingEnvironment.IsDevelopment())
             {
+                // TODO: add test for this
                 app.UseDeveloperExceptionPage();
             }
 
-            var implicitRouting = false;
+            // Wrap the entire destination pipeline in UseRouting() and UseEndpoints(), essentially:
+            // destination.UseRouting()
+            // destination.Run(source)
+            // destination.UseEndpoints()
+
+            // Set the route builder so that UseRouting will use the WebApplication as the IEndpointRouteBuilder for route matching
+            app.Properties.Add(WebApplication.GlobalEndpointRouteBuilderKey, _builtApplication);
 
-            // The endpoints were already added on the outside
+            // Only call UseRouting() if there are endpoints configured and UseRouting() wasn't called on the global route builder already
             if (_builtApplication.DataSources.Count > 0)
             {
-                // The user did not register the routing middleware so wrap the entire
-                // destination pipeline in UseRouting() and UseEndpoints(), essentially:
-                // destination.UseRouting()
-                // destination.Run(source)
-                // destination.UseEndpoints()
-
-                // Copy endpoints to the IEndpointRouteBuilder created by an explicit call to UseRouting() if possible.
-                var targetRouteBuilder = GetEndpointRouteBuilder(_builtApplication);
-
-                if (targetRouteBuilder is null)
+                // If this is set, someone called UseRouting() when a global route builder was already set
+                if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder))
                 {
-                    // The app defined endpoints without calling UseRouting() explicitly, so call UseRouting() implicitly.
                     app.UseRouting();
-
-                    // An implicitly created IEndpointRouteBuilder was addeded to app.Properties by the UseRouting() call above.
-                    targetRouteBuilder = GetEndpointRouteBuilder(app)!;
-                    implicitRouting = true;
                 }
-
-                // Copy the endpoints to the explicitly or implicitly created IEndopintRouteBuilder.
-                foreach (var ds in _builtApplication.DataSources)
+                else
                 {
-                    targetRouteBuilder.DataSources.Add(ds);
-                }
-
-                // UseEndpoints consumes the DataSources immediately to populate CompositeEndpointDataSource via RouteOptions,
-                // so it must be called after we copy the endpoints.
-                if (!implicitRouting)
-                {
-                    // UseRouting() was called explicitely, but we may still need to call UseEndpoints() implicitely at
-                    // the end of the pipeline.
-                    _builtApplication.UseEndpoints(_ => { });
+                    // UseEndpoints will be looking for the RouteBuilder so make sure it's set
+                    app.Properties[EndpointRouteBuilderKey] = localRouteBuilder;
                 }
             }
 
@@ -239,11 +228,9 @@ namespace Microsoft.AspNetCore.Builder
                 return _builtApplication.BuildRequestDelegate();
             });
 
-            // Implicitly call UseEndpoints() at the end of the pipeline if UseRouting() was called implicitly.
-            // We could add this to the end of _buildApplication instead if UseEndpoints() was not so picky about
-            // being called with the same IApplicationBluilder instance as UseRouting().
-            if (implicitRouting)
+            if (_builtApplication.DataSources.Count > 0)
             {
+                // We don't know if user code called UseEndpoints(), so we will call it just in case, UseEndpoints() will ignore duplicate DataSources
                 app.UseEndpoints(_ => { });
             }
 
@@ -252,12 +239,15 @@ namespace Microsoft.AspNetCore.Builder
             {
                 app.Properties[item.Key] = item.Value;
             }
-        }
 
-        private static IEndpointRouteBuilder? GetEndpointRouteBuilder(IApplicationBuilder app)
-        {
-            app.Properties.TryGetValue(EndpointRouteBuilderKey, out var value);
-            return (IEndpointRouteBuilder?)value;
+            // Remove the route builder to clean up the properties, we're done adding routes to the pipeline
+            app.Properties.Remove(WebApplication.GlobalEndpointRouteBuilderKey);
+
+            // reset route builder if it existed, this is needed for StartupFilters
+            if (priorRouteBuilder is not null)
+            {
+                app.Properties[EndpointRouteBuilderKey] = priorRouteBuilder;
+            }
         }
 
         private class LoggingBuilder : ILoggingBuilder
diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs
index c2e63788289..a3fea68fcc1 100644
--- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs
+++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.HostFiltering;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting.Server;
 using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.AspNetCore.TestHost;
@@ -235,7 +236,7 @@ namespace Microsoft.AspNetCore.Tests
         }
 
         [Fact]
-        public void WebApplicationBuildeSettingInvalidApplicationWillFailAssemblyLoadForUserSecrets()
+        public void WebApplicationBuilderSettingInvalidApplicationWillFailAssemblyLoadForUserSecrets()
         {
             var options = new WebApplicationOptions
             {
@@ -732,7 +733,7 @@ namespace Microsoft.AspNetCore.Tests
         }
 
         [Fact]
-        public async Task WebApplication_CanCallUseRoutingWithouUseEndpoints()
+        public async Task WebApplication_CanCallUseRoutingWithoutUseEndpoints()
         {
             var builder = WebApplication.CreateBuilder();
             builder.WebHost.UseTestServer();
@@ -769,6 +770,19 @@ namespace Microsoft.AspNetCore.Tests
             Assert.Equal("new", await oldResult.Content.ReadAsStringAsync());
         }
 
+        [Fact]
+        public async Task WebApplication_CanCallUseEndpointsWithoutUseRoutingFails()
+        {
+            var builder = WebApplication.CreateBuilder();
+            builder.WebHost.UseTestServer();
+            await using var app = builder.Build();
+
+            app.MapGet("/1", () => "1");
+
+            var ex = Assert.Throws<InvalidOperationException>(() => app.UseEndpoints(endpoints => { }));
+            Assert.Contains("UseRouting", ex.Message);
+        }
+
         [Fact]
         public void WebApplicationCreate_RegistersEventSourceLogger()
         {
@@ -860,6 +874,67 @@ namespace Microsoft.AspNetCore.Tests
             Assert.Equal(418, (int)terminalResult.StatusCode);
         }
 
+        [Fact]
+        public async Task StartupFilter_WithUseRoutingWorks()
+        {
+            var builder = WebApplication.CreateBuilder();
+            builder.WebHost.UseTestServer();
+            builder.Services.AddSingleton<IStartupFilter, UseRoutingStartupFilter>();
+            await using var app = builder.Build();
+
+            var chosenEndpoint = string.Empty;
+            app.MapGet("/", async c => {
+                chosenEndpoint = c.GetEndpoint().DisplayName;
+                await c.Response.WriteAsync("Hello World");
+            }).WithDisplayName("One");
+
+            await app.StartAsync();
+
+            var client = app.GetTestClient();
+
+            _ = await client.GetAsync("http://localhost/");
+            Assert.Equal("One", chosenEndpoint);
+
+            var response = await client.GetAsync("http://localhost/1");
+            Assert.Equal(203, ((int)response.StatusCode));
+        }
+
+        [Fact]
+        public async Task CanAddMiddlewareBeforeUseRouting()
+        {
+            var builder = WebApplication.CreateBuilder();
+            builder.WebHost.UseTestServer();
+            await using var app = builder.Build();
+
+            var chosenEndpoint = string.Empty;
+
+            app.Use((c, n) =>
+            {
+                chosenEndpoint = c.GetEndpoint()?.DisplayName;
+                Assert.Null(c.GetEndpoint());
+                return n(c);
+            });
+
+            app.UseRouting();
+
+            app.MapGet("/1", async c => {
+                chosenEndpoint = c.GetEndpoint().DisplayName;
+                await c.Response.WriteAsync("Hello World");
+            }).WithDisplayName("One");
+
+            app.UseEndpoints(e => { });
+
+            await app.StartAsync();
+
+            var client = app.GetTestClient();
+
+            _ = await client.GetAsync("http://localhost/");
+            Assert.Null(chosenEndpoint);
+
+            _ = await client.GetAsync("http://localhost/1");
+            Assert.Equal("One", chosenEndpoint);
+        }
+
         [Fact]
         public async Task WebApplicationBuilder_OnlyAddsDefaultServicesOnce()
         {
@@ -922,6 +997,182 @@ namespace Microsoft.AspNetCore.Tests
             Assert.Throws<NotSupportedException>(() => builder.Host.ConfigureWebHostDefaults(webHostBuilder => { }));
         }
 
+        [Fact]
+        public async Task EndpointDataSourceOnlyAddsOnce()
+        {
+            var builder = WebApplication.CreateBuilder();
+            await using var app = builder.Build();
+
+            app.UseRouting();
+
+            app.MapGet("/", () => "Hello World!").WithDisplayName("One");
+
+            app.UseEndpoints(routes =>
+            {
+                routes.MapGet("/hi", () => "Hi World").WithDisplayName("Two");
+                routes.MapGet("/heyo", () => "Heyo World").WithDisplayName("Three");
+            });
+
+            app.Start();
+
+            var ds = app.Services.GetRequiredService<EndpointDataSource>();
+            Assert.Equal(3, ds.Endpoints.Count);
+            Assert.Equal("One", ds.Endpoints[0].DisplayName);
+            Assert.Equal("Two", ds.Endpoints[1].DisplayName);
+            Assert.Equal("Three", ds.Endpoints[2].DisplayName);
+        }
+
+        [Fact]
+        public async Task RoutesAddedToCorrectMatcher()
+        {
+            var builder = WebApplication.CreateBuilder();
+            builder.WebHost.UseTestServer();
+            await using var app = builder.Build();
+
+            app.UseRouting();
+
+            var chosenRoute = string.Empty;
+
+            app.Use((context, next) =>
+            {
+                chosenRoute = context.GetEndpoint()?.DisplayName;
+                return next(context);
+            });
+
+            app.MapGet("/", () => "Hello World").WithDisplayName("One");
+
+            app.UseEndpoints(endpoints =>
+            {
+                endpoints.MapGet("/hi", () => "Hello Endpoints").WithDisplayName("Two");
+            });
+
+            app.UseRouting();
+            app.UseEndpoints(_ => { });
+
+            await app.StartAsync();
+
+            var client = app.GetTestClient();
+
+            _ = await client.GetAsync("http://localhost/");
+            Assert.Equal("One", chosenRoute);
+        }
+
+        [Fact]
+        public async Task WebApplication_CallsUseRoutingAndUseEndpoints()
+        {
+            var builder = WebApplication.CreateBuilder();
+            builder.WebHost.UseTestServer();
+            await using var app = builder.Build();
+
+            var chosenRoute = string.Empty;
+            app.MapGet("/", async c =>
+            {
+                chosenRoute = c.GetEndpoint()?.DisplayName;
+                await c.Response.WriteAsync("Hello World");
+            }).WithDisplayName("One");
+
+            await app.StartAsync();
+
+            var ds = app.Services.GetRequiredService<EndpointDataSource>();
+            Assert.Equal(1, ds.Endpoints.Count);
+            Assert.Equal("One", ds.Endpoints[0].DisplayName);
+
+            var client = app.GetTestClient();
+
+            _ = await client.GetAsync("http://localhost/");
+            Assert.Equal("One", chosenRoute);
+        }
+
+        [Fact]
+        public async Task BranchingPipelineHasOwnRoutes()
+        {
+            var builder = WebApplication.CreateBuilder();
+            builder.WebHost.UseTestServer();
+            await using var app = builder.Build();
+
+            app.UseRouting();
+
+            var chosenRoute = string.Empty;
+            app.MapGet("/", () => "Hello World!").WithDisplayName("One");
+
+            app.UseEndpoints(routes =>
+            {
+                routes.MapGet("/hi", async c =>
+                {
+                    chosenRoute = c.GetEndpoint()?.DisplayName;
+                    await c.Response.WriteAsync("Hello World");
+                }).WithDisplayName("Two");
+                routes.MapGet("/heyo", () => "Heyo World").WithDisplayName("Three");
+            });
+
+            var newBuilder = ((IApplicationBuilder)app).New();
+            Assert.False(newBuilder.Properties.TryGetValue(WebApplication.GlobalEndpointRouteBuilderKey, out _));
+
+            newBuilder.UseRouting();
+            newBuilder.UseEndpoints(endpoints =>
+            {
+                endpoints.MapGet("/h3", async c =>
+                {
+                    chosenRoute = c.GetEndpoint()?.DisplayName;
+                    await c.Response.WriteAsync("Hello World");
+                }).WithDisplayName("Four");
+                endpoints.MapGet("hi", async c =>
+                {
+                    chosenRoute = c.GetEndpoint()?.DisplayName;
+                    await c.Response.WriteAsync("Hi New");
+                }).WithDisplayName("Five");
+            });
+            var branch = newBuilder.Build();
+            app.Run(c => branch(c));
+
+            app.Start();
+
+            var ds = app.Services.GetRequiredService<EndpointDataSource>();
+            Assert.Equal(5, ds.Endpoints.Count);
+            Assert.Equal("One", ds.Endpoints[0].DisplayName);
+            Assert.Equal("Two", ds.Endpoints[1].DisplayName);
+            Assert.Equal("Three", ds.Endpoints[2].DisplayName);
+            Assert.Equal("Four", ds.Endpoints[3].DisplayName);
+            Assert.Equal("Five", ds.Endpoints[4].DisplayName);
+
+            var client = app.GetTestClient();
+
+            // '/hi' routes don't conflict and the non-branched one is chosen
+            _ = await client.GetAsync("http://localhost/hi");
+            Assert.Equal("Two", chosenRoute);
+
+            // Can access branched routes
+            _ = await client.GetAsync("http://localhost/h3");
+            Assert.Equal("Four", chosenRoute);
+        }
+
+        [Fact]
+        public async Task PropertiesPreservedFromInnerApplication()
+        {
+            var builder = WebApplication.CreateBuilder();
+            builder.Services.AddSingleton<IStartupFilter, PropertyFilter>();
+            await using var app = builder.Build();
+
+            ((IApplicationBuilder)app).Properties["didsomething"] = true;
+
+            app.Start();
+        }
+
+        class PropertyFilter : IStartupFilter
+        {
+            public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
+            {
+                return app =>
+                {
+                    next(app);
+
+                    // This should be true
+                    var val = app.Properties["didsomething"];
+                    Assert.True((bool)val);
+                };
+            }
+        }
+
         private class Service : IService { }
         private interface IService { }
 
@@ -1131,5 +1382,25 @@ namespace Microsoft.AspNetCore.Tests
                 };
             }
         }
+
+        class UseRoutingStartupFilter : IStartupFilter
+        {
+            public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
+            {
+                return app =>
+                {
+                    app.UseRouting();
+                    next(app);
+                    app.UseEndpoints(endpoints =>
+                    {
+                        endpoints.MapGet("/1", async c =>
+                        {
+                            c.Response.StatusCode = 203;
+                            await c.Response.WriteAsync("Hello Filter");
+                        }).WithDisplayName("Two");
+                    });
+                };
+            }
+        }
     }
 }
diff --git a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs
index 07466e2d16a..8bde4228461 100644
--- a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs
+++ b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs
@@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Builder
     public static class EndpointRoutingApplicationBuilderExtensions
     {
         private const string EndpointRouteBuilder = "__EndpointRouteBuilder";
+        private const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
 
         /// <summary>
         /// Adds a <see cref="EndpointRoutingMiddleware"/> middleware to the specified <see cref="IApplicationBuilder"/>.
@@ -44,8 +45,18 @@ namespace Microsoft.AspNetCore.Builder
 
             VerifyRoutingServicesAreRegistered(builder);
 
-            var endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
-            builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
+            IEndpointRouteBuilder endpointRouteBuilder;
+            if (builder.Properties.TryGetValue(GlobalEndpointRouteBuilderKey, out var obj))
+            {
+                endpointRouteBuilder = (IEndpointRouteBuilder)obj!;
+                // Let interested parties know if UseRouting() was called while a global route builder was set
+                builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
+            }
+            else
+            {
+                endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
+                builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
+            }
 
             return builder.UseMiddleware<EndpointRoutingMiddleware>(endpointRouteBuilder);
         }
@@ -99,7 +110,10 @@ namespace Microsoft.AspNetCore.Builder
             var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
             foreach (var dataSource in endpointRouteBuilder.DataSources)
             {
-                routeOptions.Value.EndpointDataSources.Add(dataSource);
+                if (!routeOptions.Value.EndpointDataSources.Contains(dataSource))
+                {
+                    routeOptions.Value.EndpointDataSources.Add(dataSource);
+                }
             }
 
             return builder.UseMiddleware<EndpointMiddleware>();
@@ -118,7 +132,7 @@ namespace Microsoft.AspNetCore.Builder
             }
         }
 
-        private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out DefaultEndpointRouteBuilder endpointRouteBuilder)
+        private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder)
         {
             if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj))
             {
@@ -130,12 +144,11 @@ namespace Microsoft.AspNetCore.Builder
                 throw new InvalidOperationException(message);
             }
 
-            // If someone messes with this, just let it crash.
-            endpointRouteBuilder = (DefaultEndpointRouteBuilder)obj!;
+            endpointRouteBuilder = (IEndpointRouteBuilder)obj!;
 
             // This check handles the case where Map or something else that forks the pipeline is called between the two
             // routing middleware.
-            if (!object.ReferenceEquals(app, endpointRouteBuilder.ApplicationBuilder))
+            if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder))
             {
                 var message =
                     $"The {nameof(EndpointRoutingMiddleware)} and {nameof(EndpointMiddleware)} must be added to the same {nameof(IApplicationBuilder)} instance. " +
diff --git a/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs
index 4b9d875d0a1..ec9c88e9e10 100644
--- a/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs
+++ b/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs
@@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Routing.Matching;
 using Microsoft.AspNetCore.Routing.Patterns;
 using Microsoft.AspNetCore.Routing.TestObjects;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
 using Moq;
 using Xunit;
 
@@ -295,6 +296,53 @@ namespace Microsoft.AspNetCore.Builder
                 e => Assert.Equal("Test endpoint 4", e.DisplayName));
         }
 
+        [Fact]
+        public void UseEndpoints_WithGlobalEndpointRouteBuilderHasRoutes()
+        {
+            // Arrange
+            var services = CreateServices();
+
+            var app = new ApplicationBuilder(services);
+
+            var mockRouteBuilder = new Mock<IEndpointRouteBuilder>();
+            mockRouteBuilder.Setup(m => m.DataSources).Returns(new List<EndpointDataSource>());
+
+            var routeBuilder = mockRouteBuilder.Object;
+            app.Properties.Add("__GlobalEndpointRouteBuilder", routeBuilder);
+            app.UseRouting();
+
+            app.UseEndpoints(endpoints =>
+            {
+                endpoints.Map("/1", d => Task.CompletedTask).WithDisplayName("Test endpoint 1");
+            });
+
+            var requestDelegate = app.Build();
+
+            var endpointDataSource = Assert.Single(mockRouteBuilder.Object.DataSources);
+            Assert.Collection(endpointDataSource.Endpoints,
+                e => Assert.Equal("Test endpoint 1", e.DisplayName));
+
+            var routeOptions = app.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
+            Assert.Equal(mockRouteBuilder.Object.DataSources, routeOptions.Value.EndpointDataSources);
+        }
+
+        [Fact]
+        public void UseRouting_SetsEndpointRouteBuilder_IfGlobalOneExists()
+        {
+            // Arrange
+            var services = CreateServices();
+
+            var app = new ApplicationBuilder(services);
+
+            var routeBuilder = new Mock<IEndpointRouteBuilder>().Object;
+            app.Properties.Add("__GlobalEndpointRouteBuilder", routeBuilder);
+            app.UseRouting();
+
+            Assert.True(app.Properties.TryGetValue("__EndpointRouteBuilder", out var local));
+            Assert.True(app.Properties.TryGetValue("__GlobalEndpointRouteBuilder", out var global));
+            Assert.Same(local, global);
+        }
+
         private IServiceProvider CreateServices()
         {
             return CreateServices(matcherFactory: null);
-- 
GitLab