2

I am trying to register event handlers (Task) based on a specific implementation of Interface into a property Dictionary<Type, Func<Interface, Task>> _subscriptions;

I figured that as the Implementation implements Interface, I can simply cast Func<Implementation, Task> to Func<Interface, Task> however this gives me a runtime InvalidCastException

Subscribe<HelloMessage>(OnHelloMessage);
Task OnHelloMessage(HelloMessage message)
{
// Handle message
}

void Subscribe<T>(Func<T, Task> handler)
           where T: class, IMessage, new()
{
  _subscriptions.TryAdd(typeof(T), (Func<IMessage, Task>) handler);
}

The reason I want the handler function to receive the implementation is to avoid having to write thousand times if (message is HelloMessage helloMessage)

How can I convert Func<HelloMessage, Task> to Func<IMessage, Task> ?

Miamy
  • 2,162
  • 3
  • 15
  • 32
Wesley
  • 855
  • 1
  • 9
  • 23
  • Did you tried replace `T` with `IMessage`? – Sergey Nazarov Oct 02 '20 at 11:18
  • Could you minimize your example, and make it reproducible? AFAICS the `_subscriptions` dictionary is irrelevant to the core issue, while the declaration of the important `IMessage`and `HelloMessage` types is missing. – Theodor Zoulias Oct 02 '20 at 12:12

2 Answers2

1

You cannot cast Func<T, Task> to Func<IMessage, Task> like that, for several really good reasons. What you can do however is cast it to Delegate and then cast back to the correct type on invocation. This is functionally equivalent to casting an instance to Object and back again.

Here's a sample:

public class TaskFactory
{
    private readonly Dictionary<Type, Delegate> _factories = new Dictionary<Type, Delegate>();
    
    public void Register<T>(Func<T, Task> factory)
    {
        _factories[typeof(T)] = factory;
    }
    
    public Task Invoke<T>(T value)
    {
        if (_factories.TryGetValue(typeof(T), out var del) && del is Func<T, Task> fn)
            return fn.Invoke(value);
        return Task.CompletedTask;
    }
}

This depends on the type being passed at compile time however, so if you're pass in an IMessage then it will try to find a factory method for IMessage instead of the actual concrete type.

To handle proper type determination at runtime you can either go the whole hog and construct the right kind of type to check against... or trust your code to not screw up the content of your task factory cache. Here's the trusting version:

public Task Invoke(object instance)
{
    if (_factories.TryGetValue(instance?.GetType(), out var del))
        return (Task)del.DynamicInvoke(instance);
    return Task.CompletedTask;
}

Note that here we're using DynamicInvoke to run the factory function, and casting the result back to Task. As long as nothing changes the contents of your _factories dictionary then you'll be fine. Honest.

For completeness here's the paranoid version. This one finds the instance type, fetches the factory method, then checks to make sure that the factory method matches the type signature we're expecting. If all that works out then it will use DynamicInvoke to run the func.

public Task Invoke(object instance)
{
    if (instance is null)
        throw new ArgumentNullException(nameof(instance));
        
    // Get the true type of the instance
    var type = instance.GetType();
        
    if (_factories.TryGetValue(type, out var del))
    {
        // Get the type for Func<T, Task> where T is the true type of the parameter
        var deltype = typeof(Func<,>).MakeGenericType(type, typeof(Task));
            
        // check that the delegate matches our expected delegate type then execute
        if (deltype.IsAssignableFrom(del.GetType()))
            return (Task)del.DynamicInvoke(instance);
    }
    return Task.CompletedTask;
}

I've basically ignored the IMessage interface here but you can simply plug it into the appropriate places in the code.


On the topic of type filtering in your Subscribe method, it seems a little odd. You require a class that implements IMessage and has a default constructor... but you never construct an instance of the object anywhere in Subscribe. In general you want to put only those limits that you actual require in the code, either right there or somewhere that might need those limitations later.

Corey
  • 15,524
  • 2
  • 35
  • 68
  • I require an default constructor as the message id I use as a descriminator needs to be accessed. I serialize the content of the message from client <-> server. – Wesley Oct 02 '20 at 16:51
  • Thanks for this solution example. It has resolved my issue and I've learned some more which is greatly appreciated. Thank you! – Wesley Oct 02 '20 at 20:48
0

As your code stands, the InvalidCastException occurs because type resolution rules don't permit a cast from Func<HelloMessage, Task> to a Func<IMessage, Task.

There are solutions...

Indirection

You can keep all your code intact, e.g.

Dictionary<Type, Func<IMessage, Task>> _subscriptions = new Dictionary<Type, Func<IMessage, Task>>();

except change only the subscribe method to register a Func<IMessage, Task> that calls your handler (indirection):

void Subscribe<T>(Func<T, Task> handler) where T: class, IMessage, new()
{
    _subscriptions.TryAdd(typeof(T), x => handler((T)x));
}

This allows you to call this code

Subscribe<HelloMessage>(OnHelloMessage);
CallExample();

where a (contrived) CallExample looks like

void CallExample() => _subscriptions[typeof(HelloMessage)](new HelloMessage());

Dynamic

Assuming you want to keep the generic signature of your Subscribe method and you want your delegates to retain their individual signatures, one way you can fix this is to make the value of your dictionary a dynamic.

Dictionary<Type, dynamic> _subscriptions = new Dictionary<Type, dynamic>();

and change your implementation of Subscribe to

void Subscribe<T>(Func<T, Task> handler) where T: class, IMessage, new()
{
    _subscriptions.TryAdd(typeof(T), (dynamic)handler);
}
Kit
  • 20,354
  • 4
  • 60
  • 103
  • Dude... using `dynamic` for delegates? I mean, I know it works, but still... it *feels* so wrong. And the IL isn't quite so straight-forward. – Corey Oct 02 '20 at 12:02
  • @Corey Most application-level developers don't care about the IL. Most shouldn't anyway unless performance is an issue. Anyway, I've added the alternative: indirection. There are even more alternatives, but that's for other answerers... – Kit Oct 02 '20 at 12:09
  • "Casting in general doesn't "reach inside" the generic parameters to match them up." That's not true at all. That generic argument is simply contravariant, not covariant, as the duplicate explains. The parameter can be made *more specific*, not less specific. – Servy Oct 02 '20 at 13:25
  • @Servy Fair enough. I'll remove that point. The solutions, however, work. – Kit Oct 03 '20 at 19:18