Skip to content

Filters

Filters are middleware for job execution. They wrap the job handler and run before and after each execution, letting you add cross-cutting behavior like logging, metrics, authorization, or error handling.

Implement IJobFilter. Call next(context) to continue the pipeline, or skip it to short-circuit execution.

public class LoggingFilter : IJobFilter
{
public async Task InvokeAsync(JobContext context, JobFilterDelegate next)
{
var sw = Stopwatch.StartNew();
Console.WriteLine($"Starting {context.JobName}");
await next(context);
Console.WriteLine($"Finished {context.JobName} in {sw.ElapsedMilliseconds}ms");
}
}

Global filters run on every job. Register them with UseFilter<T> in the Surefire configuration:

builder.Services.AddSurefire(options =>
{
options.UseFilter<LoggingFilter>();
options.UseFilter<TenantFilter>();
});

Filters run in the order they’re registered. The first filter registered is the outermost layer of the pipeline.

Per-job filters run only on a specific job. Add them with UseFilter<T> on the job builder:

app.AddJob("Import", async () => { /* ... */ })
.UseFilter<AuditFilter>();

Per-job filters are resolved from DI if registered, or created via ActivatorUtilities if not. Their constructor parameters are injected automatically.

Global filters wrap all jobs. Per-job filters wrap only their specific job. Filters execute in registration order.

public class SlackNotificationFilter(SlackClient slack) : IJobFilter
{
public async Task InvokeAsync(JobContext context, JobFilterDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
await slack.PostAsync($"Job {context.JobName} failed: {ex.Message}");
throw;
}
}
}
public class TimingFilter(ILogger<TimingFilter> logger) : IJobFilter
{
public async Task InvokeAsync(JobContext context, JobFilterDelegate next)
{
var sw = Stopwatch.StartNew();
await next(context);
logger.LogInformation("Job {Name} took {Ms}ms",
context.JobName, sw.ElapsedMilliseconds);
}
}