Support for untargeted delegate in RequestDelegateFactory
- 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
Assessment
This issue has not been assessed yet.