Quartz.NETQuartz.NET
Home
Features
Blog
Discussions
NuGet
GitHub
Home
Features
Blog
Discussions
NuGet
GitHub
  • Getting Started

    • Overview
    • Quartz 4 Quick Start
    • Tutorial
      • Using Quartz
      • Jobs And Triggers
      • More About Jobs & JobDetails
      • Job Data
      • More About Triggers
      • Querying Jobs and Triggers
      • Simple Triggers
      • Cron Triggers
      • RecurrenceTrigger
      • Time and TimeProvider
      • Trigger and Job Listeners
      • Scheduler Listeners
      • Job Execution Middleware
      • Job Stores
      • Configuration, Resource Usage and Building a Scheduler
      • Building a Scheduler Without a Host
      • Clustering
      • Execution Groups
      • Node Affinity (Preferred Node)
      • Testing
      • Compile-Time Checks
      • Declaring Jobs with Attributes
      • Delegate Jobs
    • Configuration Reference
    • JSON Configuration
    • Cron Expression Reference
    • Multi-Tenancy
    • Comparison
    • Frequently Asked Questions
    • Best Practices
    • Before You Go Live
    • Operating a Cluster
    • Log Events
    • Tenancy Patterns
    • Database Schema
    • Database Schema Changes
    • Migration Guide
    • Troubleshooting
    • API Documentation
  • How To's
    • One-Off Job
    • Rescheduling Jobs
    • Backfill
    • Retrying Failed Jobs
    • Pausing with a Reason
    • Job Continuations
    • Overlap Policy
    • Progress and Execution Logs
    • Job Outcomes
    • Multiple Triggers
    • Job Template
    • Running Quartz under Aspire
    • Quartz.NET with Wolverine
    • Coming from Hangfire
    • Coming from TickerQ
    • Embedding Quartz in a Library
    • Running under an External Leader Election
    • Publishing Trimmed and Native AOT
    • Extending Quartz: what is open, what is closed, and how to ask
    • A Job Store of Your Own
    • A Driver Delegate for a New Database
    • Persisting a Custom Trigger Type
    • A Lock Handler of Your Own
  • Packages

    • Quartz Core Additions

      • Jobs
      • Serialization (System.Text.Json)
      • JSON Serialization
      • Plugins
    • Integrations

      • Aspire Integration
      • ASP.NET Core Integration
      • HTTP API
      • HTTP Client
      • Dashboard
      • Dashboard Agent
      • Hosted Services Integration
      • Microsoft DI Integration
      • Multiple Schedulers with Microsoft DI
      • Observability
      • Redis Lock Handler
      • TimeZoneConverter Integration
      • Weasel Schema Management
    • 3rd Party Plugins for Quartz
  • Quartz 3.x

    • Getting Started

      • Quartz 3 Quick Start
      • Tutorial
        • Using Quartz
        • Library Overview
        • Jobs And Triggers
        • More About Jobs
        • More About Triggers
        • Execution Groups
        • Node Affinity (Preferred Node)
        • Simple Triggers
        • Cron Triggers
        • RecurrenceTrigger
        • Trigger and Job Listeners
        • Scheduler Listeners
        • Job Stores
        • Tuning the Scheduler
        • Configuration, Resource Usage and SchedulerFactory
        • Advanced (Enterprise) Features
      • Configuration Reference
      • JSON Configuration
      • Multi-Tenancy
      • Frequently Asked Questions
      • Best Practices
      • Tenancy Patterns
      • Troubleshooting
      • API Documentation
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Miscellaneous Features
    • How To's

      • One-Off Job
      • Multiple Triggers
      • Job Template
      • Using the CronTrigger
      • Rescheduling Jobs
    • Packages

      • Quartz Core Additions

        • Dashboard
        • Jobs
        • Serialization (System.Text.Json)
        • Serialization (Newtonsoft Json.NET)
        • Plugins
      • Integrations

        • ASP.NET Core Integration
        • Hosted Services Integration
        • Microsoft DI Integration
        • Multiple Schedulers with Microsoft DI
        • OpenTelemetry Integration
        • OpenTracing Integration
        • Redis Lock Handler
        • TimeZoneConverter Integration
      • 3rd Party Plugins for Quartz
  • Old Releases

    • Quartz 2.x
      • Quartz 2 Quick Start
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
        • CronTrigger Tutorial
      • Configuration Reference
      • Migration Guide
      • API Documentation
    • Quartz 1.x
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
      • API Documentation
  • License

