Skip to content

Add debug output to HttpContext and friends #48293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Extensions/Features/src/FeatureCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Shared;

Expand All @@ -12,6 +13,8 @@ namespace Microsoft.AspNetCore.Http.Features;
/// <summary>
/// Default implementation for <see cref="IFeatureCollection"/>.
/// </summary>
[DebuggerDisplay("Count = {_features?.Count ?? 0}")]
[DebuggerTypeProxy(typeof(FeatureCollectionDebugView))]
public class FeatureCollection : IFeatureCollection
{
private static readonly KeyComparer FeatureKeyComparer = new KeyComparer();
Expand Down Expand Up @@ -149,4 +152,12 @@ public int GetHashCode(KeyValuePair<Type, object> obj)
return obj.Key.GetHashCode();
}
}

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

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<string, object>[] Items => _collection.Select(pair => new KeyValuePair<string, object>(pair.Key.FullName ?? string.Empty, pair.Value)).ToArray();
}
}
22 changes: 22 additions & 0 deletions src/Http/Http.Abstractions/src/ConnectionInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Net;
using System.Security.Cryptography.X509Certificates;

Expand All @@ -9,6 +10,8 @@ namespace Microsoft.AspNetCore.Http;
/// <summary>
/// Represents the underlying connection for a request.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[DebuggerTypeProxy(typeof(ConnectionInfoDebugView))]
public abstract class ConnectionInfo
{
/// <summary>
Expand Down Expand Up @@ -56,4 +59,23 @@ public abstract class ConnectionInfo
public virtual void RequestClose()
{
}

private string DebuggerToString()
{
var remoteEndpoint = RemoteIpAddress == null ? "(null)" : new IPEndPoint(RemoteIpAddress, RemotePort).ToString();
var localEndpoint = LocalIpAddress == null ? "(null)" : new IPEndPoint(LocalIpAddress, LocalPort).ToString();
return $"Id = {Id ?? "(null)"}, Remote = {remoteEndpoint}, Local = {localEndpoint}, ClientCertificate = {ClientCertificate?.Subject ?? "(null)"}";
}

private sealed class ConnectionInfoDebugView(ConnectionInfo info)
{
private readonly ConnectionInfo _info = info;

public string Id => _info.Id;
public IPAddress? RemoteIpAddress => _info.RemoteIpAddress;
public int RemotePort => _info.RemotePort;
public IPAddress? LocalIpAddress => _info.LocalIpAddress;
public int LocalPort => _info.LocalPort;
public X509Certificate2? ClientCertificate => _info.ClientCertificate;
}
}
3 changes: 3 additions & 0 deletions src/Http/Http.Abstractions/src/FragmentString.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// 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;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Provides correct handling for FragmentString value when needed to generate a URI string
/// </summary>
[DebuggerDisplay("{Value}")]
public readonly struct FragmentString : IEquatable<FragmentString>
{
/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Http/Http.Abstractions/src/HostString.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Globalization;
using Microsoft.AspNetCore.Http.Abstractions;
using Microsoft.Extensions.Primitives;
Expand All @@ -11,6 +12,7 @@ namespace Microsoft.AspNetCore.Http;
/// Represents the host portion of a URI can be used to construct URI's properly formatted and encoded for use in
/// HTTP headers.
/// </summary>
[DebuggerDisplay("{Value}")]
public readonly struct HostString : IEquatable<HostString>
{
private readonly string _value;
Expand Down
27 changes: 27 additions & 0 deletions src/Http/Http.Abstractions/src/HttpContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Security.Claims;
using Microsoft.AspNetCore.Http.Features;

Expand All @@ -9,6 +10,8 @@ namespace Microsoft.AspNetCore.Http;
/// <summary>
/// Encapsulates all HTTP-specific information about an individual HTTP request.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[DebuggerTypeProxy(typeof(HttpContextDebugView))]
public abstract class HttpContext
{
/// <summary>
Expand Down Expand Up @@ -71,4 +74,28 @@ public abstract class HttpContext
/// Aborts the connection underlying this request.
/// </summary>
public abstract void Abort();

private string DebuggerToString()
{
return $"{Request.Method} {Request.Path.Value} {Request.ContentType}"
+ $" StatusCode = {Response.StatusCode} {Response.ContentType}";
}

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 HttpRequest Request => _context.Request;
public HttpResponse Response => _context.Response;
public ConnectionInfo Connection => _context.Connection;
public WebSocketManager WebSockets => _context.WebSockets;
public ClaimsPrincipal User => _context.User;
public IDictionary<object, object?> Items => _context.Items;
public CancellationToken RequestAborted => _context.RequestAborted;
public string TraceIdentifier => _context.TraceIdentifier;
// The normal session property throws if accessed before/without the session middleware.
public ISession? Session => _context.Features.Get<ISessionFeature>()?.Session;
}
}
33 changes: 33 additions & 0 deletions src/Http/Http.Abstractions/src/HttpRequest.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
// 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.Globalization;
using System.IO.Pipelines;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Represents the incoming side of an individual HTTP request.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[DebuggerTypeProxy(typeof(HttpRequestDebugView))]
public abstract class HttpRequest
{
/// <summary>
Expand Down Expand Up @@ -146,4 +151,32 @@ public abstract class HttpRequest
/// </summary>
/// <returns>The collection of route values for this request.</returns>
public virtual RouteValueDictionary RouteValues { get; set; } = null!;

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

private sealed class HttpRequestDebugView(HttpRequest request)
{
private readonly HttpRequest _request = request;

public string Method => _request.Method;
public string Scheme => _request.Scheme;
public bool IsHttps => _request.IsHttps;
public HostString Host => _request.Host;
public PathString PathBase => _request.PathBase;
public PathString Path => _request.Path;
public QueryString QueryString => _request.QueryString;
public IQueryCollection Query => _request.Query;
public string Protocol => _request.Protocol;
public IHeaderDictionary Headers => _request.Headers;
public IRequestCookieCollection Cookies => _request.Cookies;
public long? ContentLength => _request.ContentLength;
public string? ContentType => _request.ContentType;
public bool HasFormContentType => _request.HasFormContentType;
public IFormCollection? Form => _request.HttpContext.Features.Get<IFormFeature>()?.Form;
public RouteValueDictionary RouteValues => _request.RouteValues;
}
}
21 changes: 21 additions & 0 deletions src/Http/Http.Abstractions/src/HttpResponse.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// 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.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Pipelines;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Represents the outgoing side of an individual HTTP request.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[DebuggerTypeProxy(typeof(HttpResponseDebugView))]
public abstract class HttpResponse
{
private static readonly Func<object, Task> _callbackDelegate = callback => ((Func<Task>)callback)();
Expand Down Expand Up @@ -149,4 +153,21 @@ public abstract class HttpResponse
/// </summary>
/// <returns></returns>
public virtual Task CompleteAsync() { throw new NotImplementedException(); }

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

private sealed class HttpResponseDebugView(HttpResponse response)
{
private readonly HttpResponse _response = response;

public int StatusCode => _response.StatusCode;
public IHeaderDictionary Headers => _response.Headers;
public long? ContentLength => _response.ContentLength;
public string? ContentType => _response.ContentType;
public bool HasStarted => _response.HasStarted;
}
}
2 changes: 2 additions & 0 deletions src/Http/Http.Abstractions/src/PathString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
Expand All @@ -14,6 +15,7 @@ namespace Microsoft.AspNetCore.Http;
/// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string
/// </summary>
[TypeConverter(typeof(PathStringConverter))]
[DebuggerDisplay("{Value}")]
public readonly struct PathString : IEquatable<PathString>
{
internal const int StackAllocThreshold = 128;
Expand Down
2 changes: 2 additions & 0 deletions src/Http/Http.Abstractions/src/QueryString.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Encodings.Web;
Expand All @@ -11,6 +12,7 @@ namespace Microsoft.AspNetCore.Http;
/// <summary>
/// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string
/// </summary>
[DebuggerDisplay("{Value}")]
public readonly struct QueryString : IEquatable<QueryString>
{
/// <summary>
Expand Down
11 changes: 11 additions & 0 deletions src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http.Abstractions;
Expand All @@ -18,6 +19,8 @@ namespace Microsoft.AspNetCore.Routing;
/// <summary>
/// An <see cref="IDictionary{String, Object}"/> type for route values.
/// </summary>
[DebuggerTypeProxy(typeof(RouteValueDictionaryDebugView))]
[DebuggerDisplay("Count = {Count}")]
public class RouteValueDictionary : IDictionary<string, object?>, IReadOnlyDictionary<string, object?>
{
// 4 is a good default capacity here because that leaves enough space for area/controller/action/id
Expand Down Expand Up @@ -862,4 +865,12 @@ internal static void ClearCache(Type[]? _)
_propertyCache.Clear();
}
}

private sealed class RouteValueDictionaryDebugView(RouteValueDictionary dictionary)
{
private readonly RouteValueDictionary _dictionary = dictionary;

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<string, object?>[] Items => _dictionary.ToArray();
}
}
20 changes: 20 additions & 0 deletions src/Http/Http.Abstractions/src/WebSocketManager.cs
Original file line number Diff line number Diff line change
@@ -1,13 +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.Net.WebSockets;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Manages the establishment of WebSocket connections for a specific HTTP request.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[DebuggerTypeProxy(typeof(WebSocketManagerDebugView))]
public abstract class WebSocketManager
{
/// <summary>
Expand Down Expand Up @@ -42,4 +45,21 @@ public virtual Task<WebSocket> AcceptWebSocketAsync()
/// <param name="acceptContext"></param>
/// <returns></returns>
public virtual Task<WebSocket> AcceptWebSocketAsync(WebSocketAcceptContext acceptContext) => throw new NotImplementedException();

private string DebuggerToString()
{
return IsWebSocketRequest switch
{
false => "IsWebSocketRequest = False",
true => $"IsWebSocketRequest = True, RequestedProtocols = {string.Join(",", WebSocketRequestedProtocols)}",
};
}

private sealed class WebSocketManagerDebugView(WebSocketManager manager)
{
private readonly WebSocketManager _manager = manager;

public bool IsWebSocketRequest => _manager.IsWebSocketRequest;
public IList<string> WebSocketRequestedProtocols => _manager.WebSocketRequestedProtocols;
}
}
12 changes: 12 additions & 0 deletions src/Http/Http/src/HeaderDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

Expand All @@ -11,6 +13,8 @@ namespace Microsoft.AspNetCore.Http;
/// <summary>
/// Represents a wrapper for RequestHeaders and ResponseHeaders.
/// </summary>
[DebuggerTypeProxy(typeof(HeaderDictionaryDebugView))]
[DebuggerDisplay("Count = {Count}")]
public class HeaderDictionary : IHeaderDictionary
{
private static readonly string[] EmptyKeys = Array.Empty<string>();
Expand Down Expand Up @@ -441,4 +445,12 @@ void IEnumerator.Reset()
}
}
}

private sealed class HeaderDictionaryDebugView(HeaderDictionary dictionary)
{
private readonly HeaderDictionary _dictionary = dictionary;

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<string, string>[] Items => _dictionary.Select(pair => new KeyValuePair<string, string>(pair.Key, pair.Value.ToString())).ToArray();
}
}
Loading