Skip to content

Set EndpointName automatically from method name #35069

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

Closed
wants to merge 3 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.CodeAnalysis.CSharp.Symbols;

namespace Microsoft.AspNetCore.Builder
{
Expand Down Expand Up @@ -184,6 +186,23 @@ public static MinimalActionEndpointConventionBuilder Map(
// Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint.
builder.Metadata.Add(action.Method);

// Methods defined in a top-level program are generated as statics so the delegate
// target will be null. Inline lambdas are compiler generated properties so they can
// be filtered that way.
if (action.Target == null || !TypeHelper.IsCompilerGenerated(action.Method.Name))
{
if (GeneratedNameParser.TryParseLocalFunctionName(action.Method.Name, out var endpointName))
{
builder.Metadata.Add(new EndpointNameMetadata(endpointName));
builder.Metadata.Add(new RouteNameMetadata(endpointName));
}
else
{
builder.Metadata.Add(new EndpointNameMetadata(action.Method.Name));
builder.Metadata.Add(new RouteNameMetadata(action.Method.Name));
}
}

// Add delegate attributes as metadata
var attributes = action.Method.GetCustomAttributes();

Expand Down
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;
using System.Linq;
using Microsoft.AspNetCore.Routing;

namespace Microsoft.AspNetCore.Builder
Expand Down
4 changes: 4 additions & 0 deletions src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Microsoft.AspNetCore.Routing.RouteCollection</Description>

<ItemGroup>
<Compile Include="$(SharedSourceRoot)PropertyHelper\*.cs" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\TypeHelper.cs" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\GeneratedNameParser.cs" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\GeneratedNameKind.cs" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\GeneratedNameConstants.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,71 @@ public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder()
Assert.Equal(int.MaxValue, routeEndpointBuilder.Order);
}

[Fact]
// This test scenario simulates methods defined in a top-level program
// which are compiler generated. We currently do some manually parsing leveraging
// code in Roslyn to support this scenario. More info at https://github.com/dotnet/roslyn/issues/55651.
public void MapMethod_DoesNotEndpointNameForInnerMethod()
{
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
string InnerGetString() => "TestString";
_ = builder.MapDelete("/", InnerGetString);

var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);

var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
Assert.NotNull(endpointName);
Assert.Equal("InnerGetString", endpointName?.EndpointName);
}

[Fact]
public void MapMethod_SetsEndpointNameForMethodGroup()
{
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
_ = builder.MapDelete("/", GetString);

var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);

var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
Assert.NotNull(endpointName);
Assert.Equal("GetString", endpointName?.EndpointName);
}

[Fact]
public void WithNameOverridesDefaultEndpointName()
{
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
_ = builder.MapDelete("/", GetString).WithName("SomeCustomName");

var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);

var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
Assert.NotNull(endpointName);
Assert.Equal("SomeCustomName", endpointName?.EndpointName);
}

private string GetString() => "TestString";

[Fact]
public void MapMethod_DoesNotSetEndpointNameForLambda()
{
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier()));
_ = builder.MapDelete("/", () => { });

var dataSource = GetBuilderEndpointDataSource(builder);
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);

var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
Assert.Null(endpointName);
}

class FromRoute : Attribute, IFromRouteMetadata
{
public string? Name { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
// For now, put all methods defined the same declaring type together.
string controllerName;

if (methodInfo.DeclaringType is not null && !IsCompilerGenerated(methodInfo.DeclaringType))
if (methodInfo.DeclaringType is not null && !TypeHelper.IsCompilerGenerated(methodInfo.DeclaringType.Name, methodInfo.DeclaringType))
{
controllerName = methodInfo.DeclaringType.Name;
}
Expand Down Expand Up @@ -363,11 +363,5 @@ private static void AddActionDescriptorEndpointMetadata(
actionDescriptor.EndpointMetadata = new List<object>(endpointMetadata);
}
}

// The CompilerGeneratedAttribute doesn't always get added so we also check if the type name starts with "<"
// For example, "<>c" is a "declaring" type the C# compiler will generate without the attribute for a top-level lambda
// REVIEW: Is there a better way to do this?
private static bool IsCompilerGenerated(Type type) =>
Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute)) || type.Name.StartsWith('<');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<Compile Include="$(SharedSourceRoot)TryParseMethodCache.cs" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\TypeHelper.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
17 changes: 17 additions & 0 deletions src/Shared/RoslynUtils/GeneratedNameConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// These sources are copied from https://github.com/dotnet/roslyn/blob/7d7bf0cc73e335390d73c9de6d7afd1e49605c9d/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameConstants.cs
// and exist to address the issues with extracting original method names for
// generated local functions. See https://github.com/dotnet/roslyn/issues/55651
// for more info.
namespace Microsoft.CodeAnalysis.CSharp.Symbols
{
internal static class GeneratedNameConstants
{
internal const char DotReplacementInTypeNames = '-';
internal const string SynthesizedLocalNamePrefix = "CS$";
internal const string SuffixSeparator = "__";
internal const char LocalFunctionNameTerminator = '|';
}
}
70 changes: 70 additions & 0 deletions src/Shared/RoslynUtils/GeneratedNameKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

// These sources are copied from https://github.com/dotnet/roslyn/blob/7d7bf0cc73e335390d73c9de6d7afd1e49605c9d/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameKind.cs
// and exist to address the issues with extracting original method names for
// generated local functions. See https://github.com/dotnet/roslyn/issues/55651
// for more info.
namespace Microsoft.CodeAnalysis.CSharp.Symbols
{
internal enum GeneratedNameKind
{
None = 0,

// Used by EE:
ThisProxyField = '4',
HoistedLocalField = '5',
DisplayClassLocalOrField = '8',
LambdaMethod = 'b',
LambdaDisplayClass = 'c',
StateMachineType = 'd',
LocalFunction = 'g', // note collision with Deprecated_InitializerLocal, however this one is only used for method names

// Used by EnC:
AwaiterField = 'u',
HoistedSynthesizedLocalField = 's',

// Currently not parsed:
StateMachineStateField = '1',
IteratorCurrentBackingField = '2',
StateMachineParameterProxyField = '3',
ReusableHoistedLocalField = '7',
LambdaCacheField = '9',
FixedBufferField = 'e',
AnonymousType = 'f',
TransparentIdentifier = 'h',
AnonymousTypeField = 'i',
AnonymousTypeTypeParameter = 'j',
AutoPropertyBackingField = 'k',
IteratorCurrentThreadIdField = 'l',
IteratorFinallyMethod = 'm',
BaseMethodWrapper = 'n',
AsyncBuilderField = 't',
DynamicCallSiteContainerType = 'o',
DynamicCallSiteField = 'p',
AsyncIteratorPromiseOfValueOrEndBackingField = 'v',
DisposeModeField = 'w',
CombinedTokensField = 'x', // last

// Deprecated - emitted by Dev12, but not by Roslyn.
// Don't reuse the values because the debugger might encounter them when consuming old binaries.
[Obsolete]
Deprecated_OuterscopeLocals = '6',
[Obsolete]
Deprecated_IteratorInstance = 'a',
[Obsolete]
Deprecated_InitializerLocal = 'g',
[Obsolete]
Deprecated_DynamicDelegate = 'q',
[Obsolete]
Deprecated_ComrefCallLocal = 'r',
}

internal static class GeneratedNameKindExtensions
{
internal static bool IsTypeName(this GeneratedNameKind kind)
=> kind is GeneratedNameKind.LambdaDisplayClass or GeneratedNameKind.StateMachineType or GeneratedNameKind.DynamicCallSiteContainerType;
}
}
Loading