How to use Ninject in a multi-threaded Windows service to get new instances of a dependency (DbContext) on every tick?
Asked Answered
P

2

1

I have inherited a Windows service where all the dependencies are created when the service starts and are injected in the transient scope.

We are having a number of problems with this service, not least we have a DbContext which lives for the whole time the service is running, and different instances of it are injected each time.

I would like to refactor so that each worker thread gets it’s own DbContext injected which will live for just the duration of each tick.

I have looked at the custom scope. It looks fine for a single threaded app, but not multi-threaded. I also considered InThreadScope. Whilst that would give each thread it’s own instance, they are singletons as far as the thread is concerned so it does not fulfil the per tick requirement.

My current thinking is to use the named scope extension and to inject a scope factory which I can use to create a new scope on every tick.

Is this the way to go? Any suggestions, tips or alternatives would be appreciated.

UPDATE

Due to a time constraint we ended up using the named scope, but it wasn't as clean as @BatteryBackupUnit's solution. There were some dependencies further down the graph which needed a DbContext and we had to inject the scope factory again to get it. Using @BatteryBackupUnit's solution we could have reused the same instance from the ThreadLocal storage instead.

Pestiferous answered 10/2, 2014 at 10:27 Comment(0)
M
3

Regarding Named Scope: Consider that when you are creating a DbContext from the same thread but from an object (p.Ex. factory) which was created before the scope was created, it won't work. Either it will fail because there is no scope, or it will inject another instance of DbContext because there is a different scope. If you don't do this, then a scope like named scope or call scope can work for you.

We are doing the following instead:

When a DbContext is requested, we check a ThreadLocal (http://msdn.microsoft.com/de-de/library/dd642243%28v=vs.110%29.aspx) whether there is already one. In case there is, we use that one. Otherwise, we create a new one and assign it to the ThreadLocal<DbContext>.Value. Once all operations are done, we release the DbContext and reset the ThreadLocal<DbContext>.Value.

See this (simplified, not perfect) code for an example:

public interface IUnitOfWork
{
    IUnitOfWorkScope Start();
}

internal class UnitOfWork : IUnitOfWork
{
    public static readonly ThreadLocal<IUnitOfWorkScope> LocalUnitOfWork = new ThreadLocal<IUnitOfWorkScope>();

    private readonly IResolutionRoot resolutionRoot;

    public UnitOfWork(IResolutionRoot resolutionRoot)
    {
        this.resolutionRoot = resolutionRoot;
    }

    public IUnitOfWorkScope Start()
    {
        if (LocalUnitOfWork.Value == null)
        {
            LocalUnitOfWork.Value = this.resolutionRoot.Get<IUnitOfWorkScope>();
        }

        return LocalUnitOfWork.Value;
    }
}

public interface IUnitOfWorkScope : IDisposable
{
    Guid Id { get; }
}

public class UnitOfWorkScope : IUnitOfWorkScope
{
    public UnitOfWorkScope()
    {
        this.Id = Guid.NewGuid();
    }

    public Guid Id { get; private set; }

    public void Dispose()
    {
        UnitOfWork.LocalUnitOfWork.Value = null;
    }
}

public class UnitOfWorkIntegrationTest : IDisposable
{
    private readonly IKernel kernel;

    public UnitOfWorkIntegrationTest()
    {
        this.kernel = new StandardKernel();
        this.kernel.Bind<IUnitOfWork>().To<UnitOfWork>();
        this.kernel.Bind<IUnitOfWorkScope>().To<UnitOfWorkScope>();
    }

    [Fact]
    public void MustCreateNewScopeWhenOldOneWasDisposed()
    {
        Guid scopeId1;
        using (IUnitOfWorkScope scope = this.kernel.Get<IUnitOfWork>().Start())
        {
            scopeId1 = scope.Id;
        }

        Guid scopeId2;
        using (IUnitOfWorkScope scope = this.kernel.Get<IUnitOfWork>().Start())
        {
            scopeId2 = scope.Id;
        }

        scopeId1.Should().NotBe(scopeId2);
    }

    [Fact]
    public void NestedScope_MustReuseSameScope()
    {
        Guid scopeId1;
        Guid scopeId2;
        using (IUnitOfWorkScope scope1 = this.kernel.Get<IUnitOfWork>().Start())
        {
            scopeId1 = scope1.Id;
            using (IUnitOfWorkScope scope2 = this.kernel.Get<IUnitOfWork>().Start())
            {
                scopeId2 = scope2.Id;
            }
        }

        scopeId1.Should().Be(scopeId2);
    }

    [Fact]
    public void MultipleThreads_MustCreateNewScopePerThread()
    {
        var unitOfWork = this.kernel.Get<IUnitOfWork>();
        Guid scopeId1;
        Guid scopeId2 = Guid.Empty;
        using (IUnitOfWorkScope scope1 = unitOfWork.Start())
        {
            scopeId1 = scope1.Id;
            Task otherThread = Task.Factory.StartNew(() =>
                {
                    using (IUnitOfWorkScope scope2 = unitOfWork.Start())
                    {
                        scopeId2 = scope2.Id;
                    }
                },
                TaskCreationOptions.LongRunning);
            if (!otherThread.Wait(TimeSpan.FromSeconds(5)))
            {
                throw new TimeoutException();
            }
        }

        scopeId2.Should().NotBeEmpty();
        scopeId1.Should().NotBe(scopeId2);
    }

    public void Dispose()
    {
        this.kernel.Dispose();
    }
}

Note: i'm using nuget packages: ninject, xUnit.Net, Fluent Assertions

Also note, that you can replace the IUnitOfWork.Start with a ToProvider<IUnitOfWorkScope>() binding. Of course you need to implement the corresponding logic in the provider.

Myra answered 10/2, 2014 at 12:14 Comment(0)
H
0

A proper unit-of-work scope, implemented in Ninject.Extensions.UnitOfWork, solves this problem.

Setup:

_kernel.Bind<IService>().To<Service>().InUnitOfWorkScope();

Usage:

using(UnitOfWorkScope.Create()){
    // resolves, async/await, manual TPL ops, etc    
}
Hyperthermia answered 30/7, 2015 at 17:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.