lundi 3 mai 2021

Dynamically passing class functions to one of 19 function overloads expecting different delegate types in C#

I am having an issue simplifying some code. I've spent about 3 or 4 hours researching and trialing different ways to do it but ultimately I haven't found what I need and I'm not even sure if what I'm trying to do is possible.

Here is the current version of the code (simplified a bit for readability).

public class PrototypeHandler : HandlerBase, IPrototypeHandler
{
    public override void RegisterHandlers(HubConnection activeHubConnection)
    {
        base.RegisterHandlers(activeHubConnection);

        _activeHubConnection.On("SendSingleMessageToUser", (Func<MessageDetails, ReturnType>)SendSingleMessageToUser);
        _activeHubConnection.On("SendSingleMessage", (Action<MessageDetails>)SendSingleMessage);
    }

    public ReturnType SendSingleMessageToUser(MessageDetails messageDetails)
    {
        return new ReturnType();
    }

    public void SendSingleMessage(MessageDetails messageDetails)
    {
        return;
    }
}

PrototypeHandler is a prototype for a pattern I'd like to establish in my codebase for a bunch of handler classes, each with several methods that need to be registered with a SignalR HubConnection. In this code, each handler is responsible for registering its own methods with the HubConnection using the RegisterHandlers method. What I'd like to do is to make this dynamic so that I can put the RegisterHandlers method in the HandlerBase. Here is my vision for that

In PrototypeHandler:

public class HandlerAttribute : Attribute
{
    public string MethodName { get; set; }
}
public class PrototypeHandler : HandlerBase, IPrototypeHandler
{
    [Handler(MethodName = "SendSingleMessageToUser")]
    public ReturnType SendSingleMessageToUser(MessageDetails messageDetails)
    {
        return new ReturnType();
    }

    [Handler(MethodName = "SendSingleMessageToUser")]
    public void SendSingleMessage(MessageDetails messageDetails)
    {
        return;
    }
}

And in HandlerBase:

public void RegisterHandlers(HubConnection activeHubConnection)
{
    var methods =
        from m in this.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
        let handlerAttribute = m.GetCustomAttribute(typeof(HandlerAttribute))
        where handlerAttribute != null
        select new { Method = m, HandlerAttribute = (HandlerAttribute)handlerAttribute };

    foreach (var current in methods)
    {
        // Call the HubConnection's "On" method to register
        // this method as a handler
    }
}

The problem is, there are so many overloads to the On method in the HubConnection that support different types of delegates being passed. I want to support all of them so as to not limit our ability for the types of method signatures we can support in the handlers. Here is a list of all of the overloads for On that I'd like to support.

public static IDisposable On(this HubConnection hubConnection, string methodName, Action handler);
public static IDisposable On<T1, T2, T3, T4, T5, T6, T7>(this HubConnection hubConnection, string methodName, Func<T1, T2, T3, T4, T5, T6, T7, Task> handler);
public static IDisposable On<T1, T2, T3, T4, T5, T6, T7, T8>(this HubConnection hubConnection, string methodName, Func<T1, T2, T3, T4, T5, T6, T7, T8, Task> handler);
public static IDisposable On<T1>(this HubConnection hubConnection, string methodName, Action<T1> handler);
public static IDisposable On<T1, T2>(this HubConnection hubConnection, string methodName, Action<T1, T2> handler);
public static IDisposable On<T1, T2, T3>(this HubConnection hubConnection, string methodName, Action<T1, T2, T3> handler);
public static IDisposable On<T1, T2, T3, T4>(this HubConnection hubConnection, string methodName, Action<T1, T2, T3, T4> handler);
public static IDisposable On<T1, T2, T3, T4, T5>(this HubConnection hubConnection, string methodName, Action<T1, T2, T3, T4, T5> handler);
public static IDisposable On<T1, T2, T3, T4, T5, T6, T7>(this HubConnection hubConnection, string methodName, Action<T1, T2, T3, T4, T5, T6, T7> handler);
public static IDisposable On<T1, T2, T3, T4, T5, T6, T7, T8>(this HubConnection hubConnection, string methodName, Action<T1, T2, T3, T4, T5, T6, T7, T8> handler);
public static IDisposable On<T1, T2, T3, T4, T5, T6>(this HubConnection hubConnection, string methodName, Action<T1, T2, T3, T4, T5, T6> handler);
public static IDisposable On(this HubConnection hubConnection, string methodName, Func<Task> handler);
public static IDisposable On<T1>(this HubConnection hubConnection, string methodName, Func<T1, Task> handler);
public static IDisposable On<T1, T2>(this HubConnection hubConnection, string methodName, Func<T1, T2, Task> handler);
public static IDisposable On<T1, T2, T3>(this HubConnection hubConnection, string methodName, Func<T1, T2, T3, Task> handler);
public static IDisposable On<T1, T2, T3, T4>(this HubConnection hubConnection, string methodName, Func<T1, T2, T3, T4, Task> handler);
public static IDisposable On<T1, T2, T3, T4, T5>(this HubConnection hubConnection, string methodName, Func<T1, T2, T3, T4, T5, Task> handler);
public static IDisposable On<T1, T2, T3, T4, T5, T6>(this HubConnection hubConnection, string methodName, Func<T1, T2, T3, T4, T5, T6, Task> handler);
public static IDisposable On(this HubConnection hubConnection, string methodName, Type[] parameterTypes, Func<object[], Task> handler);

As you can see, there are overloads that take Action, Action<T1>, Action<T1, T2> and so on as well as the same for Func i.e. Func<T1, Task>, Func<T1, T2, Task> etc.

I have tried a lot of different things to get this to work but am not having success. At first I tried to just get a reference to the current.Method object as a delegate in my loop, cast it to the correct delegate type, e.g. System.Action`1[MessageDetails] and then call the On method directly, but the compiler couldn't be convinced that the cast delegate would be the correct type for any On overloads.

After that I resorted to using reflection to call the On method. That failed at runtime because it would say that System.Action`1[MessageDetails] isn't the same as System.Action<T1>, for example, but I also had trouble finding the correct On method to invoke.

Here is the most recent code I have. It is incomplete where I would find and invoke the correct On method for the HubConnection.

public void RegisterHandlers(HubConnection activeHubConnection)
{
    var methods =
        from m in this.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
        let handlerAttribute = m.GetCustomAttribute(typeof(HandlerAttribute))
        where handlerAttribute != null
        select new { Method = m, HandlerAttribute = (HandlerAttribute)handlerAttribute };

    foreach (var current in methods)
    {
        // Get a delegate type (e.g. Action, Func<T>) that matches this method type
        Type delegateType = Expression.GetDelegateType(current.Method.GetParameters().Select(p => p.ParameterType).Append(current.Method.ReturnType).ToArray());

        // Find the correct "On" method to invoke
        MethodInfo onMethod = typeof(HubConnectionExtensions).GetType().GetMethod(nameof(HubConnection.On), /*Need to find which parameters to pass to get correct "On" method*/);

        // Invoke the "On" method with correct parameters
        onMethod.Invoke(activeHubConnection, new object[] { current.HandlerAttribute.MethodName, /*missing parameters here*/ });
    }
}

I appreciate any help someone might be able to provide.





Aucun commentaire:

Enregistrer un commentaire