Shaul Behr
Shaul Behr

Reputation: 38073

Cross-thread conflicts in StructureMap

I've got an API application that uses multiple database shards, with StructureMap for dependency injection. One of the required headers in each API call is a ShardKey, which tells me which database this call is addressing. To effect this, I have an OwinMiddleware class called ShardingMiddleware, which contains the following code (snipped for clarity):

var nestedContainer = container.GetNestedContainer();
using (var db = MyDbContext.ForShard(shardKey)) // creates a new MyDbContext with connection string appropriate to shardKey
{
    nestedContainer.Configure(cfg => cfg.For<MyDbContext>().Use(db));
    await Next.Invoke(context);
}

This works beautifully in my test environment and passes a battery of integration tests.

But the integration tests are effectively single-threaded. When I deployed this into a QA environment, where a real app is hitting away at my API with multiple simultaneous calls, things start to go pear-shaped. Ferinstance:

System.ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur is you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

Or other exceptions indicating that StructureMap does not have a valid instance of MyDbContext available.

To me it seems that the multiple threads are somehow messing up each other's configuration, but for the life of me I can't understand how, seeing as I'm using a nested container to store the database context for each API call.

Any ideas what might be going wrong here?

Update: I also tried abstracting my Db context into an interface. Made no real difference; I'm still getting the error

System.InvalidOperationException: An error occurred when trying to create a controller of type 'SomeController'. Make sure that the controller has a parameterless public constructor. ---> StructureMap.StructureMapConfigurationException: No default Instance is registered and cannot be automatically determined for type 'MyNamespace.IMyDbContext'

Update 2: I solved the problem, but the bounty is still open. Please see my answer below.

Upvotes: 6

Views: 1878

Answers (3)

Todd
Todd

Reputation: 1091

From the logs what I see is that the second request/thread overrides the container and respectfully the database context of the first one so both use the same connection:

Line 2 Context=56852305, Container=85736099 

should be

Line 2 Context=56852305, Container=48376271

or am I getting it wrong, so I don't think you solve it. The System.ObjectDisposedException error is from the using clause that you use to create instance of your db context and because of it the Next delegate and the context are disposed. I also didn't understood the line

Container = context.GetNestedContainer(); 

maybe you had in mind

Container = container.GetNestedContainer(); 

? I am not familiar with StructureMap but I think the code should looks like this

var nestedContainer = Container.GetNestedContainer(c =>
                    {
                        var db = MyDbContext.ForShard(shardKey);
                        c.For<MyDbContext>().Use(db);
                    });

await Next.Invoke(context);

assuming that the container closes and disposes the db connection when it is disposed.

Upvotes: 0

Kirill Bestemyanov
Kirill Bestemyanov

Reputation: 11964

You should rewrite this:

using (var db = MyDbContext.ForShard(shardKey)) // creates a new MyDbContext with connection string appropriate to shardKey
{
    nestedContainer.Configure(cfg => cfg.For<MyDbContext>().Use(db));
    await Next.Invoke(context);
}

cause using dispose your dbcontext at the end of using.

You should register factory instead:

var dbFactory = ()=>MyDbContext.ForShard(shardKey);
nestedContainer.Configure(cfg => cfg.For<Func<MyDbContext>>().Use(dbFactory));
await Next.Invoke(context);

and inject this Func instead of dbcontext instance.

Upvotes: 2

Shaul Behr
Shaul Behr

Reputation: 38073

Well... I solved the problem, but I don't understand why this made a difference.

It boils down to some subtle differences from what I originally posted, which I left out because I thought the details were inconsequential and would have distracted from the question. My container was not, in fact, defined locally; rather it was a protected property of my middleware (it's inherited for integration testing purposes):

protected IContainer Container { get; private set; }

Then there was an initialization call inside the Invoke() method:

Container = context.GetNestedContainer(); // gets the nested container created by a previous middleware class, using the context.Environment dictionary

Using logging statements throughout the method, I got down to the following code (as mentioned in the question, with logging added):

_logger.Debug($"Line 1 Context={context.GetHashCode}, Container={Container.GetHashCode()}");
var db = MyDbContext.ForShard(shardKey.Value);  // no need for "using", since DI will automatically dispose
_logger.Debug($"Line 2 Context={context.GetHashCode}, Container={Container.GetHashCode()}");
Container.Configure(cfg => cfg.For<MyDbContext>().Use(db));
await Next.Invoke(context);

And astoundingly, here's what came out of the logs:

Line 1 Context=56852305, Container=48376271

Line 1 Context=88275661, Container=85736099

Line 2 Context=56852305, Container=85736099

Line 2 Context=88275661, Container=85736099

Amazing! The Container property of my middleware got magically replaced! This, despite the fact that it is defined with a private set, and anyway, just to be safe, I checked through the code for MyDbContext.ForShard() and found nothing that could have messed up the reference for Container.

So what was the solution? I declared a local container variable just after the initialization, and used that instead.

It works now, but I don't understand why or how this could have made a difference.

Bounty goes to the person who can explain this.

Upvotes: 2

Related Questions