dotnet / dotnet/aspnetcore

Support for untargeted delegate in RequestDelegateFactory

Open
#63,709 1 comment 0 reactions 0 assignees View on GitHub
area-minimal feature-rdf
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 5h
Merged PRs (30d)
276

Description

### Description

Now, registration of instance method of class in `IEnpointRouteBuilder.Map*` is a problem - passed delegate will capture instance of class, so handler can't access to request DI scope (except explicit method parameters).

I encountered that limitation when tried to describe API contract with interface, that should be implemented both sides on C# (ASP.NET + Refit).

Here's my workaround for this case:

```cs
/// Implemented by API contract implementation
public interface IEndpointGroup
{
public static abstract string Prefix { get; }
}

public static class EndpointRouteBuilderExtensions
{
public static IEndpointRouteBuilder MapGroup(this IEndpointRouteBuilder routeBuilder)
where E : class, IEndpointGroup
{
var groupType = typeof(E);
var groupAttributes = typeof(E).GetCustomAttributes();
var systemTypes = typeof(Action).Assembly.GetTypes().OfType().ToArray();
Dictionary funcTypeCache = [];
Dictionary actionTypeCache = [];

var group = routeBuilder.MapGroup(E.Prefix);

foreach (var groupAttribute in groupAttributes)
{
switch (groupAttribute)
{
case TagsAttribute tags:
group.WithTags([.. tags.Tags]);

break;

case AuthorizeAttribute authorizeAttribute:
group.RequireAuthorization(authorizeAttribute);

break;
}
}

foreach (var method in groupType.GetMethods(BindingFlags.Instance | BindingFlags.Public))
{
var httpAttributes = method.GetCustomAttributes()
.Where(attribute => !string.IsNullOrEmpty(attribute.Template))
.ToArray();

if (httpAttributes.Length == 0)
{
continue;
}

var parameters = method.GetParameters().Select(parameter => parameter.ParameterType).ToArray();
string delegateShapeName;
Type[] delegateTypeParameters;
Dictionary delegateTypeCache;

if (method.ReturnType == typeof(void))
{
delegateShapeName = "Action";
delegateTypeCache = actionTypeCache;
delegateTypeParameters = [groupType, .. parameters];
}
else
{
delegateShapeName = "Func";
delegateTypeCache = funcTypeCache;
delegateTypeParameters = [groupType, .. parameters, method.ReturnType];
}

if (!delegateTypeCache.TryGetValue(delegateTypeParameters.Length, out var delegateType))
{
delegateTypeCache[delegateTypeParameters.Length] = delegateType = systemTypes.First(type => type.Name.StartsWith(delegateShapeName)
&& type.IsGenericType
&& type.GenericTypeParameters.Length == delegateTypeParameters.Length);
}

var delegateConstructedType = delegateType.MakeGenericType(delegateTypeParameters);
var @delegate = method.CreateDelegate(delegateConstructedType);
var @delegate2 = @delegate.GetType()
.GetMethod(nameof(Action.Invoke))!
.CreateDelegate(delegateConstructedType, @delegate);

foreach (var httpAttribute in httpAttributes)
{
group.MapMethods(httpAttribute.Template!, httpAttribute.HttpMethods, @delegate);
}
}

return routeBuilder;
}
}
```

Notice that example above contains `@delegate2` wrapper around `@delegate`. That about problem - secondary wrapping replaces `Method`, so all related to endpoint request handler metadata is lost, but instance could be fetched from DI.
But when `@delegate` passed to `Map*` method, application builder throws error about null target for instance method.

### Describe the solution you'd like

Change `RequestDelegateFactory` so it may get first argument aka target from DI when delegate describes it as parameter.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.