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.