Skip to content

Commit

Permalink
Improve endpoint metadata debugging (dotnet#48207)
Browse files Browse the repository at this point in the history
Co-authored-by: Sean Farrow <sean.farrow@seanfarrow.co.uk>
  • Loading branch information
JamesNK and SeanFarrow authored May 19, 2023
1 parent c084d4b commit ba58563
Show file tree
Hide file tree
Showing 35 changed files with 480 additions and 24 deletions.
3 changes: 3 additions & 0 deletions src/Http/Http.Abstractions/src/Routing/Endpoint.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>
/// Represents a logical endpoint in an application.
/// </summary>
[DebuggerDisplay("{ToString(),nq}")]
public class Endpoint
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;

Expand All @@ -16,6 +17,8 @@ namespace Microsoft.AspNetCore.Http;
/// of arbitrary types. The metadata items are stored as an ordered collection with
/// items arranged in ascending order of precedence.
/// </remarks>
[DebuggerTypeProxy(typeof(EndpointMetadataCollectionDebugView))]
[DebuggerDisplay("Count = {Count}")]
public sealed class EndpointMetadataCollection : IReadOnlyList<object>
{
/// <summary>
Expand Down Expand Up @@ -215,4 +218,27 @@ public void Reset()
_current = null;
}
}

private sealed class EndpointMetadataCollectionDebugView
{
private readonly EndpointMetadataCollection _collection;

public EndpointMetadataCollectionDebugView(EndpointMetadataCollection collection)
{
ArgumentNullException.ThrowIfNull(collection);

_collection = collection;
}

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public object[] Items
{
get
{
var items = new object[_collection.Count];
_collection._items.CopyTo(items, 0);
return items;
}
}
}
}
8 changes: 8 additions & 0 deletions src/Http/Http.Extensions/src/EndpointDescriptionAttribute.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 Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Http;
Expand All @@ -13,6 +14,7 @@ namespace Microsoft.AspNetCore.Http;
/// can be used to annotate endpoints with detailed, multiline descriptors of their behavior.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[DebuggerDisplay("{ToString(),nq}")]
public sealed class EndpointDescriptionAttribute : Attribute, IEndpointDescriptionMetadata
{
/// <summary>
Expand All @@ -26,4 +28,10 @@ public EndpointDescriptionAttribute(string description)

/// <inheritdoc />
public string Description { get; }

/// <inheritdoc/>>
public override string ToString()
{
return $"Description: {Description ?? "(null)"}";
}
}
9 changes: 9 additions & 0 deletions src/Http/Http.Extensions/src/EndpointSummaryAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// 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 Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Specifies a summary in <see cref="Endpoint.Metadata"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[DebuggerDisplay("{ToString(),nq}")]
public sealed class EndpointSummaryAttribute : Attribute, IEndpointSummaryMetadata
{
/// <summary>
Expand All @@ -22,4 +25,10 @@ public EndpointSummaryAttribute(string summary)

/// <inheritdoc />
public string Summary { get; }

/// <inheritdoc/>>
public override string ToString()
{
return DebuggerHelpers.GetDebugText(nameof(Summary), Summary);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<Compile Include="$(SharedSourceRoot)ValueStringBuilder\**\*.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Debugger\DebuggerHelpers.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#nullable enable
override Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.ToString() -> string!
override Microsoft.AspNetCore.Http.EndpointSummaryAttribute.ToString() -> string!
override Microsoft.AspNetCore.Http.TagsAttribute.ToString() -> string!
static Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest! request, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<object?>
static Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse! response, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo! jsonTypeInfo, string? contentType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions)
Expand Down
9 changes: 9 additions & 0 deletions src/Http/Http.Extensions/src/TagsAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// 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 Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Http;

Expand All @@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.Http;
/// and are typically used to group operations by tags in the UI.
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
[DebuggerDisplay("{ToString(),nq}")]
public sealed class TagsAttribute : Attribute, ITagsMetadata
{
/// <summary>
Expand All @@ -29,4 +32,10 @@ public TagsAttribute(params string[] tags)
/// Gets the collection of tags associated with the endpoint.
/// </summary>
public IReadOnlyList<string> Tags { get; }

/// <inheritdoc/>>
public override string ToString()
{
return DebuggerHelpers.GetDebugText(nameof(Tags), Tags);
}
}
48 changes: 48 additions & 0 deletions src/Http/Http.Extensions/test/MetadataTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing;

public class MetadataTest
{
[Fact]
public void EndpointDescriptionAttribute_ToString()
{
// Arrange
var metadata = new EndpointDescriptionAttribute("A description");

// Act
var value = metadata.ToString();

// Assert
Assert.Equal("Description: A description", value);
}

[Fact]
public void EndpointSummaryAttribute_ToString()
{
// Arrange
var metadata = new EndpointSummaryAttribute("A summary");

// Act
var value = metadata.ToString();

// Assert
Assert.Equal("Summary: A summary", value);
}

[Fact]
public void HostAttribute_ToString()
{
// Arrange
var metadata = new TagsAttribute("Tag1", "Tag2");

// Act
var value = metadata.ToString();

// Assert
Assert.Equal("Tags: Tag1,Tag2", value);
}
}
12 changes: 10 additions & 2 deletions src/Http/Routing/src/DataTokensMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Routing;

Expand All @@ -12,6 +13,7 @@ namespace Microsoft.AspNetCore.Routing;
/// type provides data tokens value for <see cref="RouteData.DataTokens"/> associated
/// with an endpoint.
/// </summary>
[DebuggerDisplay("{ToString(),nq}")]
public sealed class DataTokensMetadata : IDataTokensMetadata
{
/// <summary>
Expand All @@ -27,4 +29,10 @@ public DataTokensMetadata(IReadOnlyDictionary<string, object?> dataTokens)
/// Get the data tokens.
/// </summary>
public IReadOnlyDictionary<string, object?> DataTokens { get; }

/// <inheritdoc/>
public override string ToString()
{
return DebuggerHelpers.GetDebugText(nameof(DataTokens), DataTokens.Select(t => $"{t.Key}={t.Value ?? "(null)"}"));
}
}
9 changes: 8 additions & 1 deletion src/Http/Routing/src/EndpointNameMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Routing;

Expand Down Expand Up @@ -29,4 +30,10 @@ public EndpointNameMetadata(string endpointName)
/// Gets the endpoint name.
/// </summary>
public string EndpointName { get; }

/// <inheritdoc/>
public override string ToString()
{
return DebuggerHelpers.GetDebugText(nameof(EndpointName), EndpointName);
}
}
11 changes: 10 additions & 1 deletion src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Routing;

