Skip to content

Commit ba58563

Browse files
JamesNKSeanFarrow
andauthored
Improve endpoint metadata debugging (#48207)
Co-authored-by: Sean Farrow <sean.farrow@seanfarrow.co.uk>
1 parent c084d4b commit ba58563

35 files changed

+480
-24
lines changed

src/Http/Http.Abstractions/src/Routing/Endpoint.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
/// Represents a logical endpoint in an application.
810
/// </summary>
11+
[DebuggerDisplay("{ToString(),nq}")]
912
public class Endpoint
1013
{
1114
/// <summary>

src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections;
55
using System.Collections.Concurrent;
6+
using System.Diagnostics;
67
using System.Linq;
78
using System.Runtime.CompilerServices;
89

@@ -16,6 +17,8 @@ namespace Microsoft.AspNetCore.Http;
1617
/// of arbitrary types. The metadata items are stored as an ordered collection with
1718
/// items arranged in ascending order of precedence.
1819
/// </remarks>
20+
[DebuggerTypeProxy(typeof(EndpointMetadataCollectionDebugView))]
21+
[DebuggerDisplay("Count = {Count}")]
1922
public sealed class EndpointMetadataCollection : IReadOnlyList<object>
2023
{
2124
/// <summary>
@@ -215,4 +218,27 @@ public void Reset()
215218
_current = null;
216219
}
217220
}
221+
222+
private sealed class EndpointMetadataCollectionDebugView
223+
{
224+
private readonly EndpointMetadataCollection _collection;
225+
226+
public EndpointMetadataCollectionDebugView(EndpointMetadataCollection collection)
227+
{
228+
ArgumentNullException.ThrowIfNull(collection);
229+
230+
_collection = collection;
231+
}
232+
233+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
234+
public object[] Items
235+
{
236+
get
237+
{
238+
var items = new object[_collection.Count];
239+
_collection._items.CopyTo(items, 0);
240+
return items;
241+
}
242+
}
243+
}
218244
}

src/Http/Http.Extensions/src/EndpointDescriptionAttribute.cs

Lines changed: 8 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 Microsoft.AspNetCore.Http.Metadata;
56

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

2729
/// <inheritdoc />
2830
public string Description { get; }
31+
32+
/// <inheritdoc/>>
33+
public override string ToString()
34+
{
35+
return $"Description: {Description ?? "(null)"}";
36+
}
2937
}

src/Http/Http.Extensions/src/EndpointSummaryAttribute.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
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 Microsoft.AspNetCore.Http.Metadata;
6+
using Microsoft.AspNetCore.Shared;
57

68
namespace Microsoft.AspNetCore.Http;
79

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

2326
/// <inheritdoc />
2427
public string Summary { get; }
28+
29+
/// <inheritdoc/>>
30+
public override string ToString()
31+
{
32+
return DebuggerHelpers.GetDebugText(nameof(Summary), Summary);
33+
}
2534
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<Compile Include="$(SharedSourceRoot)ValueStringBuilder\**\*.cs" LinkBase="Shared" />
2626
<Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared" />
2727
<Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared" />
28+
<Compile Include="$(SharedSourceRoot)Debugger\DebuggerHelpers.cs" LinkBase="Shared" />
2829
</ItemGroup>
2930

3031
<ItemGroup>

src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#nullable enable
2+
override Microsoft.AspNetCore.Http.EndpointDescriptionAttribute.ToString() -> string!
3+
override Microsoft.AspNetCore.Http.EndpointSummaryAttribute.ToString() -> string!
4+
override Microsoft.AspNetCore.Http.TagsAttribute.ToString() -> string!
25
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?>
36
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!
47
Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.set -> void (forwarded, contained in Microsoft.AspNetCore.Http.Abstractions)

src/Http/Http.Extensions/src/TagsAttribute.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
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 Microsoft.AspNetCore.Http.Metadata;
6+
using Microsoft.AspNetCore.Shared;
57

68
namespace Microsoft.AspNetCore.Http;
79

