Skip to content

Commit

Permalink
HttpContext debugger display tweaks and fixes (dotnet#48321)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored May 23, 2023
1 parent 5026817 commit 3d55a2b
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 15 deletions.
11 changes: 7 additions & 4 deletions src/Extensions/Features/src/FeatureCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Http.Features;
/// <summary>
/// Default implementation for <see cref="IFeatureCollection"/>.
/// </summary>
[DebuggerDisplay("Count = {_features?.Count ?? 0}")]
[DebuggerDisplay("Count = {GetCount()}")]
[DebuggerTypeProxy(typeof(FeatureCollectionDebugView))]
public class FeatureCollection : IFeatureCollection
{
Expand Down Expand Up @@ -140,6 +140,9 @@ public void Set<TFeature>(TFeature? instance)
this[typeof(TFeature)] = instance;
}

// Used by the debugger. Count over enumerable is required to get the correct value.
private int GetCount() => this.Count();

private sealed class KeyComparer : IEqualityComparer<KeyValuePair<Type, object>>
{
public bool Equals(KeyValuePair<Type, object> x, KeyValuePair<Type, object> y)
Expand All @@ -153,11 +156,11 @@ public int GetHashCode(KeyValuePair<Type, object> obj)
}
}

private sealed class FeatureCollectionDebugView(FeatureCollection collection)
private sealed class FeatureCollectionDebugView(FeatureCollection features)
{
private readonly FeatureCollection _collection = collection;
private readonly FeatureCollection _features = features;

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<string, object>[] Items => _collection.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
public KeyValuePair<string, object>[] Items => _features.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
}
}
17 changes: 14 additions & 3 deletions src/Http/Http.Abstractions/src/HttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Http;

Expand Down Expand Up @@ -77,18 +79,18 @@ public abstract class HttpContext

private string DebuggerToString()
{
return $"{Request.Method} {Request.Path.Value} {Request.ContentType}"
+ $" StatusCode = {Response.StatusCode} {Response.ContentType}";
return HttpContextDebugFormatter.ContextToString(this, reasonPhrase: null);
}

private sealed class HttpContextDebugView(HttpContext context)
{
private readonly HttpContext _context = context;

// Hide server specific implementations, they combine IFeatureCollection and many feature interfaces.
public IFeatureCollection Features => _context.Features as FeatureCollection ?? new FeatureCollection(_context.Features);
public HttpContextFeatureDebugView Features => new HttpContextFeatureDebugView(_context.Features);
public HttpRequest Request => _context.Request;
public HttpResponse Response => _context.Response;
public Endpoint? Endpoint => _context.GetEndpoint();
public ConnectionInfo Connection => _context.Connection;
public WebSocketManager WebSockets => _context.WebSockets;
public ClaimsPrincipal User => _context.User;
Expand All @@ -98,4 +100,13 @@ private sealed class HttpContextDebugView(HttpContext context)
// The normal session property throws if accessed before/without the session middleware.
public ISession? Session => _context.Features.Get<ISessionFeature>()?.Session;
}

[DebuggerDisplay("Count = {Items.Length}")]
private sealed class HttpContextFeatureDebugView(IFeatureCollection features)
{
private readonly IFeatureCollection _features = features;

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<string, object>[] Items => _features.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
}
}
5 changes: 2 additions & 3 deletions src/Http/Http.Abstractions/src/HttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Globalization;
using System.IO.Pipelines;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Http;

Expand Down Expand Up @@ -154,8 +154,7 @@ public abstract class HttpRequest

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

private sealed class HttpRequestDebugView(HttpRequest request)
Expand Down
7 changes: 3 additions & 4 deletions src/Http/Http.Abstractions/src/HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Pipelines;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Http;

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

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