/// <summary>
/// Indicates that this <see cref="Endpoint"/> should not be included in the generated API metadata.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)]
[DebuggerDisplay("{ToString(),nq}")]
public sealed class ExcludeFromDescriptionAttribute : Attribute, IExcludeFromDescriptionMetadata
{
/// <inheritdoc />
public bool ExcludeFromDescription => true;

/// <inheritdoc/>>
public override string ToString()
{
return DebuggerHelpers.GetDebugText(nameof(ExcludeFromDescription), ExcludeFromDescription);
}
}
8 changes: 5 additions & 3 deletions src/Http/Routing/src/HostAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Shared;

namespace Microsoft.AspNetCore.Routing;

/// <summary>
/// Attribute for providing host metdata that is used during routing.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[DebuggerDisplay("{ToString(),nq}")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class HostAttribute : Attribute, IHostMetadata
{
Expand Down Expand Up @@ -47,12 +48,13 @@ public HostAttribute(params string[] hosts)
/// </summary>
public IReadOnlyList<string> Hosts { get; }

private string DebuggerToString()
/// <inheritdoc/>
public override string ToString()
{
var hostsDisplay = (Hosts.Count == 0)
? "*:*"
: string.Join(",", Hosts.Select(h => h.Contains(':') ? h : h + ":*"));

return $"Hosts: {hostsDisplay}";
return DebuggerHelpers.GetDebugText(nameof(Hosts), hostsDisplay);
}
}
8 changes: 5 additions & 3 deletions src/Http/Routing/src/HttpMethodMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Shared;
using static Microsoft.AspNetCore.Http.HttpMethods;

namespace Microsoft.AspNetCore.Routing;

/// <summary>
/// Represents HTTP method metadata used during routing.
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
[DebuggerDisplay("{ToString(),nq}")]
public sealed class HttpMethodMetadata : IHttpMethodMetadata
{
/// <summary>
Expand Down Expand Up @@ -52,8 +53,9 @@ public HttpMethodMetadata(IEnumerable<string> httpMethods, bool acceptCorsPrefli
/// </summary>
public IReadOnlyList<string> HttpMethods { get; }

private string DebuggerToString()
/// <inheritdoc/>
public override string ToString()
{
return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}";
return DebuggerHelpers.GetDebugText(nameof(HttpMethods), HttpMethods, "Cors", AcceptCorsPreflight);
}
}
1 change: 1 addition & 0 deletions src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<Compile Include="$(SharedSourceRoot)RouteValueDictionaryTrimmerWarning.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)HttpRuleParser.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)HttpParseResult.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Debugger\DebuggerHelpers.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 8 additions & 0 deletions src/Http/Routing/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#nullable enable
Microsoft.AspNetCore.Routing.RouteHandlerServices
override Microsoft.AspNetCore.Routing.DataTokensMetadata.ToString() -> string!
override Microsoft.AspNetCore.Routing.EndpointNameMetadata.ToString() -> string!
override Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute.ToString() -> string!
override Microsoft.AspNetCore.Routing.HostAttribute.ToString() -> string!
override Microsoft.AspNetCore.Routing.HttpMethodMetadata.ToString() -> string!
override Microsoft.AspNetCore.Routing.RouteNameMetadata.ToString() -> string!
override Microsoft.AspNetCore.Routing.SuppressLinkGenerationMetadata.ToString() -> string!
override Microsoft.AspNetCore.Routing.SuppressMatchingMetadata.ToString() -> string!
Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions
Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions
static Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions.ShortCircuit(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int? statusCode = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
Expand Down
Loading

0 comments on commit ba58563

Please sign in to comment.