Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generically storing and calling delegates for event broker

I am trying to create an event broker that supports loose coupling while still allowing developers to use the familiar += syntax and intellisense. I am struggling with how to generically call the delegates without using DynamicInvoke.

General Idea:

  • All events are defined in interfaces, each event takes a single argument that derives from EventInfoBase.

    public delegate void EventDelegate<in T>(T eventInfo) where T : EventInfoBase;
    
  • On the subscriber side, the client asks the Windsor container for an instance that implements an event interface. Windsor returns a proxy that intercepts all += (add_xxx) and -= (remove_xxx) calls. The type and delegate information is stored for future lookup and execution when events are fired. Currently the delegate is stored as Delegate but would prefer to store it as something like EventDelegate.

  • On the publisher side, a publisher registers with the event broker as a source for an event interface. The event broker, via reflection, subscribes to each event with a delegate of type EventDelegate<EventInfoBase>.

  • When an event is fired, the event broker looks up any appropriate delegates and executes them.

Problem:

Adding event handlers to the publisher with the EventInfoBase base class is a legal use of contravariance. But the event broker cannot store the client subscriptions as EventDelegate<EventInfoBase>. Eric Lippert explains why in this blog post.

Any ideas of how I could store the client subscriptions (delegates) so they can be called later without the use of DynamicInvoke?

Updated with additional details:

Subscribers ask the event broker for an event interface and then subscribe to events as needed.

// a simple event interface
public class EventOneArgs : EventInfoBase { }
public class EventTwoArgs : EventInfoBase { }
public interface ISomeEvents
{
  event EventDelegate<EventOneArgs> EventOne;
  event EventDelegate<EventTwoArgs> EventTwo;
}

// client gets the event broker and requests the interface
// to the client it looks like a typical object with intellisense available
IWindsorContainer cont = BuildContainer();
var eb = cont.Resolve<IEventBroker>();
var service = eb.Request<ISomeEvents>();
service.EventOne += new EventDelegate<EventOneArgs>(service_EventOne);
service.EventTwo += new EventDelegate<EventTwoArgs>(service_EventTwo);

Under the hood the event broker knows nothing about the event interface, it returns a proxy for that interface. All += calls are intercepted and the subscriptions added dictionary of delegates.

public T Request<T>(string name = null) where T : class
{
  ProxyGenerator proxygenerator = new ProxyGenerator();
  return proxygenerator.CreateInterfaceProxyWithoutTarget(typeof(T), 
            new EventSubscriptionInterceptor(this, name)) as T;
}

public void Intercept(IInvocation invocation)
{
  if (invocation.Method.IsSpecialName)
  {
    if (invocation.Method.Name.Substring(0, s_SubscribePrefix.Length) == s_SubscribePrefix) // "add_"
    {
       // DeclaringType.FullName will be the interface type
       // Combined with the Name - prefix, it will uniquely define the event in the interface
       string uniqueName = invocation.Method.DeclaringType.FullName + "." + invocation.Method.Name.Substring(s_SubscribePrefix.Length);
       var @delegate = invocation.Arguments[0] as Delegate; 
       SubscirptionMgr.Subscribe(uniqueName, @delegate);
       return;
    }
    // ...
  }
}

The delegate stored in the SubscriptionManager is of type EventDelegate<T>, where T is the derived type defined by the event.

Publisher: The publisher registers as a source of an event interface. The goal was to eliminate the need to explicitly call the event broker and allow the typical C# syntax of EventName(args).

public class SomeEventsImpl : ISomeEvents
{
   #region ISomeEvents Members
   public event Ase.EventBroker.EventDelegate<EventOneArgs> EventOne;
   public event Ase.EventBroker.EventDelegate<EventTwoArgs> EventTwo;
   #endregion

   public SomeEventsImpl(Ase.EventBroker.IEventBroker eventBroker)
   {
      // register as a source of events
      eventBroker.RegisterSource<ISomeEvents, SomeEventsImpl>(this);
   }

   public void Fire_EventOne()
   {
      if (EventOne != null)
      {
         EventOne(new EventOneArgs());
      }
   }
}

The event broker uses reflection to subscribe to all the events (AddEventHandler) in the interface with a common handler. I have not tried combining handlers yet. I created a wrapper class in case I needed additional information available when the event is fired, like the type.

public void RegisterSource<T, U>(U instance)
   where T : class
   where U : class
{
   T instanceAsEvents = instance as T;
   string eventInterfaceName = typeof(T).FullName;
   foreach (var eventInfo in instanceAsEvents.GetType().GetEvents())
   {
      var wrapper = new PublishedEventWrapper(this, eventInterfaceName + "." + eventInfo.Name);
      eventInfo.AddEventHandler(instance, wrapper.EventHandler);
   }
}

