Cita.Core
1.0.2
dotnet add package Cita.Core --version 1.0.2
NuGet\Install-Package Cita.Core -Version 1.0.2
<PackageReference Include="Cita.Core" Version="1.0.2" />
<PackageVersion Include="Cita.Core" Version="1.0.2" />
<PackageReference Include="Cita.Core" />
paket add Cita.Core --version 1.0.2
#r "nuget: Cita.Core, 1.0.2"
#:package Cita.Core@1.0.2
#addin nuget:?package=Cita.Core&version=1.0.2
#tool nuget:?package=Cita.Core&version=1.0.2
Cita.NET
A lightweight, robust job scheduling library for .NET 9 applications. Inspired by the popular Cita job scheduler for Node.js, rebuilt natively for the .NET ecosystem with strongly-typed handlers, attribute-based definitions, and a pluggable backend architecture.
Features
- π Strongly-typed handlers β
IJobHandler<TJob>pattern with full DI support - π Flexible scheduling β Cron expressions, human-readable intervals, one-time execution
- π Automatic retries β Configurable backoff strategies (exponential, linear, fixed, conditional)
- π Distributed locking β Safe for multiple workers/instances
- π Progress tracking β Report job progress (0β100%) in real-time
- π― Job priorities β Lowest β Low β Normal β High β Highest
- π Persistent logging β TTL-managed job execution history
- π₯ Health checks β Built-in ASP.NET Core health check integration
- π REST API β Built-in management endpoints (
MapCitaApi()) - π Pluggable backends β MongoDB, Redis, & Entity Framework Core (PostgreSQL, SQL Server, SQLite), extensible via
ICitaBackend - π·οΈ Attribute-based definitions β
[JobDefinition],[Every],[Schedule]decorators - β‘ Change Streams β Real-time notifications via MongoDB Change Streams (replica set)
Installation
# Core package
dotnet add package Cita.Core
# ASP.NET Core integration (optional)
dotnet add package Cita.AspNetCore
# Backend β pick one:
dotnet add package Cita.MongoDB # MongoDB
dotnet add package Cita.Redis # Redis (StackExchange.Redis)
dotnet add package Cita.EntityFramework # EF Core (PostgreSQL, SQL Server, SQLite)
Tip:
Cita.EntityFrameworkships with the SQLite provider built-in. For PostgreSQL or SQL Server, also add the respective EF Core provider package (e.g.Npgsql.EntityFrameworkCore.PostgreSQLorMicrosoft.EntityFrameworkCore.SqlServer).
Quick Start
1. Define a Job Handler
Implement IJobHandler<TJob> to create a strongly-typed job handler with full dependency injection support:
public record SendNotificationJob(string Message);
public sealed class SendNotificationHandler(ILogger<SendNotificationHandler> logger)
: IJobHandler<SendNotificationJob>
{
public async Task HandleAsync(IJobContext<SendNotificationJob> context, CancellationToken ct)
{
logger.LogInformation("Sending: {Message}", context.Data.Message);
// Do work...
await context.TouchAsync(100, "Done", ct); // Report progress
}
}
The IJobContext<TJob> gives you access to:
context.Dataβ your deserialized job payloadcontext.JobIdβ unique job identifiercontext.JobNameβ the job namecontext.Priorityβ job priority levelcontext.FailCount/context.RetryAttemptβ retry infocontext.TouchAsync(progress, message, ct)β extend lock & report progress
2. Register and Wire Up
using Cita.AspNetCore;
using Cita.MongoDB;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCita(options =>
{
options.Name = "MyWorker";
options.ProcessEvery = TimeSpan.FromSeconds(5);
options.MaxConcurrency = 20;
})
.UseMongoDB(options =>
{
options.ConnectionString = "mongodb://localhost:27017";
options.DatabaseName = "myapp";
})
.AddJobHandler<SendNotificationJob, SendNotificationHandler>()
.AddHealthChecks();
var app = builder.Build();
app.MapCitaApi(); // REST management API at /api/cita
app.MapHealthChecks("/health");
app.Run();
3. Schedule Jobs
Use the type-safe scheduling extensions β the job name is auto-derived from the type name via kebab-case (e.g. SendNotificationJob β "send-notification-job"):
var cita = app.Services.GetRequiredService<ICita>();
// Run immediately
await cita.EnqueueAsync(new SendNotificationJob("Hello!"));
// Schedule for later (human-readable)
await cita.ScheduleAsync(new SendNotificationJob("Later!"), "in 5 minutes");
// Schedule at a specific time
await cita.ScheduleAsync(new SendNotificationJob("Timed!"), DateTimeOffset.UtcNow.AddHours(1));
// Create a recurring job
await cita.RecurringAsync("0 9 * * *", new SendNotificationJob("Daily report"));
await cita.RecurringAsync("every 30 minutes", new SendNotificationJob("Heartbeat"));
Tip: Use the
[JobName("custom-name")]attribute on a job record to override the auto-derived name.
Job Registration Patterns
Cita.NET supports two registration patterns. You can mix and match them freely.
Pattern 1: IJobHandler<TJob> (Recommended)
Register strongly-typed handlers via the builder. Handlers participate in full DI (constructor injection):
builder.Services.AddCita(options => { ... })
.AddJobHandler<SendNotificationJob, SendNotificationHandler>()
.AddJobHandler<OrderData, ProcessOrderHandler>(options =>
{
options.Concurrency = 5;
options.Priority = JobPriority.High;
options.MaxRetries = 3;
options.RemoveOnComplete = true;
});
Handler registration options (JobHandlerOptions):
| Option | Type | Default | Description |
|---|---|---|---|
Name |
string? |
auto-derived | Explicit job name (overrides kebab-case derivation) |
Concurrency |
int? |
from CitaOptions |
Max concurrent executions for this job type |
LockLimit |
int? |
0 (unlimited) | Max jobs locked at once |
LockLifetime |
TimeSpan? |
10 minutes | Lock validity duration |
Priority |
JobPriority? |
Normal | Default priority |
MaxRetries |
int? |
null | Max retry attempts |
RemoveOnComplete |
bool |
false | Remove after successful completion |
Logging |
bool? |
null | Log execution events (null = use global LoggingDefault) |
Pattern 2: Attribute-Based Controllers
Use [JobsController] and [JobDefinition] attributes for a declarative style. Recurring schedules can be specified directly via [Every] and [Schedule] attributes:
[JobsController(Namespace = "email")]
public class EmailJobs(ILogger<EmailJobs> logger)
{
[JobDefinition(Concurrency = 5, Priority = JobPriority.High)]
public async Task SendWelcomeEmail(Job<WelcomeEmailData> job, CancellationToken ct)
{
logger.LogInformation("Welcome email to {Email}", job.Data.Email);
// ...
}
[JobDefinition(Concurrency = 1)]
[Every("1 hour", Name = "cleanup-logs")]
public async Task CleanupOldLogs(Job job, CancellationToken ct)
{
// Runs every hour automatically
}
[JobDefinition]
[Schedule("0 9 * * *", Name = "daily-digest", Timezone = "America/New_York")]
public async Task SendDailyDigest(Job job, CancellationToken ct)
{
// Runs daily at 9am Eastern
}
}
public record WelcomeEmailData(string UserId, string Email, string Name);
// Register in Program.cs
builder.Services.AddCita(options => { ... })
.UseMongoDB(options => { ... })
.AddJobsController<EmailJobs>();
Available Attributes
| Attribute | Target | Description |
|---|---|---|
[JobDefinition] |
Method | Marks a method as a job handler. Properties: Concurrency, Priority, LockLifetime, MaxRetries, RemoveOnComplete, Logging, Backoff |
[JobsController] |
Class | Groups job definitions. Property: Namespace (prefix for job names) |
[Every] |
Method | Repeating schedule. Properties: Interval, Name, Timezone, Data (JSON) |
[Schedule] |
Method | Cron schedule. Properties: Cron, Name, Timezone, Data (JSON) |
[JobName] |
Class/Struct | Overrides auto-derived kebab-case name for a job data type |
Scheduling Options
Human-Readable Intervals
await cita.EveryAsync("5 minutes", "task");
await cita.EveryAsync("1 hour and 30 minutes", "task");
await cita.EveryAsync("2 days", "task");
await cita.ScheduleAsync("in 10 minutes", "task");
await cita.ScheduleAsync("tomorrow", "task");
Cron Expressions
await cita.EveryAsync("*/5 * * * *", "task"); // Every 5 minutes
await cita.EveryAsync("0 9 * * 1-5", "task"); // 9am weekdays
await cita.EveryAsync("0 0 1 * *", "task"); // First of month
await cita.EveryAsync("0 */2 * * * *", "task"); // Every 2 hours (6-field with seconds)
Fluent Job Configuration
var job = cita.Create<MyData>("process-data", new MyData { Id = 123 });
job.SetPriority(JobPriority.High)
.Schedule(DateTime.UtcNow.AddHours(1))
.SetMaxRetries(3)
.SetRemoveOnComplete(true)
.Unique(new { dataId = 123 }); // Prevent duplicates
await job.SaveAsync();
Retry & Backoff Strategies
Cita.NET includes a rich backoff strategy system:
| Strategy | Description |
|---|---|
ExponentialBackoff |
Exponential delay with configurable factor, jitter, max delay, and max retries |
FixedBackoff |
Constant delay between retries |
LinearBackoff |
Linearly increasing delay |
CompositeBackoff |
Chain multiple strategies (first match wins) |
ConditionalBackoff |
Only retry when a predicate is satisfied |
Built-in Presets
| Preset | Strategy | Details |
|---|---|---|
BackoffPresets.Normal |
Exponential | 1s β 2s β 4s β 8s β 16s (max 5 retries, 30s cap) |
BackoffPresets.Aggressive |
Exponential | 5s β 25s β 125s β 625s (max 4 retries, 10m cap) |
BackoffPresets.Patient |
Exponential | 1m β 5m β 25m β 2h β 10h (max 5 retries, 12h cap) |
BackoffPresets.Quick |
Fixed | 1s delay, max 3 retries |
BackoffPresets.None |
Fixed | No retry at all |
BackoffPresets.Forever |
Exponential | 1s initial, 2Γ factor, 1h max β retries indefinitely |
Usage with Attributes
Use the Backoff property on [JobDefinition] to select a preset by name:
[JobsController(Namespace = "tasks")]
public class TaskJobs
{
[JobDefinition(Backoff = "aggressive")]
public async Task CriticalTask(Job job, CancellationToken ct) { /* ... */ }
[JobDefinition(Backoff = "patient")]
public async Task ApiSync(Job job, CancellationToken ct) { /* ... */ }
[JobDefinition(Backoff = "none")]
public async Task FireAndForget(Job job, CancellationToken ct) { /* ... */ }
}
Available preset names: "normal", "aggressive", "patient", "quick", "none", "infinite".
Usage with Define() (Advanced)
For dynamic or custom backoff strategies, use the lower-level Define() API:
var cita = app.Services.GetRequiredService<ICita>();
// Built-in preset
cita.Define("critical-task", async (job, ct) => { /* ... */ }, def =>
{
def.Backoff = BackoffPresets.Aggressive;
});
// Custom exponential backoff
cita.Define("my-job", async (job, ct) => { /* ... */ }, def =>
{
def.Backoff = new ExponentialBackoff(
initialDelay: TimeSpan.FromSeconds(5),
factor: 2.0,
maxDelay: TimeSpan.FromMinutes(30),
maxRetries: 5,
jitter: 0.1 // Add randomness to prevent thundering herd
);
});
// Conditional retry β only retry on transient exceptions
cita.Define("selective-retry", async (job, ct) => { /* ... */ }, def =>
{
def.Backoff = BackoffPresets.OnlyOn<TransientException>(BackoffPresets.Normal);
});
Progress Tracking
// Inside a handler
public async Task HandleAsync(IJobContext<MyJob> context, CancellationToken ct)
{
for (int i = 0; i < 100; i++)
{
// Do work...
await context.TouchAsync(i, $"Processing item {i}/100", ct);
}
}
// Subscribe to progress events
cita.JobProgress += (sender, e) =>
{
Console.WriteLine($"Job {e.Job.Name}: {e.Progress}% - {e.Message}");
};
Persistent Job Logging
Cita.NET includes a built-in persistent logging system that records job execution events (start, success, fail, complete, locked) to the configured backend.
Enabling Logging
Logging is enabled globally via CitaOptions.EnableLogging (default: true). Each backend stores logs with configurable TTL (default: 14 days).
Per-Definition Control
Individual job definitions can override the global default. When Logging is null (the default), the global LoggingDefault is used:
builder.Services.AddCita(options =>
{
options.EnableLogging = true; // Master switch
options.LoggingDefault = true; // Default for definitions with Logging = null
})
.AddJobHandler<NoisyJob, NoisyJobHandler>(o => o.Logging = false) // Disable for this job
.AddJobHandler<ImportantJob, ImportantJobHandler>(o => o.Logging = true); // Always log
Log Events
| Event | Level | When |
|---|---|---|
Locked |
Debug | Job acquired by a worker |
Start |
Information | Handler execution begins |
Success |
Information | Handler completed without error |
Fail |
Error | Handler threw an exception |
Complete |
Information | After success or fail (always emitted) |
Each log entry includes metadata: workerName, processedAt, citaName, optional retryDelay, and error details on failure.
Querying Logs
var cita = app.Services.GetRequiredService<ICita>();
// Query logs with filters
var result = await cita.GetLogsAsync(new JobLogQuery
{
JobName = "send-notification-job",
Level = JobLogLevel.Error,
From = DateTimeOffset.UtcNow.AddDays(-7),
Limit = 50
});
foreach (var entry in result.Entries)
Console.WriteLine($"[{entry.Level}] {entry.Event} β {entry.Message}");
// Clear old logs
var deleted = await cita.ClearLogsAsync(new JobLogQuery
{
To = DateTimeOffset.UtcNow.AddDays(-30)
});
The REST API also exposes log endpoints:
# Query logs
curl "http://localhost:5114/api/cita/logs?jobName=send-notification-job&level=Error&limit=50"
# Clear logs
curl -X DELETE "http://localhost:5114/api/cita/logs?jobName=send-notification-job"
Cross-Backend Logging (External Logger)
Use ExternalLogger to send logs to a different backend than the one storing jobs β for example, store jobs in Redis but logs in MongoDB:
var mongoLogger = new MongoJobLogger(mongoDatabase, "jobLogs", TimeSpan.FromDays(30));
builder.Services.AddCita(options =>
{
options.EnableLogging = true;
options.ExternalLogger = mongoLogger;
})
.UseRedis("localhost:6379");
Standalone Logger Constructors
Each backend provides a public constructor for standalone use:
// MongoDB
var mongoLogger = new MongoJobLogger(database, "jobLogs", TimeSpan.FromDays(14));
// Redis
var redisLogger = new RedisJobLogger(connectionMultiplexer, keyPrefix: "cita:", database: 0);
// EF Core
var efLogger = new EfJobLogger(dbContextFactory, logsTableName: "cita_logs", schemaName: null);
Job Events
cita.Ready += (s, e) => Console.WriteLine("Cita is ready!");
cita.Error += (s, e) => Console.WriteLine($"Error: {e.Exception.Message}");
cita.JobStarted += (s, e) => Console.WriteLine($"Started: {e.Job.Name}");
cita.JobCompleted += (s, e) => Console.WriteLine($"Completed: {e.Job.Name}");
cita.JobSucceeded += (s, e) => Console.WriteLine($"Success: {e.Job.Name}");
cita.JobFailed += (s, e) => Console.WriteLine($"Failed: {e.Job.Name} - {e.Exception.Message}");
cita.JobProgress += (s, e) => Console.WriteLine($"Progress: {e.Job.Name} {e.Progress}%");
cita.JobRetry += (s, e) => Console.WriteLine($"Retrying: {e.Job.Name} (attempt {e.Attempt})");
cita.JobRetryExhausted += (s, e) => Console.WriteLine($"Gave up: {e.Job.Name}");
REST API
Map the built-in management API:
app.MapCitaApi(); // Default prefix: /api/cita
app.MapCitaApi("/my-jobs"); // Custom prefix
Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/cita/jobs |
List jobs (query params: name, skip, limit, sort) |
GET |
/api/cita/jobs/{id} |
Get a specific job by ID |
POST |
/api/cita/jobs/{name}/now |
Run a job immediately (optional JSON body for data) |
POST |
/api/cita/jobs/{name}/schedule |
Schedule a job (body: { "when": "...", "data": {} }) |
POST |
/api/cita/jobs/{name}/every |
Create a recurring job (body: { "interval": "...", "data": {} }) |
POST |
/api/cita/jobs/{id}/run |
Trigger a specific job now |
POST |
/api/cita/jobs/{id}/disable |
Disable a job |
POST |
/api/cita/jobs/{id}/enable |
Enable a job |
DELETE |
/api/cita/jobs/{id} |
Delete a job |
DELETE |
/api/cita/jobs/{name} |
Cancel/delete jobs by name |
GET |
/api/cita/definitions |
List all defined job names |
GET |
/api/cita/status |
Get worker status (name, isRunning, definition count) |
GET |
/api/cita/logs |
Query job logs (params: jobId, jobName, level, event, from, to, skip, limit) |
DELETE |
/api/cita/logs |
Clear job logs (params: jobId, jobName, from, to) |
Configuration
Cita Options
builder.Services.AddCita(options =>
{
// Worker identification
options.Name = "Worker-1"; // Default: "cita-{guid}"
// Polling interval
options.ProcessEvery = TimeSpan.FromSeconds(5); // Default: 5 seconds
// Concurrency limits
options.MaxConcurrency = 20; // Global max concurrent jobs (default: 20)
options.DefaultConcurrency = 5; // Per job-type default (default: 5)
// Lock settings
options.LockLimit = 0; // Global max locked (0 = unlimited)
options.DefaultLockLimit = 0; // Per job-type max locked (0 = unlimited)
options.DefaultLockLifetime = TimeSpan.FromMinutes(10); // Default: 10 minutes
// Behavior
options.RemoveOnComplete = false; // Keep completed jobs (default: false)
options.EnableLogging = true; // Persistent job logs (default: true)
options.LoggingDefault = true; // Per-definition default when Logging is null (default: true)
options.ExternalLogger = null; // Cross-backend IJobLogger (default: null)
options.AutoStart = true; // Start processing on app start (default: true)
});
MongoDB Options
.UseMongoDB(options =>
{
options.ConnectionString = "mongodb://localhost:27017"; // Default
options.DatabaseName = "cita"; // Default
options.JobsCollectionName = "jobs"; // Default
options.LogsCollectionName = "jobLogs"; // Default
options.EnsureIndexes = true; // Auto-create indexes (default: true)
options.EnableChangeStreams = false; // Requires replica set (default: false)
options.LogRetention = TimeSpan.FromDays(14); // TTL for log entries (default: 14 days)
})
// Or use the shorthand:
.UseMongoDB("mongodb://localhost:27017", "myapp")
Redis Options
The Cita.Redis backend stores jobs as Redis Hashes with Sets and Sorted Sets for indexing. It supports real-time notifications via Pub/Sub or a polling fallback.
using Cita.Redis;
// Full options
.UseRedis(options =>
{
options.ConnectionString = "localhost:6379"; // Default
options.KeyPrefix = "cita:"; // Default: key namespace
options.Database = 0; // Default: Redis database index
options.NotificationMode = RedisNotificationMode.PubSub; // PubSub | Polling | None
options.ChannelName = "cita:notifications"; // Default: Pub/Sub channel
options.PollInterval = TimeSpan.FromSeconds(1); // Default: polling interval
options.LogRetention = TimeSpan.FromDays(14); // Default: 14 days
options.LastModifiedBy = "worker-1"; // Default: "cita"
})
// Or use the shorthand:
.UseRedis("localhost:6379")
// Or provide an external IConnectionMultiplexer:
var mux = ConnectionMultiplexer.Connect("localhost:6379");
.UseRedis(options => { options.ConnectionMultiplexer = mux; })
| Option | Type | Default | Description |
|---|---|---|---|
ConnectionString |
string |
"localhost:6379" |
Redis connection string |
Configuration |
ConfigurationOptions? |
null |
Advanced StackExchange.Redis config (overrides ConnectionString) |
ConnectionMultiplexer |
IConnectionMultiplexer? |
null |
External connection (backend won't own it) |
KeyPrefix |
string |
"cita:" |
Prefix for all Redis keys |
Database |
int |
0 |
Redis database index |
ChannelName |
string |
"cita:notifications" |
Pub/Sub channel name |
NotificationMode |
RedisNotificationMode |
PubSub |
PubSub, Polling, or None |
PollInterval |
TimeSpan |
1 second | Polling interval (when mode is Polling) |
LogRetention |
TimeSpan |
14 days | TTL for log entries |
LastModifiedBy |
string |
"cita" |
Worker name stamped on locked jobs |
Notifications:
PubSubmode uses Redis Pub/Sub for instant job notifications across workers.Pollingmode periodically scans for updated jobs.Nonedisables notifications (rely on polling loop only).
Atomicity: The find-and-lock operation uses a Lua script for atomic job acquisition, preventing race conditions across distributed workers.
Entity Framework Core Options
The Cita.EntityFramework backend supports PostgreSQL, SQL Server, and SQLite via the same EF Core abstraction.
using Cita.EntityFramework;
// Full options
.UseEntityFramework(options =>
{
options.ConfigureDbContext = db => db.UseNpgsql("Host=localhost;Database=cita");
options.JobsTableName = "cita_jobs"; // Default
options.LogsTableName = "cita_logs"; // Default
options.SchemaName = "public"; // Default: provider default
options.UseMigrations = false; // Default: uses EnsureCreated
options.LogRetention = TimeSpan.FromDays(14); // Default: 14 days
options.EnableNotifications = false; // Polling-based notifications
options.NotificationPollInterval = TimeSpan.FromSeconds(1);
options.LastModifiedBy = "worker-1"; // Default: "cita"
})
// Or use the shorthand:
.UseEntityFramework(db => db.UseNpgsql("Host=localhost;Database=cita"))
// SQLite (great for development/testing)
.UseEntityFramework(db => db.UseSqlite("Data Source=cita.db"))
// SQL Server
.UseEntityFramework(db => db.UseSqlServer("Server=.;Database=Cita;Trusted_Connection=True"))
| Option | Type | Default | Description |
|---|---|---|---|
ConfigureDbContext |
Action<DbContextOptionsBuilder> |
β | EF provider configuration (UseNpgsql, UseSqlServer, UseSqlite) |
JobsTableName |
string |
"cita_jobs" |
Name of the jobs table |
LogsTableName |
string |
"cita_logs" |
Name of the logs table |
SchemaName |
string? |
null |
Database schema (e.g. "public", "dbo") |
UseMigrations |
bool |
false |
Use MigrateAsync() instead of EnsureCreatedAsync() |
LogRetention |
TimeSpan |
14 days | TTL for log entries |
EnableNotifications |
bool |
false |
Enable polling-based notification channel |
NotificationPollInterval |
TimeSpan |
1 second | Polling interval (when notifications enabled) |
LastModifiedBy |
string? |
"cita" |
Worker name stamped on locked jobs |
Schema creation: By default,
EnsureCreatedAsync()creates tables on first connect. For production, setUseMigrations = trueand usedotnet ef migrationswith the includedCitaDesignTimeDbContextFactory(uses SQLite by default β create your own for your target provider).
Provider detection: The dialect (PostgreSQL, SQL Server, SQLite) is auto-detected from the EF provider name. No manual configuration needed.
Health Checks
builder.Services.AddCita(options => { ... })
.UseMongoDB(options => { ... }) // or .UseEntityFramework(...)
.AddHealthChecks(); // Registers IHealthCheck
app.MapHealthChecks("/health");
The health check reports Healthy when the cita is running, Unhealthy otherwise.
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your Application β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
β β ICita β β REST API β β Health Checks β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββββββ¬βββββββββββ β
βββββββββββΌβββββββββββββββββΌβββββββββββββββββββββΌββββββββββββββ€
β β Cita.AspNetCore β β
βββββββββββΌββββββββββββββββββββββββββββββββββββββΌββββββββββββββ€
β ββββββββ΄βββββββ βββββββββββββββ ββββββββββββ΄ββββββββββ β
β βCitaService ββββJobProcessor ββββ Channel<Job> β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββββββββββββββββ β
β β β β
β ββββββββ΄βββββββββββββββββ΄βββββββββββββββββββββββββββββββββ β
β β Cita.Core β β
β β Models Β· Interfaces Β· Backoff Β· Scheduling β β
β β Attributes Β· Registration Β· Services β β
β ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ€
β ββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββ β
β β ICitaBackend (Pluggable) β β
β β βββββββββββββββββ βββββββββββββββ ββββββββββββββββ β β
β β β Cita.MongoDB β β Cita.Redis. β βCita.EF Coreβ β β β
β β β β β β β ββββββββββββ β β β
β β β β β Pub/Sub β β β PG ββ SQLβ β β β
β β β β β Lua Scriptsβ β ββββββββββββ β β β
β β β β β β β ββββββββ β β β
β β β β β β β βSQLiteβ β β β
β β βββββββββββββββββ βββββββββββββββ ββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Creating Custom Backends
Implement ICitaBackend, IJobRepository, and optionally IJobLogger / INotificationChannel:
public class MyCustomBackend : ICitaBackend
{
public string Name => "MyBackend";
public IJobRepository Repository { get; }
public INotificationChannel? NotificationChannel { get; }
public IJobLogger? Logger { get; }
public bool OwnsConnection => true;
public Task ConnectAsync(CancellationToken ct) { /* ... */ }
public Task DisconnectAsync(CancellationToken ct) { /* ... */ }
}
Register your backend:
builder.Services.AddCita(options => { ... })
.UseBackend(sp => new MyCustomBackend(/* ... */));
Development
Prerequisites
- .NET 9 SDK
- Docker (required for MongoDB integration tests via Testcontainers)
Repository Structure
Cita.sln
βββ src/
β βββ Cita.Core/ # Core abstractions, models, scheduling, backoff, attributes
β βββ Cita.AspNetCore/ # ASP.NET Core integration (DI, REST API, health checks)
β βββ Cita.MongoDB/ # MongoDB backend implementation
β βββ Cita.Redis/ # Redis backend (StackExchange.Redis, Lua scripts, Pub/Sub)
β βββ Cita.EntityFramework/ # EF Core backend (PostgreSQL, SQL Server, SQLite)
βββ tests/
β βββ Cita.Core.Tests/ # Unit tests for core logic (~324 tests)
β βββ Cita.MongoDB.Tests/ # Integration tests with Testcontainers (~130 tests)
β βββ Cita.Redis.Tests/ # Redis integration tests with Testcontainers (~56 tests)
β βββ Cita.EntityFramework.Tests/ # EF Core integration tests (~50 tests, SQLite in-memory)
βββ example/
βββ Cita.Sample/ # Sample ASP.NET Core application
Building
cd packages/cita-dotnet
# Restore & build the entire solution
dotnet build
# Build a specific project
dotnet build src/Cita.Core
Running Tests
# Run all tests
dotnet test
# Run only core unit tests (fast, no Docker required)
dotnet test tests/Cita.Core.Tests
# Run MongoDB integration tests (requires Docker)
dotnet test tests/Cita.MongoDB.Tests
# Run Redis integration tests (requires Docker)
dotnet test tests/Cita.Redis.Tests
# Run EF Core integration tests (SQLite in-memory, no Docker required)
dotnet test tests/Cita.EntityFramework.Tests
# Detailed per-test output
dotnet test --logger "console;verbosity=detailed"
# Run a specific test class
dotnet test --filter "FullyQualifiedName~MongoJobRepositoryTests"
dotnet test --filter "FullyQualifiedName~EfJobRepositoryTests"
# Run a specific test method
dotnet test --filter "FullyQualifiedName~SaveJobAsync_NewJob_AssignsId"
Note: The MongoDB and Redis integration tests use Testcontainers to automatically spin up containers (MongoDB 7.0 / Redis 7 Alpine). Make sure Docker Desktop is running before executing them. The EF Core tests use SQLite in-memory databases and require no external dependencies.
Running the Sample App
The example/Cita.Sample project is a complete ASP.NET Core application that demonstrates both IJobHandler<TJob> registrations and attribute-based controllers. It supports multiple backends via a --backend argument.
It includes:
SendNotificationHandlerβ a simple handler forSendNotificationJobProcessOrderHandlerβ handler with progress reporting (25% β 50% β 75% β 100%)EmailJobsβ attribute-based controller with[Every]and[Schedule]definitions
1. Choose a Backend
Option A: MongoDB (default)
docker run -d --name cita-mongo -p 27017:27017 mongo:7.0
Or set a custom connection string:
export ConnectionStrings__MongoDB="mongodb://localhost:27017"
Option B: SQLite (no external dependencies)
cd example/Cita.Sample
dotnet run -- --backend sqlite
This creates an cita.db file in the project directory β perfect for local development.
Option C: PostgreSQL
docker run -d --name cita-pg -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=cita postgres:16
cd example/Cita.Sample
dotnet run -- --backend postgres
Or set a custom connection string:
export ConnectionStrings__PostgreSQL="Host=localhost;Database=cita;Username=postgres;Password=postgres"
Option D: SQL Server
cd example/Cita.Sample
dotnet run -- --backend sqlserver
Set the connection string:
export ConnectionStrings__SqlServer="Server=localhost;Database=Cita;Trusted_Connection=True;TrustServerCertificate=True"
Option E: Redis
docker run -d --name cita-redis -p 6379:6379 redis:7-alpine
cd example/Cita.Sample
dotnet run -- --backend redis
Or set a custom connection string:
export ConnectionStrings__Redis="localhost:6379"
2. Run the App
cd example/Cita.Sample
# MongoDB (default)
dotnet run
# SQLite (zero-config)
dotnet run -- --backend sqlite
# PostgreSQL
dotnet run -- --backend postgres
# Redis
dotnet run -- --backend redis
The app starts at http://localhost:5114. You'll see event logs in the console as jobs are processed:
[EVENT] Job started: send-notification-job (abc123)
[EVENT] Job succeeded: send-notification-job (abc123)
3. Try the Demo Endpoints
Enqueue a job immediately:
curl -X POST "http://localhost:5114/demo/notification?message=Hello"
# β { "jobId": "...", "message": "Notification job scheduled" }
Process an order (with progress tracking):
curl -X POST http://localhost:5114/demo/order \
-H "Content-Type: application/json" \
-d '{"orderId": "ORD-001", "amount": 99.99, "customerId": "CUST-1"}'
# β { "jobId": "...", "message": "Order processing job scheduled" }
Schedule for later (human-readable):
curl -X POST "http://localhost:5114/demo/schedule?when=in+5+minutes&message=Later"
# β { "jobId": "...", "scheduledFor": "2026-02-06T12:05:00Z" }
Create a recurring job:
curl -X POST "http://localhost:5114/demo/recurring?interval=every+30+seconds&name=heartbeat"
# β { "jobId": "...", "interval": "every 30 seconds", "nextRun": "..." }
4. Explore the Management API
# Worker status
curl http://localhost:5114/api/cita/status
# β { "name": "SampleWorker", "isRunning": true, ... }
# List all jobs
curl http://localhost:5114/api/cita/jobs
# List registered job definitions
curl http://localhost:5114/api/cita/definitions
# Disable a job
curl -X POST http://localhost:5114/api/cita/jobs/{id}/disable
# Health check
curl http://localhost:5114/health
# β Healthy
5. OpenAPI / Swagger
The sample also exposes an OpenAPI spec in development mode:
curl http://localhost:5114/openapi/v1.json
Testing Frameworks
| Package | Purpose |
|---|---|
| xUnit | Test framework |
| FluentAssertions | Assertion library |
| NSubstitute | Mocking framework |
| Testcontainers | Docker-based integration tests |
Key Interfaces for Contributors
| Interface | Package | Purpose |
|---|---|---|
ICita |
Core | Main scheduling API |
ICitaBackend |
Core | Backend abstraction (connect, disconnect, expose repo/logger/channel) |
IJobRepository |
Core | CRUD + locking operations for jobs |
IJobLogger |
Core | Persistent job execution log |
INotificationChannel |
Core | Real-time job change notifications |
IJobHandler<TJob> |
Core | Strongly-typed job handler |
EfCitaBackend |
EntityFramework | EF Core backend (Postgres, SQL Server, SQLite) |
IDatabaseDialect |
EntityFramework | Provider-specific SQL abstraction (internal) |
RedisCitaBackend |
Redis | Redis backend (Hashes, Sorted Sets, Lua scripts) |
RedisJobRepository |
Redis | IJobRepository via Redis Hashes + Sets + Sorted Sets |
RedisPubSubNotificationChannel |
Redis | Real-time notifications via Redis Pub/Sub |
License
MIT License β see LICENSE for details.
Contributing
Contributions are welcome! Please read our Contributing Guide first.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Cronos (>= 0.11.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
- Microsoft.Extensions.Options (>= 10.0.3)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Cita.Core:
| Package | Downloads |
|---|---|
|
Cita.AspNetCore
ASP.NET Core integration for Cita job scheduler |
|
|
Cita.MongoDB
MongoDB backend for Cita job scheduler |
|
|
Cita.EntityFramework
Package Description |
|
|
Cita.Redis
Redis backend for Cita job scheduler |
GitHub repositories
This package is not used by any popular GitHub repositories.