Skip to content

Commit 14a4f45

Browse files
authored
Improve WebApplication debugging (#48827)
1 parent 2806968 commit 14a4f45

File tree

10 files changed

+374
-24
lines changed

10 files changed

+374
-24
lines changed

src/DefaultBuilder/src/WebApplication.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using Microsoft.AspNetCore.Hosting;
67
using Microsoft.AspNetCore.Hosting.Server;
@@ -18,6 +19,8 @@ namespace Microsoft.AspNetCore.Builder;
1819
/// <summary>
1920
/// The web application used to configure the HTTP pipeline, and routes.
2021
/// </summary>
22+
[DebuggerDisplay("{DebuggerToString(),nq}")]
23+
[DebuggerTypeProxy(typeof(WebApplicationDebugView))]
2124
public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
2225
{
2326
internal const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
@@ -237,4 +240,39 @@ private void Listen(string? url)
237240
addresses.Clear();
238241
addresses.Add(url);
239242
}
243+
244+
private string DebuggerToString()
245+
{
246+
return $@"ApplicationName = ""{Environment.ApplicationName}"", IsRunning = {(IsRunning ? "true" : "false")}";
247+
}
248+
249+
// Web app is running if the app has been started and hasn't been stopped.
250+
private bool IsRunning => Lifetime.ApplicationStarted.IsCancellationRequested && !Lifetime.ApplicationStopped.IsCancellationRequested;
251+
252+
internal sealed class WebApplicationDebugView(WebApplication webApplication)
253+
{
254+
private readonly WebApplication _webApplication = webApplication;
255+
256+
public IServiceProvider Services => _webApplication.Services;
257+
public IConfiguration Configuration => _webApplication.Configuration;
258+
public IWebHostEnvironment Environment => _webApplication.Environment;
259+
public IHostApplicationLifetime Lifetime => _webApplication.Lifetime;
260+
public ILogger Logger => _webApplication.Logger;
261+
public string Urls => string.Join(", ", _webApplication.Urls);
262+
public IReadOnlyList<Endpoint> Endpoints => _webApplication.Services.GetRequiredService<EndpointDataSource>().Endpoints;
263+
public bool IsRunning => _webApplication.IsRunning;
264+
public IList<string>? Middleware
265+
{
266+
get
267+
{
268+
if (_webApplication.Properties.TryGetValue("__MiddlewareDescriptions", out var value) &&
269+
value is IList<string> descriptions)
270+
{
271+
return descriptions;
272+
}
273+
274+
throw new NotSupportedException("Unable to get configured middleware.");
275+
}
276+
}
277+
}
240278
}

src/DefaultBuilder/src/WebApplicationBuilder.cs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.AspNetCore.Authentication;
77
using Microsoft.AspNetCore.Authorization;
88
using Microsoft.AspNetCore.Hosting;
9+
using Microsoft.AspNetCore.Http;
910
using Microsoft.Extensions.Configuration;
1011
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.Extensions.Hosting;
@@ -386,18 +387,17 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
386387
}
387388

388389
// Wire the source pipeline to run in the destination pipeline
389-
app.Use(next =>
390-
{
391-
_builtApplication.Run(next);
392-
return _builtApplication.BuildRequestDelegate();
393-
});
390+
var wireSourcePipeline = new WireSourcePipeline(_builtApplication);
391+
app.Use(wireSourcePipeline.CreateMiddleware);
394392

395393
if (_builtApplication.DataSources.Count > 0)
396394
{
397395
// We don't know if user code called UseEndpoints(), so we will call it just in case, UseEndpoints() will ignore duplicate DataSources
398396
app.UseEndpoints(_ => { });
399397
}
400398

