EF Core - Multiple Contexts and Transactions - c#

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());

Related

Entity Framework Core - transaction cannot be roll back after commit

It is using Entity Framework Core to update database.
dbContextTransaction.Commit(); is working fine, and after this it is some file operation with bad result. And then it throws an error, so it tries to roll back using dbContextTransaction.Rollback(); but results in:
This SqlTransaction has completed; it is no longer usable.
DbContext dbContext = scope.ServiceProvider.GetService<DbContext>();
IDbContextTransaction dbContextTransaction = dbContext.Database.BeginTransaction();
try
{
IOrderDao orderDao = scope.ServiceProvider.GetService<IOrderDao>();
IItemSoldPriceDao itemSoldPriceDao = scope.ServiceProvider.GetService<IItemSoldPriceDao>();
ItemSoldPrice itemSoldPrice = new ItemSoldPrice
{
...
};
itemSoldPriceDao.AddItemSoldPrice(itemSoldPrice);
order.SoldPriceCaptured = true;
dbContext.SaveChanges();
dbContextTransaction.Commit();
//... some other file operation throws out error
throw new Exception("aaa");
}
catch (Exception ex)
{
CommonLog.Error("CaptureSoldPrice", ex);
dbContextTransaction.Rollback();
}
After a transaction is committed, it cannot be rolled back?
When using Entity Framework, explicit transactions are only required when you want to link the success or failure of operations against the DbContext with other operations outside of the scope of the DbContext. All operations within a DbContext prior to SaveChanges are already grouped within a transaction. So for instance saving entities across two or more tables within a DbContext do not require setting up an explicit transaction, they will both be committed or rolled back together if EF cannot save one or the other.
When using an explicit transaction, the Commit() call should be the last operation for what forms essentially a unit of work. It will be the last operation to determine whether everything in the transaction scope is successful or not. So as a general rule, all operations, whether Database-based, file based, or such should register with and listen to the success or failure of the transaction.
An example of using a transaction: Say we have a system that accesses two databases via two separate DbContexts. One is an order system that tracks orders and has a record for a Customer and one is a CRM that tracks customer information. When we accept a new order from a new customer we check the CRM system for a customer and create a customer record in both systems if it is someone new.
using (var orderContext = new OrderDbContext())
{
var transaction = orderContext.Database.BeginTransaction();
try
{
var order = new Order
{
// TODO: Populate order details..
}
if(customerDetails == null && registerNewCustomer) // Where customerDetails = details loaded if an existing customer logged in and authenticated...
{
using(var crmContext = new CrmDbContext())
{
crmContext.Database.UseTransaction(transaction);
var customer = new Customer
{
UserName = orderDetails.CustomerEmailAddress,
FirstName = orderDetails.CustomerFirstName,
LastName = orderDetails.CustomerLastName,
Address = orderDetails.BillingAddress
};
crmContext.Customers.Add(customer);
crmContext.SaveChanges();
var orderCustomer = new Orders.Customer
{
CustomerId = customer.CustomerId,
FirstName = customer.FirstName,
LastName = customer.LastName
}
orderContext.Customers.Add(orderCustomer);
}
}
order.CustomerId = crmContext.Customers
.Select(c => c.CustomerId)
.Single(c => c.UserName == customerDetails.UserName);
orderContext.Orders.Add(order);
orderContext.SaveChanges();
transaction.Commit();
}
catch(Exception ex)
{
// TODO: Log exception....
transaction.Rollback();
}
}
The order DB customer is just a thin wrapper of the CRM customer where we would go for all of the customer details. The CRM customer manages the Customer IDs which would correlate to an Order Customer record. This is by no means a production code type example, but rather just to outline how a Transaction might coordinate multiple operations when used.
In this way if there is any exception raised at any point, such as after a new Customer record has been created and saved, all saved changes will be rolled back and we can inspect any logged exception details along with recorded values to determine what went wrong.
When dealing with combinations of DbContext operations and other operations that we might want to support a rolling back process flow on failure then we can leverage constructs like the TransactionScope wrapper. However this should be used with caution and only in cases where you explicitly need to marry these operations rather than attempting to use the pattern as a standard across all operations. In most cases you will not need explicit transactions.

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.

