I have an application which runs multiple threads to insert data into a SQL Server 2017 database table using EF Core 5.
The C# code for inserting the domain model entities using EF Core 5 is as follows:
using (var ctx = this.dbContextFactory.CreateDbContext())
{
//ctx.Database.AutoTransactionsEnabled = false;
foreach (var rootEntity in request.RootEntities)
{
ctx.ChangeTracker.TrackGraph(rootEntity, node =>
{
if ((request.EntityTypes != null && request.EntityTypes.Contains(node.Entry.Entity.GetType()))
|| rootEntity == node.Entry.Entity)
{
if (node.Entry.IsKeySet)
node.Entry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
else
node.Entry.State = Microsoft.EntityFrameworkCore.EntityState.Added;
}
});
}
await ctx.SaveChangesAsync(cancellationToken);
}
Each thread is responsible for instantiating its own DbContext instance hence the use of dbContextFactory.
Some example SQL generated for the INSERT (MERGE) is as follows:
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([OrderId] bigint, [_Position] [int]);
MERGE [dbo].[Orders] USING (
VALUES (@p0, 0),
(@p1, 1),
(@p2, 2),
...
(@43, 41)) AS i ([SomeColumn], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([SomeColumn])
VALUES (i.[SomeColumn])
OUTPUT INSERTED.[OrderId], i._Position
INTO @inserted0;
SELECT [t].[OrderId] FROM [dbo].[Orders] t
INNER JOIN @inserted0 i ON ([t].[OrderId] = [i].[OrderId])
ORDER BY [i].[_Position];
As these threads frequently run at the same time I get the following SQL exception:
Transaction (Process ID 99) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
EF Core implicitly sets the isolation level to READ COMMITTED.
Using SQL Profiler the transaction deadlock was caused by the following:
My concerns:
- Frustratingly, the SQL generated by EF Core includes two statements: a MERGE, and then a SELECT. I do not understand the purpose of the SELECT given the identities of the primary key are available from the @inserted0 table variable. Given this answer, the MERGE statement in isolation would be sufficient enough to make this atomic.
I believe it is this SELECT which is causing the transaction deadlock.
- I tried to resolve the problem by using READ COMMITTED SNAPSHOT to avoid the conflict with the primary key lookup, however I still got the same error even though this isolation level should avoid locks and use row versioning instead.
My attempt at solving the problem:
The only way I could find to solve this problem was to explicitly prevent a transaction being started by EF Core, hence the following code:
ctx.Database.AutoTransactionsEnabled = false;
I have tested this numerous times and haven't received a transaction deadlock. Given the logic is merely inserting new records I believe this can be done.
Does anyone have any advice to fixing this problem?
Thanks for your time.