Aspire Integration

Quartz.Aspire is the Quartz.NET client integration for Aspire. One call turns an Aspire connection name into a persistent job store:

builder.AddQuartzPersistentStore("quartz");
builder.AddQuartz();
builder.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);

The call:

  1. reads the connection string the AppHost injected under quartz;
  2. infers the database and chooses its driver delegate;
  3. takes connections from a DbDataSource already in the container, if there is one;
  4. registers the scheduler's health check;
  5. adds Quartz's activity source and meter to the OpenTelemetry pipeline AddServiceDefaults() built.

The recipe (the AppHost, the dashboard, and each step written out by hand) is Running Quartz under Aspire. Every store setting is in Job Stores and the Configuration Reference.

Tips

Quartz 4.0 or later required. Documented against Aspire 13.5.

Installation

dotnet add package Quartz.Aspire

The application still references the ADO.NET driver for its database, as without Aspire: Npgsql for PostgreSQL, Microsoft.Data.SqlClient for SQL Server, and so on (the full table). An Aspire client integration such as Aspire.Npgsql brings its driver; a worker that only reads a connection string from configuration needs it added:

dotnet add package Npgsql

The package has no Aspire.* package reference; IHostApplicationBuilder is its whole contract. It is not tied to Aspire's release cadence and works in any generic-host application with a ConnectionStrings: entry. Aspire.Npgsql 13.5.3 has no Aspire.* dependency either.

The call

public static IHostApplicationBuilder AddQuartzPersistentStore(
    this IHostApplicationBuilder builder,
    string connectionName,
    Action<QuartzAspireSettings>? configureSettings = null);

connectionName is the AppHost's name for the database resource. The connection string arrives under ConnectionStrings:<connectionName>, and a keyed DbDataSource would use it as its service key.

  • Additive and order-independent. The store is contributed through ConfigureAllQuartzSchedulers, so the call can come before or after AddQuartz. AddQuartz still reads the Quartz configuration section and configures the scheduler.
  • One ordering matters: call a client integration that registers a DbDataSource first. The connection source is decided at this call, against the service collection as it is then.

Settings

QuartzAspireSettings covers only what follows from an Aspire connection. Quartz:Scheduler, Quartz:JobStore and the rest still apply through AddQuartz.

SettingTypeDefaultWhat it decides
ConnectionStringstring?ConnectionStrings:<name>The connection string, when not the one Aspire injected
Providerstring?inferred from the connection stringWhich ADO.NET driver reaches the database
SchedulerNamestring?every scheduler in the containerWhich scheduler this store belongs to
TablePrefixstring?whatever AdoJobStoreOptions.TablePrefix already hadPrefix on the Quartz table names
SchemaProvisioningSchemaProvisioning?unset: creates under Development, nothing elsewhereWhat the store does about its schema at start
ClusteredboolfalseJoin a cluster on the database, deriving an instance id
DisableHealthChecksboolfalseLeave the scheduler's health check unregistered
DisableTracingboolfalseLeave the Quartz activity source unsubscribed
DisableMetricsboolfalseLeave the Quartz meter unsubscribed
  • The flags are Disable* because Aspire requires a fresh settings instance (what an absent section binds to) to hold the recommended values, and an unbound bool is false. First-party integrations have used this spelling since Aspire 8.0.
  • SchemaProvisioning is nullable because the recommended value depends on the environment. Unset means "ask the environment". It is the same enum as AdoJobStoreOptions.SchemaProvisioning.
  • No setting undoes existing configuration. TablePrefix unset keeps what Quartz:JobStore:TablePrefix or an earlier ConfigureStore set. Clustered = false does not un-cluster a scheduler that Quartz:JobStore:Clustering:Enabled or UseClustering() already clustered.

Where the settings come from

Each source overrides the one before:

  1. Aspire:Quartz: every Quartz connection in the application.
  2. Aspire:Quartz:<connectionName>: that connection only.
  3. ConnectionStrings:<connectionName>: supplies ConnectionString, when present.
  4. The configureSettings callback.

