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