Skip to content
bitzorcas
EN

Reference

Jobs building block

Quartz.NET background job scheduling — independent JobHost process, Cron-based scheduling, audit retention cleanup, and multi-instance safety.

Last updated

BitzOrcas uses Quartz.NET for background job scheduling, running in a separate JobHost process to keep the API Host AOT-compatible and trim-safe.

Architecture

┌───────────────────────┐ ┌───────────────────────┐
│ API Host │ │ JobHost │
│ (trimmed, AOT-safe) │ │ (untrimmed Worker) │
│ │ │ │
│ No Quartz reference │ │ Quartz.NET │
│ No CAP consumer │ │ SqlSugar audit store │
│ No background jobs │ │ CAP consumers │
└───────────┬───────────┘ └───────────┬───────────┘
│ │
└─────────────┬───────────────┘
┌─────────────┐
│ SQL Server │
│ (shared) │
└─────────────┘

JobHost composition

The JobHost (BitzOrcas.JobHost) is a minimal .NET Worker service:

var builder = Host.CreateApplicationBuilder(args);
// Clock + Audit pipeline
builder.Services.AddSingleton<IAppClock, SystemClock>();
builder.Services.AddBitzOrcasAuditDefaults();
builder.Services.AddBitzOrcasAuditPipeline();
// SqlSugar + Audit store (if connection string configured)
builder.Services.AddBitzOrcasSqlSugar(opt => opt.ConnectionString = connectionString);
builder.Services.AddBitzOrcasSqlSugarAuditStore();
// Quartz scheduling
builder.Services.AddQuartz(q =>
{
var retentionOptions = new AuditPersistenceOptions();
builder.Configuration.GetSection("Audit").Bind(retentionOptions);
if (retentionOptions.Retention.Enabled)
{
var jobKey = new JobKey("audit-retention");
q.AddJob<AuditRetentionQuartzJob>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithCronSchedule(retentionOptions.Retention.CronExpression));
}
});
builder.Services.AddQuartzHostedService(opt => opt.WaitForJobsToComplete = true);
// OpenTelemetry + Health Checks (shared with API Host)
builder.AddServiceDefaults();

Registered jobs

JobTriggerPurpose
AuditRetentionQuartzJobCron (configurable)Periodic cleanup of audit log records per retention policy

Audit retention configuration

{
"Audit": {
"Retention": {
"Enabled": true,
"CronExpression": "0 0 2 * * ?", // Daily at 2 AM
"MaxAgeDays": 90
}
}
}

Job identity in tenant context

The JobHost has no HTTP context, so it uses AnonymousCurrentUser by default. For jobs that need tenant-scoped operations:

// Use ICurrentUserAccessor.BeginScope to temporarily set a user context
await currentUserAccessor.BeginScopeAsync(userId, tenantId, async () =>
{
// Job code runs as the specified user/tenant
await auditStore.CleanupAsync(tenantId, cutoffDate);
});

Adding a new job

  1. Create a class implementing IJob in BitzOrcas.JobHost
  2. Register it in Program.cs with AddJob<T>() and AddTrigger()
  3. Add Cron schedule to configuration
  4. Job has full access to SqlSugar repositories and the audit pipeline

ServiceDefaults shared infrastructure

Both API Host and JobHost share the same BitzOrcas.ServiceDefaults:

  • OpenTelemetry — traces + metrics via OTLP exporter
  • Health Checks — liveness/readiness probes
  • Resource attributes — service.name, service.version, deployment.environment

Assembly reference

BitzOrcas.JobHost
├── Program.cs → Host setup + Quartz registration
├── AuditRetentionQuartzJob.cs → Audit retention cleanup job
└── BitzOrcas.JobHost.csproj → Worker SDK + Quartz packages
BitzOrcas.ServiceDefaults
├── Extensions/ServiceDefaultsExtensions.cs → AddServiceDefaults()
└── Extensions/BitzOrcasActivitySources.cs → Custom ActivitySource names