Every first-party client integration uses this order. ConnectionStrings:<name> is what the AppHost's WithReference injected, so it beats a stale ConnectionString in appsettings.json.

With one database, only the outer section is needed:

{
  "Aspire": {
    "Quartz": {
      "Provider": "Npgsql",
      "Clustered": true
    }
  }
}

With two, the inner section wins where they overlap:

{
  "Aspire": {
    "Quartz": {
      "Clustered": true,
      "TablePrefix": "QRTZ_",
      "orders-db": { "SchedulerName": "orders" },
      "billing-db": { "SchedulerName": "billing", "TablePrefix": "BILLING_QRTZ_" }
    }
  }
}

Both stores are clustered; only billing reads BILLING_QRTZ_ tables.

Code overrides both:

builder.AddQuartzPersistentStore("quartz", settings =>
{
    settings.Provider = DataSourceOptions.Providers.Npgsql;
    settings.Clustered = true;
});

The package ships a ConfigurationSchema.json at its root, wired up by a buildTransitive targets file, so an IDE completes and validates the Aspire section of appsettings.json. It is hand-written and held to the settings type by a test, because Aspire's generator for it (microsoft/aspire#3309) has never shipped.

Which database the connection string is for

Unset, Provider is inferred from the connection string's keywords. They are parsed with DbConnectionStringBuilder (never substring-matched) and compared without spaces and underscores, so User ID, userid and User_Id are one keyword.

ProviderInferred when the connection string
Npgsqlhas Host and Database, and no Uid
SqlServerhas a server keyword¹ and one of Initial Catalog, Database, Integrated Security, Trusted_Connection; and no Port or Host
MySqlConnectorhas a server keyword¹ but not Host, plus Port and one of Uid, User Id, Username, User
SQLite-Microsofthas Data Source of :memory: or ending in .db, .db3, .sqlite, .sqlite3; or has Mode=Memory
OracleODPManagedhas Data Source holding a TNS descriptor or EZ-connect host:port/service, and no Host or Port

¹ Server, Data Source, Address, Addr or Network Address. Microsoft.Data.SqlClient accepts neither Port nor Host.

An ambiguous or unrecognised string throws

Zero or two matches throw a SchedulerConfigException at startup, naming QuartzAspireSettings.Provider and DataSourceOptions.Providers. A guess would instead start a scheduler that issues SQL the database cannot run, found only at the first trigger acquisition.

  • Three of the eight shipped provider names are never inferred. MySql and SQLite accept the same strings as MySqlConnector and SQLite-Microsoft, and a Firebird string looks like several others. Name them in Provider.
  • A provider name Quartz has no description for goes to UseGenericDatabase: the generic SQL dialect, with the description from the application's registered DbMetadataFactory. See A Driver Delegate for a New Database.
  • Not recognised (set Provider): a MySQL string written with Host= and Username=, and a bare Oracle TNS alias (Data Source=orcl), which looks like a SQL Server instance name.

Where connections come from

The package sets one connection setting on DataSourceOptions. The first matching row wins:

ConditionThe store is configured with
The provider is SqlServerConnectionString and ConnectionStringName (no probe)
A keyed DbDataSource under connectionNameDataSourceServiceKey = connectionName
An unkeyed DbDataSourceUseRegisteredDataSource = true
Anything elseConnectionString and ConnectionStringName

builder.AddKeyedNpgsqlDataSource("quartz") gives the second row and builder.AddNpgsqlDataSource("quartz") the third. Both register a singleton System.Data.Common.DbDataSource, the service type probed for. A keyed source is used because two databases cannot both be the container's one unkeyed data source.

  • A data source is preferred: commands are made by its connection, so its type mappers, logging and connection multiplexing apply to Quartz's statements.
  • The connection-string path names the driver's connection, command and parameter types as strings, which is why AddQuartzPersistentStore carries [RequiresUnreferencedCode].
  • SQL Server never uses a data source. Microsoft.Data.SqlClient has no DbDataSource implementation, and Aspire's SQL Server integration registers a scoped SqlConnection. A probe would find another database's data source, or none, and fail at first use.

What happens to the schema

