Skip to content

Commit 3d55a2b

Browse files
authored
HttpContext debugger display tweaks and fixes (#48321)
1 parent 5026817 commit 3d55a2b

File tree

10 files changed

+109
-15
lines changed

10 files changed

+109
-15
lines changed

src/Extensions/Features/src/FeatureCollection.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Http.Features;
1313
/// <summary>
1414
/// Default implementation for <see cref="IFeatureCollection"/>.
1515
/// </summary>
16-
[DebuggerDisplay("Count = {_features?.Count ?? 0}")]
16+
[DebuggerDisplay("Count = {GetCount()}")]
1717
[DebuggerTypeProxy(typeof(FeatureCollectionDebugView))]
1818
public class FeatureCollection : IFeatureCollection
1919
{
@@ -140,6 +140,9 @@ public void Set<TFeature>(TFeature? instance)
140140
this[typeof(TFeature)] = instance;
141141
}
142142

143+
// Used by the debugger. Count over enumerable is required to get the correct value.
144+
private int GetCount() => this.Count();
145+
143146
private sealed class KeyComparer : IEqualityComparer<KeyValuePair<Type, object>>
144147
{
145148
public bool Equals(KeyValuePair<Type, object> x, KeyValuePair<Type, object> y)
@@ -153,11 +156,11 @@ public int GetHashCode(KeyValuePair<Type, object> obj)
153156
}
154157
}
155158

156-
private sealed class FeatureCollectionDebugView(FeatureCollection collection)
159+
private sealed class FeatureCollectionDebugView(FeatureCollection features)
157160
{
158-
private readonly FeatureCollection _collection = collection;
161+
private readonly FeatureCollection _features = features;
159162

160163
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
161-
public KeyValuePair<string, object>[] Items => _collection.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
164+
public KeyValuePair<string, object>[] Items => _features.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
162165
}
163166
}

src/Http/Http.Abstractions/src/HttpContext.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Linq;
56
using System.Security.Claims;
67
using Microsoft.AspNetCore.Http.Features;
8+
using Microsoft.AspNetCore.Shared;
79

810
namespace Microsoft.AspNetCore.Http;
911

@@ -77,18 +79,18 @@ public abstract class HttpContext
7779

7880
private string DebuggerToString()
7981
{
80-
return $"{Request.Method} {Request.Path.Value} {Request.ContentType}"
81-
+ $" StatusCode = {Response.StatusCode} {Response.ContentType}";
82+
return HttpContextDebugFormatter.ContextToString(this, reasonPhrase: null);
8283
}
8384

8485
private sealed class HttpContextDebugView(HttpContext context)
8586
{
8687
private readonly HttpContext _context = context;
8788

8889
// Hide server specific implementations, they combine IFeatureCollection and many feature interfaces.
89-
public IFeatureCollection Features => _context.Features as FeatureCollection ?? new FeatureCollection(_context.Features);
90+
public HttpContextFeatureDebugView Features => new HttpContextFeatureDebugView(_context.Features);
9091
public HttpRequest Request => _context.Request;
9192
public HttpResponse Response => _context.Response;
93+
public Endpoint? Endpoint => _context.GetEndpoint();
9294
public ConnectionInfo Connection => _context.Connection;
9395
public WebSocketManager WebSockets => _context.WebSockets;
9496
public ClaimsPrincipal User => _context.User;
@@ -98,4 +100,13 @@ private sealed class HttpContextDebugView(HttpContext context)
98100
// The normal session property throws if accessed before/without the session middleware.
99101
public ISession? Session => _context.Features.Get<ISessionFeature>()?.Session;
100102
}
103+
104+
[DebuggerDisplay("Count = {Items.Length}")]
105+
private sealed class HttpContextFeatureDebugView(IFeatureCollection features)
106+
{
107+
private readonly IFeatureCollection _features = features;
108+
109+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
110+
public KeyValuePair<string, object>[] Items => _features.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
111+
}
101112
}

src/Http/Http.Abstractions/src/HttpRequest.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5-
using System.Globalization;
65
using System.IO.Pipelines;
76
using Microsoft.AspNetCore.Http.Features;
87
using Microsoft.AspNetCore.Routing;
8+
using Microsoft.AspNetCore.Shared;
99

