Let me say that again: DbContext is NOT threadsafe.
Not clear enough? Well, let’s make an example. Actually, I’ll show something that happened to me at work.
Let me give you some context just for the sake of it, even though it’s not extremely relevant to the issue.
In this project I have a CQRS-like architecture with a pub/sub mechanism that I use to regenerate the Query models. All nice and clean, works like a charm. On a single machine. Mine.
Actually it worked pretty well also when deployed to the Dev server. Still a single instance per service though.
Things started getting messy when I moved to Staging: for some reason that will remain unknown, the deploy script decided to create multiple instances of the subscriber.
I don’t mind as I’m always striving for immutability and statelessness so IDEALLY, I should be able to deploy as many instances of my services as I want. And that was true except for a single event handler in that subscriber.
Code was somewhat like this:
It is removing the old models and replacing them with new data. Plain and simple. Everything is also wrapped in a transaction.
So where’s the problem? Apparently, combining the two operations may lead to unexpected results, like bad data being persisted. Or not persisted at all.
This fixed the issue:
Calling SaveChangesAsync() for every operation did the trick.
I’m not 100% sure why this is working, but I suspect the reason lies here:
EF Core does not support multiple parallel operations being run on the same context instance. You should always wait for an operation to complete before beginning the next operation. This is typically done by using theAsynchronous Saving
awaitkeyword on each asynchronous operation.
This basically means that it’s a very bad idea to share instances of DbContext between classes. Or between instances of the same classes in different threads.
Using a proper DI container, the solution would be to setup the DbContext with a Transient lifetime:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
this way a new instance will be created when needed and disposed as soon as possible.
Avoid setting the lifetime to Scoped or Singleton, otherwise you might be sharing the state, which is always a bad idea.