SchemaProvisioningAdoJobStoreOptions.SchemaProvisioning becomes
unset (the default)CreateIfMissing under Development; untouched elsewhere
CreateIfMissingCreateIfMissing, in any environment
ValidateValidate, in any environment (already the default, so nothing is set)
NoneNone, in any environment; the startup check is skipped
  • builder.Environment is read at the AddQuartzPersistentStore call, not at scheduler start.
  • Why by environment: an AppHost's database container starts empty with a new volume, while a production account usually has no DDL permission.
  • A SchemaProvisioning the application set (through ConfigureStore or Quartz:JobStore:SchemaProvisioning) is left alone. The exception is Validate, which cannot be told apart from the default: in Development, set SchemaProvisioning = SchemaProvisioning.Validate here instead.

What provisioning runs, why it is safe when a cluster starts at once, and the two configurations that cannot provision or would provision the wrong schema are in Creating the schema. The production recipe is in Running Quartz under Aspire.

Clustering

Clustered = true calls UseClustering(), which also turns on database locking, and makes the scheduler derive its InstanceId.

Cluster nodes recognise their own check-in row and fired triggers by InstanceId. Every scheduler starts with QuartzSchedulerOptions.DefaultInstanceId, which is NON_CLUSTERED, and an Aspire replica set (WithReplicas(2)) gives replicas no identity of their own. Without a derived id, all nodes would share one, the worst failure a cluster can have.

It never overrides: an application that set GenerateInstanceId, or set InstanceId in code or in Quartz:Scheduler:InstanceId, keeps it.

Health and telemetry

  • Health check: IQuartzBuilder.AddQuartzHealthChecks() from the core Quartz package, on the same IHealthChecksBuilder as ServiceDefaults' self check, so MapDefaultEndpoints() serves both. One check per scheduler, each under its own name. What it reports and what survives an HTTP probe: the how-to's Health section.
  • Telemetry: AddSource("Quartz") and AddMeter("Quartz") on the existing AddOpenTelemetry() builder.
  • No exporter is added. AddServiceDefaults() calls UseOtlpExporter() when OTEL_EXPORTER_OTLP_ENDPOINT is set (the AppHost sets it). UseOtlpExporter may be called only once and cannot be combined with a signal-specific AddOtlpExporter(); either mistake throws NotSupportedException. Aspire also rules exporters out of a client integration's scope.

Turn any of the three off:

builder.AddQuartzPersistentStore("quartz", settings =>
{
    settings.DisableTracing = true;
    settings.DisableMetrics = true;
    settings.DisableHealthChecks = true;
});

Observability lists every span, instrument and attribute.

More than one scheduler

Unset, SchedulerName gives the store to every scheduler in the container. With two schedulers on two databases, set it to the name AddQuartz(name, …) registered:

builder.AddQuartz("orders");
builder.AddQuartz("billing");

builder.AddQuartzPersistentStore("orders-db", settings => settings.SchedulerName = "orders");
builder.AddQuartzPersistentStore("billing-db", settings => settings.SchedulerName = "billing");

Named schedulers and how their parts are keyed: Multiple Schedulers.

What this package deliberately does not do

  • No Quartz.Aspire.Hosting. A hosting integration adds AppHost resources (an AddQuartz() resource, a WithQuartzDashboard()). Quartz runs inside an existing project resource, so there is nothing to orchestrate; the AppHost already declares the database and passes it on.
  • No AddKeyedQuartzPersistentStore. A second database goes to a second, named scheduler, selected with SchedulerName. Aspire's guidance makes a keyed form "consider, if applicable", not a requirement.
  • No health-check status codes. HealthCheckOptions.ResultStatusCodes is an ASP.NET Core type and the application's decision. The how-to explains why the default mapping loses a standby scheduler, and what to write instead.
  • No tables in production. The store creates its schema under Development and validates it elsewhere. It never migrates a schema, because nothing in a Quartz schema records its version. The how-to has the migration-service recipe for production.
  • No OpenTelemetry exporter, for the reason above.

Aspire's contributor guidance asks client integrations to support every supported .NET version at the Aspire release, which for 13.x means net8.0. Quartz.Aspire targets net10.0 only, like every Quartz 4.0 package.

AddQuartzHostedService, and the health check outside Aspire, are in Hosted Services Integration.

Help us by improving this page!
Last Updated: 10/16/26, 2:35 AM
Contributors: Marko Lahma, Claude Opus 5.5
Next
ASP.NET Core Integration