Subscribing to events dynamically using C#

When types from another assembly cannot be referenced statically at compile-time in a .NET application, subscribing to events defined in that assembly and subsequently accessing event arguments that are passed to event handlers can seem a little tricky.

Thankfully, with the combination of the Reflection API and the dynamic operator, it is possible to subscribe and unsubscribe from an external event dynamically and access its event arguments within an event handler method. This capability is typically most useful when working with a .NET assembly that is not referenced directly and is instead loaded at runtime.

In this article, I will cover how you can ‘dynamically’ subscribe and unsubscribe from events and work with custom event arguments using C# even though the types that are involved are not available when compiling your application.

Scenario

Let me start by discussing the most common scenario where I have found the ability to dynamically add and remove event handlers from objects to be useful.

Imagine a scenario where you have an application that needs to use an instance of a class from another assembly. However, the aforementioned assembly cannot be referenced directly by the application and therefore must be loaded at runtime instead.

In this scenario, it is not possible to create an instance of the class you need to use in the usual way using the new operator. Instead, you will need to turn to the Reflection API to retrieve the Type for the class and then use the static Activator.CreateInstance method to create an object of that type.

Now, imagine that the class you are using has a public method that you want to call. That’s fine, as you can use Reflection to call the method too. However, what if the class has an event that you need to subscribe to? Additionally, what if the event features custom event arguments containing data that the event wants to pass to event handlers?

How can we work with events effectively in a scenario like this?

We’ll discuss the solution to this shortly.

Example specifics

Before diving into the solution, let’s make things more concrete by considering the specifics of an example class contained within an external assembly that we need to interact with dynamically from our application.

Let’s suppose that we need to load an assembly named ‘ExternalAssembly’ at runtime so that we can access the public Worker class that it exposes. The implementation of the Worker class is included below for reference.

namespace ExternalAssembly;
 
/// <summary>
/// Class which simulates work in progress.
/// </summary>
public class Worker
{
    #region Events
 
    public event EventHandler<ProgressEventArgs>? ProgressUpdate;
 
    #endregion
 
    #region Methods
 
    /// <summary>
    /// Simulates work.
    /// </summary>
    public void DoWork()
    {
        for (int i = 1; i <= 100; i++)
        {
            // Simulate an expensive operation.
            Thread.Sleep(10);
 
            // Report a progress update.
            ProgressUpdate?.Invoke(this, new ProgressEventArgs($"Working ({i}%)"));
        }
    }
 
    #endregion
}

The Worker class has a public method named DoWork which simulates carrying out expensive work by calling the Thread.Sleep method in a loop 100 times. The Worker class also has a public event named ProgressUpdate which is fired on each loop iteration within the DoWork method to report progress updates.

The ProgressUpdate event specifies that the type of event arguments that will be passed to any subscribed event handlers when the event is raised is of type ProgressEventArgs. The ProgressEventArgs type is another class defined within the external assembly as follows.

namespace ExternalAssembly;
 
/// <summary>
/// Progress event arguments.
/// </summary>
public class ProgressEventArgs : EventArgs
{
    #region Properties
 
    public string ProgressMessage { get; set; }
 
    #endregion
 
    #region Constructor
 
    /// <summary>
    /// Constructor.
    /// </summary>
    /// <param name="progressMessage">The progress message</param>
    public ProgressEventArgs(string progressMessage)
    {
        ProgressMessage = progressMessage;
    }
 
    #endregion
}

The ProgressEventArgs class derives from the standard EventArgs base class and features a single property named ProgressMessage. This property can be set via the constructor for convenience when firing the ProgressUpdate event from the Worker class.

The question now is how do we write the code that will not only create an instance of the Worker class and call the DoWork method, but also subscribe and unsubscribe from the ProgressUpdate event dynamically when interacting with these types from another assembly?

We’ll cover all of this in the next section.

Solution

In the following sub-section, we’ll review the most straightforward code we can write to achieve the desired result.

After that, we’ll look at a more in-depth solution using extension methods to help hide some of the complexity.

Bare-bones example

Let’s implement some code within our application that will load the external assembly and use Reflection to subscribe to the event, call the public method, and then unsubscribe from the event.

The code below demonstrates the simplest ‘bare-bones’ version of the code that can fulfil the requirements, with error handling and additional null checks excluded for the sake of simplicity.

