Dmitry Shechtman's Blog

November 27, 2014

AsyncActivator: Handling Cancellation


This is the second article in a series concerning AsyncActivator, a generalization of the .NET Asynchronous Factory pattern:

  1. Basics
  2. Cancellation
  3. Portability
  4. Silverlight
  5. Dependency injection

The Problem

Recall UniversalAnswerService from the previous article:

class UniversalAnswerService : AsyncInitBase<UniversalAnswerService>
{
    private UniversalAnswerService()
    {
    }

    protected override async Task InitAsync()
    {
        await Task.Delay(TimeSpan.FromDays(7500000 * 365.25));
        Answer = 42;
    }

    public int Answer { get; private set; }
}

Unlike Stephen Cleary’s original example, our implementation conveys a more realistic delay. Let’s assume that we’re able to come up with a server that’s actually capable of running this code. We’re now bound to deal with particularly impatient end users refusing to wait for the computation to complete. What we need is a way for our service to respond to cancellation requests.

Although this is a somewhat contrived example, cancellation is a very serious issue that needs to be addressed in most real-life scenarios.

The Solution

Fortunately, Task Parallel Library provides a built-in facility for cancelling background tasks. For example, Task.Delay() has an overload with an additional CancellationToken parameter, which is just what we need:

class UniversalAnswerService
{
    private UniversalAnswerService()
    {
    }

    public async Task InitAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(TimeSpan.FromDays(7500000 * 365.25), cancellationToken);
        Answer = 42;
    }

    public static async Task<UniversalAnswerService> CreateAsync(CancellationToken cancellationToken)
    {
        var value = new UniversalAnswerService();
        await value.InitAsync(cancellationToken);
        return value;
    }

    public int Answer { get; private set; }
}

As before, we’re after a generic solution, so let’s start by defining an interface:

public interface ICancelableAsyncInit
{
    Task InitAsync(CancellationToken cancellationToken);
}

Note that ICancelableAsyncInit doesn’t derive from IAsyncInit. This leaves implementers with more choice — to implement IAsyncInit, ICancelableAsyncInit, or both.

Next, augment AsyncActivator with the following overload:

    public static async Task<T> CreateAsync<T>(CancellationToken cancellationToken)
        where T : ICancelableAsyncInit
    {
        T value = (T)Activator.CreateInstance(typeof(T), true);
        await value.InitAsync(cancellationToken);
        return value;
    }

Update 2014-11-30: The code above is not PCL-compliant. See the third article for the portable version.

Finally, provide a base class:

public abstract class CancelableAsyncInitBase<T> : AsyncInitBase<T>, ICancelableAsyncInit
    where T : CancelableAsyncInitBase<T>
{
    public static Task<T> CreateAsync(CancellationToken cancellationToken)
    {
        return AsyncActivator.CreateAsync<T>(cancellationToken);
    }

    protected override Task InitAsync()
    {
        return InitAsync(CancellationToken.None);
    }

    protected abstract Task InitAsync(CancellationToken cancellationToken);

    Task ICancelableAsyncInit.InitAsync(CancellationToken cancellationToken)
    {
        return InitAsync(cancellationToken);
    }
}

This time, CancelableAsyncInitBase does extend AsyncInitBase, since in most cases the concrete class will expose both overloads of InitAsync(). The provided default implementation leaves deriving classes with just InitAsync(CancellationToken) missing, although they’re free to override the parameterless one should the need arise.

The final UniversalAnswerService:

class UniversalAnswerService : CancelableAsyncInitBase<UniversalAnswerService>
{
    private UniversalAnswerService()
    {
    }

    protected override async Task InitAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(TimeSpan.FromDays(7500000 * 365.25), cancellationToken);
        Answer = 42;
    }

    public int Answer { get; private set; }
}

Generalization

As before, generalizing ICancelabelAsyncInit, CancelableAsyncInitBase and AsyncActivator to support initialization parameters is pretty straightforward. You may find the complete code on GitHub.

This is what CustomUniversalAnswerService (implementing all four interfaces) would look like:

public class CustomUniversalAnswerService : CancelableAsyncInitBase<CustomUniversalAnswerService, TimeSpan>, ICancelableAsyncInit, IAsyncInit
{
    private CustomUniversalAnswerService()
    {
    }

    protected override async Task InitAsync(TimeSpan delay, CancellationToken cancellationToken)
    {
        await Task.Delay(delay, cancellationToken);
        Answer = new Random(delay.Milliseconds).Next();
    }

    Task ICancelableAsyncInit.InitAsync(CancellationToken cancellationToken)
    {
        return InitAsync(TimeSpan.FromDays(7500000 * 365.25), cancellationToken);
    }

    Task IAsyncInit.InitAsync()
    {
        return InitAsync(TimeSpan.FromDays(7500000 * 365.25));
    }

    public static Task<CustomUniversalAnswerService> CreateAsync(CancellationToken cancellationToken)
    {
        return AsyncActivator.CreateAsync<CustomUniversalAnswerService>(cancellationToken);
    }

    public int Answer { get; private set; }
}

Summary

We discussed adding cancellation support to AsyncActivator, an implementation of the asynchronous factory pattern.

Before using it you should consider:

  • the number of initialization arguments and
  • whether you’ll provide cancelable API (you really should)

AsyncActivator is the factory class. Consult the following table for the interface/base class to implement/extend:

Cancelable Non-Cancelable
No arguments ICancelableAsyncInit or CancelableAsyncInitBase IAsyncInit or AsyncInitBase
1 argument ICancelableAsyncInit or CancelableAsyncInitBase IAsyncInit or AsyncInitBase
>1 argument same as above, with the parameters encapsulated in a parameter object
≥0 arguments same as ≥1 with a little extra work, see example

You may find the updated sources in the GitHub repository.

Advertisements

3 Comments »

  1. […] Cancellation […]

    Pingback by AsyncActivator: Targetting Multiple Platforms | Dmitry Shechtman's Blog — November 30, 2014 @ 03:10

  2. […] Cancellation […]

    Pingback by AsyncActivator: Targeting Silverlight | Dmitry Shechtman's Blog — December 13, 2014 @ 21:02


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.