Reputation: 451
Context: I'm trying to publish database events and data to a queue, but only if the data/transaction is committed successfully. We're doing this so we can run subsequent logic upon this data (post-processing). We've formatted our events to be something like [objecttype][action]
(e.g.: userAdded
, userDeleted
, etc). I don't want to trigger off of a call to SaveChanges()
simply because there is the possibility that there are multiple calls to SaveChanges()
(wrapped inside a TransactionScope
) where the corresponding post-processing logic may need data from the later calls to SaveChanges()
. I don't want to have to deal with race conditions, so I want to wait for the transaction to commit and push the event to the queue when it does.
Attempted Solution: I created an implementation of IDbTransactionInterceptor and added it to our interceptors on startup. I've implemented only the void Committed(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext)
method with our logic (the remaining methods are just empty).
Issue: The Committed
method in the interceptor is never hit. The transaction runs properly (if there's an error, nothing is in the DB). The TransactionScope is set up as new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled)
If I switch scope to Suppressed (TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)
), then the Committed method does trigger, but for each individual call to SaveChanges()
(expected, since EF does transactions on its own).
Am I missing something? I can't find much in the way of details of TransactionScopes + IDbTransactionInterceptor. Does the interceptor not work with scopes and only actual EF Transactions (e.g. context.Database.BeginTransaction
)?
Code:
TransactionScopeFactory:
public static TransactionScope GetAsyncTransactionScope()
{
return new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled);
}
WebApi Controller:
public async Threading.Task<SaveNewMessageResponse> SaveNewMessage(SaveNewMessageRequest request)
{
SaveNewMessageResponse response;
using (TransactionScope tx = TransactionScopeFactory.GetAsyncTransactionScope())
{
using (var messageBA = IoCContainer.Resolve<IMessageBusinessAccess>())
{
response = await messageBA.SaveNewMessage(request);
}
tx.Complete();
}
return response;
}
Business Class:
public async Threading.Task<SaveNewMessageResponse> SaveNewMessage(SaveNewMessageRequest request)
{
// some async/await stuff, eventually a call to SaveChanges()
}
Interceptor:
public void Committed(DbTransaction transaction, DbTransactionInterceptionContext interceptionContext)
{
foreach (var entry in interceptionContext.DbContexts.First().ChangeTracker.Entries())
{
var entity = entry.Entity;
var changeType = entry.State;
using (var queue = IoCContainer.Resolve<IQueueController>())
{
var eventName = entry.GetType().Name + changeType;
queue.Publish(eventName, entity);
}
}
}
Upvotes: 1
Views: 789
Reputation: 579
I know this question is a bit dated, but I will answer for anyone who comes to this looking for answers like I did.
It looks like you had already guessed at the answer:
Am I missing something? I can't find much in the way of details of TransactionScopes + IDbTransactionInterceptor. Does the interceptor not work with scopes and only actual EF Transactions (e.g. context.Database.BeginTransaction)?
I ran some experiments with the IDbTransactionInterceptor. When using the TransactionScope in System.Transactions.dll the interceptor will never get called. This actually makes sense as the System.Transactions.dll was written long before EF interception was added.
If you switch to using context.Database.BeginTransaction() then the interceptor will now work. Partially, as it appear that a couple of methods are never called:
IsolationLevelGetting() IsolationLevelGot()
Upvotes: 1