3

I have an application with a factory service to allow construction of instances while resolving the necessary dependency injection. For instance, I use this to construct dialog view models. I have a service interface that looks like this:

public interface IAsyncFactory
{
    Task<T> Build<T>() where T: class, IAsyncInitialize;
}

Ideally, what I'd like to have is something like this (pseudo-syntax, as this isn't directly achievable)

public interface IFactory
{
    Task<T> Build<T>() where T: class, IAsyncInitialize;

    T Build<T>() where T: class, !IAsyncInitialize;
}

The idea here is that if a class supports IAsyncInitialize, I'd like the compiler to resolve to the method that returns Task<T> so that it's obvious from the consuming code that it needs to wait for initialization. If the class does not support IAsyncInitialize, I'd like to return the class directly. The C# syntax doesn't allow this, but is there a different way to achieve what I'm after? The main goal here is to help remind the consumer of the class of the correct way to instantiate it, so that for classes with an asynchronous initialization component, I don't try to use it before it has been initialized.

The closest I can think of is to create separate Build and BuildAsync methods, with a runtime error if you call Build for an IAsyncInitialize type, but this doesn't have the benefit of catching errors at compile time.

Dan Bryant
  • 27,329
  • 4
  • 56
  • 102

2 Answers2

4

In general Microsoft suggests to add async suffix when naming asynchronous methods. Thus, your assumption of creating two methods named as Build and BuildAsync makes sense.

I think there is no way to enforce something like "all types that do not implement IAsyncInitialize shall use Build method instead of BuildAsync" unless you force the developers to mark synchronous methods with another interface like ISynchronousInitialize.

You may try the following;

  1. instead of having to separate methods, just implement one BuildAsync method which has the following signature:

    Task<T> BuildAsync<T>() where T: class
    
  2. In the BuildAsync method check whether T implements IAsyncInitialize. If this is the case, just call related initialization code after creating the object of type T. Otherwise, just create a TaskCompletionSource object and run the synchronous initialization code as if it is asynchronous.

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
daryal
  • 14,643
  • 4
  • 38
  • 54
  • This would work, but it has the unfortunate side effect of forcing consumers to treat all build operation as if they might be asynchronous, which in turn forces their own initialization to be asynchronous and so on up the chain. This has the benefit of standardizing the initialization process, but it's seems a bit heavy when only a subset of the classes actually require asynchronous initialization. – Dan Bryant Apr 11 '14 at 16:04
  • Actually, the `ISynchronousInitialize` requirement might not be a bad way to go. By implementing one of the two interfaces, the classes essentially 'opt-in' to construction via this factory, which is reasonable for my design. This opens up some interesting possibilities for allowing additional arguments during initialization as well, which is always tricky when performing dependency injection. – Dan Bryant Apr 11 '14 at 16:08
3

The following approach might not be the best, but I find it very convenient. When both asynchronous and synchronous initializers are available (or possibly can be available), I wrap the synchronous one as asynchronous with Task.FromResult, and only expose the asynchronous method to the client:

public interface IAsyncInitialize
{
    Task InitAsync();
    int Data { get; }
}

// sync version
class SyncClass : IAsyncInitialize
{
    readonly int _data = 1;

    public Task InitAsync()
    {
        return Task.FromResult(true);
    }

    public int Data { get { return _data; } }
}

// async version
class AsyncClass: IAsyncInitialize
{
    int? _data;

    public async Task InitAsync()
    {
        await Task.Delay(1000);
        _data = 1;
    }

    public int Data
    {
        get 
        {
            if (!_data.HasValue)
                throw new ApplicationException("Data uninitalized.");
            return _data.Value; 
        }
    }
}

This leaves only the asynchronous version of the factory:

public interface IAsyncFactory
{
    // Build can create either SyncClass or AsyncClass
    Task<T> Build<T>() where T: class, IAsyncInitialize;
}

Furthermore, I prefer to avoid dedicated initializer methods like InitAsync, and rather expose asynchronous properties directly as tasks:

public interface IAsyncData
{
    Task<int> AsyncData { get; }
}

// sync version
class SyncClass : IAsyncData
{
    readonly Task<int> _data = Task.FromResult(1);

    public Task<int> AsyncData
    {
        get { return _data; }
    }
}

// async versions
class AsyncClass : IAsyncData
{
    readonly Task<int> _data = GetDataAsync();

    public Task<int> AsyncData
    {
        get { return _data; }
    }

    private static async Task<int> GetDataAsync()
    {
        await Task.Delay(1000);
        return 1;
    }
}

In either case, it always imposes asynchrony on the client code, i.e.:

var sum = await provider1.AsyncData + await provider2.AsyncData;

However, I don't think it's an issue as the overhead of Task.FromResult and await Task.FromResult for the synchronous version is quite low. I'm going to post some benchmarks.

The approach using asynchronous properties can be further improved with Lazy<T>, e.g. like this:

public class AsyncData<T>
{
    readonly Lazy<Task<T>> _data;

    // expose async initializer 
    public AsyncData(Func<Task<T>> asyncInit, bool makeThreadSafe = true)
    {
        _data = new Lazy<Task<T>>(asyncInit, makeThreadSafe);
    }

    // expose sync initializer as async
    public AsyncData(Func<T> syncInit, bool makeThreadSafe = true)
    {
        _data = new Lazy<Task<T>>(() => 
            Task.FromResult(syncInit()), makeThreadSafe);
    }

    public Task<T> AsyncValue
    {
        get { return _data.Value; }
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • For anyone looking for a wrapper like this, you [might find the answer of this question I asked useful](http://stackoverflow.com/a/33946166/11635) – Ruben Bartelink May 23 '16 at 09:11