SimpleInjector icon indicating copy to clipboard operation
SimpleInjector copied to clipboard

Support for parallel scopes with different life spans

Open torarnegustad opened this issue 3 years ago • 3 comments

Hi.

We have a service application which runs multiple scheduled tasks/jobs (using Quartz.Net). Using ExecutionContextScope we wrote it so every task gets its own instances of every dependency when it starts. Since we don't know when the task will finished we keep a list of tasks and their scopes so we are able to dispose the scope from a custom job factory when the task ends.

However, each task has a variable life span based on how much work it has to do. This means that multiple tasks will often overlap.

Since every Scope has a ParentScope this means that overlapping tasks will be dependent on each other. Example:

  1. Task A starts.
  2. Task B starts, and gets assigned a ParentScope from Task A.
  3. Task A ends, and its Scope gets disposed.
  4. Since Task B is attached through the ParentScope, task B also loses its instances.
  5. Task B fails.

Using SimpleInjector v2.8.3 we solved this by simply detaching the ParentScope when a new Scope was created, using reflection. This was possible since the ParentScope property actually had a private Setter, but this is no longer the case.

Question / Request

Could you implement a way for the user to "unconditionally" detach a Scope from its ParentScope? This could be as simple as a method in the Scope base class.

Alternatively, is there a way to implement this without the need to detach the parent scope? We need to be able to create and dispose the scope manually, and not with a "using(..)" block.

torarnegustad avatar Jan 21 '22 10:01 torarnegustad

Hi @torarnegustad,

Please consider migrating to a newer version of Simple Injector and test whether this works in that version. I suggest testing your case in v5, because in that case I can more easily give you support on this.

I tested the behavior in v5.3.2 and I'm not saying the behavior you described, so this is something that likely improved in newer versions. Here's my test code:

using SimpleInjector;
using SimpleInjector.Lifestyles;

var container = new Container();

container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

container.Register<MyScoped>(Lifestyle.Scoped);

Console.WriteLine("Running. Press enter to stop...");

Task.Factory.StartNew(() =>
{
    using (AsyncScopedLifestyle.BeginScope(container))
    {
        Console.WriteLine($"{DateTime.Now:T} Running Task A on thread {Environment.CurrentManagedThreadId}.");
        container.GetInstance<MyScoped>();

        Task.Factory.StartNew(() =>
        {
            Console.WriteLine($"{DateTime.Now:T} Running Task B on thread {Environment.CurrentManagedThreadId}.");
            using (AsyncScopedLifestyle.BeginScope(container))
            {
                container.GetInstance<MyScoped>();

                Thread.Sleep(4000);
            }

            Console.WriteLine($"{DateTime.Now:T} Task B completed on thread {Environment.CurrentManagedThreadId}.");
        });

        Thread.Sleep(2000);
    }

    Console.WriteLine($"{DateTime.Now:T} Task A completed on thread {Environment.CurrentManagedThreadId}.");
});

Console.ReadLine();



class MyScoped : IDisposable
{
    static int counter = 1;
    int id = counter++;

    public MyScoped() => Console.WriteLine($"{DateTime.Now:T} Created {id} on thread {Environment.CurrentManagedThreadId}.");
    public void Dispose() => Console.WriteLine($"{DateTime.Now:T} Disposed {id} on thread {Environment.CurrentManagedThreadId}.");
}

This is the application's output:

Running. Press enter to stop...
12:04:28 Running Task A on thread 4.
12:04:28 Created 1 on thread 4.
12:04:28 Running Task B on thread 7.
12:04:28 Created 2 on thread 7.
12:04:30 Disposed 1 on thread 4.
12:04:30 Task A completed on thread 4.
12:04:32 Disposed 2 on thread 7.
12:04:32 Task B completed on thread 7.

dotnetjunkie avatar Jan 21 '22 11:01 dotnetjunkie

Hi @dotnetjunkie, and thank you for your quick reply.

AsyncScopedLifestyle seems to match the requirements perfectly - I'll give this a try in v5.3.2 and report back.

torarnegustad avatar Jan 21 '22 11:01 torarnegustad

In case you're migrating, please follow the migration guidelines. This basically means only upgrade on major version at the time. So in other words:

  • Migrate to v3.3.1, fix all compile errors and warnings, and test the application before continuing.
  • Migrate to v4.10.3, fix all compile errors and warnings, and test the application before continuing.
  • Migrate to v5.3.2, fix all compile errors and warnings, and test the application.

dotnetjunkie avatar Jan 21 '22 11:01 dotnetjunkie

Hi - I have finally rewritten the code to use AsyncScopedLifestyle, and it works exactly the way I needed it to.

Thanks again for your help, I am closing this issue now.

torarnegustad avatar Aug 16 '22 10:08 torarnegustad