Skip to content

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:

DB Structure


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:

Default 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"):

Manually triggering a Job

Which will run and show the following output:

Manually Triggered Job 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:

Manually triggering a Job with a Payload

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 use CronTicker.
  • 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:

Programmatically calling the 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.