using System.Reflection;
 
public class DynamicEventHandlerDemo
{
    public void Run()
    {
        #pragma warning disable CS8604 // Possible null reference argument.
 
        // Load the external assembly.
        Assembly externalAssembly = Assembly.LoadFrom(
            "../../../../ExternalAssembly/bin/Debug/net6.0/ExternalAssembly.dll");
 
        // Get the Worker class type.
        Type? workerType = externalAssembly.GetType("ExternalAssembly.Worker");
 
        // Create an instance of the Worker class.
        dynamic? worker = Activator.CreateInstance(workerType);
 
        // Get the Event Info for the event to subscribe to.
        EventInfo? eventInfo = workerType.GetEvent("ProgressUpdate");
 
        // Get the Method Info for the event handler method.
        MethodInfo? methodInfo = this.GetType().GetMethod(
            nameof(OnProgressUpdate),
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
 
        // Create the event handler delegate.
        Delegate handler = Delegate.CreateDelegate(
            eventInfo?.EventHandlerType,
            this,
            methodInfo);
 
        // Wire up the event handler.
        eventInfo.AddEventHandler(worker, handler);
 
        // Call the 'DoWork' method dynamically.
        worker?.DoWork();
 
// Unwire the event handler.
eventInfo.RemoveEventHandler(worker, handler);
        #pragma warning restore CS8604 // Possible null reference argument.     }     private void OnProgressUpdate(object sender, EventArgs e)     {        dynamic args = e;         Console.WriteLine("Progress: {0}", args.ProgressMessage);     } }

Note the use of the #pragma directive to avoid compiler warnings regarding possible null references. When writing production code, remove the #pragma directive and add null reference checks instead.

Comments have been included in the above code to help explain each step involved, let me take the time to cover some of the key aspects in more detail below.

The Assembly.LoadFrom method is used to load the external assembly and, of course, the path to this will vary on a case-by-case basis depending on where the assembly you need to load is located on the filesystem.

The GetType method is used to retrieve a Type instance for the Worker class from the external assembly. The Type object is then passed to the Activator.CreateInstance method to create a Worker instance.

EventInfo and MethodInfo instances are retrieved by calling the GetEvent and GetMethod methods respectively. The nameof expression is used to get the name of the OnProgressUpdate event handler method which is defined at the bottom of the class. Notice that the OnProgressUpdate method has a standard generic event handler method signature, with the first method parameter of type object and the second parameter of type EventArgs. If we tried to specify the second parameter as ProgressEventArgs our code would not compile, as this type is defined in the external assembly which isn’t referenced at compile-time. However, with the magic of the dynamic operator, we can convert the EventArgs to a dynamic object and access its properties, well, dynamically – pretty neat!

The Delegate.CreateDelegate method needs to know the type of event that is being subscribed to and this is specified via the EventHandlerType property on the EventInfo object. Additionally, a reference to the object containing the event handler and the MethodInfo instance relating to the event handler method is specified.

With all of this in place, the AddEventHandler method on the EventInfo object is called. This will register the specified event handler for the event on the Worker instance.

To make things easier, the DoWork method can be called dynamically since we specified the Worker object variable as dynamic.

Lastly, the RemoveEventHandler method on the EventInfo object is called to remove the event handler registration.

Refined example

Now that we’ve covered the bare-bones example, how can we make things more generic and easier to reuse?

One obvious solution is to create methods that hide some of the Reflection verboseness and thereby provide a reusable way of subscribing and unsubscribing from events with different names and event handler details.

An example of wrapping up the dynamic event wiring logic within Extension Methods is shown below.

using System.Reflection;
 
namespace JC.Samples.DynamicEventHandlers.Extensions;
 
/// <summary>
/// Contains extension methods that deal with objects.
/// </summary>
public static class ObjectExtensions
{
    #region Methods
 
    /// <summary>
    /// Adds an event handler to an object dynamically.
    /// </summary>
    /// <typeparam name="T1">The target object type</typeparam>
    /// <typeparam name="T2">The handler source object type</typeparam>
    /// <param name="target">The target object</param>
    /// <param name="eventName">The name of the event to subcribe to</param>
    /// <param name="handlerName">The name of the method that will handle the event</param>
    /// <param name="handlerSource">The source object containing the handler method</param>
    /// <returns>A reference to the event handler delegate that was added</returns>
    /// <exception cref="MissingMemberException"></exception>
/// <exception cref="MissingMethodException"></exception>
    public static Delegate AddDynamicEventHandler<T1, T2>(         this T1 target,         string eventName,         string handlerName,        T2 handlerSource) where T1 : class where T2 : class     {         EventInfo? eventInfo = target.GetType().GetEvent(eventName);         if (eventInfo == null || eventInfo.EventHandlerType == null)         {             throw new MissingMemberException($"Could not get event named '{eventName}'");         }         MethodInfo? methodInfo = handlerSource.GetType().GetMethod(            handlerName,             BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);         if (methodInfo == null)         {             throw new MissingMethodException($"Could not get method named '{eventName}'");         }        Delegate handler = Delegate.CreateDelegate(             eventInfo.EventHandlerType,             handlerSource,            methodInfo);         eventInfo.AddEventHandler(target, handler);         return handler;     }     /// <summary>     /// Removes an event handler from an object dynamically.     /// </summary>     /// <typeparam name="T">The target object type</typeparam>     /// <param name="target">The target object</param>     /// <param name="eventName">The name of the event to unsubcribe from</param>     /// <param name="handler">The event handler delegate to remove</param>     /// <exception cref="MissingMemberException"></exception>     public static void RemoveDynamicEventHandler<T>(         this T target,          string eventName,         Delegate handler) where T : class     {         EventInfo? eventInfo = target.GetType().GetEvent(eventName);         if (eventInfo == null)         {             throw new MissingMemberException($"Could not get event named '{eventName}'");         }         eventInfo.RemoveEventHandler(target, handler);     }     #endregion }

The above code is doing essentially the same thing as the bare-bones example, but with some additional checks to prevent compiler warnings regarding possible null references.

The AddDynamicEventHandler and RemoveDynamicEventHandler methods feature Generic Type Parameters with associated where type constraints so that the extension methods can only be called on reference types.

The AddDynamicEventHandler method operates on the object containing the event that is being subscribed to, which naturally is the extension method’s first parameter. The method also accepts the name of the event that is being subscribed to, the name of the method that will handle the event, and the object that contains the event handler method.

The RemoveDynamicEventHandler method operates on the object containing the event that is to be unsubscribed from, which again is the extension method’s first parameter. Aside from this, the method only needs the name of the event to unsubscribe from and the Delegate object that points to the event handler method to be removed.

We can use the extension methods documented above to help simplify things and reduce the amount of code we need to write when adding and removing event handlers dynamically.

The code below documents a WorkerClient class that shows how we can interact with the Worker class from the external assembly.

using JC.Samples.DynamicEventHandlers.Extensions;
using System.Reflection;
 
namespace JC.Samples.DynamicEventHandlers;
 
/// <summary>
/// Interacts with a Worker class from an external assembly.
/// </summary>
public class WorkerClient
{
    #region Methods
 
    #region Public
 
    /// <summary>
    /// Runs Worker code from an external assembly.
    /// </summary>
    /// <exception cref="TypeLoadException"></exception>
    public void RunWorker()
    {
        // Load the external assembly.
        Assembly externalAssembly = Assembly.LoadFrom(
            "../../../../ExternalAssembly/bin/Debug/net6.0/ExternalAssembly.dll");
 
        // Get the Worker class type.
        Type? workerType = externalAssembly.GetType("ExternalAssembly.Worker");
 
        if (workerType == null)
        {
            throw new TypeLoadException("Could not get Worker type.");
        }
 
        // Create an instance of the Worker class.
        var worker = Activator.CreateInstance(workerType);
 
        if (worker == null)
        {
            throw new TypeLoadException("Could not create Worker instance.");
        }
 
        Delegate handler = null!;
 
        try
        {
            // Wire up the 'ProgressUpdate' event handler.
            handler = worker.AddDynamicEventHandler("ProgressUpdate", nameof(OnProgressUpdate), this);
 
            // Call the 'DoWork' method dynamically.
            ((dynamic)worker).DoWork();
        }
        finally
        {
            // Unwire the 'ProgressUpdate' event handler.
            worker.RemoveDynamicEventHandler("ProgressUpdate", handler);
        }
    }
 
    #endregion
 
    #region Private
 
    /// <summary>
    /// Handles the 'ProgressUpdate' event.
    /// </summary>
    /// <param name="sender">The object that fired the event</param>
    /// <param name="e">The event arguments</param>
    private void OnProgressUpdate(object sender, EventArgs e)
    {
        dynamic args = e;
        Console.WriteLine("Progress: {0}", args.ProgressMessage);
    }
 
    #endregion
 
    #endregion
}

The public RunWorker method defined in the code above is similar in some ways to the bare-bones solution that we looked at initially. However, there is less Reflection code verbosity on display and additional null checks have been added to prevent compiler warnings regarding possible null references.

We are still loading the assembly, getting the Worker type, and creating an instance of the Worker class using Reflection. However, we can now avail of the AddDynamicEventHandler and RemoveDynamicEventHandler extension methods to make the remainder of the code more readable. A tryfinally construct is used to ensure that the event handler is always unwired even if the DoWork method throws an exception. The Worker instance is cast to a dynamic object so that the DoWork method can be called without using more Reflection code.

Below is an example of calling the RunWorker method within a .NET Console application.

using JC.Samples.DynamicEventHandlers;
 
Console.WriteLine("Running Worker Client...");
 
var client = new WorkerClient();
client.RunWorker();
 
Console.WriteLine("Finished running Worker Client");

The output of the above program is shown below.

Running Worker Client...
Progress: Working (1%)
Progress: Working (2%)
Progress: Working (3%)
< OTHER 94 PROGRESS UPDATES EXCLUDED FOR BREVITY >
Progress: Working (98%)
Progress: Working (99%)
Progress: Working (100%)
Finished running Worker Client

All of the code from this ‘Refined sample’ section can be found on the companion GitHub repository. I encourage you to download/clone the repository and run it so you can see the code in action.

Note that since the Console project doesn’t reference the external assembly directly, you’ll need to build the ‘ExternalAssembly’ project within the solution before running the program so that the assembly file can be found when the program attempts to load it.

Summary

In this article, I have covered how you can subscribe and unsubscribe from events that are defined within classes that are part of an external .NET assembly that is loaded at application runtime.

I started by walking through the primary scenario that I’ve seen the solution apply to and provided details regarding a specific example of the scenario.

I then walked through the solution, starting with the simplest possible code for demonstration purposes. After this, I documented a more refined solution that uses extension methods to make the code more readable and to hide the verbosity of the Reflection method calls and null reference checks.

I trust that you found the article interesting and hope that you can either directly use or adapt the code samples to suit your specific scenario. Remember to check out the GitHub repository for the full code.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments

Joe

Nice article! Was trying it out on a COM library class that provides multiple events and am noticing that the first call to AddDynamicEventHandler works fine, but any subsequent call to AddDynamicEventHandler to attach more that one event throws an exception in AddDynamicEventHandler on the call to eventInfo.AddEventHandler. If I rearrange the AddDynamicEventHandler calls the first is always ok and the second always fails.
for example:
instance = Activator.CreateInstance(objClassType);
handlerOnState = instance.AddDynamicEventHandler(“OnState”, nameof(OnState), this);
handlerOnUpdate = instance.AddDynamicEventHandler(“OnUpdate”, nameof(OnUpdate), this);
Curious if you had any ideas?

January 30, 2023

Jonathan Crozier

Thanks, Joe. That’s an unusual one, your code looks correct in principle. Are you able to share the exact error message and type of exception that is being thrown? Is the COM library well-known or quite bespoke to your project?

January 31, 2023

Joe

It’s ‘a COM Exception error ‘Exception from HRESULT: 0x80040202”.

It’s a third party library that has been around for years but does require special dongle licensing to use.

Anyways, possibly something related to this COM library implementation I suspect. I don’t think there is any issue with your event handler sample code at all It. It all seems to be quite good.

Was just curious if you had encountered anything similar.

February 1, 2023

Jonathan Crozier

Thanks for the extra information, Joe.

I’d recommend looking at questions related to this error code on StackOverflow, check out the following link.

https://stackoverflow.com/search?q=HRESULT%3A+0x80040202

Some of the issues other people are having seem similar to yours. They are getting an error when trying to wire up event handlers for events defined in a COM library.

February 1, 2023

Peter

Excellent, just what I was looking for, works well. Thanks!

May 12, 2023

Jonathan Crozier

Thanks, Peter, I’m glad the solution is working well for you 🙂

May 12, 2023