sql-serverentity-frameworktransactionsentity-framework-6transactionscope

How to log errors in DB within a transaction that fails with Entity Framework 6


If I'm in the middle of doing a bunch of updates within a transaction, and it throws an exception, I still want to log some information in a table in the same db context so I know what happened. I mistakenly thought, per this answer, that all I needed to do was create a new instance of the context and those db inserts would be separate from the transaction. However they too are being rolled back. And I'm not sure if it is because I am missing the new Database() wrapper, but it won't let me create this statement as it seems the Database class doesn't have a public constructor that allows this.

Then I saw this answer which likely explains why. And I could see that you may want to have multiple contexts included in the transaction.

So how can I, within a transaction, exclude some db inserts from that transaction so they are not rolled back?

This is an overly simplified example. In reality, the TransactionScope() is created at the top level in the controller, and the try{} occurs many method calls deep.

var contextA = new ContextA();
using (var scope = new TransactionScope())
{
    // Log Errors in DB with new instance of same context
    var contextAforLogging = new ContextA()
    
    // Save entity in context A
    contextA.Save(...);
    
    //log step
    contextAforLogging.Save(...);
    contextAforLogging.SaveChanges();

    // More updates to original entity in context A
    contextA.Save(...);
    
    //log step
    contextAforLogging.Save(...);
    contextAforLogging.SaveChanges();
    
    // Commit tx-scope
    scope.Complete();          
}
catch (Exception ex) {
   return....;                        
}
     

Solution

  • Firstly, check that you are using the right tool for the job. Are you using a transaction to coordinate database inserts/updates using EF with DbContexts across different database connections or other transaction-supporting services? If "Yes" then TransactionScope is warranted. If "No" then are you coordinating database inserts/updates across multiple EF DbContexts using the same database connection? If "Yes" consider using:

    using var transaction = context1.BeginTransaction();  
    context2.Database.UseTransaction(transaction.GetDbTransaction());
    

    If "No" then you most likely do not need an explicit transaction. EF already wraps operations in a transaction and leveraging navigation properties for inserting related entities as an aggregate root will see things like FKs set automatically. Any fully separate entities will also all be saved within the scope of a transaction when SaveChanges is called.

    If you do need a TransactionScope then your logging code can opt out of a transaction scope. I'd recommend using a Log() method to encapsulate this so it isn't missed somewhere:

    private void Log(string message)
    {
        using var txScope = new TransactionScope(TransactionScopeOption.Suppress));
        using var contextForLogging = new AppDbContext(); // or better a DbContextFactory.Create()
    //log step
    
        var logEntry = new LogEntry(message);
        contextForLogging.Logs.Add(logEntry);
        contextforLogging.SaveChanges();
        txScope.Commit();
    }
    

    Ideally when processing work with a single DbContext you should avoid calling SaveChanges() multiple times. This is usually a sign that you are neglecting navigation properties and trying to manage relationships between entities manually where EF supports that entirely behind the scenes.