Check type visibility prior to dynamic double dispatch

179 views Asked by At

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.

4

There are 4 answers

1
George Vovos On BEST ANSWER

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

5
dahlbyk On

How can I prevent this runtime exception?

You really can't, that's the nature of dynamic.

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

You could probably check typeof(TEvent).IsPublic before adding the subscriber.

That said, I'm not sure you really need dynamic for double dispatch. What if subscribers were a Dictionary<Type, List<Action<IDomainEvent>>> and you looked up subscribers in Publish(IDomainEvent domainEvent) based on domainEvent.GetType()?

0
default.kramer On

Since your Subscribe method already has the generic type, you can make this easy change:

private readonly List<Action<object>> subscribers = new List<Action<object>>();

public void Subscribe<TEvent>(Action<TEvent> subscriber) where TEvent : class
{
    subscribers.Add((object evnt) =>
    {
        var correctType = evnt as TEvent;
        if (correctType != null)
        {
            subscriber(correctType);
        }
    });
}

public void Publish(object evnt)
{
    foreach (var subscriber in subscribers)
    {
        subscriber(evnt);
    }
}

If you are missing the compile-time type information on both the publish- and the subscribe-side, you can still eliminate the dynamic cast. See this Expression building example.

0
Ivan Stoev On

Rather than trying to figure out why the dynamic call fails, I would concentrate on providing a working solution, because the way I understand the contract, you have a valid subscriber, hence you should be able to dispatch the calls to it.

Fortunately there are a couple non dynamic call based solutions.

Invoking Publish method via reflection:

private static readonly MethodInfo PublishMethod = typeof(DomainEventDispatcher).GetMethod("Publish"); // .GetMethods().Single(m => m.Name == "Publish" && m.IsGenericMethodDefinition);

public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
    foreach (var domainEvent in domainEvents)
    {
        var publish = PublishMethod.MakeGenericMethod(domainEvent.GetType());
        publish.Invoke(this, new[] { domainEvent });
    }
}

Invoking subscriber via reflection:

public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
    foreach (var domainEvent in domainEvents)
    {
        var eventType = typeof(Action<>).MakeGenericType(domainEvent.GetType());
        foreach (var subscriber in subscribers)
        {
            if (eventType.IsAssignableFrom(subscriber.GetType()))
                subscriber.DynamicInvoke(domainEvent);
        }
    }
}

Invoking Publish method via precompiled cached delegate:

private static Action<DomainEventDispatcher, IDomainEvent> CreatePublishFunc(Type eventType)
{
    var dispatcher = Expression.Parameter(typeof(DomainEventDispatcher), "dispatcher");
    var domainEvent = Expression.Parameter(typeof(IDomainEvent), "domainEvent");
    var call = Expression.Lambda<Action<DomainEventDispatcher, IDomainEvent>>(
        Expression.Call(dispatcher, "Publish", new [] { eventType },
            Expression.Convert(domainEvent, eventType)),
        dispatcher, domainEvent);
    return call.Compile();
}

private static readonly Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>> publishFuncCache = new Dictionary<Type, Action<DomainEventDispatcher, IDomainEvent>>();

private static Action<DomainEventDispatcher, IDomainEvent> GetPublishFunc(Type eventType)
{
    lock (publishFuncCache)
    {
        Action<DomainEventDispatcher, IDomainEvent> func;
        if (!publishFuncCache.TryGetValue(eventType, out func))
            publishFuncCache.Add(eventType, func = CreatePublishFunc(eventType));
        return func;
    }
}

public void PublishQueue(IEnumerable<IDomainEvent> domainEvents)
{
    foreach (var domainEvent in domainEvents)
    {
        var publish = GetPublishFunc(domainEvent.GetType());
        publish(this, domainEvent);
    }
}

The delegates are lazily created and cached on demand using compiled System.Linq.Expressions.

This method so far should be the fastest. It also is the closest to the dynamic call implementation, with the difference that it works :)