Saturday, June 21, 2014

Waiting for Events with Tasks in .NET

Would you like to just await the next time that an event fires? It takes a little setup, but you can!

Migrating your code from using an Event-based Asynchronous Pattern to an Task-based Asynchronous Pattern can be very tricky. You can use the TaskCompletionSource to manage your Task, and then you just need to create the wire up around registering and unregistering your event handler. Unfortunately this process is not nearly as generic as I would like.

Event-based Asynchronous Pattern Tests

Here is a way of waiting for an event to fire by simply sleeping while we wait.

This is a terrible solution because we can not know how long we will have to wait for the event. This means we have to wait for one long time period, or we have to periodically poll to see if the event has fired.

public delegate void SingleParamTestDelegate(int key, string value);
 
public class EventAsynchronousProgrammingTest
{
    public event SingleParamTestDelegate SingleParamTestEvent;
 
    private int _lastKey;
    private string _lastValue;
 
    private void OnSingleParamTest(int key, string value)
    {
        _lastKey = key;
        _lastValue = value;
    }
 
    [Fact]
    public void Success()
    {
        // Simulate event invoke in 100 milliseconds
        Task
            .Run(async () =>
            {
                await Task.Delay(100);
                SingleParamTestEvent.Invoke(1, "Hello World");
            })
            .FireAndForget();
 
        try
        {
            // Subscribe for event.
            SingleParamTestEvent += OnSingleParamTest;
 
            // Wait for event to be fired.
            Task.Delay(200).Wait();
 
            // Assert event was fired.
            Assert.Equal(_lastKey, 1);
            Assert.Equal(_lastValue, "Hello World");
 
        }
        finally
        {
            // Unsubscribe from event.
            SingleParamTestEvent -= OnSingleParamTest;                
        }
    }
 
    [Fact]
    public void Failure()
    {
        try
        {
            // Subscribe for event.
            SingleParamTestEvent += OnSingleParamTest;
 
            // Wait for event to be fired.
            Task.Delay(200).Wait();
 
            // Assert event was NOT fired.
            Assert.NotEqual(_lastKey, 1);
            Assert.NotEqual(_lastValue, "Hello World");
 
        }
        finally
        {
            // Unsubscribe from event.
            SingleParamTestEvent -= OnSingleParamTest;
        }
    }
}

Task-based Asynchronous Pattern Tests

Here is an example of how to create a task that we can await to find out the next time that an event fires. The task result will return a Tuple with the arguments from the event invoke.

The process of creating the Task is not as simple or generic as I would like, but it does work quite well. Below there is a TaskHelpers.FromEvent method that does most of the work, but you will need create a wrapper around that for each event that you want to use.

As I mention in the comments below, in one of my projects I actually used code generation with T4 templates to automatically generate awaitable methods for my event handlers.

public class TaskAsynchronousProgrammingTest
{
    public event SingleParamTestDelegate SingleParamTestEvent;
 
    public Task<Tuple<int, string>> SingleParamTestEvenAsync()
    {
        // Use our helper method to create a Task that waits for the event to
        // fire. This task will return a Tuple with the arguments from the
        // method's Invoke.
 
        return TaskHelpers
            .FromEvent<SingleParamTestDelegate, Tuple<int, string>>(
                tcs => (k, v) =>
                {
                    var result = Tuple.Create(k, v);
                    tcs.SetResult(result);
                },
                del => SingleParamTestEvent += del,
                del => SingleParamTestEvent -= del,
                TimeSpan.FromMilliseconds(200));
 
        // A fun side note, in one of my projects I am using code generation
        // with T4 to automatically generate these methods from public events
        // on my classes!
    }
 
    [Fact]
    public async Task Success()
    {
        // Simulate event invoke in 100 milliseconds
        Task
            .Run(async () =>
            {
                await Task.Delay(100);
                SingleParamTestEvent.Invoke(1, "Goodnight Moon");
            })
            .FireAndForget();
 
        // Start a Stopwatch to see how long we waited.
        var sw = Stopwatch.StartNew();
 
        // Wait for the result from our task.
        var result = await SingleParamTestEvenAsync();
            
        // Stop the stopwatch
        sw.Stop();
 
        // Assert that the event fired.
        Assert.Equal(result.Item1, 1);
        Assert.Equal(result.Item2, "Goodnight Moon");
 
        // Assert we only waited the 100 milliseconds!
        Assert.InRange(sw.ElapsedMilliseconds, 80, 120);
    }
 
    [Fact]
    public void Failure()
    {
        Assert.Throws(typeof(AggregateException), () =>
        {
            // Wait for task to timeout.
            var task = SingleParamTestEvenAsync();
            task.Wait();
        });
    }
}
 
public static class TaskHelpers
{
    public static async Task<TResult> FromEvent<TDelegate, TResult>(
        Func<TaskCompletionSource<TResult>, TDelegate> createDelegate,
        Action<TDelegate> registerDelegate,
        Action<TDelegate> unregisterDelegate,
        TimeSpan timeout)
    {
        // Create a task completion source.
        var tcs = new TaskCompletionSource<TResult>();
 
        // Create a cancellation token and have it cancel our
        // task completion source when it times out.
        var cts = new CancellationTokenSource(timeout);
        cts.Token.Register(() => taskCompletionSource.TrySetCanceled());
 
        // Create the deleate.
        var del = createDelegate(tcs);
 
        try
        {
            // Subscribe for event.
            registerDelegate(del);
 
            // Await the result.
            return await tcs.Task;
        }
        finally
        {
            // Unsubscribe from event.
            unregisterDelegate(del);
        }
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void FireAndForget(this Task task)
    {
        // Do Nothing
    }
}

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics