HangfireIO / HangfireIO/Hangfire

Pending local transaction is not assigned to SQL commands

Open
#1,008 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
10.1k
Forks
1.8k
Avg merge
1h 19m
Merged PRs (30d)
1

Description

I am trying to use Hangfire with an existing connection and transaction. Ultimate goal is to save changes together with one or more background jobs in a single transaction.

This fails because the transaction is not set on the SQL commands executed by Hangfire.
I assume this would work on the full framework because one can use a TransactionScope and the SQL commands would auto enlist in the ambient transaction? This is not an option on .NET Core though.

## Exception details
```
Hangfire.BackgroundJobClientException: Background job creation failed. See inner exception for details. ---> System.InvalidOperationException: ExecuteScalar requires the command to have a transaction when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized.
at System.Data.SqlClient.SqlCommand.ValidateCommand(Boolean async, String method)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite, String method)
at System.Data.SqlClient.SqlCommand.ExecuteScalar()
at Dapper.SqlMapper.ExecuteScalarImpl[T](IDbConnection cnn, CommandDefinition& command)
at Dapper.SqlMapper.ExecuteScalar[T](IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Nullable`1 commandTimeout, Nullable`1 commandType)
at Hangfire.SqlServer.SqlServerConnection.<>c__DisplayClass7_0.b__0(DbConnection connection)
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.b__0()
at Hangfire.Client.BackgroundJobFactory.InvokeClientFilter(IClientFilter filter, CreatingContext preContext, Func`1 continuation)
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)
```

## Steps to reproduce
```
using (var context = new TestDbContext())
{
context.Database.Migrate();
context.InitializeHangfireIfNecessary();

context.SaveChanges();
}
```
```
public class TestDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=.;Database=HangfireTest;Integrated Security=SSPI");
//optionsBuilder.ConfigureWarnings(x => x.Ignore(RelationalEventId.AmbientTransactionWarning));
base.OnConfiguring(optionsBuilder);
}

public override int SaveChanges()
{
//TransactionScope is not supported on .NET Core.
//Exceptions can be suppressed, but hangfire commands are not enlisted in the transaction
//using (var tx = new TransactionScope())
using (var tx = Database.BeginTransaction())
{
//Reuse existing connection because we want a single transaction
var storage = new SqlServerStorage(Database.GetDbConnection(),
new SqlServerStorageOptions { PrepareSchemaIfNecessary = false });

var client = new BackgroundJobClient(storage);

client.Enqueue(() => Console.WriteLine("Fire-and-forget!"));
var result = base.SaveChanges();
tx.Commit();

return result;
}
}

public void InitializeHangfireIfNecessary()
{
// Initialize Hangfire storage
var storage = new SqlServerStorage(Database.GetDbConnection());
}
}
```

Contributor guide

Open the contributing guide

Research direction

Start at Hangfire.SqlServer.SqlServerConnection.CreateExpiredJob and SqlServerStorage.UseConnection, following the Dapper SQL command shown in the exception. Reproduce the sample with DbContext.Database.BeginTransaction and verify that creating the job no longer fails with a pending local transaction and that the job and saved changes commit together.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.