Skip to content

tinglesoftware/dotnet-periodic-tasks

Repository files navigation

Simplified periodic task scheduling for .NET

NuGet Nuget GitHub Workflow Status Dependabot license

This repository contains the code for the Tingle.PeriodicTasks libraries. This project exists to simplify the amount of work required to add periodic tasks to .NET projects. The existing libraries seem to have numerous complexities in setup especially when it comes to the use of framework concepts like dependency inject and options configuration. At Tingle Software, we use this for all our periodic tasks that is based on .NET. However, the other libraries have more features such as persistence and user interfaces which are not yet available here.

Packages

Package Description
Tingle.PeriodicTasks Basic implementation of periodic tasks in .NET
Tingle.PeriodicTasks.AspNetCore AspNetCore endpoints for managing periodic tasks.
Tingle.PeriodicTasks.EventBus Support for triggering periodic tasks using events from Tingle.EventBus.

Documentation

Getting started

Install the necessary library/libraries using Package Manager

Install-Package Tingle.PeriodicTasks
Install-Package Tingle.PeriodicTasks.AspNetCore
Install-Package Tingle.PeriodicTasks.EventBus

Install the necessary library/libraries using dotnet CLI

dotnet add Tingle.PeriodicTasks
dotnet add Tingle.PeriodicTasks.AspNetCore
dotnet add Tingle.PeriodicTasks.EventBus

Create a periodic task

class DatabaseCleanerTask : IPeriodicTask
{
    private readonly ILogger logger;

    public DatabaseCleanerTask(ILogger<DatabaseCleanerTask> logger)
    {
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task ExecuteAsync(PeriodicTaskExecutionContext context, CancellationToken cancellationToken = default)
    {
        logger.LogInformation("Cleaned up old records from the database");
        await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
    }
}

Register the periodic task in your Program.cs file:

services.AddPeriodicTasks(builder =>
{
    builder.AddTask<DatabaseCleanerTask>(o => o.Schedule = "*/1 * * * *"); // every minute
});

To support running on multiple machines, distributed locking is used. See library for more information.

You need to register IDistributedLockProvider this in your Program.cs file which can be backed by multiple sources. For this case, we use file-based locks.

// register IDistributedLockProvider
services.AddSingleton<Medallion.Threading.IDistributedLockProvider>(provider =>
{
    return new Medallion.Threading.FileSystem.FileDistributedSynchronizationProvider(Directory.CreateDirectory("distributed-locks"));
});

Management via endpoints in AspNetCore

You can choose to manage the periodic tasks in using endpoints in AspNetCore. Update your application setup as follows.

  var app = builder.Build();

  app.MapGet("/", () => "Hello World!");

+ app.MapPeriodicTasks();

  await app.RunAsync();

Remember to add authorization policies are needed. For example:

app.MapPeriodicTasks().RequireAuthorization("policy-name-here");

Endpoints available:

  • GET /registrations: list the registered periodic tasks

  • GET /registrations/{name}: retrieve the registration of a given periodic task by name

  • GET /registrations/{name}/history: retrieve the execution history of a given periodic task

  • POST /execute: execute a periodic task

    {
      "name": "DatabaseCleanerTask", // can also be DatabaseCleaner, databasecleaner
      "wait": true, // Whether to await execution to complete.
      "throw": true // Whether to throw an exception if one is encountered.
    }

Triggering via Tingle.EventBus

This helps trigger periodic tasks on demand or from another internal source without blocking. Update your EventBus setup as follows.

services.AddEventBus(builder =>
{
      builder.AddInMemoryTransport();
+     builder.AddPeriodicTasksTrigger();
});

Publish events using the TriggerPeriodicTaskEvent type. In JSON:

{
    "id": "...",
    "event": {
        "name": "DatabaseCleanerTask", // can also be DatabaseCleaner, databasecleaner
        "wait": true, // Whether to await execution to complete.
        "throw": true // Whether to throw an exception if one is encountered.
    },
    // omitted for brevity
}

Disabling a task

By default, all registered periodic tasks are enabled. This means that they will always run as per schedule. In some scenarios you may want to disable them by default but execute them on demand via AspNetCore endpoints or via the EventBus.

To disable a periodic task on registration:

services.AddPeriodicTasks(builder =>
{
    builder.AddTask<DatabaseCleanerTask>(o =>
    {
+         o.Enable = false;
          o.Schedule = "*/1 * * * *";
    });
});

To disable a periodic task via IConfiguration, update your appsettings.json:

{
  "PeriodicTasks": {
    "Tasks": {
      "DatabaseCleanerTask": { // Can use the FullName of the type
        "Enable": false
      }
    }
  }
}

To disable via environment variable:

Name Value
PeriodicTasks__Tasks__DatabaseCleanerTask__Enable "false"

Manual triggers

In some cases, you want to run a periodic task instead of your application. This is supported so long as your app uses the IHost pattern.

Update your configuration or environment variables to add:

Name/Key Value
PERIODIC_TASK_NAME "DatabaseCleanerTask"

Next, update your application startup:

- app.RunAsync();
+ app.RunOrExecutePeriodicTaskAsync();

A common practice is to build one application project for your AspNetCore application housing your periodic tasks, but you need to run the database cleanup task in a separate process (e.g. a Kubernetes CronJob or Azure Container App Job). Offloading longer running or greedier tasks to a separate process, or just for easier visibility on sensitive ones is a good thing.

For Azure Container App Job, your bicep file would like:

resource job 'Microsoft.App/jobs@2023-05-01' = {
  name: '...'
  properties: {
    environmentId: appEnvironment.id
    configuration: {
      triggerType: 'Schedule'
      scheduleTriggerConfig: {
        cronExpression: '10 * * * *'
      }
    }
    template: {
      containers: [
        {
          image: '...'
          name: containerName
          env: [
            { name: 'PERIODIC_TASK_NAME', value: 'DatabaseCleanerTask' }
            { name: 'EventBus__DefaultTransportWaitStarted', value: 'false' } // Ensure EventBus is available
          ]
        }
      ]
    }
  }
  // omitted for brevity
}

Samples

Issues & Comments

Please leave all comments, bugs, requests, and issues on the Issues page. We'll respond to your request ASAP!

License

The Library is licensed under the MIT license. Refer to the LICENSE file for more information.