Two nested Entity Framework contexts, sharing a transaction

I have code that looks like the example below. There's an explicit transaction involved because of some database tomfoolery that needs to be done via a SP, and a save changes in the middle of it all. (Exception handling, rollbacks, etc.. omitted):
void OuterMethod(MyDatbase context)
{
using(var dbTrans = context.Database.BeginTransaction())
{
// some stuff, the save puts the data where the SP can see it
Stuff(context);
context.SaveChanges();
// now some SP stuff
context.Database.ExecuteSqlCommand(#"spFoo", params);
// more stuff
MoreStuff(context);
AlmostUnrelatedCode(context);
context.SaveChanges();
dbTrans.Commit();
}
}
Right now the method AlmostUnrelatedCode() -- which is only marginally related to the process above -- needs a nice, fast, disposable read-only context 99% of the time. I have a factory that will serve me up the right kind of context when I need it. The 1% of the time it's called from the middle of that block above.
MyDatabase localReadOnlyContext;
void AlmostUnrelatedCode(MyDatabase context)
{
if ( context.Database.CurrentTransaction != null )
{
// Must use the context passed or everything deadlocks :(
localReadOnlyContext = context;
disposeContextLater = false;
}
else
{
// I just want to do this all the time
localReadOnlyContext = _contextFactory.CreateReadOptimized();
disposeContextLater = true;
}
// Do many, many things with my read-optimized context...
// The Dispose() on the class will check for disposeContextLater
}
What I'd like to do is to get rid of that transaction check, and in fact not need to pass the outer context at all if I can help it.
What I've tried:
Just ignoring what's going on in the outer transaction and using the context I generate all the time. Problem: deadlocks.
Trying to get the outermost transaction into the EF context I create with the _contextFactory. Problem: EF context constructors don't allow you to pass an existing transaction; also Database.CurrentTransaction has no setter.
Pulling the whole transaction out into a TransactionScope that wraps everything up. Problem: the method OuterMethod passes in the context, and I don't have control of the caller.
What I can't try:
Dirty reads/nolock. AlmostUnrelatedCode() needs the data as written so far.
I'd rather not:
Just keep using the outer context while inside of AlmostUnrelatedCode. AlmostUnrelatedCode deals with a lot of data trees and that context gets fat and unhappy really fast. It pollutes its context with crap really fast, and I'd rather just dispose of it when I'm done.
you can prevent the deadlocks by using one connection for multiple contexts.
example
var efConnectionString = ConfigurationManager.ConnectionStrings["SomeEntities"].ConnectionString;
// note EntityConnection, not SqlConnection
using (var conn = new EntityConnection(efConnectionString)) {
// important to prevent escalation
await conn.OpenAsync();
using (var c1 = new SomeEntities(conn, contextOwnsConnection: false)) {
//Use some stored procedures etc.
count1 = await c1.SomeEntity1.CountAsync();
}
using (var c2 = new SomeEntities(conn, contextOwnsConnection: false)) {
//Use some stored procedures etc.
count2 = await c2.SomeEntity21.CountAsync();
}
}
in your case just get the connection from the context and reuse it
context.Database.Connection
Can't you separate things done in AlmostUnrelatedCode like this:
void AlmostUnrelatedCode()
{
var context = _contextFactory.CreateReadOptimized();
AlmostUnrelatedCode(context);
context.Dispose();
}
void AlmostUnrelatedCode(MyDatabase context)
{
// Do many, many things with context...
}
Now you can call AlmostUnrelatedCode(with param) from your OuterMethod. And maybe there is even more to be separated. Consider SOLID.

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.

Transaction fails

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.

Categories

Resources