class PublishedEventWrapper
{
   private IEventPublisher m_publisher = null;
   private readonly EventDelegate<EventInfoBase> m_handler;

   private void EventInfoBaseHandler(EventInfoBase args)
   {
      if (m_publisher != null)
      {
         m_publisher.Publish(this, args);
      }
   }

   public PublishedEventWrapper(IEventPublisher publisher, string eventName)
   {
      m_publisher = publisher;
      EventName = eventName;
      m_handler = new EventDelegate<EventInfoBase>(EventInfoBaseHandler);
   }

   public string EventName { get; private set; }
   public EventDelegate<EventInfoBase> EventHandler
   {
      get { return m_handler; }
   }
}

The problem I have been struggling with lies in the Publish. The Publish method looks up the delegates for the event and needs to execute them. Because of the performance problems with DynamicInvoke I would like to cast the delegate to the correct EventDelegate<T> form and call it directly but have not figured out a way to do it.

I have certainly learned a lot trying to figure this out but am running out of time. I am probably missing something simple. I have tried wrapping the delegate in another one (dynamically generated) that essentially looks like:

private static void WrapDelegate(Delegate d, DerivedInfo args)
{
   var t = d as EventDelegate<DerivedInfo>;
   if (t != null)
   {
      t(args);
   }
}

Any guidance would be appreciated.

like image 546
Ted Schnackertz Avatar asked Dec 27 '25 21:12

Ted Schnackertz


2 Answers

Any ideas of how I could store the client subscriptions (delegates) so they can be called later without the use of DynamicInvoke?

You could use Dictionary<Type, Delegate>, and then cast appropriately:

public void Subscribe<T>(EventDelegate<T> handler) where T : EventInfoBase
{
    Delegate existingHandlerPlain;
    // We don't actually care about the return value here...
    dictionary.TryGetValue(typeof(T), out existingHandlerPlain);
    EventDelegate<T> existingHandler = (EventDelegate<T>) existingHandlerPlain;
    EventDelegate<T> newHandler = existingHandler + handler;
    dictionary[typeof(T)] = newHandler;
}

public void Publish<T>(EventInfo<T> info) where T : EventInfoBase
{
    Delegate handlerPlain;
    if (dictionary.TryGetValue(typeof(T), out handlerPlain))
    {
        EventDelegate<T> handler = (EventDelegate<T>) handlerPlain;
        handler(info);
    }
}

The casts should always be safe because you're managing the contents yourself.

You'll still potentially get problems with variance though, if you try to combine eventhandlers which are actually of different types. If that's a problem, you'll need to explicitly use a List<EventHandler<T>> instead of using the "normal" combining operations.

like image 155
Jon Skeet Avatar answered Dec 30 '25 09:12

Jon Skeet


Solution to wrapping the delegate:

I did manage to find a way to wrap the generic delegates with a delegate of a known type. This allows for standard calling convention of SomeDelegate(args) to be used. This solution takes in a generic delegate defined by:

public delegate void EventDelegate<in T>(T eventInfo) where T : EventInfoBase;

The generic delegate is wrapped by a delegate with a known signature which can be called by the infrastructure code. I have not yet verified the performance of this approach. The cost of calling MethodInfo.Invoke is incurred with the subscription to an event. The cost of firing an event is the code in the DelegateWrapper.

  private static EventDelegate<EventInfoBase> DelegateWrapper<T>(Delegate @delegate) where T : EventInfoBase
  {
     return (o =>
        {
           var t = @delegate as EventDelegate<T>;
           var args = o as T;
           if (t != null && o != null)
           {
              t(args);
           }
        }
     );
  }

  private static readonly MethodInfo s_eventMethodInfo = typeof(EventSubscriptionInterceptor).GetMethod("DelegateWrapper", BindingFlags.NonPublic | BindingFlags.Static);

  private EventDelegate<EventInfoBase> GenerateDelegate(Delegate d)
  {
     MethodInfo closedMethod = s_eventMethodInfo.MakeGenericMethod(d.Method.GetParameters()[0].ParameterType);
     var newDel = closedMethod.Invoke(null, new object[] { d }) as EventDelegate<EventInfoBase>;
     return newDel;
  }

At least this gives me a working prototype of a loosely coupled event broker with ‘mostly’ C# syntax.

like image 25
Ted Schnackertz Avatar answered Dec 30 '25 09:12

Ted Schnackertz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!