Dependency Injection & Hosted Service
NBatch integrates natively with Microsoft.Extensions.DependencyInjection. Register your jobs once, then run them on-demand via IJobRunner or automatically as background workers.
Getting Started
using NBatch.Core;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddNBatch(nbatch =>
{
nbatch.AddJob("csv-import", job => job
.AddStep("import", step => step
.ReadFrom(new CsvReader<Product>("products.csv", mapFn))
.WriteTo(new DbWriter<Product>(dbContext))
.WithChunkSize(100)));
});
var app = builder.Build();
app.Run();
AddNBatch()
The AddNBatch() extension method on IServiceCollection is your single entry point. It:
- Accepts an
Action<NBatchBuilder>delegate where you register jobs. - Registers
IJobRunneras a singleton — inject it anywhere to run jobs on demand. - For each scheduled job (
RunOnce()orRunEvery()), registers a dedicatedIHostedServicebackground worker.
builder.Services.AddNBatch(nbatch =>
{
// Register jobs here
});
Registering Jobs
Simple Job (No DI Dependencies)
nbatch.AddJob("csv-import", job => job
.AddStep("import", step => step
.ReadFrom(new CsvReader<Product>("products.csv", mapFn))
.WriteTo(new DbWriter<Product>(dbContext))
.WithChunkSize(100)));
Job with DI Services
Use the (IServiceProvider, JobBuilder) overload to resolve services from the container. NBatch creates a new DI scope per job run, so scoped services like DbContext work correctly:
nbatch.AddJob("csv-import", (sp, job) => job
.UseJobStore(connStr) // optional
.WithLogger(sp.GetRequiredService<ILoggerFactory>()
.CreateLogger("CsvImport"))
.AddStep("import", step => step
.ReadFrom(new CsvReader<Product>("products.csv", mapFn))
.WriteTo(new DbWriter<Product>(sp.GetRequiredService<AppDbContext>()))
.WithChunkSize(100)));
Scoped services: Each call to
IJobRunner.RunAsync()creates a newIServiceScope. This means every run gets its ownDbContextinstance, and it’s disposed after the run completes.
Scheduling
Every AddJob() call returns a JobRegistration that you can optionally schedule:
RunOnce() — Execute Once at Startup
nbatch.AddJob("seed-database", job => job
.AddStep("seed", step => step
.Execute(async () =>
{
await dbContext.Database.MigrateAsync();
await SeedDefaultDataAsync(dbContext);
})))
.RunOnce();
The background worker runs the job once when the host starts, then exits. The job remains available on-demand via IJobRunner.
RunEvery() — Recurring Background Job
nbatch.AddJob("hourly-sync", (sp, job) => job
.AddStep("sync", step => step
.ReadFrom(new DbReader<Order>(
sp.GetRequiredService<AppDbContext>(),
q => q.Where(o => o.Status == "new").OrderBy(o => o.Id)))
.WriteTo(new FlatFileItemWriter<Order>("orders.csv"))
.WithChunkSize(200)))
.RunEvery(TimeSpan.FromHours(1));
The interval is measured from the completion of each run, so runs never overlap. If a run fails, the error is logged and the next run starts after the interval.
On-Demand Only (No Schedule)
If you don’t call RunOnce() or RunEvery(), the job is registered but no background worker is created. Trigger it manually:
nbatch.AddJob("export-orders", (sp, job) => job
.AddStep("export", step => step
.ReadFrom(new DbReader<Order>(
sp.GetRequiredService<AppDbContext>(),
q => q.OrderBy(o => o.Id)))
.WriteTo(new FlatFileItemWriter<Order>("orders.csv"))
.WithChunkSize(200)));
// No .RunOnce() or .RunEvery() — on-demand only
IJobRunner — On-Demand Execution
Inject IJobRunner anywhere in your application to trigger jobs programmatically:
public interface IJobRunner
{
Task<JobResult> RunAsync(string jobName, CancellationToken cancellationToken = default);
}
From a Minimal API Endpoint
app.MapPost("/jobs/{name}/run", async (string name, IJobRunner runner, CancellationToken ct) =>
{
var result = await runner.RunAsync(name, ct);
return result.Success ? Results.Ok(result) : Results.StatusCode(500);
});
From an MVC Controller
[ApiController]
[Route("api/[controller]")]
public class BatchController(IJobRunner jobRunner) : ControllerBase
{
[HttpPost("{jobName}/run")]
public async Task<IActionResult> RunJob(string jobName, CancellationToken ct)
{
var result = await jobRunner.RunAsync(jobName, ct);
return result.Success ? Ok(result) : StatusCode(500, result);
}
}
From Another Service
public class OrderService(IJobRunner jobRunner)
{
public async Task ProcessNewOrdersAsync(CancellationToken ct)
{
// ... business logic ...
await jobRunner.RunAsync("process-orders", ct);
}
}
Multiple Jobs
Register as many jobs as you need. Each scheduled job gets its own independent background worker:
builder.Services.AddNBatch(nbatch =>
{
// Runs every 30 minutes
nbatch.AddJob("import-products", (sp, job) => job
.AddStep("import", step => step
.ReadFrom(new CsvReader<Product>("products.csv", mapFn))
.WriteTo(new DbWriter<Product>(sp.GetRequiredService<AppDbContext>()))
.WithChunkSize(100)))
.RunEvery(TimeSpan.FromMinutes(30));
// Runs once at startup
nbatch.AddJob("warmup-cache", job => job
.AddStep("warmup", step => step
.Execute(() => CacheService.WarmUpAsync())))
.RunOnce();
// On-demand only — triggered via IJobRunner
nbatch.AddJob("export-orders", (sp, job) => job
.AddStep("export", step => step
.ReadFrom(new DbReader<Order>(
sp.GetRequiredService<AppDbContext>(),
q => q.OrderBy(o => o.Id)))
.WriteTo(new FlatFileItemWriter<Order>("orders.csv"))
.WithChunkSize(200)));
});
Complete Example
A full ASP.NET Core application with NBatch:
using NBatch.Core;
var builder = WebApplication.CreateBuilder(args);
// Register your EF Core DbContext
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// Register NBatch jobs
builder.Services.AddNBatch(nbatch =>
{
var connStr = builder.Configuration.GetConnectionString("Default")!;
// Background job: import CSV every hour
nbatch.AddJob("csv-import", (sp, job) => job
.UseJobStore(connStr)
.WithLogger(sp.GetRequiredService<ILoggerFactory>().CreateLogger("CsvImport"))
.AddStep("import", step => step
.ReadFrom(new CsvReader<Product>("products.csv", row => new Product
{
Name = row.GetString("Name"),
Price = row.GetDecimal("Price")
}))
.WriteTo(new DbWriter<Product>(sp.GetRequiredService<AppDbContext>()))
.WithSkipPolicy(SkipPolicy.For<FlatFileParseException>(maxSkips: 5))
.WithChunkSize(100))
.AddStep("notify", step => step
.Execute(() => Console.WriteLine("Import complete!"))))
.RunEvery(TimeSpan.FromHours(1));
// On-demand job: export
nbatch.AddJob("export-products", (sp, job) => job
.AddStep("export", step => step
.ReadFrom(new DbReader<Product>(
sp.GetRequiredService<AppDbContext>(),
q => q.OrderBy(p => p.Id)))
.WriteTo(new FlatFileItemWriter<Product>("products-export.csv").WithToken(','))
.WithChunkSize(200)));
});
var app = builder.Build();
// Expose job trigger endpoint
app.MapPost("/jobs/{name}/run", async (string name, IJobRunner runner, CancellationToken ct) =>
{
var result = await runner.RunAsync(name, ct);
return result.Success ? Results.Ok(result) : Results.StatusCode(500);
});
app.Run();
How It Works Under the Hood
AddNBatch()builds aNBatchBuildercontaining job factories (name →Func<IServiceProvider, Job>).- A singleton
JobRunneris registered. It holds all factories and creates a new DI scope perRunAsync()call. - For each
RunOnce()orRunEvery()registration, anNBatchJobWorkerService(aBackgroundService) is registered as anIHostedService. - Workers yield immediately on startup to avoid blocking other hosted services, then begin their schedule.
- If a scheduled job throws (non-cancellation), the error is logged and the next run starts after the interval. The worker never crashes.
Next: Listeners →