-
Notifications
You must be signed in to change notification settings - Fork 10.5k
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:
/// Implemented by API contract implementation
public interface IEndpointGroup
{
public static abstract string Prefix { get; }
}
public static class EndpointRouteBuilderExtensions
{
public static IEndpointRouteBuilder MapGroup<E>(this IEndpointRouteBuilder routeBuilder)
where E : class, IEndpointGroup
{
var groupType = typeof(E);
var groupAttributes = typeof(E).GetCustomAttributes();
var systemTypes = typeof(Action).Assembly.GetTypes().OfType<TypeInfo>().ToArray();
Dictionary<int, Type> funcTypeCache = [];
Dictionary<int, Type> 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<HttpMethodAttribute>()
.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<int, Type> 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.