Dloizides.Jobs.EntityFrameworkCore
1.2.0
dotnet add package Dloizides.Jobs.EntityFrameworkCore --version 1.2.0
NuGet\Install-Package Dloizides.Jobs.EntityFrameworkCore -Version 1.2.0
<PackageReference Include="Dloizides.Jobs.EntityFrameworkCore" Version="1.2.0" />
<PackageVersion Include="Dloizides.Jobs.EntityFrameworkCore" Version="1.2.0" />
<PackageReference Include="Dloizides.Jobs.EntityFrameworkCore" />
paket add Dloizides.Jobs.EntityFrameworkCore --version 1.2.0
#r "nuget: Dloizides.Jobs.EntityFrameworkCore, 1.2.0"
#:package Dloizides.Jobs.EntityFrameworkCore@1.2.0
#addin nuget:?package=Dloizides.Jobs.EntityFrameworkCore&version=1.2.0
#tool nuget:?package=Dloizides.Jobs.EntityFrameworkCore&version=1.2.0
Dloizides.Jobs.EntityFrameworkCore
The Entity Framework Core store for Dloizides.Jobs —
the persistence half of the checkpointed background-jobs standard. It maps JobRun (jsonb checkpoint /
progress on Postgres), creates the single-flight partial unique index, and implements every state
transition as a compare-and-set ExecuteUpdate so a reclaim-vs-complete race is a benign no-op instead
of a poll-killing DbUpdateConcurrencyException.
Why compare-and-set, not tracked SaveChanges
The runner's heartbeat advances a run's row while the job works. A tracked completion carries the row
version it read AT CLAIM TIME, matches 0 rows against the heartbeated row, and throws
DbUpdateConcurrencyException — which aborted the whole poll and re-ran the job forever. The store's
conditional UPDATE ... WHERE Id = @id AND Outcome = 'running' AND ClaimedBy = @owner carries no tracker
state: it finalises the row only while it is still running under this owner, so the owner's own
heartbeats never collide with its own completion, and a reclaimed row simply matches 0 rows — a benign
no-op the runner drops cleanly.
Wiring
- Map
JobRunin yourDbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyJobRunConfiguration(); // auto-detects Npgsql -> jsonb columns
// or: modelBuilder.ApplyJobRunConfiguration(Database.IsNpgsql());
}
Add a migration for the new
JobRunstable + the single-flight index.Select the store inside
AddDloizidesJobs:
builder.AddDloizidesJobs(jobs =>
{
jobs.AddJob<IngestJob>();
jobs.UseEntityFrameworkStore<AppDbContext>();
});
What it maps
Checkpoint/Progress→ jsonb on Postgres (plain string/TEXT elsewhere); on SQLite the timestamps convert to sortable UTC ticks so the claim/history queries translate.- Single-flight index
IX_JobRuns_SingleFlight—UNIQUE (JobName) WHERE Outcome IN ('queued','running'). - CAS transitions:
ClaimNextAsync(claim queued or reclaim a lapsed lease, preservingStartedAtand the checkpoint for resume),HeartbeatAsync,SaveCheckpointAsync,ReportProgressAsync,CompleteAsync— each conditioned on the observed state, returningfalse/nullon 0 rows.
Postgres status backplane (real-time PUSH)
This package also ships the Postgres LISTEN/NOTIFY status backplane — cross-pod real-time push with
no new infra, reusing the same database as the store. Select it inside AddDloizidesJobs:
jobs.UseEntityFrameworkStore<AppDbContext>();
jobs.UsePostgresStatusBackplane<AppDbContext>(); // reads the connection string from AppDbContext
"Jobs": { "Status": { "Backplane": "Postgres", "Channel": "dloizides_jobs" } }
The publishing pod issues pg_notify(channel, payload); every pod holds one dedicated LISTEN connection
and re-emits each event to local subscribers (the Dloizides.Jobs.AspNetCore
wire then re-reads the durable JobStatus and streams it). Persist first, push second — NOTIFY is not
durable, so a notification lost to a reconnect is simply healed by the next poll. The LISTEN loop is a
hosted service that stays dormant unless Jobs:Status:Backplane=Postgres, and reconnects with backoff.
This adds a transitive Npgsql (raw ADO driver) reference —
LISTEN/NOTIFYhas no provider-agnostic EF surface. It is inert unless the Postgres backplane is selected.
Requires a relational provider
The store throws NotSupportedException on the in-memory provider by design: it ignores both ExecuteUpdate
and the unique index, so it cannot exercise the concurrency guarantees this package exists to provide. Use
Postgres in production and SQLite (or a Postgres Testcontainer) in tests — the bundled test suite runs on
SQLite so the compare-and-set genuinely fires.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Dloizides.Jobs (>= 1.2.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.6)
- Npgsql (>= 9.0.2)
-
net8.0
- Dloizides.Jobs (>= 1.2.0)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.11)
- Npgsql (>= 8.0.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.