Net.Extensions.JobScheduler is a lightweight yet powerful framework for scheduling and executing background jobs in .NET applications. It was designed to be simple, extensible, and fully compatible with modern .NET idioms like Dependency Injection (DI) and the Generic Host (IHost). The framework supports both recurring and one-time jobs, with flexible execution policies such as retry and timeout, and a modular architecture that allows custom extensions for job storage, policies, and execution logic.
License
—
Deps
2
Install Size
—
Vulns
✓ 0
Published
Jun 28, 2025
$ dotnet add package JoeDevSharp.Net.Extensions.JobSchedulerNet.Extensions.JobScheduler is a lightweight yet powerful framework for scheduling and executing background jobs in .NET applications. It was designed to be simple, extensible, and fully compatible with modern .NET idioms like Dependency Injection (DI) and the Generic Host (IHost). The framework supports both recurring and one-time jobs, with flexible execution policies such as retry and timeout, and a modular architecture that allows custom extensions for job storage, policies, and execution logic.
This README provides a comprehensive guide to understanding, configuring, and extending the framework for various use cases — from small console apps to large industrial or legacy system modernization projects.
Many applications require background tasks — for example, sending emails, cleaning up data, synchronizing with external systems, or running periodic reports. While .NET offers several options (Quartz.NET, Hangfire, Windows Services), these can sometimes be overly complex, heavyweight, or not fully customizable for specific business needs.
Net.Extensions.JobScheduler targets scenarios where you need:
The project is organized into several core folders representing clear responsibilities:
Defines the main interfaces and contracts that the rest of the system implements:
JobDescriptor). Allows for pluggable persistence (in-memory, database, etc.).This separation ensures your scheduler remains decoupled and testable.
Contains the main engine classes:
Predefined job types like:
You can extend and create your own job types based on IJob.
This framework provides convenient extension methods to specify recurring job intervals:
EverySeconds(int seconds)EveryMinutes(int minutes)EveryHours(int hours)EveryDays(int days)EveryWeeks(int weeks)EveryMonths(int months) (approximate)EveryYears(int years) (approximate)Important:
For EveryMonths and EveryYears, the scheduling uses a fixed TimeSpan approximation:
This means the actual calendar variation (months of 28-31 days, leap years) is not accounted for.
TimeSpan represents a fixed duration and does not natively support calendar-aware intervals. Therefore, using these methods schedules jobs at approximately these intervals, which is usually acceptable for many scenarios but not suitable when precise calendar dates/times are critical (e.g., running on the 1st day of each month).
If your use case requires precise calendar scheduling — accounting for variable month lengths or leap years — a different scheduling logic must be implemented, such as calculating the next run date explicitly using calendar-aware APIs (e.g., DateTime.AddMonths(), DateTime.AddYears()).
Fluent API classes and extension methods to build and configure jobs cleanly, for example:
.EveryMinutes(int) to define recurring schedules..WithSimpleRetry() to add a retry policy with default settings.Execution policies control how jobs are run:
Policies implement IJobPolicy and wrap job executions, enforcing constraints or behaviors.
Implementations of IJobStore. The default:
Helpers for integrating the scheduler with the .NET DI and Hosting ecosystem, including extension methods to register scheduler services and start the scheduler when the host runs.
The fundamental unit of work.
public interface IJob
{
Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default);
}ExecuteAsync contains the logic your job performs.JobContext provides metadata like job ID and user-defined info.CancellationToken.Manages jobs and their execution lifecycle:
public interface IJobScheduler
{
void Register(JobDescriptor descriptor);
Task StartAsync(CancellationToken cancellationToken = default);
Task StopAsync(CancellationToken cancellationToken = default);
}Register adds jobs to the scheduler.StartAsync launches all scheduled jobs asynchronously.StopAsync requests cancellation and graceful shutdown.Defines how jobs are executed beyond just calling ExecuteAsync:
public interface IJobPolicy
{
Task<JobResult> ExecuteAsync(IJob job, JobContext context, CancellationToken cancellationToken = default);
}Allows behaviors like retrying, timing out, logging, or other cross-cutting concerns without modifying job code.
Storage abstraction for job metadata:
public interface IJobStore
{
void Save(JobDescriptor descriptor);
IEnumerable<JobDescriptor> GetAll();
}Allows persisting job schedules and metadata, decoupling scheduling logic from persistence.
Jobs are defined as classes implementing IJob. For example:
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger) => _logger = logger;
public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Running job {JobId} at {Time}", context.JobId, DateTimeOffset.Now);
// Job logic here
return Task.CompletedTask;
}
}Register the job with the scheduler using the fluent builder API:
scheduler.Register(JobBuilder.Create()
.WithId("myjob")
.WithJobType<MyJob>()
.EveryMinutes(10)
.WithSimpleRetry()
.Build());Both job types can be extended or customized.
Retries job execution on failure.
var retryPolicy = new RetryPolicy(maxAttempts: 3, delayBetweenAttempts: TimeSpan.FromSeconds(5));Cancels job execution after a timeout period.
var timeoutPolicy = new TimeoutPolicy(TimeSpan.FromSeconds(30));Combine multiple policies for advanced behavior.
var combined = retryPolicy.Wrap(timeoutPolicy);The scheduler resolves job instances from the DI container, creating a new scope for each execution. This allows using scoped services inside jobs (e.g., DbContext). Internally, this is done via IServiceScopeFactory.CreateScope() to ensure clean scope lifetimes.
Designed to integrate naturally with IHost:
ConfigureServices.StartAsync or via extension method.Example:
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddJobScheduler();
services.AddTransient<MyJob>();
})
.Build();
var scheduler = host.Services.GetRequiredService<IJobScheduler>();
scheduler.Register(...);
await scheduler.StartAsync();
await host.RunAsync();IJobPolicy.IJobStore to persist to database or distributed caches.StartAsync() after registering jobs.TimeoutPolicy to limit execution time.CompositePolicy to combine retry and timeout logically.Contributions are welcome! Please follow the usual GitHub flow: