Skip to content

Commit 631f523

Browse files
TratcherJamesNK
andauthored
Add debug output to HttpContext and friends (#48293)
Co-authored-by: James Newton-King <[email protected]>
1 parent 7bad400 commit 631f523

File tree

21 files changed

+292
-7
lines changed

21 files changed

+292
-7
lines changed

src/Extensions/Features/src/FeatureCollection.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Linq;
89
using Microsoft.AspNetCore.Shared;
910

@@ -12,6 +13,8 @@ namespace Microsoft.AspNetCore.Http.Features;
1213
/// <summary>
1314
/// Default implementation for <see cref="IFeatureCollection"/>.
1415
/// </summary>
16+
[DebuggerDisplay("Count = {_features?.Count ?? 0}")]
17+
[DebuggerTypeProxy(typeof(FeatureCollectionDebugView))]
1518
public class FeatureCollection : IFeatureCollection
1619
{
1720
private static readonly KeyComparer FeatureKeyComparer = new KeyComparer();
@@ -149,4 +152,12 @@ public int GetHashCode(KeyValuePair<Type, object> obj)
149152
return obj.Key.GetHashCode();
150153
}
151154
}
155+
156+
private sealed class FeatureCollectionDebugView(FeatureCollection collection)
157+
{
158+
private readonly FeatureCollection _collection = collection;
159+
160+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
161+
public KeyValuePair<string, object>[] Items => _collection.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
162+
}
152163
}

src/Http/Http.Abstractions/src/ConnectionInfo.cs

Lines changed: 22 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.Net;
56
using System.Security.Cryptography.X509Certificates;
67

@@ -9,6 +10,8 @@ namespace Microsoft.AspNetCore.Http;
910
/// <summary>
1011
/// Represents the underlying connection for a request.
1112
/// </summary>
13+
[DebuggerDisplay("{DebuggerToString(),nq}")]
14+
[DebuggerTypeProxy(typeof(ConnectionInfoDebugView))]
1215
public abstract class ConnectionInfo
1316
{
1417
/// <summary>
@@ -56,4 +59,23 @@ public abstract class ConnectionInfo
5659
public virtual void RequestClose()
5760
{
5861
}
62+
63+
private string DebuggerToString()
64+
{
65+
var remoteEndpoint = RemoteIpAddress == null ? "(null)" : new IPEndPoint(RemoteIpAddress, RemotePort).ToString();
66+
var localEndpoint = LocalIpAddress == null ? "(null)" : new IPEndPoint(LocalIpAddress, LocalPort).ToString();
67+
return $"Id = {Id ?? "(null)"}, Remote = {remoteEndpoint}, Local = {localEndpoint}, ClientCertificate = {ClientCertificate?.Subject ?? "(null)"}";
68+
}
69+
70+
private sealed class ConnectionInfoDebugView(ConnectionInfo info)
71+
{
72+
private readonly ConnectionInfo _info = info;
73+
74+
public string Id => _info.Id;
75+
public IPAddress? RemoteIpAddress => _info.RemoteIpAddress;
76+
public int RemotePort => _info.RemotePort;
77+
public IPAddress? LocalIpAddress => _info.LocalIpAddress;
78+
public int LocalPort => _info.LocalPort;
79+
public X509Certificate2? ClientCertificate => _info.ClientCertificate;
80+
}
5981
}

src/Http/Http.Abstractions/src/FragmentString.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
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;
5+
46
namespace Microsoft.AspNetCore.Http;
57

68
/// <summary>
79
/// Provides correct handling for FragmentString value when needed to generate a URI string
810
/// </summary>
11+
[DebuggerDisplay("{Value}")]
912
public readonly struct FragmentString : IEquatable<FragmentString>
1013
{
1114
/// <summary>

src/Http/Http.Abstractions/src/HostString.cs

Lines changed: 2 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.Globalization;
56
using Microsoft.AspNetCore.Http.Abstractions;
67
using Microsoft.Extensions.Primitives;
@@ -11,6 +12,7 @@ namespace Microsoft.AspNetCore.Http;
1112
/// Represents the host portion of a URI can be used to construct URI's properly formatted and encoded for use in
1213
/// HTTP headers.
1314
/// </summary>
15+
[DebuggerDisplay("{Value}")]
1416
public readonly struct HostString : IEquatable<HostString>
1517
{
1618
private readonly string _value;

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

Lines changed: 27 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.Security.Claims;
56
using Microsoft.AspNetCore.Http.Features;
67

@@ -9,6 +10,8 @@ namespace Microsoft.AspNetCore.Http;
910
/// <summary>
1011
/// Encapsulates all HTTP-specific information about an individual HTTP request.
1112
/// </summary>
13+
[DebuggerDisplay("{DebuggerToString(),nq}")]
14+
[DebuggerTypeProxy(typeof(HttpContextDebugView))]
1215
public abstract class HttpContext
1316
{
1417
/// <summary>
@@ -71,4 +74,28 @@ public abstract class HttpContext
7174
/// Aborts the connection underlying this request.
7275
/// </summary>
7376
public abstract void Abort();
77+
78+
private string DebuggerToString()
79+
{
80+
return $"{Request.Method} {Request.Path.Value} {Request.ContentType}"
81+
+ $" StatusCode = {Response.StatusCode} {Response.ContentType}";
82+
}
83+
84+
private sealed class HttpContextDebugView(HttpContext context)
85+
{
86+
private readonly HttpContext _context = context;
87+
88+
// 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 HttpRequest Request => _context.Request;
91+
public HttpResponse Response => _context.Response;
92+
public ConnectionInfo Connection => _context.Connection;
93+
public WebSocketManager WebSockets => _context.WebSockets;
94+
public ClaimsPrincipal User => _context.User;
95+
public IDictionary<object, object?> Items => _context.Items;
96+
public CancellationToken RequestAborted => _context.RequestAborted;
97+
public string TraceIdentifier => _context.TraceIdentifier;
98+
// The normal session property throws if accessed before/without the session middleware.
99+
public ISession? Session => _context.Features.Get<ISessionFeature>()?.Session;
100+
}
74101
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
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;
5+
using System.Globalization;
46
using System.IO.Pipelines;
7+
using Microsoft.AspNetCore.Http.Features;
58
using Microsoft.AspNetCore.Routing;
69

710
namespace Microsoft.AspNetCore.Http;
811

912
/// <summary>
1013
/// Represents the incoming side of an individual HTTP request.
1114
/// </summary>
15+
[DebuggerDisplay("{DebuggerToString(),nq}")]
16+
[DebuggerTypeProxy(typeof(HttpRequestDebugView))]
1217
public abstract class HttpRequest
1318
{
1419
/// <summary>
@@ -146,4 +151,32 @@ public abstract class HttpRequest
146151
/// </summary>
147152
/// <returns>The collection of route values for this request.</returns>
148153
public virtual RouteValueDictionary RouteValues { get; set; } = null!;
154+
155+
private string DebuggerToString()
156+
{
157+
return $"{Protocol} {Method} {Scheme}://{Host.Value}{PathBase.Value}{Path.Value}{QueryString.Value} {ContentType}"
158+
+ $" Length = {ContentLength?.ToString(CultureInfo.InvariantCulture) ?? "(null)"}";
159+
}
160+
161+
private sealed class HttpRequestDebugView(HttpRequest request)
162+
{
163+
private readonly HttpRequest _request = request;
164+
165+
public string Method => _request.Method;
166+
public string Scheme => _request.Scheme;
167+
public bool IsHttps => _request.IsHttps;
168+
public HostString Host => _request.Host;
169+
public PathString PathBase => _request.PathBase;
170+
public PathString Path => _request.Path;
171+
public QueryString QueryString => _request.QueryString;
172+
public IQueryCollection Query => _request.Query;
173+
public string Protocol => _request.Protocol;
174+
public IHeaderDictionary Headers => _request.Headers;
175+
public IRequestCookieCollection Cookies => _request.Cookies;
176+
public long? ContentLength => _request.ContentLength;
177+
public string? ContentType => _request.ContentType;
178+
public bool HasFormContentType => _request.HasFormContentType;
179+
public IFormCollection? Form => _request.HttpContext.Features.Get<IFormFeature>()?.Form;
180+
public RouteValueDictionary RouteValues => _request.RouteValues;
181+
}
149182
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
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;
6+
using System.Globalization;
57
using System.IO.Pipelines;
68

79
namespace Microsoft.AspNetCore.Http;
810

911
/// <summary>
1012
/// Represents the outgoing side of an individual HTTP request.
1113
/// </summary>
14+
[DebuggerDisplay("{DebuggerToString(),nq}")]
15+
[DebuggerTypeProxy(typeof(HttpResponseDebugView))]
1216
public abstract class HttpResponse
1317
{
1418
private static readonly Func<object, Task> _callbackDelegate = callback => ((Func<Task>)callback)();
@@ -149,4 +153,21 @@ public abstract class HttpResponse
149153
/// </summary>
150154
/// <returns></returns>
151155
public virtual Task CompleteAsync() { throw new NotImplementedException(); }
156+
157+
private string DebuggerToString()
158+
{
159+
return $"StatusCode = {StatusCode}, HasStarted = {HasStarted},"
160+
+ $" Length = {ContentLength?.ToString(CultureInfo.InvariantCulture) ?? "(null)"} {ContentType}";
161+
}
162+
163+
private sealed class HttpResponseDebugView(HttpResponse response)
164+
{
165+
private readonly HttpResponse _response = response;
166+
167+
public int StatusCode => _response.StatusCode;
168+
public IHeaderDictionary Headers => _response.Headers;
169+
public long? ContentLength => _response.ContentLength;
170+
public string? ContentType => _response.ContentType;
171+
public bool HasStarted => _response.HasStarted;
172+
}
152173
}

src/Http/Http.Abstractions/src/PathString.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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.Globalization;
78
using System.Text;
@@ -14,6 +15,7 @@ namespace Microsoft.AspNetCore.Http;
1415
/// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string
1516
/// </summary>
1617
[TypeConverter(typeof(PathStringConverter))]
18+
[DebuggerDisplay("{Value}")]
1719
public readonly struct PathString : IEquatable<PathString>
1820
{
1921
internal const int StackAllocThreshold = 128;

src/Http/Http.Abstractions/src/QueryString.cs

Lines changed: 2 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 System.Text;
67
using System.Text.Encodings.Web;
@@ -11,6 +12,7 @@ namespace Microsoft.AspNetCore.Http;
1112
/// <summary>
1213
/// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string
1314
/// </summary>
15+
[DebuggerDisplay("{Value}")]
1416
public readonly struct QueryString : IEquatable<QueryString>
1517
{
1618
/// <summary>

src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Concurrent;
66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
8+
using System.Linq;
89
using System.Reflection.Metadata;
910
using System.Runtime.CompilerServices;
1011
using Microsoft.AspNetCore.Http.Abstractions;
@@ -18,6 +19,8 @@ namespace Microsoft.AspNetCore.Routing;
1819
/// <summary>
1920
/// An <see cref="IDictionary{String, Object}"/> type for route values.
2021
/// </summary>
22+
[DebuggerTypeProxy(typeof(RouteValueDictionaryDebugView))]
23+
[DebuggerDisplay("Count = {Count}")]
2124
public class RouteValueDictionary : IDictionary<string, object?>, IReadOnlyDictionary<string, object?>
2225
{
2326
// 4 is a good default capacity here because that leaves enough space for area/controller/action/id
@@ -862,4 +865,12 @@ internal static void ClearCache(Type[]? _)
862865
_propertyCache.Clear();
863866
}
864867
}
868+
869+
private sealed class RouteValueDictionaryDebugView(RouteValueDictionary dictionary)
870+
{
871+
private readonly RouteValueDictionary _dictionary = dictionary;
872+
873+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
874+
public KeyValuePair<string, object?>[] Items => _dictionary.ToArray();
875+
}
865876
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +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.Net.WebSockets;
56

67
namespace Microsoft.AspNetCore.Http;
78

89
/// <summary>
910
/// Manages the establishment of WebSocket connections for a specific HTTP request.
1011
/// </summary>
12+
[DebuggerDisplay("{DebuggerToString(),nq}")]
13+
[DebuggerTypeProxy(typeof(WebSocketManagerDebugView))]
1114
public abstract class WebSocketManager
1215
{
1316
/// <summary>
@@ -42,4 +45,21 @@ public virtual Task<WebSocket> AcceptWebSocketAsync()
4245
/// <param name="acceptContext"></param>
4346
/// <returns></returns>
4447
public virtual Task<WebSocket> AcceptWebSocketAsync(WebSocketAcceptContext acceptContext) => throw new NotImplementedException();
48+
49+
private string DebuggerToString()
50+
{
51+
return IsWebSocketRequest switch
52+
{
53+
false => "IsWebSocketRequest = False",
54+
true => $"IsWebSocketRequest = True, RequestedProtocols = {string.Join(",", WebSocketRequestedProtocols)}",
55+
};
56+
}
57+
58+
private sealed class WebSocketManagerDebugView(WebSocketManager manager)
59+
{
60+
private readonly WebSocketManager _manager = manager;
61+
62+
public bool IsWebSocketRequest => _manager.IsWebSocketRequest;
63+
public IList<string> WebSocketRequestedProtocols => _manager.WebSocketRequestedProtocols;
64+
}
4565
}

src/Http/Http/src/HeaderDictionary.cs

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

44
using System.Collections;
5+
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
68
using Microsoft.Extensions.Primitives;
79
using Microsoft.Net.Http.Headers;
810

@@ -11,6 +13,8 @@ namespace Microsoft.AspNetCore.Http;
1113
/// <summary>
1214
/// Represents a wrapper for RequestHeaders and ResponseHeaders.
1315
/// </summary>
16+
[DebuggerTypeProxy(typeof(HeaderDictionaryDebugView))]
17+
[DebuggerDisplay("Count = {Count}")]
1418
public class HeaderDictionary : IHeaderDictionary
1519
{
1620
private static readonly string[] EmptyKeys = Array.Empty<string>();
@@ -441,4 +445,12 @@ void IEnumerator.Reset()
441445
}
442446
}
443447
}
448+
449+
private sealed class HeaderDictionaryDebugView(HeaderDictionary dictionary)
450+
{
451+
private readonly HeaderDictionary _dictionary = dictionary;
452+
453+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
454+
public KeyValuePair<string, string>[] Items => _dictionary.Select(pair => new KeyValuePair<string, string>(pair.Key, pair.Value.ToString())).ToArray();
455+
}
444456
}

0 commit comments

Comments
 (0)