Reputation: 38073
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
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
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
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