Skip to content

Migration overview

Plan a move to Immediate.Jobs from Hangfire, Quartz.NET, Coravel or hand-written background services.

4 min read

Most .NET job libraries share the same building blocks: fire-and-forget work, delayed work, recurring schedules, retries, queues and a dashboard. Immediate.Jobs covers the same ground, but it declares each job as an Immediate.Handlers handler and generates a typed scheduler for it at compile time. Jobs are not recorded as method calls, and the runtime never finds them by reflection.

Concept map

ConceptHangfireQuartz.NETCoravelImmediate.Jobs
Unit of workAny public methodIJob classIInvocable class[Handler, Job] partial class
Job inputSerialized method argumentsJobDataMapIInvocableWithPayload<T>Typed payload record
Run nowBackgroundJob.EnqueueTriggerJob or a one-shot triggerIQueue.QueueInvocableScheduler.EnqueueAsync
Run laterBackgroundJob.ScheduleSimple trigger with a start timeNot supportedScheduler.ScheduleAsync
RecurringRecurringJob.AddOrUpdateCron triggerUseScheduler fluent schedule[Job(Cron = ...)] or IRecurringJobScheduler
Retries[AutomaticRetry]JobExecutionException in ExecuteManualMaxAttempts, Backoff, BackoffBase
Avoid overlap[DisableConcurrentExecution][DisallowConcurrentExecution]PreventOverlappingOverlapPolicy, MaxConcurrency
Missed schedulesMisfireHandling (1.8+)Misfire instructionsNot trackedMisfireHandlingMode
PrioritiesNamed queues in orderTrigger priorityNot supported[QueueDefinition(Priority, Concurrency)]
WorkflowsContinuations; batches in Hangfire.ProListeners or chained jobsNot supportedBatches and continuations
PersistenceSQL Server, Redis and othersADO.NET job storeNone (in memory)In-memory, EF Core, LinqToDB or Redis
DashboardHangfire DashboardThird-partyNoneImmediate.Jobs.Dashboard

What changes for your code

  • Jobs are declared, not captured. A Hangfire expression such as x => x.Send(id) becomes a job class with a payload record. The job name, payload shape, queue name and context keys are saved with every job, so treat them like a database schema.
  • Handlers get dependency injection and behaviors. Each attempt runs in a fresh DI scope through the Immediate.Handlers pipeline, so logging, validation or tenant behaviors work the same as in request handlers.
  • Recurring jobs are payloadless. A recurring job receives EmptyJobRequest. When a schedule used arguments, have the recurring job load its input, or fan out one payload job per item.
  • Delivery is at least once. Like Hangfire and clustered Quartz, a job can run again after a crash. Keep handlers idempotent.
  • Storage is explicit. Pick a provider and a mode in ConfigureStorage. Batches and fair queues need a provider with graph support, which Redis does not have.

A safe migration plan

  1. Inventory every job. List each enqueue call, schedule, recurring definition, queue, retry attribute, concurrency lock and dashboard dependency. Include jobs that only exist in storage, such as schedules added at runtime.
  2. Add Immediate.Jobs next to the old library. Install Immediate.Jobs, register handlers and jobs, and pick storage. Keep the old server running so work that is already queued can finish.
  3. Port one job at a time. Create the job class, choose a stable Name, move the method body into HandleAsync, and switch its callers to the generated scheduler. Add a JobTestHarness test for each job.
  4. Move recurring schedules in one deployment. Add Cron to the new job and remove the old recurring definition in the same release, so a schedule never runs in both systems.
  5. Drain, then remove. Stop enqueueing into the old library, wait until its queues and scheduled jobs are empty, then remove its packages, storage and dashboard.

Other libraries

The same concept map applies to other schedulers:

  • TickerQ time and cron tickers map to delayed jobs and recurring jobs. Function names become job names, and request payloads become payload records.
  • Azure Functions timer triggers and Kubernetes CronJobs map to code-defined recurring jobs that run inside your application.
  • MassTransit or NServiceBus message scheduling is part of a message bus. Keep the bus for messaging between services, and move work that only schedules code within one application to Immediate.Jobs.

Migrate with an agent

The agent prompt on each guide is written for that library. Use this one when you use several libraries, or one without its own guide.

Migrate with an AI agent

Copy this prompt into Claude Code, Codex, Copilot or another coding agent from the root of your repository.