Although it is theoretically possible to use async/await during object resolution, you should consider the following constraints:
- Constructors can’t be asynchronous, and
- Construction of object graphs should be simple, reliable and fast
Because of these constraints, it’s best to postpone everything that involves I/O 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.