Skip to content

Background Jobs

Hangfire-based background job processing.

Overview

flowchart TB
    subgraph Triggers
        A[Cron Schedule]
        B[API Enqueue]
    end

    subgraph Hangfire
        C[Job Queue]
        D[Job Processor]
    end

    subgraph Jobs
        E[CleanupJob]
        F[EmailSendJob]
        G[ProcessRecurringTransactionsJob]
        H[PendingUsersReminderJob]
        I[ExpireUnconfirmedRegistrationsJob]
    end

    A --> C
    B --> C
    C --> D
    D --> E
    D --> F
    D --> G
    D --> H
    D --> I

Job Base Class

All jobs extend JobBase<TJob>:

public abstract class JobBase<TJob> : IJob where TJob : JobBase<TJob>
{
    public string CronExpression =>
        _configuration.GetValue<string>($"Hangfire:JobsCron:{GetType().Name}") ?? CronNever;

    public abstract Task ExecuteAsync();

    public async Task Process(CancellationToken cancellationToken = default)
    {
        CancellationToken = cancellationToken;
        var jobName = GetType().Name;
        try
        {
            Logger.LogInformation("[{JobName}] Starting job execution", jobName);
            await ExecuteAsync();
            Logger.LogInformation("[{JobName}] Job completed successfully", jobName);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            Logger.LogWarning("[{JobName}] Job was cancelled", jobName);
            throw;
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, "[{JobName}] Job failed with error", jobName);
            throw;
        }
    }
}

Jobs

CleanupJob

Single cleanup job that handles multiple tasks:

Task Purpose
CleanupExpiredSessionsAsync Delete expired refresh tokens
EmptyTrashBinAsync Permanently delete soft-deleted markers after retention period
CleanupOrphanedFilesAsync Remove files with ProcessingStatus.Pending that exceeded upload timeout

Each task runs independently - if one fails, others continue. Failures aggregate into AggregateException.

EmailSendJob

Processes email queue from AppTask table. See Email Outbox for details.

  • Processes tasks with TaskType.EmailSend and TaskStatus.Pending
  • Priority ordering with retry logic
  • Exponential backoff on failures

ProcessRecurringTransactionsJob

Creates transactions from RecurringTransaction definitions:

  • Checks each enabled recurring transaction
  • Supports monthly, weekly, and yearly frequencies
  • Handles edge cases (last day of month, leap years)
  • Tracks LastProcessedDate to prevent duplicates

PendingUsersReminderJob

Notifies admins about users waiting for approval:

  • Finds users with RegistrationStatus.Pending and confirmed email
  • Only triggers after PendingUserNotificationDays have passed
  • Sends single email to all admins with list of pending users
  • Marks users with PendingReminderSentAt to prevent repeat notifications

ExpireUnconfirmedRegistrationsJob

Handles registration expiry workflow:

  1. Warning phase — Sends warning email to users approaching expiry with new confirmation link
  2. Expiry phase — Sets RegistrationStatus.Declined for expired registrations

Uses AppTask to queue expiry emails.

Cron Configuration

Schedules are configurable in appsettings.json under Hangfire:JobsCron. Default values:

{
  "Hangfire": {
    "JobsCron": {
      "CleanupJob": "0 23 * * *",
      "EmailSendJob": "*/2 * * * *",
      "ProcessRecurringTransactionsJob": "5 0 * * *",
      "PendingUsersReminderJob": "0 6 * * *",
      "ExpireUnconfirmedRegistrationsJob": "0 5 * * *"
    }
  }
}

Override any entry to change the schedule without code changes. Jobs without cron config default to CronNever (0 0 31 2 * — Feb 31st, never runs).

Job Registration

RecurringJob.AddOrUpdate<CleanupJob>(
    nameof(CleanupJob),
    job => job.Process(CancellationToken.None),
    job.CronExpression);

Dashboard

Hangfire dashboard available at /hangfire (admin-only).

Access controlled via JWT stored in hangfire_jwt cookie.