@@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.Http;
1416
/// and are typically used to group operations by tags in the UI.
1517
/// </remarks>
1618
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
19+
[DebuggerDisplay("{ToString(),nq}")]
1720
public sealed class TagsAttribute : Attribute, ITagsMetadata
1821
{
1922
/// <summary>
@@ -29,4 +32,10 @@ public TagsAttribute(params string[] tags)
2932
/// Gets the collection of tags associated with the endpoint.
3033
/// </summary>
3134
public IReadOnlyList<string> Tags { get; }
35+
36+
/// <inheritdoc/>>
37+
public override string ToString()
38+
{
39+
return DebuggerHelpers.GetDebugText(nameof(Tags), Tags);
40+
}
3241
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Routing;
7+
8+
public class MetadataTest
9+
{
10+
[Fact]
11+
public void EndpointDescriptionAttribute_ToString()
12+
{
13+
// Arrange
14+
var metadata = new EndpointDescriptionAttribute("A description");
15+
16+
// Act
17+
var value = metadata.ToString();
18+
19+
// Assert
20+
Assert.Equal("Description: A description", value);
21+
}
22+
23+
[Fact]
24+
public void EndpointSummaryAttribute_ToString()
25+
{
26+
// Arrange
27+
var metadata = new EndpointSummaryAttribute("A summary");
28+
29+
// Act
30+
var value = metadata.ToString();
31+
32+
// Assert
33+
Assert.Equal("Summary: A summary", value);
34+
}
35+
36+
[Fact]
37+
public void HostAttribute_ToString()
38+
{
39+
// Arrange
40+
var metadata = new TagsAttribute("Tag1", "Tag2");
41+
42+
// Act
43+
var value = metadata.ToString();
44+
45+
// Assert
46+
Assert.Equal("Tags: Tag1,Tag2", value);
47+
}
48+
}

src/Http/Routing/src/DataTokensMetadata.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
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-
#nullable enable
5-
4+
using System.Diagnostics;
5+
using System.Linq;
66
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Shared;
78

89
namespace Microsoft.AspNetCore.Routing;
910

@@ -12,6 +13,7 @@ namespace Microsoft.AspNetCore.Routing;
1213
/// type provides data tokens value for <see cref="RouteData.DataTokens"/> associated
1314
/// with an endpoint.
1415
/// </summary>
16+
[DebuggerDisplay("{ToString(),nq}")]
1517
public sealed class DataTokensMetadata : IDataTokensMetadata
1618
{
1719
/// <summary>
@@ -27,4 +29,10 @@ public DataTokensMetadata(IReadOnlyDictionary<string, object?> dataTokens)
2729
/// Get the data tokens.
2830
/// </summary>
2931
public IReadOnlyDictionary<string, object?> DataTokens { get; }
32+
33+
/// <inheritdoc/>
34+
public override string ToString()
35+
{
36+
return DebuggerHelpers.GetDebugText(nameof(DataTokens), DataTokens.Select(t => $"{t.Key}={t.Value ?? "(null)"}"));
37+
}
3038
}

src/Http/Routing/src/EndpointNameMetadata.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Shared;
56

67
namespace Microsoft.AspNetCore.Routing;
78

@@ -29,4 +30,10 @@ public EndpointNameMetadata(string endpointName)
2930
/// Gets the endpoint name.
3031
/// </summary>
3132
public string EndpointName { get; }
33+
34+
/// <inheritdoc/>
35+
public override string ToString()
36+
{
37+
return DebuggerHelpers.GetDebugText(nameof(EndpointName), EndpointName);
38+
}
3239
}
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// 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 Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Shared;
57

68
namespace Microsoft.AspNetCore.Routing;
79

810
/// <summary>
911
/// Indicates that this <see cref="Endpoint"/> should not be included in the generated API metadata.
1012
/// </summary>
1113
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)]
14+
[DebuggerDisplay("{ToString(),nq}")]
1215
public sealed class ExcludeFromDescriptionAttribute : Attribute, IExcludeFromDescriptionMetadata
1316
{
1417
/// <inheritdoc />
1518
public bool ExcludeFromDescription => true;
19+
20+
/// <inheritdoc/>>
21+
public override string ToString()
22+
{
23+
return DebuggerHelpers.GetDebugText(nameof(ExcludeFromDescription), ExcludeFromDescription);
24+
}
1625
}