1010
namespace Microsoft.AspNetCore.Http;
1111

@@ -154,8 +154,7 @@ public abstract class HttpRequest
154154

155155
private string DebuggerToString()
156156
{
157-
return $"{Protocol} {Method} {Scheme}://{Host.Value}{PathBase.Value}{Path.Value}{QueryString.Value} {ContentType}"
158-
+ $" Length = {ContentLength?.ToString(CultureInfo.InvariantCulture) ?? "(null)"}";
157+
return HttpContextDebugFormatter.RequestToString(this);
159158
}
160159

161160
private sealed class HttpRequestDebugView(HttpRequest request)

src/Http/Http.Abstractions/src/HttpResponse.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
6-
using System.Globalization;
76
using System.IO.Pipelines;
7+
using Microsoft.AspNetCore.Shared;
88

99
namespace Microsoft.AspNetCore.Http;
1010

@@ -154,10 +154,9 @@ public abstract class HttpResponse
154154
/// <returns></returns>
155155
public virtual Task CompleteAsync() { throw new NotImplementedException(); }
156156

157-
private string DebuggerToString()
157+
internal string DebuggerToString()
158158
{
159-
return $"StatusCode = {StatusCode}, HasStarted = {HasStarted},"
160-
+ $" Length = {ContentLength?.ToString(CultureInfo.InvariantCulture) ?? "(null)"} {ContentType}";
159+
return HttpContextDebugFormatter.ResponseToString(this, reasonPhrase: null);
161160
}
162161

163162
private sealed class HttpResponseDebugView(HttpResponse response)

src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
2727
<Compile Include="$(SharedSourceRoot)\UrlDecoder\UrlDecoder.cs" Link="UrlDecoder.cs" />
2828
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
2929
<Compile Include="$(SharedSourceRoot)Reroute.cs" />
30+
<Compile Include="$(SharedSourceRoot)Debugger\HttpContextDebugFormatter.cs" LinkBase="Shared" />
3031
</ItemGroup>
3132

3233
<ItemGroup>

src/Http/Http.Abstractions/src/WebSocketManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,6 @@ private sealed class WebSocketManagerDebugView(WebSocketManager manager)
6060
private readonly WebSocketManager _manager = manager;
6161

6262
public bool IsWebSocketRequest => _manager.IsWebSocketRequest;
63-
public IList<string> WebSocketRequestedProtocols => _manager.WebSocketRequestedProtocols;
63+
public IList<string> WebSocketRequestedProtocols => new List<string>(_manager.WebSocketRequestedProtocols);
6464
}
6565
}

src/Http/Http/src/DefaultHttpContext.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel;
5+
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Security.Claims;
78
using Microsoft.AspNetCore.Http.Features;
89
using Microsoft.AspNetCore.Http.Features.Authentication;
10+
using Microsoft.AspNetCore.Shared;
11+
using Microsoft.AspNetCore.WebUtilities;
912
using Microsoft.Extensions.DependencyInjection;
1013

1114
namespace Microsoft.AspNetCore.Http;
1215

1316
/// <summary>
1417
/// Represents an implementation of the HTTP Context class.
1518
/// </summary>
19+
// DebuggerDisplayAttribute is inherited but we're replacing it on this implementation to include reason phrase.
20+
[DebuggerDisplay("{DebuggerToString(),nq}")]
1621
public sealed class DefaultHttpContext : HttpContext
1722
{
1823
// The initial size of the feature collection when using the default constructor; based on number of common features
@@ -236,6 +241,12 @@ private static void ThrowContextDisposed()
236241
throw new ObjectDisposedException(nameof(HttpContext), $"Request has finished and {nameof(HttpContext)} disposed.");
237242
}
238243

