Skip to content

Effects Composition

Background Jobs compose with the Deepstaging effects system via [EffectsModule], [Runtime], and [Uses] — the same pattern used by all other modules.

Declaring the Effects Module

Wrap IJobScheduler and IJobStore as effect modules:

using Deepstaging.Jobs;
using Deepstaging.Effects;

[EffectsModule(typeof(IJobScheduler))]
[EffectsModule(typeof(IJobStore))]
public sealed partial class JobEffects;

Composing with a Runtime

Add job effects to your runtime with [Uses]:

using Deepstaging.Effects;

[Runtime]
[Uses(typeof(JobEffects))]
[Uses(typeof(EmailEffects))]
public sealed partial class AppRuntime;

This generates typed accessors on the runtime so handlers can resolve IJobScheduler and IJobStore through the effect system.

Effect-Wrapped Job Scheduling

With the effects module registered, use the generated accessors in LINQ expressions:

// Enqueue a job for immediate execution
from jobId in JobEffects.JobScheduler.EnqueueAsync<AppRuntime>(
    new SendWelcomeEmail(userId))
select jobId;

// Schedule a job for later
from jobId in JobEffects.JobScheduler.ScheduleAsync<AppRuntime>(
    new SyncInventory("warehouse-1"),
    DateTimeOffset.UtcNow.AddMinutes(30))
select jobId;

// Monitor dead-letter queue
from deadLettered in JobEffects.JobStore.GetDeadLetterAsync<AppRuntime>(limit: 50)
select deadLettered;

JobHandler Returning Eff<RT, Unit>

Job handlers return Eff<RT, Unit> to participate in the effect system. This gives handlers access to all capabilities composed into the runtime:

using Deepstaging.Jobs;
using LanguageExt;

public static class InventoryJobs
{
    [JobHandler]
    public static Eff<AppRuntime, Unit> Handle(SyncInventory job) =>
        from inventory in WarehouseEffects.Warehouse
            .GetInventoryAsync<AppRuntime>(job.WarehouseId)
        from _ in WarehouseEffects.Warehouse
            .SyncAsync<AppRuntime>(inventory)
                  >> AuditEffects.Audit.RecordAsync<AppRuntime>(
                      "inventory_synced", job.WarehouseId)
        select unit;
}

Because the handler runs inside Eff<AppRuntime, Unit>, it can compose with any effect available on AppRuntime — audit logging, notifications, other job scheduling, etc.

Integration with Dispatch Pipeline

Job handlers integrate with the same dispatch pipeline used by command and query handlers. The generator:

  1. Discovers all [JobHandler] methods in the assembly
  2. Matches each handler's first parameter type to a [BackgroundJob] record
  3. Emits a strongly-typed dispatch table — the job type determines routing at compile time
  4. Wires retry, timeout, and dead-letter logic around the handler invocation

This means jobs benefit from the same compile-time safety as dispatch — no runtime reflection, no dictionary lookups, full type checking at build time.