src/Http/Routing/src/HostAttribute.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33

44
using System.Diagnostics;
55
using System.Linq;
6+
using Microsoft.AspNetCore.Shared;
67

78
namespace Microsoft.AspNetCore.Routing;
89

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

50-
private string DebuggerToString()
51+
/// <inheritdoc/>
52+
public override string ToString()
5153
{
5254
var hostsDisplay = (Hosts.Count == 0)
5355
? "*:*"
5456
: string.Join(",", Hosts.Select(h => h.Contains(':') ? h : h + ":*"));
5557

56-
return $"Hosts: {hostsDisplay}";
58+
return DebuggerHelpers.GetDebugText(nameof(Hosts), hostsDisplay);
5759
}
5860
}

src/Http/Routing/src/HttpMethodMetadata.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
using System.Diagnostics;
55
using System.Linq;
6+
using Microsoft.AspNetCore.Shared;
67
using static Microsoft.AspNetCore.Http.HttpMethods;
78

89
namespace Microsoft.AspNetCore.Routing;
910

1011
/// <summary>
1112
/// Represents HTTP method metadata used during routing.
1213
/// </summary>
13-
[DebuggerDisplay("{DebuggerToString(),nq}")]
14+
[DebuggerDisplay("{ToString(),nq}")]
1415
public sealed class HttpMethodMetadata : IHttpMethodMetadata
1516
{
1617
/// <summary>
@@ -52,8 +53,9 @@ public HttpMethodMetadata(IEnumerable<string> httpMethods, bool acceptCorsPrefli
5253
/// </summary>
5354
public IReadOnlyList<string> HttpMethods { get; }
5455

55-
private string DebuggerToString()
56+
/// <inheritdoc/>
57+
public override string ToString()
5658
{
57-
return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}";
59+
return DebuggerHelpers.GetDebugText(nameof(HttpMethods), HttpMethods, "Cors", AcceptCorsPreflight);
5860
}
5961
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Compile Include="$(SharedSourceRoot)RouteValueDictionaryTrimmerWarning.cs" LinkBase="Shared" />
3636
<Compile Include="$(SharedSourceRoot)HttpRuleParser.cs" LinkBase="Shared" />
3737
<Compile Include="$(SharedSourceRoot)HttpParseResult.cs" LinkBase="Shared" />
38+
<Compile Include="$(SharedSourceRoot)Debugger\DebuggerHelpers.cs" LinkBase="Shared" />
3839
</ItemGroup>
3940

4041
<ItemGroup>

src/Http/Routing/src/PublicAPI.Unshipped.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
#nullable enable
22
Microsoft.AspNetCore.Routing.RouteHandlerServices
3+
override Microsoft.AspNetCore.Routing.DataTokensMetadata.ToString() -> string!
4+
override Microsoft.AspNetCore.Routing.EndpointNameMetadata.ToString() -> string!
5+
override Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute.ToString() -> string!
6+
override Microsoft.AspNetCore.Routing.HostAttribute.ToString() -> string!
7+
override Microsoft.AspNetCore.Routing.HttpMethodMetadata.ToString() -> string!
8+
override Microsoft.AspNetCore.Routing.RouteNameMetadata.ToString() -> string!
9+
override Microsoft.AspNetCore.Routing.SuppressLinkGenerationMetadata.ToString() -> string!
10+
override Microsoft.AspNetCore.Routing.SuppressMatchingMetadata.ToString() -> string!
311
Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions
412
Microsoft.AspNetCore.Routing.RouteShortCircuitEndpointRouteBuilderExtensions
513
static Microsoft.AspNetCore.Builder.RouteShortCircuitEndpointConventionBuilderExtensions.ShortCircuit(this Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! builder, int? statusCode = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!

0 commit comments

Comments
 (0)