Transaction fails - c#

I got some method:
public SaveMyData(...)
{
var transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.Snapshot };
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
{
var dataInDb = dbDataService.LoadData();
dataInDb.SomeField = someNewValue;
dbDataService.SaveData(dataInDb);
transactionScope.Complete();
}
}
So it takes string serialized Dto from database and changes field. Than save it.
SaveMyData can be called from different threads in same time, so i got error:
You cannot use snapshot isolation to access table 'some table'
directly or indirectly in database 'some db' to update, delete, or
insert the row that has been modified or deleted by another
transaction. Retry the transaction or change the isolation level for
the update/delete statement
How can i avoid this error? I need to use different isolation level?
Inside SaveData() method i create new EF context and save changes.
The way i want to make it work is:
CallerA call SaveMyData, it locks and if CallerB calls it in same time he will wait untill CallerA commit. So i need to not allow CallerB read data before CallerA write changes.

Related

EF Core - Multiple Contexts and Transactions

We are splitting our large DbContext into smaller contexts, each one taking care of a small domain bounded context. The contexts save operations are orchestrated by a unit of work as shown below.
The domain has two bounded contexts, Partners and Employees. The Unit Of Work manages two DbContexts, PartnerContext and EmployeeContext. We run all save operations within a transaction to ensure the operation is atomic.
A simplified version of the problem is available on github
public class UnitOfWork {
public Task SaveChanges(){
// EmployeeContext begins a transaction and shares it with other contexts
var strategy = employeeContext.Database.CreateExecutionStrategy();
return strategy.ExecuteAsync(async () =>
{
await using var transaction = await employeeContext.Database.BeginTransactionAsync();
await partnerContext.Database.UseTransactionAsync(transaction.GetDbTransaction());
await partnerContext.SaveChangesAsync();
await employeeContext.SaveChangesAsync();
await transaction.CommitAsync();
});
}
}
The following code works fine. Changes are all executed within one single transaction
var unitOfWork = new unitOfWork();
... perform updates to both contexts
await unitOfWork.SaveChanges();
However, the following code throws when attempting to save changes a second time.
var unitOfWork = new unitOfWork();
... perform updates to both contexts
await unitOfWork.SaveChanges(); <-- Work fine
... doing a bit more work
await unitOfWork.SaveChanges(); <-- Crashes
The line above fails with The connection is already in a transaction and cannot participate in another transaction. error message.
The resulting SQL log is:
**** The first save operation logs start here:
SET NOCOUNT ON;
INSERT INTO [Person] ([Discriminator], [ManagerId], [Name])
VALUES (#p0, #p1, #p2);
SELECT [Id]
FROM [Person]
WHERE ##ROWCOUNT = 1 AND [Id] = scope_identity();
Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Committing transaction.
Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Disposing transaction.
**** The second save operation logs start here:
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Opening connection to database 'EF_DDD' on server 'localhost'.
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Opened connection to database 'EF_DDD' on server 'localhost'.
Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Beginning transaction with isolation level 'Unspecified'.
Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Began transaction with isolation level 'ReadCommitted'.
Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Disposing transaction.
Would anyone know the reason behind the second unitOfWork.SaveChanges() complaining about an open transaction despite the fact the first transaction was committed and disposed (as you can see from the logs above)?
Update
I removed all async code and the execution strategy (retry) to narrow the issue down, the code now looks like this:
static void Main(string[] args)
{
var employeeContext = new EmployeeContext(ConnectionString);
var partnersContext = new PartnersContext(employeeContext.Database.GetDbConnection());
var unitOfWork = new UnitOfWork();
unitOfWork.Update(employeeContext, partnersContext, 1);
unitOfWork.Update(employeeContext, partnersContext, 2);
}
public class UnitOfWork
{
public void Update(EmployeeContext employeeContext, PartnersContext partnerContext, int count)
{
partnerContext.Partners.Add(new Partner($"John Smith {count}"));
employeeContext.Persons.Add(new Person() { Name = $"Richard Keno {count}" });
using var trans = employeeContext.Database.BeginTransaction();
partnerContext.Database.UseTransaction(trans.GetDbTransaction());
partnerContext.SaveChanges();
employeeContext.SaveChanges();
trans.Commit();
}
}
The first one goes through, and the database is updated, but the second call fails with the error below.
Update 2
Using TransactionScope instead of BeginTransaction seems to work. The following code works and updates the database accordingly.
var strategy = employeeContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
partnerContext.Partners.Add(new Partner($"John Smith {count}"));
employeeContext.Persons.Add(new Person() { Name = $"Richard Keno {count}" });
await partnerContext.SaveChangesAsync();
await employeeContext.SaveChangesAsync();
scope.Complete();
});
Looks like you are hitting some internal EF Core 3 implementation defect/bug which has been fixed down the road, because the issue does not reproduce in the latest at this time EF Core 6.0 (but does reproduce in EF Core 3.1).
The problem is with cleanup of the shared underlying db connection and db transaction. It can be solved (which also helps the future EF Core versions) by disposing the EF Core transaction wrapper (IDbContextTransaction) returned by the UseTransaction{Async} call, e.g.
using var trans2 = await partnerContext.Database.UseTransactionAsync(transaction.GetDbTransaction());
or
using var trans2 = partnerContext.Database.UseTransaction(trans.GetDbTransaction());

