Scheduling in C# with TickerQ
Note: This was adapted from this YouTube video and the official documentation
TickerQ is a .NET scheduling library that can be used as an alternative to Hangfire, Quartz and other scheduling libraries. The official documentation can be found here.
Typically, to use TickerQ you will need to add the following three packages:
- TickerQ
- TickerQ.Dashboard
- TickerQ.EntityFrameworkCore - required for persistence, to be able to run jobs after a restart, etc.
Basic Configuration
With these packages added to our project we must add a connection to our database to store all the jobs we will be running and add a basic setup for TickerQ:
// Use builder.Configuration instead of Configuration
builder.Services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnectionString")
, options => options.CommandTimeout(180)));
builder.Services.AddTickerQ(options =>
{
options.AddOperationalStore<MyDbContext>(efOpt =>
{
efOpt.UseModelCustomizerForMigrations();
efOpt.CancelMissedTickersOnAppStart();
});
options.AddDashboard(dbopt =>
{
// Mount path for the dashboard UI (default: "/tickerq-dashboard").
dbopt.BasePath = "/tickerq-dashboard";
// Allowed CORS origins for dashboard API (default: ["*"]).
//dbopt.CorsOrigins = new[] { "https://arcenox.com" };
// Backend API domain (scheme/SSL prefix supported).
//dbopt.BackendDomain = "ssl:arcenox.com";
// Authentication
dbopt.EnableBuiltInAuth = true; // Use TickerQ’s built-in auth (default).
dbopt.UseHostAuthentication = false; // Use host auth instead (off by default).
dbopt.RequiredRoles = new[] { "Admin", "Ops" };
dbopt.RequiredPolicies = new[] { "TickerQDashboardAccess" };
// Basic auth toggle (default: false).
dbopt.EnableBasicAuth = true;
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseTickerQ();
Adding the Database Configuration
Then we need to set up our configuration in the DbContext class
using Microsoft.EntityFrameworkCore;
using TickerQ.EntityFrameworkCore.Configurations;
namespace TickerQDemo;
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new TimeTickerConfigurations()); // Run at a certain time
modelBuilder.ApplyConfiguration(new CronTickerConfigurations()); // Run as a cron configuration
modelBuilder.ApplyConfiguration(new CronTickerOccurrenceConfigurations());
// Alternate to the 3 lines above we can just set up configurations by scanning the assembly
//modelBuilder.ApplyConfigurationsFromAssembly(typeof(TimeTickerConfigurations).Assembly);
}
}
Now in the terminal from the project directory we have to create the migrations and apply them:
dotnet-ef migrations add "TickerQInitialCreate" -c MyDbContext
dotnet-ef database update TickerQInitialCreate
This should result in the database being created (if required) along with the required tables:
Note: Setting up the database is not covered here. I use my usual Docker instance in the associated code.
Note: You may need to install EF Tools first:
dotnet tool install --global dotnet-ef
Checking Our Progress
Now we can run the project and navigate to the associated TickerQ dashboard:
Adding a Scheduled Job
With all this in place we can now start adding scheduled jobs. For clarity we would normally add these in their own directory.
Jobs will usually be either set to run at a certain time (a timed job) or a CRON style job (which can run at intervals, etc.)
Jobs are defined via annotations, so a simple job could look something like this:
public class MyJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
[TickerFunction("CleanUpLogs")] // Runs daily at midnight
public void CleanUpLogs()
{
_logger.LogInformation("Cleaning up logs...");
// Logic to clean up logs would go here in a real application.
}
}
There are no timing parameters on this job, but we can trigger it manually from the dashboard Add Tracker option from one of the tracker types at the top of the page (for example "Time Trackers"):
Which will run and show the following output:
Passing an Object to a Scheduled Job
To pass an object to a scheduled job, for example a Point
structure, we use a TickerFunctionContext
object typed to whatever we want to pass:
[TickerFunction("WithAnObject")]
public void WithObject(TickerFunctionContext<Point> tickerContext)
{
var point = tickerContext.Request;
_logger.LogInformation("Point received: X={x}, Y={y}", point.X, point.Y);
}
Which we can run in a similar way:
Scheduling a Job in Code
To schedule a job in code we can use parameters on the annotation, for example a job to run every minute in a CRON style would look like this:
[TickerFunction("CleanUpLogs", "*/1 * * * *")] // Runs every minute
public void CleanUpLogs()
{
_logger.LogInformation("Cleaning up logs...");
// Logic to clean up logs would go here in a real application.
}
The running of this job will show on the CRON TICKERS tab of the dashboard.
Programmatically scheduling a job
As an example, let's do this on an endpoint:
app.MapPost("/schedule", async (Point point, ITimeTickerManager<TimeTicker> timeTickerManager) =>
{
await timeTickerManager.AddAsync(new TimeTicker
{
Request = TickerHelper.CreateTickerRequest<Point>(point),
ExecutionTime = DateTime.UtcNow.AddSeconds(10), // Schedule to run after 10 seconds
Function = nameof(MyJob.WithObject),
Description = "A job with an object",
Retries = 3,
RetryIntervals = [1, 2, 3]
});
return Results.Ok();
});
- This is a post request so we can pass down the point data.
- The ITimeTickerManager is getting a
TimeTicker
, but we could useCronTicker
. - Request sets a request with the data (the variable
point
). - Function sets the function to call when the job is triggered
- etc.
If we run this and post to the /schedule
endpoint:
Global Exception Handlers
A global exception handler can also be added is a problem occurs with a job. I was going to add some notes here but the official documentation describes this simply.
Source Code
I have implemented this example a Git repo in GitHub here.