Why would you use TransactionScope for Read-Only Database Calls in NET C#?

Viewed 445

I've inherited a legacy system that's got a significant Memory leak caused by millions of allocated InternalTransaction objects on the heap. I've traced this to a database access class. In this class there are methods which read from the database which use TransactionScope.

For example:

    public IEnumerable<IEvent> GetFullEventHistory()
    {
        using (new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { Timeout = TimeSpan.Zero, IsolationLevel = IsolationLevel.ReadCommitted }))
        {
            var events = _store.GetFullEventHistory();
            IList<IEvent> deserialisedEvents = GetEvents(events);
            return deserialisedEvents;
        }
    }

My gut feel is to remove the using() block containing the TransactionScope. This is a Read-Only database call so the transaction doesn't do anything.

Is this a correct assumption? Or is there something I am missing here?

By the way... The Write DB calls have no TransactionScope specified.

2 Answers

Firstly, in most relational databases, both read and write operations on database objects will always be executed inside a transaction. Even if you don't explicitly start a transaction, one will be started implicitly. This should at minimum answer the question of whether it makes sense to use transactions for read statements – in most cases you will end up with a transaction anyway. An example of a case where transactions are not implicitly created is a query like SELECT 2 * 3, that involves no database objects.

When we talk about transactions, we must also talk about isolation levels. Transactions do matter for read statements, the same way they do for all other kinds of database operations. The isolation level controls in which consistency state data is read, and if read locks are placed. In your example, the isolation level of the transaction is set to READ COMMITTED, which could have been done on purpose. Imagine your database has its default transaction level set to READ UNCOMMITTED - using the wrong isolation level here could have undesired side effects, depending on what you are trying to achieve with that query.

I would not remove the transaction scope, just because it's a read operation. You should first figure out how your database is configured and how the data is written to the table you are reading from.

Regarding the memory leak: Your scope is apparently properly disposed, so I don't really see why the scope declaration itself would cause memory leaks. It could also be a bug in the version of the database driver you are using. More likely is, that the method is called very often, thus creating lots of transaction related objects.

It's impossible for me to say what the correct approach is, as I don't know enough about your application and how the method is called. Nevertheless, here are a few things you could consider:

1) Use Required instead of RequiresNew. One of the methods in the call hierarchy, may already have a transaction scope declared. If you create a nested transaction scope using RequiresNew, like it is in your case, it will not reuse that existing transaction, but instead it will create a new one. Changing the scope option to Required will make it reuse the transaction from the parent scope, or create a new one, if there is no parent scope.

2) You could try to move the scope declaration a few levels up in the call hierarchy. The TransactionScope is ambient, so the transaction scope is "inherited" in subsequent method calls. That way you can potentially reduce the number of objects created.

EDIT: This answer is wrong, but I'm leaving it here because I learnt something from it. @stuartd in comments:

Err... you don't have to assign a using declaration to a variable. dotnetfiddle.net/U0PLBw

ORIGINAL WRONG ANSWER: The using should ensure that the new TransactionScope object gets disposed, which should ensure that it doesn't get leaked. However, looking closer, the new TransactionScope doesn't get assigned to any local variable, and so probable isn't available for the Dispose to be called at the end of the using block.

I would suggest you are right to remove the using entirely (as the TransactionScope doesn't get used anyway - it can't be); also I'd report this to Microsoft with a suggestion that such a construct should generate a compiler warning if it doesn't already.

Related