EF Core Integration
The storage angle on EF Core: which adapters support transactional enqueue, co-resident versus cross-engine, and package registration.
Most BackWave storage adapters can write a Job row on a transaction you already own, so a business write and the Job that depends on it commit or roll back together. EF Core is how a .NET application usually owns that transaction. The BackWave.EntityFrameworkCore package is the small piece that bridges the two: it reads the open transaction off your DbContext and hands it to the storage adapter. Here we cover it from the storage side, which adapters support it and the one constraint that has to hold for the atomicity to be real. The step-by-step pattern lives in the Transactional Enqueue with EF Core guide.
What the package is, and is not#
The whole BackWave.EntityFrameworkCore package is one static extension method on BackWaveClient. There is no EF Core specific job store, no DbSet for jobs, no SaveChanges interceptor, and no change-tracked Job entity. The Job is written by a raw INSERT on the connection your DbContext already has open, not through EF's change tracker. The extension reads the transaction off the context and forwards to the regular client.
That shape has a consequence for registration: you do not add anything from this package to your DI container. There is no AddBackWaveEfCore and nothing to wire up. You install the package and call the extension method at the point you enqueue. The only thing you register is your own DbContext, the same one you would register for any EF Core application.
The same-provider, same-database rule#
This is the storage rule that makes everything else work. BackWave never opens the business transaction. It reads the transaction you began on your DbContext and writes its Job row on that same connection, inside that same transaction. For that to be possible, the transaction your context hands over has to be one the storage adapter can enlist in. Each adapter only accepts its own provider's native transaction type and rejects anything else with an ArgumentException. So your DbContext and your BackWave store have to use the same provider against the same database:
| Adapter | Accepts | Provider on your DbContext |
|---|---|---|
| Postgres | NpgsqlTransaction | UseNpgsql |
| SQL Server | SqlTransaction | UseSqlServer |
| SQLite | SqliteTransaction | UseSqlite |
A Postgres context cannot share a transaction with a SQL Server store, and BackWave will not pretend otherwise. The same rule rules out cross-database and cross-engine atomicity entirely: there is no distributed transaction, no TransactionScope across two engines, and no MSDTC. Atomicity requires one database, which for SQLite means one file.
This is also why the same-provider rule is your responsibility rather than BackWave's. The package depends only on Microsoft.EntityFrameworkCore.Relational, never on a specific EF provider. The concrete provider, Npgsql, SQL Server, or SQLite, comes from your application's AddDbContext. BackWave never picks it, so it cannot enforce that it matches the store; it can only refuse the transaction at the boundary if it does not.
Which adapters support transactional enqueue#
Transactional Enqueue is a Storage Adapter capability, not a universal guarantee. Every adapter advertises whether it has it through a SupportsTransactionalEnqueue flag you can read at runtime, which is useful when a single build runs against different deployments. Across the shipped production adapters:
| Adapter | Transactional Enqueue |
|---|---|
| Postgres | Supported |
| SQL Server | Supported |
| SQLite | Supported, co-resident deployment only |
All three production adapters support it. If you hand a transaction to a store whose adapter does not, the client throws a NotSupportedException whose message names SupportsTransactionalEnqueue, before it touches the store, rather than degrading to a non-atomic write.
Co-resident versus cross-engine#
The capability table tells you what an adapter can do; the deployment tells you whether you can actually use it. Two arrangements are worth naming.
In a co-resident deployment, BackWave's tables live in the same database as your business data, reached by the same provider. One transaction spans both your row and the Job row. This is the tightest Transactional Enqueue there is, and it is the arrangement the EF Core overload is built for. In practice it means your AddDbContext and your BackWave store point at the same connection string.
In a cross-engine or dedicated deployment, BackWave's store is a different engine, or for SQLite a different file, from your business data. No single transaction can span the two, so transactional enqueue is unavailable even when the adapter reports the capability. There you use the plain client.EnqueueAsync(job, dueTime) overload and accept that the business write and the enqueue are separate commits.
SQLite is the adapter where this distinction bites, because it has both deployments. The co-resident SQLite deployment, where BackWave's tables share your application's SQLite file, can use Transactional Enqueue. The dedicated SQLite deployment, where BackWave runs on its own file, cannot, because no transaction can span two files. The SQLite storage page covers that split, including the same-file guard that protects you from committing a Job into the wrong file.
What registration looks like#
There is nothing BackWave-specific to register. You register a plain DbContext against the same connection string your BackWave store is configured for:
services.AddDbContext<SampleDbContext>(options => options.UseNpgsql(connectionString));With the context pointed at the same database as the store, a single transaction can span both, and the call site is the one method this package adds:
await using var tx = await context.Database.BeginTransactionAsync();
context.Orders.Add(order);
await context.SaveChangesAsync();
var jobId = await client.EnqueueAsync(new SendReceipt(order.Id), context, DateTimeOffset.UtcNow);
await tx.CommitAsync(); // the order and the job commit togetherThe mechanics of that call, the mandatory open transaction, the rollback path, when the Job becomes visible, and enqueueing several Jobs in one transaction, are all covered in the guide. The one storage-level note worth carrying here: under transactional enqueue some adapters hold the Wake-Up Hint back until commit, which affects only how quickly a Worker notices the Job, never correctness. Polling picks it up regardless. See Wake-up hints for the detail.
Where to go next#
- Transactional Enqueue with EF Core for the full step-by-step pattern, the rollback path, and visibility timing.
- Storage overview for the storage model and which adapters support Transactional Enqueue.
- SQLite storage for the co-resident versus dedicated split and the same-file guard.
- Wake-up hints for the latency behavior under transactional enqueue.
- Storage contract for
SupportsTransactionalEnqueueas a contract term. - Client API reference for every enqueue overload and its parameters.