Skip to content

Add ability to modify UseRouting/UseEndpoint behavior for WebApplicationBuilder #35336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/DefaultBuilder/src/WebApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteB
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>
Expand Down Expand Up @@ -170,15 +174,21 @@ public void Run(string? url = null)
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)
{
ApplicationBuilder.Use(middleware);
return this;
}

IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => ApplicationBuilder.New();
IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => ((IApplicationBuilder)this).New();

private void Listen(string? url)
{
Expand Down
74 changes: 32 additions & 42 deletions src/DefaultBuilder/src/WebApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -187,48 +185,39 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
{
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
Copy link
Member

@davidfowl davidfowl Aug 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have coverage for this? File an issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}
}

Expand All @@ -239,11 +228,9 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
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(_ => { });
}

Expand All @@ -252,12 +239,15 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
{
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
Expand Down
Loading