Skip to content

Commit 32c457f

Browse files
authored
Always call into next middleware in WebApplicationBuilder (dotnet#34448)
1 parent e444c2e commit 32c457f

File tree

4 files changed

+121
-59
lines changed

4 files changed

+121
-59
lines changed

src/DefaultBuilder/src/WebApplication.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
using System.Collections.Generic;
64
using System.Reflection;
7-
using System.Threading;
8-
using System.Threading.Tasks;
95
using Microsoft.AspNetCore.Hosting;
106
using Microsoft.AspNetCore.Hosting.Server;
117
using Microsoft.AspNetCore.Hosting.Server.Features;
@@ -24,8 +20,6 @@ namespace Microsoft.AspNetCore.Builder
2420
/// </summary>
2521
public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
2622
{
27-
internal const string EndpointRouteBuilder = "__EndpointRouteBuilder";
28-
2923
private readonly IHost _host;
3024
private readonly List<EndpointDataSource> _dataSources = new();
3125

@@ -82,15 +76,6 @@ IServiceProvider IApplicationBuilder.ApplicationServices
8276
internal ICollection<EndpointDataSource> DataSources => _dataSources;
8377
ICollection<EndpointDataSource> IEndpointRouteBuilder.DataSources => DataSources;
8478

85-
internal IEndpointRouteBuilder RouteBuilder
86-
{
87-
get
88-
{
89-
Properties.TryGetValue(EndpointRouteBuilder, out var value);
90-
return (IEndpointRouteBuilder)value!;
91-
}
92-
}
93-
9479
internal ApplicationBuilder ApplicationBuilder { get; }
9580

9681
IServiceProvider IEndpointRouteBuilder.ServiceProvider => Services;

src/DefaultBuilder/src/WebApplicationBuilder.cs

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Builder
1717
/// </summary>
1818
public sealed class WebApplicationBuilder
1919
{
20+
private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
21+
2022
private readonly HostBuilder _hostBuilder = new();
2123
private readonly ConfigureHostBuilder _deferredHostBuilder;
2224
private readonly ConfigureWebHostBuilder _deferredWebHostBuilder;
@@ -112,6 +114,8 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
112114
{
113115
Debug.Assert(_builtApplication is not null);
114116

117+
var implicitRouting = false;
118+
115119
// The endpoints were already added on the outside
116120
if (_builtApplication.DataSources.Count > 0)
117121
{
@@ -120,49 +124,49 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
120124
// destination.UseRouting()
121125
// destination.Run(source)
122126
// destination.UseEndpoints()
123-
if (_builtApplication.RouteBuilder == null)
124-
{
125-
app.UseRouting();
126127

127-
// Copy the route data sources over to the destination pipeline, this should be available since we just called
128-
// UseRouting()
129-
var routes = (IEndpointRouteBuilder)app.Properties[WebApplication.EndpointRouteBuilder]!;
128+
// Copy endpoints to the IEndpointRouteBuilder created by an explicit call to UseRouting() if possible.
129+
var targetRouteBuilder = GetEndpointRouteBuilder(_builtApplication);
130130

131-
foreach (var ds in _builtApplication.DataSources)
132-
{
133-
routes.DataSources.Add(ds);
134-
}
131+
if (targetRouteBuilder is null)
132+
{
133+
// The app defined endpoints without calling UseRouting() explicitly, so call UseRouting() implicitly.
134+
app.UseRouting();
135135

136-
// Chain the execution of the source pipeline into the destination pipeline
137-
app.Use(next =>
138-
{
139-
_builtApplication.Run(next);
140-
return _builtApplication.BuildRequestDelegate();
141-
});
136+
// An implicitly created IEndpointRouteBuilder was addeded to app.Properties by the UseRouting() call above.
137+
targetRouteBuilder = GetEndpointRouteBuilder(app)!;
138+
implicitRouting = true;
139+
}
142140

143-
// Add a UseEndpoints at the end
144-
app.UseEndpoints(e => { });
141+
// Copy the endpoints to the explicitly or implicitly created IEndopintRouteBuilder.
142+
foreach (var ds in _builtApplication.DataSources)
143+
{
144+
targetRouteBuilder.DataSources.Add(ds);
145145
}
146-
else
146+
147+
// UseEndpoints consumes the DataSources immediately to populate CompositeEndpointDataSource via RouteOptions,
148+
// so it must be called after we copy the endpoints.
149+
if (!implicitRouting)
147150
{
148-
// Since we register routes into the source pipeline's route builder directly,
149-
// if the user called UseRouting, we need to copy the data sources
150-
foreach (var ds in _builtApplication.DataSources)
151-
{
152-
_builtApplication.RouteBuilder.DataSources.Add(ds);
153-
}
154-
155-
// We then implicitly call UseEndpoints at the end of the pipeline
151+
// UseRouting() was called explicitely, but we may still need to call UseEndpoints() implicitely at
152+
// the end of the pipeline.
156153
_builtApplication.UseEndpoints(_ => { });
157-
158-
// Wire the source pipeline to run in the destination pipeline
159-
app.Run(_builtApplication.BuildRequestDelegate());
160154
}
161155
}
162-
else
156+
157+
// Wire the source pipeline to run in the destination pipeline
158+
app.Use(next =>
159+
{
160+
_builtApplication.Run(next);
161+
return _builtApplication.BuildRequestDelegate();
162+
});
163+
164+
// Implicitly call UseEndpoints() at the end of the pipeline if UseRouting() was called implicitly.
165+
// We could add this to the end of _buildApplication instead if UseEndpoints() was not so picky about
166+
// being called with the same IApplicationBluilder instance as UseRouting().
167+
if (implicitRouting)
163168
{
164-
// Wire the source pipeline to run in the destination pipeline
165-
app.Run(_builtApplication.BuildRequestDelegate());
169+
app.UseEndpoints(_ => { });
166170
}
167171

168172
// Copy the properties to the destination app builder
@@ -223,6 +227,12 @@ private void ConfigureWebHost(IWebHostBuilder genericWebHostBuilder)
223227
_environment.ApplyEnvironmentSettings(genericWebHostBuilder);
224228
}
225229

230+
private static IEndpointRouteBuilder? GetEndpointRouteBuilder(IApplicationBuilder app)
231+
{
232+
app.Properties.TryGetValue(EndpointRouteBuilderKey, out var value);
233+
return (IEndpointRouteBuilder?)value;
234+
}
235+
226236
private class LoggingBuilder : ILoggingBuilder
227237
{
228238
public LoggingBuilder(IServiceCollection services)

src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
54
using System.Collections.Concurrent;
6-
using System.Collections.Generic;
75
using System.Diagnostics.Tracing;
8-
using System.IO;
9-
using System.Linq;
10-
using System.Threading;
11-
using System.Threading.Tasks;
126
using Microsoft.AspNetCore.Builder;
137
using Microsoft.AspNetCore.HostFiltering;
148
using Microsoft.AspNetCore.Hosting;
@@ -262,10 +256,7 @@ public async Task WebApplicationConfiguration_EnablesForwardedHeadersFromConfig(
262256
{
263257
var builder = WebApplication.CreateBuilder();
264258
builder.WebHost.UseTestServer();
265-
builder.Configuration.AddInMemoryCollection(new[]
266-
{
267-
new KeyValuePair<string, string>("FORWARDEDHEADERS_ENABLED", "true" ),
268-
});
259+
builder.Configuration["FORWARDEDHEADERS_ENABLED"] = "true";
269260
await using var app = builder.Build();
270261

271262
app.Run(context =>
@@ -275,6 +266,7 @@ public async Task WebApplicationConfiguration_EnablesForwardedHeadersFromConfig(
275266
});
276267

277268
await app.StartAsync();
269+
278270
var client = app.GetTestClient();
279271
client.DefaultRequestHeaders.Add("x-forwarded-proto", "https");
280272
var result = await client.GetAsync("http://localhost/");
@@ -335,6 +327,44 @@ public void WebApplication_CanResolveDefaultServicesFromServiceCollectionInCorre
335327
Assert.Equal(hostLifetimes1.Length, hostLifetimes0.Length);
336328
}
337329

330+
[Fact]
331+
public async Task WebApplication_CanCallUseRoutingWithouUseEndpoints()
332+
{
333+
var builder = WebApplication.CreateBuilder();
334+
builder.WebHost.UseTestServer();
335+
await using var app = builder.Build();
336+
337+
app.MapGet("/new", () => "new");
338+
339+
// Rewrite "/old" to "/new" before matching routes
340+
app.Use((context, next) =>
341+
{
342+
if (context.Request.Path == "/old")
343+
{
344+
context.Request.Path = "/new";
345+
}
346+
347+
return next(context);
348+
});
349+
350+
app.UseRouting();
351+
352+
await app.StartAsync();
353+
354+
var endpointDataSource = app.Services.GetRequiredService<EndpointDataSource>();
355+
356+
var newEndpoint = Assert.Single(endpointDataSource.Endpoints);
357+
var newRouteEndpoint = Assert.IsType<RouteEndpoint>(newEndpoint);
358+
Assert.Equal("/new", newRouteEndpoint.RoutePattern.RawText);
359+
360+
var client = app.GetTestClient();
361+
362+
var oldResult = await client.GetAsync("http://localhost/old");
363+
oldResult.EnsureSuccessStatusCode();
364+
365+
Assert.Equal("new", await oldResult.Content.ReadAsStringAsync());
366+
}
367+
338368
[Fact]
339369
public void WebApplicationCreate_RegistersEventSourceLogger()
340370
{
@@ -394,6 +424,27 @@ public void WebApplicationBuilder_CanSetWebRootPaths(bool useSetter)
394424
Assert.Equal(fullWebRootPath, app.Environment.WebRootPath);
395425
}
396426

427+
[Fact]
428+
public async Task WebApplicationBuilder_StartupFilterCanAddTerminalMiddleware()
429+
{
430+
var builder = WebApplication.CreateBuilder();
431+
builder.WebHost.UseTestServer();
432+
builder.Services.AddSingleton<IStartupFilter, TerminalMiddlewareStartupFilter>();
433+
await using var app = builder.Build();
434+
435+
app.MapGet("/defined", () => { });
436+
437+
await app.StartAsync();
438+
439+
var client = app.GetTestClient();
440+
441+
var definedResult = await client.GetAsync("http://localhost/defined");
442+
definedResult.EnsureSuccessStatusCode();
443+
444+
var terminalResult = await client.GetAsync("http://localhost/undefined");
445+
Assert.Equal(418, (int)terminalResult.StatusCode);
446+
}
447+
397448
private class CustomHostLifetime : IHostLifetime
398449
{
399450
public Task StopAsync(CancellationToken cancellationToken)
@@ -505,5 +556,21 @@ private class MockServerAddressesFeature : IServerAddressesFeature
505556
public bool PreferHostingUrls { get; set; }
506557
}
507558
}
559+
560+
private class TerminalMiddlewareStartupFilter : IStartupFilter
561+
{
562+
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
563+
{
564+
return app =>
565+
{
566+
next(app);
567+
app.Run(context =>
568+
{
569+
context.Response.StatusCode = 418; // I'm a teapot
570+
return Task.CompletedTask;
571+
});
572+
};
573+
}
574+
}
508575
}
509576
}

src/ProjectTemplates/Shared/AspNetProcess.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public async Task ContainsLinks(Page page)
151151
IHtmlAnchorElement anchor = (IHtmlAnchorElement)link;
152152
if (string.Equals(anchor.Protocol, "about:"))
153153
{
154-
Assert.True(anchor.PathName.EndsWith(expectedLink, StringComparison.Ordinal), $"Expected next link on {page.Url} to be {expectedLink} but it was {anchor.PathName}.");
154+
Assert.True(anchor.PathName.EndsWith(expectedLink, StringComparison.Ordinal), $"Expected next link on {page.Url} to be {expectedLink} but it was {anchor.PathName}: {html.Source.Text}");
155155
await AssertOk(anchor.PathName);
156156
}
157157
else

0 commit comments

Comments
 (0)