30
votes

I'm looking for a way to execute a query while a TransactionScope is alive, and ignore the TransactionScope - basically, I want to execute this particular query no matter what.

I'm using EF code-first, and the way the application is designed, a new data context is opened many times throughout a single call, each with its own changes, and all of those are contained within a single TransactionScope, which has Complete() called at the end assuming no failures. Inside of the context we've overridden SaveChanges so that if any exception happens on base.SaveChanges(), we can catch it and log to the database before rolling back the transaction.

Since the SaveChanges happens inside the transaction, the logging obviously doesn't happen, because it belongs to the same transaction as the original call. I'm trying to ignore the TransactionScope altogether just for the logging code.

Here's some stripped-down code:

// From the context
public override int SaveChanges() {
    try {
        return base.SaveChanges();
    } catch (Exception ex) {

        // Writes to the log table - I want this to run no matter what
        LogRepo.Log(/*stuff to log from the context*/);

        throw;
    }
}

// Inside the business logic
public void DoSomething() {
    try {
        using (var scope = new TransactionScope()) {

            using (var context = new FooContext()) {
                // Do something
                context.SaveChanges();
            }
            using (var context = new FooContext()) {
                // Do something else
                context.SaveChanges();
            }

            scope.Complete();
        }
    } catch (Exception ex) {
        // scope.Complete is never called, so the transaction is rolled back
    }
}

I tried using regular ADO.NET instead of EF for the logging, but still the same results - it gets rolled back too.

I need the error handling to happen inside of SaveChanges, because what I'm logging is the state of the entities that are being saved - so I can't just easily move the logging somewhere else. I could build the message while inside the SaveChanges catch, and throw it and let DoSomething catch log it, but there are dozens of DoSomething methods, and I'd much rather just deal with this in one place.

3

3 Answers

45
votes

If you wrap your log call inside of another transaction scope with the suppress option enabled, transaction scope will not be used.

public override int SaveChanges() {
    try {
        return base.SaveChanges();
    } catch (Exception ex) {
        using (var scope = new TransactionScope(TransactionScopeOption.Suppress)) {
            LogRepo.Log(message); // stuff to log from the context
        }

        throw;
    }
}
1
votes

Just my initial thought, but you need to put your LogRepo on it's own DataContext (DC2) so that the surrounding TransactionScope (with DC1) won't roll it back when it's not committed.

Basically, you need to make your logging self-contained and atomic.

EDIT In looking at it some more, it seems to me that if you moved your Logging out from the SaveChanges into the catch() on DoSomething(), your logging would work. But, your logging still needs to be self-contained and atomic.

1
votes

I found one solution that I'm not really happy with, but seems to work. TransactionScope apparently only affects the current thread, so logging using a new thread seems to work ok.

public override int SaveChanges() {
    try {
        return base.SaveChanges();
    } catch (Exception ex) {

        string message = /*stuff to log from the context*/;
        new Thread(msg => {    

            LogRepo.Log(msg);

        }).Start(message);

        throw;
    }
}