Skip to content

Commit 9ff9267

Browse files
authored
Set EndpointName automatically from method name
1 parent 5a04636 commit 9ff9267

File tree

6 files changed

+89
-7
lines changed

6 files changed

+89
-7
lines changed

src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Reflection;
8+
using System.Runtime.CompilerServices;
89
using Microsoft.AspNetCore.Http;
910
using Microsoft.AspNetCore.Routing;
1011
using Microsoft.AspNetCore.Routing.Patterns;
@@ -184,6 +185,14 @@ public static MinimalActionEndpointConventionBuilder Map(
184185
// Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint.
185186
builder.Metadata.Add(action.Method);
186187

188+
// Methods defined in a top-level program are generated as statics so the delegate
189+
// target will be null. Inline lambdas are compiler generated properties so they can
190+
// be filtered that way.
191+
if (action.Target == null || !TypeHelper.IsCompilerGenerated(action.Method.Name))
192+
{
193+
builder.Metadata.Add(new EndpointNameMetadata(action.Method.Name));
194+
}
195+
187196
// Add delegate attributes as metadata
188197
var attributes = action.Method.GetCustomAttributes();
189198

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Microsoft.AspNetCore.Routing.RouteCollection</Description>
2424

2525
<ItemGroup>
2626
<Compile Include="$(SharedSourceRoot)PropertyHelper\*.cs" />
27+
<Compile Include="$(SharedSourceRoot)TypeHelper.cs" />
2728
</ItemGroup>
2829

2930
<ItemGroup>

src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,55 @@ public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder()
359359
Assert.Equal(int.MaxValue, routeEndpointBuilder.Order);
360360
}
361361

362+
[Fact]
363+
// This test scenario simulates methods defined in a top-level program
364+
// which are compiler generated.
365+
public void MapMethod_SetsEndpointNameForInnerMethod()
366+
{
367+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
368+
string InnerGetString() => "TestString";
369+
_ = builder.MapDelete("/", InnerGetString);
370+
371+
var dataSource = GetBuilderEndpointDataSource(builder);
372+
// Trigger Endpoint build by calling getter.
373+
var endpoint = Assert.Single(dataSource.Endpoints);
374+
375+
var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
376+
Assert.NotNull(endpointName);
377+
Assert.Equal("InnerGetString", endpointName?.EndpointName);
378+
}
379+
380+
[Fact]
381+
public void MapMethod_SetsEndpointNameForMethodGroup()
382+
{
383+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
384+
_ = builder.MapDelete("/", GetString);
385+
386+
var dataSource = GetBuilderEndpointDataSource(builder);
387+
// Trigger Endpoint build by calling getter.
388+
var endpoint = Assert.Single(dataSource.Endpoints);
389+
390+
var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
391+
Assert.NotNull(endpointName);
392+
Assert.Equal("GetString", endpointName?.EndpointName);
393+
}
394+
395+
private string GetString() => "TestString";
396+
397+
[Fact]
398+
public void MapMethod_DoesNotSetEndpointNameForLambda()
399+
{
400+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
401+
_ = builder.MapDelete("/", () => { });
402+
403+
var dataSource = GetBuilderEndpointDataSource(builder);
404+
// Trigger Endpoint build by calling getter.
405+
var endpoint = Assert.Single(dataSource.Endpoints);
406+
407+
var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
408+
Assert.Null(endpointName);
409+
}
410+
362411
class FromRoute : Attribute, IFromRouteMetadata
363412
{
364413
public string? Name { get; set; }

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
7878
// For now, put all methods defined the same declaring type together.
7979
string controllerName;
8080

81-
if (methodInfo.DeclaringType is not null && !IsCompilerGenerated(methodInfo.DeclaringType))
81+
if (methodInfo.DeclaringType is not null && !TypeHelper.IsCompilerGenerated(methodInfo.DeclaringType.Name, methodInfo.DeclaringType))
8282
{
8383
controllerName = methodInfo.DeclaringType.Name;
8484
}
@@ -363,11 +363,5 @@ private static void AddActionDescriptorEndpointMetadata(
363363
actionDescriptor.EndpointMetadata = new List<object>(endpointMetadata);
364364
}
365365
}
366-
367-
// The CompilerGeneratedAttribute doesn't always get added so we also check if the type name starts with "<"
368-
// For example, "<>c" is a "declaring" type the C# compiler will generate without the attribute for a top-level lambda
369-
// REVIEW: Is there a better way to do this?
370-
private static bool IsCompilerGenerated(Type type) =>
371-
Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute)) || type.Name.StartsWith('<');
372366
}
373367
}

src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<Compile Include="$(SharedSourceRoot)TryParseMethodCache.cs" />
14+
<Compile Include="$(SharedSourceRoot)TypeHelper.cs" />
1415
</ItemGroup>
1516

1617
<ItemGroup>

src/Shared/TypeHelper.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
namespace System.Runtime.CompilerServices
5+
{
6+
internal static class TypeHelper
7+
{
8+
/// <summary>
9+
/// Checks to see if a given type is compiler generated.
10+
/// <remarks>
11+
/// The compiler doesn't always annotate every time it generates with the
12+
/// CompilerGeneratedAttribute so sometimes we have to check if the type's
13+
/// identifier represents a generated type. Follows the same heuristics seen
14+
/// in https://github.com/dotnet/roslyn/blob/b57c1f89c1483da8704cde7b535a20fd029748db/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/GeneratedMetadataNames.cs#L19
15+
/// </remarks>
16+
/// </summary>
17+
/// <param name="type">The type to evaluate. Can be null if evaluating only on name. </param>
18+
/// <param name="name">The identifier associated wit the type.</param>
19+
/// <returns><see langword="true" /> if <paramref name="type"/> is compiler generated
20+
/// or <paramref name="name"/> represents a compiler generated identifier.</returns>
21+
internal static bool IsCompilerGenerated(string name, Type? type = null)
22+
{
23+
return (type is Type && Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute)))
24+
|| name.StartsWith("<", StringComparison.Ordinal)
25+
|| (name.IndexOf('$') >= 0);
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)