244+
private string DebuggerToString()
245+
{
246+
// DebuggerToString is also on this type because this project has access to ReasonPhrases.
247+
return HttpContextDebugFormatter.ContextToString(this, ReasonPhrases.GetReasonPhrase(Response.StatusCode));
248+
}
249+
239250
struct FeatureInterfaces
240251
{
241252
public IItemsFeature? Items;

src/Http/Http/src/Internal/DefaultHttpResponse.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
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.IO.Pipelines;
56
using Microsoft.AspNetCore.Http.Features;
7+
using Microsoft.AspNetCore.Shared;
8+
using Microsoft.AspNetCore.WebUtilities;
69

710
namespace Microsoft.AspNetCore.Http;
811

12+
// DebuggerDisplayAttribute is inherited but we're replacing it on this implementation to include reason phrase.
13+
[DebuggerDisplay("{DebuggerToString(),nq}")]
914
internal sealed class DefaultHttpResponse : HttpResponse
1015
{
1116
// Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
@@ -159,6 +164,12 @@ public override Task StartAsync(CancellationToken cancellationToken = default)
159164

160165
public override Task CompleteAsync() => HttpResponseBodyFeature.CompleteAsync();
161166

167+
internal string DebuggerToString()
168+
{
169+
// DebuggerToString is also on this type because this project has access to ReasonPhrases.
170+
return HttpContextDebugFormatter.ResponseToString(this, ReasonPhrases.GetReasonPhrase(StatusCode));
171+
}
172+
162173
struct FeatureInterfaces
163174
{
164175
public IHttpResponseFeature? Response;

src/Http/Http/src/Microsoft.AspNetCore.Http.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<Compile Include="..\..\Shared\CookieHeaderParserShared.cs" Link="Internal\CookieHeaderParserShared.cs" />
2020
<Compile Include="$(SharedSourceRoot)HttpRuleParser.cs" LinkBase="Shared" />
2121
<Compile Include="$(SharedSourceRoot)HttpParseResult.cs" LinkBase="Shared" />
22+
<Compile Include="$(SharedSourceRoot)Debugger\HttpContextDebugFormatter.cs" LinkBase="Shared" />
2223
<Compile Include="..\..\WebUtilities\src\AspNetCoreTempDirectory.cs" LinkBase="Internal" />
2324
<Compile Include="..\..\..\Shared\Dictionary\AdaptiveCapacityDictionary.cs" LinkBase="Internal" />
2425
</ItemGroup>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Http.Features;
7+
8+
namespace Microsoft.AspNetCore.Shared;
9+
10+
internal static class HttpContextDebugFormatter
11+
{
12+
public static string ResponseToString(HttpResponse response, string? reasonPhrase)
13+
{
14+
var text = response.StatusCode.ToString(CultureInfo.InvariantCulture);
15+
var resolvedReasonPhrase = ResolveReasonPhrase(response, reasonPhrase);
16+
if (!string.IsNullOrEmpty(resolvedReasonPhrase))
17+
{
18+
text += $" {resolvedReasonPhrase}";
19+
}
20+
if (!string.IsNullOrEmpty(response.ContentType))
21+
{
22+
text += $" {response.ContentType}";
23+
}
24+
return text;
25+
}
26+
27+
private static string? ResolveReasonPhrase(HttpResponse response, string? reasonPhrase)
28+
{
29+
return response.HttpContext.Features.Get<IHttpResponseFeature>()?.ReasonPhrase ?? reasonPhrase;
30+
}
31+
32+
public static string RequestToString(HttpRequest request)
33+
{
34+
var text = $"{request.Method} {GetRequestUrl(request, includeQueryString: true)} {request.Protocol}";
35+
if (!string.IsNullOrEmpty(request.ContentType))
36+
{
37+
text += $" {request.ContentType}";
38+
}
39+
return text;
40+
}
41+
42+
public static string ContextToString(HttpContext context, string? reasonPhrase)
43+
{
44+
var text = $"{context.Request.Method} {GetRequestUrl(context.Request, includeQueryString: false)} {context.Response.StatusCode}";
45+
var resolvedReasonPhrase = ResolveReasonPhrase(context.Response, reasonPhrase);
46+
if (!string.IsNullOrEmpty(resolvedReasonPhrase))
47+
{
48+
text += $" {resolvedReasonPhrase}";
49+
}
50+
51+
return text;
52+
}
53+
54+
private static string GetRequestUrl(HttpRequest request, bool includeQueryString)
55+
{
56+
return $"{request.Scheme}://{request.Host.Value}{request.PathBase.Value}{request.Path.Value}{(includeQueryString ? request.QueryString.Value : string.Empty)}";
57+
}
58+
}

0 commit comments

Comments
 (0)