5

In .Net Framework code which is below, it is ensured that someEntity object is inserted into db and Publish opertion will be executed after. However, in .Net Core I could not manage to do this. When I try to run this piece of code, Platform Exception occurs.

using (var transaction = new TransactionScope())
{
    SomeEntity someEntity = new SomeEntity();
    someEntity.Gui = Guid.NewGuid().ToString();

    _dataContext.SomeEntities.Add(someEntity);
    _dataContext.SaveChanges();

    _backgroundJobClient.Enqueue(() => PublishSomeEntityCreatedEvent(someEntity.Id)));

    transaction.Complete();
}

Is there any known good solution for this situation?

Note: .Net Core 2.2 Console application, EntityFrameworkCore 2.1 and Hangfire 1.6.21 are used for testing


Update : Whole stacktrace

Hangfire.BackgroundJobClientException: Background job creation failed. See inner exception for details. ---> System.PlatformNotSupportedException: This platform
 does not support distributed transactions.
   at System.Transactions.Distributed.DistributedTransactionManager.GetDistributedTransactionFromTransmitterPropagationToken(Byte[] propagationToken)
   at System.Transactions.TransactionInterop.GetDistributedTransactionFromTransmitterPropagationToken(Byte[] propagationToken)
   at System.Transactions.TransactionStatePSPEOperation.PSPEPromote(InternalTransaction tx)
   at System.Transactions.TransactionStateDelegatedBase.EnterState(InternalTransaction tx)
   at System.Transactions.EnlistableStates.Promote(InternalTransaction tx)
   at System.Transactions.Transaction.Promote()
   at System.Transactions.TransactionInterop.ConvertToDistributedTransaction(Transaction transaction)
   at System.Transactions.TransactionInterop.GetExportCookie(Transaction transaction, Byte[] whereabouts)
   at System.Data.SqlClient.SqlInternalConnection.GetTransactionCookie(Transaction transaction, Byte[] whereAbouts)
   at System.Data.SqlClient.SqlInternalConnection.EnlistNonNull(Transaction tx)
   at System.Data.SqlClient.SqlInternalConnection.Enlist(Transaction tx)
   at System.Data.SqlClient.SqlInternalConnectionTds.Activate(Transaction transaction)
   at System.Data.ProviderBase.DbConnectionInternal.ActivateConnection(Transaction transaction)
   at System.Data.ProviderBase.DbConnectionPool.PrepareConnection(DbConnection owningObject, DbConnectionInternal obj, Transaction transaction)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
   at System.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
   at System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry)
   at System.Data.SqlClient.SqlConnection.Open()
   at Hangfire.SqlServer.SqlServerStorage.CreateAndOpenConnection()
   at Hangfire.SqlServer.SqlServerStorage.UseConnection[T](DbConnection dedicatedConnection, Func`2 func)
   at Hangfire.SqlServer.SqlServerConnection.CreateExpiredJob(Job job, IDictionary`2 parameters, DateTime createdAt, TimeSpan expireIn)
   at Hangfire.Client.CoreBackgroundJobFactory.Create(CreateContext context)
   at Hangfire.Client.BackgroundJobFactory.<>c__DisplayClass7_0.<CreateWithFilters>b__0()
   at Hangfire.Client.BackgroundJobFactory.InvokeClientFilter(IClientFilter filter, CreatingContext preContext, Func`1 continuation)
   at Hangfire.Client.BackgroundJobFactory.<>c__DisplayClass7_1.<CreateWithFilters>b__2()
   at Hangfire.Client.BackgroundJobFactory.CreateWithFilters(CreateContext context, IEnumerable`1 filters)
   at Hangfire.Client.BackgroundJobFactory.Create(CreateContext context)
   at Hangfire.BackgroundJobClient.Create(Job job, IState state)
   --- End of inner exception stack trace ---
   at Hangfire.BackgroundJobClient.Create(Job job, IState state)
   at Hangfire.BackgroundJobClientExtensions.Create(IBackgroundJobClient client, Expression`1 methodCall, IState state)
   at Hangfire.BackgroundJobClientExtensions.Enqueue(IBackgroundJobClient client, Expression`1 methodCall)
   at TopShelf_Hangfire_NetCore.BusinessService.Execute(DateTime utcNow) in C:\Projects\Practices\TopShelf_Hangfire_NetCore\BusinessService.cs:line 31
   at TopShelf_Hangfire_NetCore.StartupService._timer_Elapsed(Object sender, ElapsedEventArgs e) in C:\Projects\Practices\TopShelf_Hangfire_NetCore\StartupService.cs:line 35
