# EF Core Integration

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](/docs/guides/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](/docs/storage/postgres) | `NpgsqlTransaction` | `UseNpgsql` |
| [SQL Server](/docs/storage/sql-server) | `SqlTransaction` | `UseSqlServer` |
| [SQLite](/docs/storage/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](/docs/storage/overview) 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](/docs/storage/sqlite) 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:

```csharp title="Program.cs"
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:

```csharp title="OrderEndpoint.cs" {5}
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 together
```

The 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](/docs/storage/wakeup-hints) for the detail.

## Where to go next

- [Transactional Enqueue with EF Core](/docs/guides/transactional-enqueue-with-ef-core) for the full step-by-step pattern, the rollback path, and visibility timing.
- [Storage overview](/docs/storage/overview) for the storage model and which adapters support Transactional Enqueue.
- [SQLite storage](/docs/storage/sqlite) for the co-resident versus dedicated split and the same-file guard.
- [Wake-up hints](/docs/storage/wakeup-hints) for the latency behavior under transactional enqueue.
- [Storage contract](/docs/reference/storage-contract) for `SupportsTransactionalEnqueue` as a contract term.
- [Client API reference](/docs/reference/client-api) for every enqueue overload and its parameters.
