39

I'm just wondering if it's possible to have async/await during DI.

Doing the following, the DI fails to resolve my service.

services.AddScoped(async provider => 
{
  var client = new MyClient();
  await client.ConnectAsync();
  return client;
});

where as the following works perfectly fine.

services.AddScoped(provider => 
{
  var client = new MyClient();
  client.ConnectAsync().Wait();
  return client;
});
Steven
  • 166,672
  • 24
  • 332
  • 435
rethabile
  • 3,029
  • 6
  • 34
  • 68

1 Answers1

64

Although it is theoretically possible to use async/await during object resolution, you should consider the following constraints:

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.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • hm! about that. I'm afraid I might have a problem down the line as I can't guarantee the first method to make a call to `MyClient`. – rethabile Apr 05 '17 at 20:41
  • oh! right.I see what you mean. I think it's because I might have a special case here. I have two method which I call in succession (i.e. read before write). I'll have to think if I'll have a use case where I'll have to call only the second method (which currently will fail). – rethabile Apr 05 '17 at 21:11
  • If you connect on first use, it shouldn't fail on the second method. – Steven Apr 05 '17 at 21:17
  • yeah. that part is clear. with `fail` I was just trying to think of any use case where I'll have to call only the second method. which is currently not the case. Thanks! – rethabile Apr 05 '17 at 21:25
  • Your problem probably is that you have no control over the code of MyClient. This means you are violating the Dependency Inversion Principle if your application code uses it directly. You should hide it behind an abstraction. This way you can create an adapter that ensures that the client is connected on the first all to the adapter. You don't want to make your application code be in control of this, because that would be really fragile. – Steven Apr 08 '17 at 07:00
  • I'm not sure i get this one. mind elaborating with example code. – rethabile Apr 08 '17 at 08:13
  • The teal question here is: is MyClient _your code_ and can you change it, or is it 3rd party code? – Steven Apr 08 '17 at 08:16
  • `MyClient` is a 3rd party code, and I inject it into my repository-like class which read result and convert it into my own objects. – rethabile Apr 08 '17 at 17:12
  • wow. I've never heard of `Layzy`. One question, what if i want unit tesf `MyClientAdapter`? Isn't it being dishonest about its dependencies? wouldn't i have inject `connectedClient.Value` into constructor to allow mocking? – rethabile Apr 08 '17 at 18:23
  • @no0b: There's probably little use of mocking `MyClient`, since `MyClientAdapter` has no other logic than the logic that is required to adapt to `MyClient`. This means that if you're interested in testing, you are typically only interested in an integration test that connect to a real endpoint. That said, there are multiple solutions here, either you can make `MyClient` an implementation detail of `MyClientAdapter`, or you can inject it, for instance by injecting the `Task` moving the responsibility of connecting to it part of the Composition Root. – Steven Apr 08 '17 at 18:26
  • @no0b: The use of `Lazy` is not a requirement; you can easily achieve the same by hand. `Lazy` however makes this easier. It just implements the local cache for you. – Steven Apr 08 '17 at 18:28
  • IMyAppService should implement IDisposable so DI can dispose your context when done. You can check connectedClient.iSValueCreated to see if the context exists and then dispose if it does. – Doug Lampe Jun 19 '17 at 14:15
  • 2
    Hi @DougLampe, I would argue against that. The _implementation_ might need to implement `IDisposable`, but the _abstraction_ sure doesn't, because the consuming components should not be able to call `Dispose` at all. Since only the DI Container should dispose this adapter, `IDisposable` is only required at the implementation. In general, implementing `IDisposable` on an abstraction leads to a Leaky Abstraction. – Steven Jun 19 '17 at 14:45
  • Good point @steven. I was assuming that this interface would always be used for IDisposable types. On a related note, this technique does not allow DI to dispose anyway because the Lazy.IsValueCreated will be referencing whether the task is created and not the value it returns. If you want to have a synchronous property for Value that you can grab if IsValueCreated is true, you would have to implement that outside of Lazy. – Doug Lampe Jun 23 '17 at 18:42
  • 2
    This is a really cool pattern. However, what happens if I want to run the `ConnectAsync()` on app bootup instead of just-in-time (I'm trying to avoid latency and timeouts for a low-latency operations on initial connections) – gabe Dec 03 '20 at 23:25
  • @gabe see: https://stackoverflow.com/questions/45924027/avoiding-all-di-antipatterns-for-types-requiring-asynchronous-initialization – Steven Dec 04 '20 at 07:41
  • @Steven I see there is IDisposable added even you argue against it in the comments ? Or am i missing something ? – cah1r Aug 03 '22 at 11:08
  • 2
    @cah1r: I argued against marking the `IMyAppService` *abstraction* with the `IDisposable` interface. It is perfectly fine to have *implementations* such as the `MyClientAdapter` class implement `IDisposable`. – Steven Aug 03 '22 at 11:55
  • @Steven in one of the example I tried using this compiler shows an error with information to use AsyncLazy instead - https://github.com/microsoft/vs-threading/blob/main/doc/analyzers/VSTHRD011.md . Is this something that should be considered as a better option here when the code is async? – cah1r Aug 05 '22 at 12:53
  • If there is now an AsyncLazy provided by the framework, you should definitely prefer that over Lazy. I don't think it was supported by the time I wrote my answer. – Steven Aug 05 '22 at 16:53
  • This is great. I couldn't find a good example for proper Lazy use. – GorillaApe Jun 04 '23 at 20:02