First off, please upvote gds03's answer. Because I would not have gotten this far without it.
I have contributed to "closing out the transaction" and getting the timing right for IDataReader/DbDataReader situations too.
Basically, with IDataReader/DbDataReader situations, you do NOT close the transaction on the "ReaderExecuted"(Async) methods (emphasis on the "ed" of Executed), but let it "fall through" to (override of) DataReaderDisposing.
But if you read some of the other answers (here) (and comments), I think SetEndTransaction is an important part of .. not getting voodoo from the connection-pool (if you don't probably close out the transaction with the (for me) read-uncommitted).
using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace My.Interceptors
{
public class IsolationLevelInterceptor : DbCommandInterceptor
{
private IsolationLevel _isolationLevel;
public IsolationLevelInterceptor(IsolationLevel level)
{
_isolationLevel = level;
}
//[ThreadStatic]
//private DbCommand _command;
public override InterceptionResult DataReaderDisposing(DbCommand command, DataReaderDisposingEventData eventData, InterceptionResult result)
{
InterceptionResult returnItem = base.DataReaderDisposing(command, eventData, result);
SetEndTransaction(command);
return returnItem;
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
SetStartTransaction(command);
InterceptionResult<DbDataReader> returnItem = base.ReaderExecuting(command, eventData, result);
return returnItem;
}
public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
{
DbDataReader returnItem = base.ReaderExecuted(command, eventData, result);
//SetEndTransaction(command); // DO NOT DO THIS HERE .. datareader still open and working .. fall back on DataReaderDisposing... you don't really need this override, but left in to show the possible issue.
return returnItem;
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
SetStartTransaction(command);
ValueTask<InterceptionResult<DbDataReader>> returnItem = base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
return returnItem;
}
public override ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default)
{
ValueTask<DbDataReader> returnItem = base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
//SetEndTransaction(command); // DO NOT DO THIS HERE .. datareader still open and working .. fall back on DataReaderDisposing... you don't really need this override, but left in to show the possible issue.
return returnItem;
}
public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> interceptionContext)
{
SetStartTransaction(command);
InterceptionResult<object> returnItem = base.ScalarExecuting(command, eventData, interceptionContext);
return returnItem;
}
public override object ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object result)
{
SetEndTransaction(command);
object returnItem = base.ScalarExecuted(command, eventData, result);
return returnItem;
}
public override ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result, CancellationToken cancellationToken = default)
{
SetStartTransaction(command);
ValueTask<InterceptionResult<object>> returnItem = base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
return returnItem;
}
public override ValueTask<object> ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object result, CancellationToken cancellationToken = default)
{
SetEndTransaction(command);
ValueTask<object> returnItem = base.ScalarExecutedAsync(command, eventData, result, cancellationToken);
return returnItem;
}
/* start maybe not needed on queries that only do "reading", but listed here anyways */
public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
{
SetStartTransaction(command);
InterceptionResult<int> returnItem = base.NonQueryExecuting(command, eventData, result);
return returnItem;
}
public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)
{
int returnValue = base.NonQueryExecuted(command, eventData, result);
SetEndTransaction(command);
return returnValue;
}
public override ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
SetStartTransaction(command);
ValueTask<InterceptionResult<int>> returnItem = base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);
return returnItem;
}
public override ValueTask<int> NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default)
{
ValueTask<int> returnValue = base.NonQueryExecutedAsync(command, eventData, result, cancellationToken);
SetEndTransaction(command);
return returnValue;
}
/* end maybe not needed on queries that only do "reading", but listed here anyways */
private void SetStartTransaction(DbCommand command)
{
if (command != null)
{
if (command.Transaction == null)
{
DbTransaction t = command.Connection.BeginTransaction(_isolationLevel);
command.Transaction = t;
//_command = command;
}
}
}
private void SetEndTransaction(DbCommand command)
{
if (command != null)
{
if (command.Transaction != null)
{
command.Transaction.Commit();
//_command = command;
}
command.Dispose();
}
}
}
}
This article was helpful in "seeing" all the "ing" and "ed" methods.
https://lizzy-gallagher.github.io/query-interception-entity-framework/
Please note my answer is EntityFrameworkCore (3.1.+), but I think it will "back port" to EF-for-DN-Framework.
The more important parts of my answer is the "timing" of certain methods...especially the IDataReader/DbDataReader methods.