Adem Catamak
  • 1,987
  • 2
  • 17
  • 25

2 Answers2

2

This is now working when using EntityFramework Core 3.0 or greater. The reason this did not work in EFCore 2.x was that EFCore 2.x did not close the connection when not used. Instead the DbConnection was kept open until the context was disposed.

This behavior has changed in EF Core 3.0: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#database-connection-is-now-closed-if-not-used-anymore-before-the-transactionscope-has-been-completed

Starting with 3.0, EF Core closes the connection as soon as it's done using it. This enables your scenario, where you want to enlist EfCore and Hangfire in the same transaction, without escalating to MSDTC.

David Roth
  • 677
  • 4
  • 14
  • How does this actually work? You're not passing your existing connection or DbContext to Hangfire? Are you sure it's tagging onto the existing transaction? – Simon_Weaver May 18 '21 at 01:22
  • Yes. TransactionScope is ambient. That means that Hangfire will enlist into the current ambient transaction scope. – David Roth May 28 '21 at 08:51
  • Actually, I try to use the "Schedule" method of hangfire in command of mediatr with transactionscope pipelinebehavior but I get error – Mohsen Saniee Sep 03 '22 at 17:39
0

It looks like a distrubutrd transaction is beeing initiated, abd that is not supported under .net core.

Since you are accessing more than one resource manager (your db and hangfire's db) the transactionscope tries to escalate the transaction to be ditributed.

You can take hangfire's _backgroundJobClient.Enqueue() out of the scope , and so the escalation will not happen.

You will have to find another way to assure both actions are performed (db update, hangfire enqueue)

EDIT: since you cannot have a transaction you have to design your services to handle the possible failure situations. for instance: The account service will:

  1. Persist the created used to the db

  2. Then call hangfire Enqueue

  3. Log the fact that the hangfire job was created in the same user db.

  4. the user service must poll the db, to see if there were users created but a notification was not logged.

The other micro services, should be able to handle duplicate notifications.

in this way , if a user was created but a notification was not sent, your service will resend (4).

The receiving services will ignore a duplicate request if the failure happens between (2) and (3)

Menahem
  • 3,974
  • 1
  • 28
  • 43
  • I notice that. If I take out hangfire’s enqueue process from transaction, I could not ensure both operation will be executed. Moreover, I do not need transaction. My real question is “is there any known good solution”. Ps: hangfire and ef connect the same db – Adem Catamak Jan 17 '19 at 12:53
  • For .net core distributed transactions are not possible. Even if you use the same database it will in not help since there is more than one connection. To answer you real question: In our time of micro services, the consistency is handled using patterns such as the Saga pattern, and eventual consistency. Namely you can only compensate in case of a failure , you cannot roll back. – Menahem Jan 17 '19 at 13:06
  • I am aware of this. In my account microservice, when new user register, I try to publish AccountCreatedEvent. These operations are transactional. My microservices not responsible about user registeration only. It responsible notify other domains alsoç.I have to ensure message publish operation and user store operation is transactional. I try to figure out how can I achieve this without distributed transaction. – Adem Catamak Jan 17 '19 at 13:26
  • My desire is to use hangfire and other db operations in unit of work. My main aim with asking this question is to learn about any possible solutions without using transactionscope. Would it be a possible solution if db connection is the same/shared? Thanks for the response. I consider pooling technique as a last resource solution. – Adem Catamak Jan 17 '19 at 21:01
  • 1
    Since you don't control hangfire's database context, i dont see how you can share the connection. – Menahem Jan 17 '19 at 21:21
  • https://github.com/HangfireIO/Hangfire/pull/1003 this feature request may solve my problem. However improvement is still in progress. There is no proper solution at right now I guess. Thanks for your ideas – Adem Catamak Jan 18 '19 at 07:49
  • How to control dbContext hangfire? – Mohsen Saniee Sep 03 '22 at 17:42
  • I am not sure you can at this point. See the link Adem posted above – Menahem Sep 04 '22 at 14:47