Binding lambdas to typed event handlers with reflection
A common type of unit test in .NET is to check to make sure an event is raised when an action is performed. Unfortunately, this often generates a lot of boilerplate code, especially with MSTest. Wouldn’t it be great to remove some of that boilerplate?
The boilerplate-heavy way to write this test is to do something like the following, using a closure defined inline:
var didEventRaise = false;
var markEventAsRaised = (s, e) => {
didEventRaise = true;
};
try {
thing.EventHappened += new SomethingHappenedEventHandler(markEventAsRaised);
thing.DoSomethingThatRaisesEvent();
Assert.IsTrue(didEventRaise);
}
finally {
thing.EventHappened -= markEventAsRaised;
}
This works great because the compiler can infer the direct usage of this lambda, but we don’t want to be copy-pasting all of this stuff and making unreadable (or worse, fragile) tests. What we want is a generic method that just checks if a given event was raised when you do a certain thing.
Problems happen when you start trying to side-step the delegate system using reflection.
You can’t directly cast a lambda to a specific event handler, like this:
var toBind = typeof(Thing).GetEvent("EventHappened");
toBind.AddEventHandler(thing, markEventAsRaised);
// Runtime error!
// System.ArgumentException: 'Object of type 'System.Action`2[System.Object,System.EventArgs]' cannot be converted to type 'AssertEventIsReceived.Program+Thing+SomethingHappenedEventHandler'.'
You also can’t use Activator.CreateInstance
and pass the lambda as a constructor
argument, because there is no way of knowing that your generic type argument
TEventHandler
is a delegate type (and there is no generic type constraint currently
in .NET to ensure that it is):
var markEventAsRaisedWrapper = (TEventHandler)Activator.CreateInstance(typeof(TEventHandler), new { markEventAsRaised });
toBind.AddEventHandler(underTest, markEventAsRaisedWrapper);
// Compile error!
// Argument 2: cannot convert from 'TEventHandler' to 'System.Delegate'
It won’t even believe a blind cast like (Delegate)markEventAsRaisedWrapper
.
In the end, I got around this by wrapping the lambda invoke inside a delegate of the correct
type using Delegate.CreateDelegate
:
var markEventAsRaisedWrapper = Delegate.CreateDelegate(typeof(TEventHandler), markEventAsRaised, "Invoke");
...
toBind.AddEventHandler(underTest, markEventAsRaisedWrapper);
toBind.Invoke(underTest, null); // trigger the event...
This makes the delegate “type system” happy, because the explicit type of the delegate is handled. You’ll have some problems dealing with delegates that take different kinds of arguments than (object, EventArgs) but this should give you a good starting point to develop a more robust solution.
The full method looks like this:
[TestMethod] public void TestEventHappens()
{
AssertEventIsReceived<Thing, Thing.SomethingHappenedEventHandler>(nameof(Thing.SomethingHappened), nameof(Thing.TriggerEvent));
}
class Thing
{
public void TriggerEvent()
{
SomethingHappened?.Invoke(this, new EventArgs());
}
public event SomethingHappenedEventHandler SomethingHappened;
public delegate void SomethingHappenedEventHandler(object sender, EventArgs e);
}
static void AssertEventIsReceived<TEventSender, TEventHandler>(string eventName, string triggerMethodName)
{
var wasEventEverRaised = false;
Action<object, EventArgs> markEventAsRaised =
(s, e) =>
{
wasEventEverRaised = true;
};
var underTest = Activator.CreateInstance<TEventSender>();
var toBind = typeof(TEventSender).GetEvent(eventName);
var triggerMethod = typeof(TEventSender).GetMethod(triggerMethodName);
var markEventAsRaisedWrapper = Delegate.CreateDelegate(typeof(TEventHandler), markEventAsRaised, nameof(markEventAsRaised.Invoke));
try
{
toBind.AddEventHandler(underTest, markEventAsRaisedWrapper);
triggerMethod.Invoke(underTest, null);
}
finally
{
toBind.RemoveEventHandler(underTest, markEventAsRaisedWrapper);
}
AssertIsTrue(wasEventEverRaised);
}
Note: You can’t find the Delegate.CreateDelegate
method in .NET Core as of the
time of writing, which is a pity.