private sealed class HttpResponseDebugView(HttpResponse response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
<Compile Include="$(SharedSourceRoot)\UrlDecoder\UrlDecoder.cs" Link="UrlDecoder.cs" />
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
<Compile Include="$(SharedSourceRoot)Reroute.cs" />
<Compile Include="$(SharedSourceRoot)Debugger\HttpContextDebugFormatter.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Http/Http.Abstractions/src/WebSocketManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ private sealed class WebSocketManagerDebugView(WebSocketManager manager)
private readonly WebSocketManager _manager = manager;

public bool IsWebSocketRequest => _manager.IsWebSocketRequest;
public IList<string> WebSocketRequestedProtocols => _manager.WebSocketRequestedProtocols;
public IList<string> WebSocketRequestedProtocols => new List<string>(_manager.WebSocketRequestedProtocols);
}
}
11 changes: 11 additions & 0 deletions src/Http/Http/src/DefaultHttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Shared;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Http;

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

private string DebuggerToString()
{
// DebuggerToString is also on this type because this project has access to ReasonPhrases.
return HttpContextDebugFormatter.ContextToString(this, ReasonPhrases.GetReasonPhrase(Response.StatusCode));
}

struct FeatureInterfaces
{
public IItemsFeature? Items;
Expand Down
11 changes: 11 additions & 0 deletions src/Http/Http/src/Internal/DefaultHttpResponse.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.IO.Pipelines;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Shared;
using Microsoft.AspNetCore.WebUtilities;

namespace Microsoft.AspNetCore.Http;

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

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

internal string DebuggerToString()
{
// DebuggerToString is also on this type because this project has access to ReasonPhrases.
return HttpContextDebugFormatter.ResponseToString(this, ReasonPhrases.GetReasonPhrase(StatusCode));
}

struct FeatureInterfaces
{
public IHttpResponseFeature? Response;
Expand Down
1 change: 1 addition & 0 deletions src/Http/Http/src/Microsoft.AspNetCore.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Compile Include="..\..\Shared\CookieHeaderParserShared.cs" Link="Internal\CookieHeaderParserShared.cs" />
<Compile Include="$(SharedSourceRoot)HttpRuleParser.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)HttpParseResult.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Debugger\HttpContextDebugFormatter.cs" LinkBase="Shared" />
<Compile Include="..\..\WebUtilities\src\AspNetCoreTempDirectory.cs" LinkBase="Internal" />
<Compile Include="..\..\..\Shared\Dictionary\AdaptiveCapacityDictionary.cs" LinkBase="Internal" />
</ItemGroup>
Expand Down
58 changes: 58 additions & 0 deletions src/Shared/Debugger/HttpContextDebugFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.Shared;

internal static class HttpContextDebugFormatter
{
public static string ResponseToString(HttpResponse response, string? reasonPhrase)
{
var text = response.StatusCode.ToString(CultureInfo.InvariantCulture);
var resolvedReasonPhrase = ResolveReasonPhrase(response, reasonPhrase);
if (!string.IsNullOrEmpty(resolvedReasonPhrase))
{
text += $" {resolvedReasonPhrase}";
}
if (!string.IsNullOrEmpty(response.ContentType))
{
text += $" {response.ContentType}";
}
return text;
}

private static string? ResolveReasonPhrase(HttpResponse response, string? reasonPhrase)
{
return response.HttpContext.Features.Get<IHttpResponseFeature>()?.ReasonPhrase ?? reasonPhrase;
}

public static string RequestToString(HttpRequest request)
{
var text = $"{request.Method} {GetRequestUrl(request, includeQueryString: true)} {request.Protocol}";
if (!string.IsNullOrEmpty(request.ContentType))
{
text += $" {request.ContentType}";
}
return text;
}

public static string ContextToString(HttpContext context, string? reasonPhrase)
{
var text = $"{context.Request.Method} {GetRequestUrl(context.Request, includeQueryString: false)} {context.Response.StatusCode}";
var resolvedReasonPhrase = ResolveReasonPhrase(context.Response, reasonPhrase);
if (!string.IsNullOrEmpty(resolvedReasonPhrase))
{
text += $" {resolvedReasonPhrase}";
}

return text;
}

private static string GetRequestUrl(HttpRequest request, bool includeQueryString)
{
return $"{request.Scheme}://{request.Host.Value}{request.PathBase.Value}{request.Path.Value}{(includeQueryString ? request.QueryString.Value : string.Empty)}";
}
}

0 comments on commit 3d55a2b

Please sign in to comment.