Remember kids: DbContext is not threadsafe

Remember kids: DbContext is not threadsafe

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:

using (var tran = await _dbContext.BeginTransactionAsync())
{
try
{
var modelsToRemove = await _dbContext.QueryModels
.Where(f => /* some filter here */)
.ToArrayAsync();
if (modelsToRemove.Any())
_dbContext.QueryModels.RemoveRange(modelsToRemove);
if (newModels?.Any())
_dbContext.QueryModels.AddRange(newModels);
await _dbContext.SaveChangesAsync();
tran.Commit();
}
catch (Exception ex)
{
tran.Rollback();
}
}

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:

using (var tran = await _dbContext.BeginTransactionAsync())
{
try
{
var modelsToRemove = await _dbContext.QueryModels
.Where(f => /* some filter here */)
.ToArrayAsync();
if (modelsToRemove.Any())
_dbContext.QueryModels.RemoveRange(modelsToRemove);
// remenber to call SaveChangesAsync() after every write in order to
// ensure that the operation has been performed.
// The Ef DbContext is not thread safe and if there are multiple instances of the
// service, data might not be persisted correctly.
await _dbContext.SaveChangesAsync();
if (newModels?.Any())
_dbContext.QueryModels.AddRange(newModels);
await _dbContext.SaveChangesAsync();
tran.Commit();
}
catch (Exception ex)
{
tran.Rollback();
}
}
view raw AsyncTransaction.cs hosted with ❤ by GitHub

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 the await keyword on each asynchronous operation.

Asynchronous Saving

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.

Remember kids: DbContext is not threadsafe
Scroll to top