Entity Framework 6 does not ignore TransactionScope with enlist=false

Here is a very simplistic example of my current state:
using(var scope = new TransactionScope(TransactionScopeOption.Required, transactionScopeTimeout, TransactionScopeAsyncFlowOption.Enabled))
{
const string ConnectionString="server=localhost;username=;password=;enlist=false;Initial Catalog=blubber"
using(var myContext = new MyContext(ConnectionString))
{
var myTestObject = new TestObjectBuilder().WithId(1).Build();
myContext.Add(myTestObject);
myContext.SaveChanges();
}
// ...
using(var myContext = new MyContext(ConnectionString))
{
var myObj = myContext.MyObjects.Single(s => s.Id == 1); // This is the same object as above
}
}
I have a TransactionScope which will be used here, but in my connectionstring I explicitly say I don't wanna enlist my Entity Framework (6.2) Transactions.
The current behavior is that on the Single(s => s.Id == 1) Expression I get an error after 30 seconds (the default timeout) that the element could not be found.
First of all: Why do I not get a timeout-Exception or any SqlException? Second: Why is my data-row locked in the database?
Also via Sql Server Management Studio I can not query that exact row (only with the NOLOCK hint).
If I remove the TransactionScope or set the ISOLATION LEVEL to READ UNCOMMITED before the Single query everything works fine.
Also because of the Dispose of the transaction-scope at the end all data will be removed / rollbacked which also should not happen if I didn't enlist to this AmbientTransaction.
So my expected behavior is, that I get no lock and my data is persisted even the transactionscope is disposed and rollbacked. EF should ignore the transactionscope here.
Do I miss something critical here?
EDIT: I tried the same thing with NHibernate and here it works like a charm.

How entity framework SaveChanges works?

I try to understand how EF creates DB requests from object manipulations in code. My test scenario is simple:
using(var context = new Context())
{
var entity = context.Entities.First();
entity.A = "TST";
entity.B = "WrongValue";
context.SaveChanges();
}
My idea is to test, how EF deal with transaction.
Change A with correct value and change B with wrong value (non existing FK)
I track what happening in SQL Server DB.
I execute code and nothing change in DB, that was expected.
Strange part is that there are two independant SQL request and I don't understand how EF revert first one.
Both Entity Framework and EntityFramework core code are open source. You can check the code at
Entity Framework - Link
Entity Framework Core - Link
If you see the internal code of Save method (pasted the code snapshot below) then you can validate that it internally it creates a transaction if an external transaction is not provided.
internal int SaveChangesInternal(SaveOptions options, bool executeInExistingTransaction)
{
AsyncMonitor.EnsureNotEntered();
PrepareToSaveChanges(options);
var entriesAffected = 0;
// if there are no changes to save, perform fast exit to avoid interacting with or starting of new transactions
if (ObjectStateManager.HasChanges())
{
if (executeInExistingTransaction)
{
entriesAffected = SaveChangesToStore(options, null, startLocalTransaction: false);
}
else
{
var executionStrategy = DbProviderServices.GetExecutionStrategy(Connection, MetadataWorkspace);
entriesAffected = executionStrategy.Execute(
() => SaveChangesToStore(options, executionStrategy, startLocalTransaction: true));
}
}
ObjectStateManager.AssertAllForeignKeyIndexEntriesAreValid();
return entriesAffected;
}
So your below code will internally wrapped inside a transaction which you can validate in SQL Profiler..
using(var context = new Context())
{
var entity = context.Entities.First();
entity.A = "TST";
entity.B = "WrongValue";
context.SaveChanges();
}
However, SQL profiler does not start logging transaction so you need to configure that in trace setting. See the below screenshot of SQL profiler new Trace setting, here, I have checked Show All events. After that Transaction category is being displayed. You can subscribe for Begin Tran, Commit Tran and Rollback Tran events to validate transaction statements. When you will run your scenario, you can see that Begin and Rollback should be logged.

