I'm completely frustrated and confused about the behavior of EF6.1 and the SQL Server.
I want to have a table with (at the moment) one row which contains a value I want to use for other tasks. Every time a user is executing this function, I want to get the content of the value - for simplicity just an int - and set a new value. Because I want to use this value also as a primary key for other tables, it has to be unique and it should be gap-less.
My idea was to create a transaction to get the value, calculate a new one and update it in the db. I want to block the table as long as the new value is commited. Then the next one can get and set the new value.
But after 5 hours of testing it does not work yet.
To simulate (just 3) simultaneous accesses, I wanted to create a console app and start it 3 times with a build-in Thread.Sleep(200)
.
Here is my code I am testing with:
[Table("Tests")]
public class Test
{
public int Id { get; set; }
public int Value { get; set; }
}
The table already exists and it has one row with { Id = 1, Value = 0 } in it.
In my console app I have:
for( int i = 1; i <= 200; i++ )
{
Thread.Sleep( 200 );
using( var db = new MyDbContext( connectionString ) )
{
using( var trans = db.Database.BeginTransaction( IsolationLevel.RepeatableRead ) )
{
var entry = db.Tests.Find( 1 );
db.Entry( entry ).Entity.Value += 1;
// Just for testing
Console.WriteLine( string.Format( "{0,5}", i ) + " " + string.Format( "{0,7}", db.Entry( entry ).Entity.Value ) );
db.SaveChanges();
trans.Commit();
}
}
}
My problem:
Sometimes it is enough to start the second app and I get deadlocks.
I tried to set the isolation level to IsolationLevel.ReadCommited
, but after doing that I got duplicates.
How I have got absolutely no idea what I am doing wrong. Can someone help me, please?
UPDATE
When I take the for into the transaction, the second started app will wait as long as the first one runs (about 20 seconds) and then start to read and update the value. This works as expected, but why is the "simulation" above not working?
using( var db = new MyDbContext( connectionString ) )
{
using( var trans = db.Database.BeginTransaction( IsolationLevel.Serializable ) )
{
for( int i = 1; i <= 2000; i++ )
{
var entry = db.Tests.Find( 1 );
db.Entry( entry ).Entity.Value += 1;
// Just for testing
Console.WriteLine( string.Format( "{0,5}", i ) + " " + string.Format( "{0,7}", db.Entry( entry ).Entity.Value ) );
db.SaveChanges();
}
trans.Commit();
}
}
SOLUTION
Thanks to Travis J here the solution:
for( int i = 1; i <= 2000; i++ )
{
using( var db = new MyDbContext( connectionString ) )
{
using( var trans = db.Database.BeginTransaction( IsolationLevel.Serializable ) )
{
db.Database.ExecuteSqlCommand( "SELECT TOP 1 *FROM Tests WITH (TABLOCKX, HOLDLOCK)" );
var entry = db.Tests.Find( 1 );
db.Entry( entry ).Entity.Value += 1;
// Just for testing
Console.WriteLine( string.Format( "{0,5}", i ) + " " + string.Format( "{0,7}", db.Entry( entry ).Entity.Value ) );
db.SaveChanges();
trans.Commit();
}
}
}
... and let me add a comment:
In my case it also works with IsolationLevel.RepeatableRead
.
db.Database.ExecuteSqlCommand( "SELECT TOP 1 *FROM Tests WITH (TABLOCKX, HOLDLOCK)" );
only works inside the transaction. As I turned out I had tried this code without any transaction and the result was identically to use a transaction with IsolationLevel.ReadCommited
.
Travis, thank you!