Async provider in .NET Core DI

Although it is theoretically possible to use async/await during object resolution, it doesn’t make much sense when resolving dependencies, because:

This means that everything that involves I/O should be postponed until after the object graph has been constructed.

So instead of injecting a connected MyClient, MyClient should connect when it is used for the first time, not when it is created.

Since your MyClient is not an application component but a third-party component, this means that you can’t ensure that it “connect[s] when it is used for the first time.”

This shouldn’t be a problem, however, because the Dependency Inversion Principle already teaches us that:

the abstracts are owned by the upper/policy layers

This means that application components should not depend on third-party components directly, but instead they should depend on abstractions defined by the application itself. As part of the Composition Root, adapters can be written that implement these abstractions and adapt application code to the third-party libraries.

An important advantage of this is that you are in control over the API that your application components use, which is the key to success here, as it allows the connectivity issues to be hidden behind the abstraction completely.

Here’s an example of how your application-tailored abstraction might look like:

public interface IMyAppService
{
    Task<Data> GetData();
    Task SendData(Data data);
}

Do note that this abstraction lacks an ConnectAsync method; this is hidden behind the abstraction. Take a look at the following adapter for instance:

public sealed class MyClientAdapter : IMyAppService, IDisposable
{
    private readonly Lazy<Task<MyClient>> connectedClient;

    public MyClientAdapter()
    {
        this.connectedClient = new Lazy<Task<MyClient>>(async () =>
        {
            var client = new MyClient();
            await client.ConnectAsync();
            return client;
        });
    }

    public async Task<Data> GetData()
    {
        var client = await this.connectedClient.Value;
        return await client.GetData();
    }

    public async Task SendData(Data data)
    {
        var client = await this.connectedClient.Value;
        await client.SendData(data);
    }

    public void Dispose()
    {
        if (this.connectedClient.IsValueCreated)
        {
            this.connectedClient.Value.Dispose();
        }
    }
}

The adapter hides the connectivity details from the application code. It wraps the creation and connection of MyClient in a Lazy<T>, which allows the client to be connected just once, independently of in which order the GetData and SendData methods are called, and how many times.

This allows you to let your application components depend on IMyAppService instead of MyClient and register the MyClientAdapter as IMyAppService with the appropriate lifestyle.

Leave a Comment