Entity Framework not picking up Transaction Scope

I have pretty much standard EF 6.1 'create object in a database' code wrapped in transaction scope. For whatever reason the data persists in db after the transaction fails (to complete).
Code:
using (var db = this.Container.Resolve<SharedDataEntities>()) // << new instance of DbContext
{
using (TransactionScope ts = new TransactionScope())
{
SubscriptionTypes st = this.SubscriptionType.Value;
if (st == SubscriptionTypes.Lite && this.ProTrial)
st = SubscriptionTypes.ProTrial;
Domain domain = new Domain()
{
Address = this.Address.Trim(),
AdminUserId = (Guid)user.ProviderUserKey,
AdminUserName = user.UserName,
Description = this.Description.TrimSafe(),
DomainKey = Guid.NewGuid(),
Enabled = !masterSettings.DomainEnableControlled.Value,
Name = this.Name.Trim(),
SubscriptionType = (int)st,
Timezone = this.Timezone,
Website = this.Website.TrimSafe(),
IsPrivate = this.IsPrivate
};
foreach (var countryId in this.Countries)
{
domain.DomainCountries.Add(new DomainCountry() { CountryId = countryId, Domain = domain });
}
db.Domains.Add(domain);
db.SaveChanges(); // << This is the Saving that should not be commited until we call 'ts.Complete()'
this.ResendActivation(domain); // << This is where the Exception occurs
using (TransactionScope ts2 = new TransactionScope(TransactionScopeOption.Suppress))
{
this.DomainMembership.CreateDomainUser(domain.Id, (Guid)user.ProviderUserKey, user.UserName, DomainRoles.DomainSuperAdmin | DomainRoles.Driver);
ts2.Complete();
}
this.Enabled = domain.Enabled;
ts.Complete(); // << Transaction commit never happens
}
}
After SaveChanges() exception is thrown inside ResendActivation(...) so the changes should not be saved. However the records stay in database.
There is no other TransactionScope wrapping the code that I've pasted, it's triggered by an MVC Action call.
after more investigations, turns out that something - probably Entity Framework upgrade or database update process had put
Enlist=false;
into the database connection string. That effectively stops EF from picking up Transaction Scope.
So the solution is to set it to true, or remove it, I think by default it's true
Try using the transaction from the db instance it self, db.Database.BeginTransaction() if I recall it correctly instead of using the transaction scope.
using (var ts = db.Database.BeginTransaction())
{
..
}
Assuming that db is your entity framework context.
Context class by default support transactions. but with every new instance of context class, a new transaction will be created. This new transaction is a nested transaction and it will get committed once the SaveChanges() on the associated context class gets called.
In the given code it seems, we are calling a method that is responsible for creating a domain user i.e. CreateDomainUser and perhaps that has its own context object. thus this problem.
If this is the case(that this method has own context) the perhaps we don't even need TransactionScope here. We can simply pass the same context(that we are using before this call) to the function that is creating the domain user. We can then check for result of both operations and if they are successful, we simply need to call SaveChanges() method.
TransactionScope is usually needed when we are mixing ADO.NET calls with Entity framework. We can use it with context also, but it would be an overkill as the context class already has a transaction and we can simply use the same context to manage the transaction. If this is the case in the above code then the trick to fix the issue is to let the context class know that you want to use it with your own transaction. Since the transaction gets associated with the connection object in the scope, we need to use the context class with the connection that is being associated with the transaction scope. Also, we need to let the context class know that it cannot own the connection as it is being owned by the calling code. so we can do something like:
using (var scope = new TransactionScope(TransactionScopeOption.Required))
{
using (var conn = new SqlConnection("..."))
{
conn.Open();
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.CommandText =
#"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
sqlCommand.ExecuteNonQuery();
using (var context =
new BloggingContext(conn, contextOwnsConnection: false))
{
var query = context.Posts.Where(p => p.Blog.Rating > 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
}
}
scope.Complete();
}
See: http://msdn.microsoft.com/en-us/data/dn456843.aspx
You should be able to use TransactionScope with EF, I know our projects do. However, I think you want the EF context instantiated inside the transaction scope -- that is, I believe you need to swap the outermost / first two using statements, as the answer from #rahuls suggests.
Even if it works the other way ... if you have a service / app / business layer method that needs to update several tables, and you want those updates to be atomic, you'd need to do it this way. So for the sake of consistency (and your own sanity), I would recommend transaction scope first, context second.

Transactions with IsolationLevel Snapshot cannot be promoted

I'm trying to wrap a TransactionScope around my call to a stored procedure via Entity v.4.0.30319. I keep encountering the following exception:
Transactions with IsolationLevel Snapshot cannot be promoted.
How can I get around this?
The underlying stored procedure is basically one big insert statement into a table.
My code is as follows:
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, GetTransactionOptions()))
{
int? status;
status = GetStatusIDFromEnum(newMatterCredential);
using (MatterCredentialsEntities db = new MatterCredentialsEntities())
{
DateTime? objDateAnnounced = GenerateNullForDateTime(newMatterCredential.DateAnnounced);
DateTime? objDateClosed = GenerateNullForDateTime(newMatterCredential.DateClosed);
DateTime? objDateFinancialClosed = GenerateNullForDateTime(newMatterCredential.DateFinancialClosed);
db.prcCreateCredential(Common.GetUserProfID(), newMatterCredential.MatterID, status, newMatterCredential.DescriptionSummary, newMatterCredential.DescriptionDetailed, newMatterCredential.BusinessEntitySectorID, newMatterCredential.BusinessEntityRoleID, newMatterCredential.UNGeographyID, newMatterCredential.ProjectName, newMatterCredential.ClientIndustryId, newMatterCredential.TransactionValue, newMatterCredential.TransactionCurrencyID, newMatterCredential.OtherParties, newMatterCredential.LegalAdvisers, newMatterCredential.DateAnnounced, newMatterCredential.DateClosed, newMatterCredential.DateFinancialClosed, newMatterCredential.Award, newMatterCredential.NotifyPartner, newMatterCredential.Notes);
}
scope.Complete();
}
public static TransactionOptions GetTransactionOptions()
{
TransactionOptions tranOpt = new TransactionOptions();
tranOpt.IsolationLevel = IsolationLevel.Snapshot;
return tranOpt;
}
MSDN says you cannot promote a transaction with snapshot isolation.
MSDN - IsolationLevel Enumeration
Snapshot - Volatile data can be read. Before a transaction modifies data, it verifies if another transaction has changed the data after it was initially read. If the data has been updated, an error is raised. This allows a transaction to get to the previously committed value of the data.
When you try to promote a transaction that was created with this isolation level, an InvalidOperationException is thrown with the error message:
Transactions with IsolationLevel Snapshot cannot be promoted
Something else must be changing the data since you started the transaction, if this is part of larger transaction that it is participating in
I suggest changing the transaction to serializable.
public static TransactionOptions GetTransactionOptions()
{
TransactionOptions tranOpt = new TransactionOptions();
tranOpt.IsolationLevel = IsolationLevel.Serializable;
return tranOpt;
}
Edit: See below ensure you have MSDTC running as this wants to create a distributed transaction.

Categories

Resources