399+
MergeMiddlewareDescriptions(app);
400+
401401
// Copy the properties to the destination app builder
402402
foreach (var item in _builtApplication.Properties)
403403
{
@@ -416,4 +416,45 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
416416

417417
void IHostApplicationBuilder.ConfigureContainer<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory, Action<TContainerBuilder>? configure) =>
418418
_hostApplicationBuilder.ConfigureContainer(factory, configure);
419+
420+
private void MergeMiddlewareDescriptions(IApplicationBuilder app)
421+
{
422+
// A user's app builds up a list of middleware. Then when the WebApplication is started, middleware is automatically added
423+
// if it is required. For example, the app has mapped endpoints but hasn't configured UseRouting/UseEndpoints.
424+
//
425+
// This method updates the middleware descriptions to include automatically added middleware.
426+
// The app's middleware list is inserted into the new pipeline to create the best representation possible of the middleware pipeline.
427+
//
428+
// If the debugger isn't attached then there won't be middleware description collections in the properties and this does nothing.
429+
430+
Debug.Assert(_builtApplication is not null);
431+
432+
const string MiddlewareDescriptionsKey = "__MiddlewareDescriptions";
433+
if (_builtApplication.Properties.TryGetValue(MiddlewareDescriptionsKey, out var sourceValue) &&
434+
app.Properties.TryGetValue(MiddlewareDescriptionsKey, out var destinationValue) &&
435+
sourceValue is List<string> sourceDescriptions &&
436+
destinationValue is List<string> destinationDescriptions)
437+
{
438+
var wireUpIndex = destinationDescriptions.IndexOf(typeof(WireSourcePipeline).FullName!);
439+
if (wireUpIndex != -1)
440+
{
441+
destinationDescriptions.RemoveAt(wireUpIndex);
442+
destinationDescriptions.InsertRange(wireUpIndex, sourceDescriptions);
443+
444+
_builtApplication.Properties[MiddlewareDescriptionsKey] = destinationDescriptions;
445+
}
446+
}
447+
}
448+
449+
// This type exists so the place where the source pipeline is wired into the destination pipeline can be identified.
450+
private sealed class WireSourcePipeline(IApplicationBuilder builtApplication)
451+
{
452+
private readonly IApplicationBuilder _builtApplication = builtApplication;
453+
454+
public RequestDelegate CreateMiddleware(RequestDelegate next)
455+
{
456+
_builtApplication.Run(next);
457+
return _builtApplication.Build();
458+
}
459+
}
419460
}

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

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,7 @@ public void WebApplicationBuilder_ThrowsFromExtensionMethodsNotSupportedByHostAn
17291729
public async Task EndpointDataSourceOnlyAddsOnce(CreateBuilderFunc createBuilder)
17301730
{
17311731
var builder = createBuilder();
1732+
builder.WebHost.UseTestServer();
17321733
await using var app = builder.Build();
17331734

17341735
app.UseRouting();
@@ -1883,6 +1884,7 @@ public async Task BranchingPipelineHasOwnRoutes(CreateBuilderFunc createBuilder)
18831884
public async Task PropertiesPreservedFromInnerApplication(CreateBuilderFunc createBuilder)
18841885
{
18851886
var builder = createBuilder();
1887+
builder.WebHost.UseTestServer();
18861888
builder.Services.AddSingleton<IStartupFilter, PropertyFilter>();
18871889
await using var app = builder.Build();
18881890

@@ -2526,6 +2528,141 @@ public async Task UsingCreateSlimBuilderWorksIfRegexConstraintAddedViaAddRouting
25262528
Assert.Equal("RegexRoute", chosenRoute);
25272529
}
25282530

2531+
private sealed class TestDebugger : IDebugger
2532+
{
2533+
private bool _isAttached;
2534+
public TestDebugger(bool isAttached) => _isAttached = isAttached;
2535+
public bool IsAttached => _isAttached;
2536+
}
2537+
2538+
[Fact]
2539+
public void UseMiddleware_DebugView_HasMiddleware()
2540+
{
2541+
var builder = WebApplication.CreateBuilder();
2542+
builder.Services.AddSingleton<IDebugger>(new TestDebugger(true));
2543+
2544+
var app = builder.Build();
2545+
2546+
app.UseMiddleware<MiddlewareWithInterface>();
2547+
app.UseAuthentication();
2548+
app.Use(next =>
2549+
{
2550+
return next;
2551+
});
2552+
2553+
var debugView = new WebApplication.WebApplicationDebugView(app);
2554+
2555+
// Contains three strings:
2556+
// 1. Middleware that implements IMiddleware from app.UseMiddleware<T>()
2557+
// 2. AuthenticationMiddleware type from app.UseAuthentication()
2558+
// 3. Generated delegate name from app.Use(...)
2559+
Assert.Collection(debugView.Middleware,
2560+
m => Assert.Equal(typeof(MiddlewareWithInterface).FullName, m),
2561+
m => Assert.Equal("Microsoft.AspNetCore.Authentication.AuthenticationMiddleware", m),
2562+
m =>
2563+
{
2564+
Assert.Contains(nameof(UseMiddleware_DebugView_HasMiddleware), m);
2565+
Assert.DoesNotContain(nameof(RequestDelegate), m);
2566+
});
2567+
}
2568+
2569+
[Fact]
2570+
public void NoDebugger_DebugView_NoMiddleware()
2571+
{
2572+
var builder = WebApplication.CreateBuilder();
2573+
builder.Services.AddSingleton<IDebugger>(new TestDebugger(false));
2574+
2575+
var app = builder.Build();
2576+
2577+
app.UseMiddleware<MiddlewareWithInterface>();
2578+
app.UseAuthentication();
2579+
app.Use(next =>
2580+
{
2581+
return next;
2582+
});
2583+
2584+
var debugView = new WebApplication.WebApplicationDebugView(app);
2585+
2586+
Assert.Throws<NotSupportedException>(() => debugView.Middleware);
2587+
}
2588+
2589+
[Fact]
2590+
public async Task UseMiddleware_HasEndpointsAndAuth_Run_DebugView_HasAutomaticMiddleware()
2591+
{
2592+
var builder = WebApplication.CreateBuilder();
2593+
builder.WebHost.UseTestServer();
2594+
builder.Services.AddAuthenticationCore();
2595+
builder.Services.AddAuthorization();
2596+
builder.Services.AddSingleton<IDebugger>(new TestDebugger(true));
2597+
2598+
await using var app = builder.Build();
2599+
2600+
app.UseMiddleware<MiddlewareWithInterface>();
2601+
app.MapGet("/hello", () => "hello world");
2602+
2603+
// Starting the app automatically adds middleware as needed.
2604+
_ = app.RunAsync();
2605+
2606+
var debugView = new WebApplication.WebApplicationDebugView(app);
2607+
2608+
Assert.Collection(debugView.Middleware,
2609+
m => Assert.Equal("Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware", m),
2610+
m => Assert.Equal("Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware", m),
2611+
m => Assert.Equal("Microsoft.AspNetCore.Authentication.AuthenticationMiddleware", m),
2612+
m => Assert.Equal("Microsoft.AspNetCore.Authorization.AuthorizationMiddleware", m),
2613+
m => Assert.Equal(typeof(MiddlewareWithInterface).FullName, m),
2614+
m => Assert.Equal("Microsoft.AspNetCore.Routing.EndpointMiddleware", m));
2615+
}
2616+
2617+
[Fact]
2618+
public async Task NoMiddleware_Run_DebugView_HasAutomaticMiddleware()
2619+
{
2620+
var builder = WebApplication.CreateBuilder();
2621+
builder.WebHost.UseTestServer();
2622+
builder.Services.AddSingleton<IDebugger>(new TestDebugger(true));
2623+
2624+
await using var app = builder.Build();
2625+
2626+
// Starting the app automatically adds middleware as needed.
2627+
_ = app.RunAsync();
2628+
2629+
var debugView = new WebApplication.WebApplicationDebugView(app);
2630+
2631+
Assert.Collection(debugView.Middleware,
2632+
m => Assert.Equal("Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware", m));
2633+
}
2634+
2635+
[Fact]
2636+
public void NestedMiddleware_DebugView_OnlyContainsTopLevelMiddleware()
2637+
{
2638+
var builder = WebApplication.CreateBuilder();
2639+
builder.Services.AddSingleton<IDebugger>(new TestDebugger(true));
2640+
2641+
var app = builder.Build();
2642+
2643+
app.MapWhen(c => true, nested =>
2644+
{
2645+
nested.UseStatusCodePages();
2646+
});
2647+
app.UseWhen(c => false, nested =>
2648+
{
2649+
nested.UseDeveloperExceptionPage();
2650+
});
2651+
app.UseExceptionHandler();
2652+
2653+
var debugView = new WebApplication.WebApplicationDebugView(app);
2654+
2655+
Assert.Equal(3, debugView.Middleware.Count);
2656+
}
2657+
2658+
private class MiddlewareWithInterface : IMiddleware
2659+
{
2660+
public Task InvokeAsync(HttpContext context, RequestDelegate next)
2661+
{
2662+
throw new NotImplementedException();
2663+
}
2664+
}
2665+
25292666
private class UberHandler : AuthenticationHandler<AuthenticationSchemeOptions>
25302667
{
25312668
public UberHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { }

src/Hosting/Hosting/src/Internal/ApplicationLifetime.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using Microsoft.Extensions.Hosting;
56
using Microsoft.Extensions.Logging;
67

@@ -9,6 +10,9 @@ namespace Microsoft.AspNetCore.Hosting;
910
/// <summary>
1011
/// Allows consumers to perform cleanup during a graceful shutdown.
1112
/// </summary>
13+
[DebuggerDisplay("ApplicationStarted = {ApplicationStarted.IsCancellationRequested}, " +
14+
"ApplicationStopping = {ApplicationStopping.IsCancellationRequested}, " +
15+
"ApplicationStopped = {ApplicationStopped.IsCancellationRequested}")]
1216
#pragma warning disable CS0618 // Type or member is obsolete
1317
internal sealed class ApplicationLifetime : IApplicationLifetime, Extensions.Hosting.IApplicationLifetime, IHostApplicationLifetime
1418
#pragma warning restore CS0618 // Type or member is obsolete

src/Hosting/Hosting/src/Internal/HostingEnvironment.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using Microsoft.Extensions.FileProviders;
56

67
namespace Microsoft.AspNetCore.Hosting;
8+
9+
[DebuggerDisplay("ApplicationName = {ApplicationName}, EnvironmentName = {EnvironmentName}")]
710
#pragma warning disable CS0618 // Type or member is obsolete
811
internal sealed class HostingEnvironment : IHostingEnvironment, Extensions.Hosting.IHostingEnvironment, IWebHostEnvironment
912
#pragma warning restore CS0618 // Type or member is obsolete

0 commit comments

Comments
 (0)