TurboMediator
Scheduling

Entity Framework Core

Persist scheduled jobs using EF Core with optimistic concurrency

Installation

dotnet add package TurboMediator.Scheduling.EntityFramework

Quick Setup

builder.Services.AddDbContext<SchedulingDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddTurboMediator(m => m
    .WithScheduling(scheduling =>
    {
        scheduling.UseEfCoreStore();

        scheduling.AddRecurringJob<CleanupCommand>("cleanup")
            .WithCron("0 3 * * *")
            .WithRetry(RetryStrategy.Fixed(3, 60))
            .WithData(() => new CleanupCommand());
    })
);

SchedulingDbContext

The SchedulingDbContext provides two tables:

public class SchedulingDbContext : DbContext
{
    public DbSet<RecurringJobEntity> RecurringJobs => Set<RecurringJobEntity>();
    public DbSet<JobOccurrenceEntity> JobOccurrences => Set<JobOccurrenceEntity>();
}

Table: SchedulingRecurringJobs

ColumnTypeNotes
JobIdvarchar(256)Primary key
MessageTypeNamenvarchar(max)
CronExpressionnvarchar(max)Nullable
IntervalTicksbigintTimeSpan stored as ticks
NextRunAtdatetimeoffsetNullable
LastRunAtdatetimeoffsetNullable
Statusnvarchar(max)String conversion
RetryIntervalSecondsJsonnvarchar(max)JSON array
SkipIfAlreadyRunningbit
Prioritynvarchar(max)String conversion
MessagePayloadnvarchar(max)Nullable
TimeZoneIdnvarchar(max)Nullable
RowVersionrowversionOptimistic concurrency

Index: IX_SchedulingRecurringJobs_Status_NextRunAt on (Status, NextRunAt)

Table: SchedulingJobOccurrences

ColumnTypeNotes
IduniqueidentifierPrimary key
JobIdnvarchar(256)
StartedAtdatetimeoffset
CompletedAtdatetimeoffsetNullable
Statusnvarchar(max)String conversion
RetryCountint
Errornvarchar(max)Nullable
ParentOccurrenceIduniqueidentifierNullable
RunConditionnvarchar(max)String conversion

Index: IX_SchedulingJobOccurrences_JobId_StartedAt on (JobId, StartedAt)

Migrations

Generate migrations using the standard EF Core tooling:

dotnet ef migrations add InitScheduling --context SchedulingDbContext
dotnet ef database update --context SchedulingDbContext

If you're using a separate migration assembly:

builder.Services.AddDbContext<SchedulingDbContext>(options =>
    options.UseSqlServer(connectionString, sql =>
        sql.MigrationsAssembly("MyApp.Migrations")));

Optimistic Concurrency

The EfCoreJobStore.TryLockJobAsync uses EF Core's RowVersion concurrency token:

  1. Load the job
  2. Set Status = Running
  3. Call SaveChangesAsync
  4. If DbUpdateConcurrencyException is thrown, another instance locked it first → return false

This ensures safe multi-instance deployments without distributed locks.

Instance A: Load job → Status=Running → Save ✓ (locked)
Instance B: Load job → Status=Running → Save ✗ (concurrency conflict)

Custom DbContext Configuration

You can extend or customize the SchedulingDbContext:

builder.Services.AddDbContext<SchedulingDbContext>(options =>
    options.UseNpgsql(connectionString)  // PostgreSQL
           .EnableDetailedErrors()
           .EnableSensitiveDataLogging());

The SchedulingDbContext is independent from your application's DbContext. Make sure to run migrations separately for the scheduling context.

Supported Databases

Any EF Core provider works. Tested with:

  • SQL Server
  • PostgreSQL (Npgsql)
  • SQLite
  • In-Memory (for testing)
// Testing with InMemory
builder.Services.AddDbContext<SchedulingDbContext>(options =>
    options.UseInMemoryDatabase("SchedulingTests"));

On this page