simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.22k stars 152 forks source link

Thread Scoped vs Async Scoped #935

Closed stigc closed 2 years ago

stigc commented 2 years ago

I am a little confused when to use Thread Scope instead of Async Scoped.

This is the AsyncScopedLifestyle example from the documentation

using (AsyncScopedLifestyle.BeginScope(container))
{
    var uow1 = container.GetInstance<IUnitOfWork>();
    await SomeAsyncOperation();

    var uow2 = container.GetInstance<IUnitOfWork>();
    await SomeOtherAsyncOperation();

    Assert.AreSame(uow1, uow2);
}
  1. It is important to use AsyncScopedLifestyle here, since there are multiple calls to GetInstance with async/await code in between, right?
  2. If we had only 1 call to GetInstance, ThreadScopedLifestyle would be fine, right? (assuming there is no async/await code before the GetInstance)
  3. Why would I ever use ThreadScopedLifestyle, is AsyncScopedLifestyle more expensive?
dotnetjunkie commented 2 years ago

ThreadScopedLifestyle is a legacy feature. It exists for application types where asynchronous scoping is not supported. This typically means platforms that only support .NET Standard 1.0, 1.1, or 1.2.

As there is no noticable performance penalty between ThreadScopedLifestyle and AsyncScopedLifestyle, and considering that you can safely use AsyncScopedLifestyle for single-threaded operations as well, our advise it to always use AsyncScopedLifestyle as your default scoped lifestyle.

ThreadScopedLifestyle will likely be removed from a future version of Simple Injector (e.g. 6.0 or 7.0).

  1. If we had only 1 call to GetInstance, ThreadScopedLifestyle would be fine, right? (assuming there is no async/await code before the GetInstance)

In theory, yes. But consider that its possible to have calls to GetInstance from deep inside the call graph at any point in time, for instance when doing dispatching of messages to (a list of) registered service(s). This makes it quite tricky to use the ThreadScopedLifestyle for applications that apply asynchronous programming techniques.

stigc commented 2 years ago

Thanks, you explained it well enough, we well change to AsyncScopedLifestyle. The documentation is not that clear :)

@dotnetjunkie If we had only 1 call to GetInstance, ThreadScopedLifestyle would be fine, right? (assuming there is no async/await code before the GetInstance) In theory, yes. But consider that its possible to have calls to GetInstance from deep inside the call graph at any point in time, for instance when doing dispatching of messages to (a list of) registered service(s). This makes it quite tricky to use the ThreadScopedLifestyle for applications that apply asynchronous programming techniques.

I was assuming that the container is not accessible later in the code. Also there must not be any async/await code between the GetInstance and the dispose (when the using is ending), right?

dotnetjunkie commented 2 years ago

I was assuming that the container is not accessible later in the code. Also there must not be any async/await code between the GetInstance and the dispose (when the using is ending), right?

Consider the following code:

using (ThreadScopedLifestyle.BeginScope(container))
{
    var controller = container.GetInstance<OrdersController>();

    await controller.ShipOrder(id);
}

This code might imply that object resolution always takes place that the thread that started the ThreadScopedLifestyle, but that might not be the case. Consider the following implementation for OrdersController:

public record OrdersController(IMediator mediator) : Controller
{
    public Task ShipOrder(Guid id)
    {
        await SomeValidation();
        await this.mediator.Dispatch(new ShipOrderCommand(id));
    }
}

A Mediator is an object that would dispatch to dynamically loaded implementations, for instance:

public record SimpleInjectorMediator(Container container) : IMediator
{
    public Task Dispatch<TCommand>(TCommand command)
    {
        var handler = this.container.GetInstance<ICommandHandler<TCommand>>();

        await handler.Handle(command);
    }
}

While the IMediator interface might be defined in a core library of the application, its SimpleInjectorMediator implementation would be implemented in the Composition Root which allows it access to the DI Container. The SimpleInjectorMediator only starts resolving a command handler after its Dispatch function is called.

If we remove the OrdersController and SimpleInjectorMediator and flattern everything down to a single method, you can more easily see that even the seemingly innocent first example, calls GetInstance after calling await:

using (ThreadScopedLifestyle.BeginScope(container))
{
    // GetInstance before await
    var controller = container.GetInstance<OrdersController>();

    await controller.SomeValidation();

    // GetInstance *after* await
    var handler = container.GetInstance<ICommandHandler<ShipOrderCommand>>();

    await handler.Handle(new ShipOrderCommand(id));
}
stigc commented 2 years ago

Thanks @dotnetjunkie this is apricated. I see there are a lot of pitfalls, when also using the more advanced SimpleInjector technics.

dotnetjunkie commented 2 years ago

Asynchronous programming is one big pitfall in itself. Unfortunately, there's no way we can avoid it nowadays.