Search code examples
c#dynamicaccess-modifiersdouble-dispatch

Check type visibility prior to dynamic double dispatch


Implementing double dispatch using dynamic:

public interface IDomainEvent {}

public class DomainEventDispatcher
{
    private readonly List<Delegate> subscribers = new List<Delegate>();

    public void Subscribe<TEvent>(Action<TEvent> subscriber) where TEvent : IDomainEvent
    {
        subscribers.Add(subscriber);
    }

    public void Publish<TEvent>(TEvent domainEvent) where TEvent : IDomainEvent
    {
        foreach (Action<TEvent> subscriber in subscribers.OfType<Action<TEvent>>())
        {
            subscriber(domainEvent);
        }
    }

    public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
    {
        foreach (IDomainEvent domainEvent in domainEvents)
        {
            // Force double dispatch - bind to runtime type.
            Publish(domainEvent as dynamic);
        }
    }
}

public class ProcessCompleted : IDomainEvent { public string Name { get; set; } }

Works in most cases:

var dispatcher = new DomainEventDispatcher();

dispatcher.Subscribe((ProcessCompleted e) => Console.WriteLine("Completed " + e.Name));

dispatcher.PublishQueue(new [] { new ProcessCompleted { Name = "one" },
                                 new ProcessCompleted { Name = "two" } });

Completed one

Completed two

But if the subclasses are not visible to the dispatch code, this results in a runtime error:

public static class Bomb
{
    public static void Subscribe(DomainEventDispatcher dispatcher)
    {
        dispatcher.Subscribe((Exploded e) => Console.WriteLine("Bomb exploded"));
    }
    public static IDomainEvent GetEvent()
    {
        return new Exploded();
    }
    private class Exploded : IDomainEvent {}
}
// ...

Bomb.Subscribe(dispatcher);  // no error here
// elsewhere, much later...
dispatcher.PublishQueue(new [] { Bomb.GetEvent() });  // exception

RuntimeBinderException

The type 'object' cannot be used as type parameter 'TEvent' in the generic type or method 'DomainEventDispatcher.Publish(TEvent)'

This is a contrived example; a more realistic one would be an event that is internal to another assembly.

How can I prevent this runtime exception? If that isn't feasible, how can I detect this case in the Subscribe method and fail fast?

Edit: Solutions that eliminate the dynamic cast are acceptable, so long as they do not require a Visitor-style class that knows about all of the subclasses.


Solution

  • All you have to do is change your Publish method to:

    foreach(var subscriber in subscribers) 
        if(subscriber.GetMethodInfo().GetParameters().Single().ParameterType == domainEvent.GetType())
             subscriber.DynamicInvoke(domainEvent);
    

    Update
    You also have to change the call to

     Publish(domainEvent); //Remove the as dynamic
    

    This way you don't have to change Publish's signature

    I prefer my other answer though: C# subscribe to events based on parameter type?

    Update 2
    About your question

    I am curious as to why this dynamic invocation works where my original one fails.

    Keep in mind that dynamic is not a special type.
    Basically the compiler:
    1)Replaces it with object
    2)Refactors you code to more complicated code
    3)Removes compile time checks (these checks are done in runtime )

    If you try to replace

    Publish(domainEvent as dynamic);
    

    with

    Publish(domainEvent as object);
    

    You will get the same message ,but this time in compile time. The error message is self explanatory:

    The type 'object' cannot be used as type parameter 'TEvent' in the generic type or method 'DomainEventDispatcher.Publish(TEvent)'

    As a final note.
    dynamic was designed for specific scenarios,99,9% of the time you don't need it and you can replace it with statically typed code.
    If you think you need it(like the above case) you are probably doing something wrong