Skip to content

Support for untargeted delegate in RequestDelegateFactory #63709

@kemocca

Description

@kemocca

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-rdf

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions