Quartz.NETQuartz.NET
Home
Features
Discussions
NuGet
GitHub
Home
Features
Discussions
NuGet
GitHub
  • 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
  • Unreleased Releases

    • Quartz 4.x
      • 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
      • Configuration Reference
      • JSON Configuration
      • Cron Expression Reference
      • Multi-Tenancy
      • Frequently Asked Questions
      • Best Practices
      • Operating a Cluster
      • Tenancy Patterns
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Troubleshooting
      • API Documentation
      • How To's
        • One-Off Job
        • Rescheduling Jobs
        • Retrying Failed Jobs
        • Multiple Triggers
        • Job Template
        • Running Quartz under Aspire
        • Quartz.NET with Wolverine
        • Embedding Quartz in a Library
        • Running under an External Leader Election
        • Publishing Trimmed and Native AOT
        • 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
          • Hosted Services Integration
          • Microsoft DI Integration
          • Multiple Schedulers with Microsoft DI
          • Observability
          • 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. It turns an Aspire connection name into a persistent job store, in one call:

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

That reads the connection string the AppHost injected under quartz, works out which database it is for, chooses the driver delegate that speaks that database's SQL, takes connections from a DbDataSource the container already holds, registers the scheduler's health check, and names Quartz's activity source and meter to the OpenTelemetry pipeline AddServiceDefaults() built.

This page is the reference: every setting, where it is read from, and every rule the package decides something by. Running Quartz under Aspire is the recipe — the AppHost beside this worker, what the dashboard shows, and how to write any part of this call out by hand instead.

Tips

Quartz 4.0 or later required. Documented against Aspire 13.5.

Installation

dotnet add package Quartz.Aspire

The package takes no Aspire.* package reference. IHostApplicationBuilder is the whole of its contract, so it is not tied to Aspire's release cadence and works in any generic-host application that has a ConnectionStrings: entry — an AppHost is where the string usually comes from, not something the package requires. That is the ordinary shape for a client integration rather than a peculiarity of this one: Aspire.Npgsql 13.5.3, the first-party integration this package sits beside, has no Aspire.* dependency either.

The call

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

connectionName is the name the AppHost gave the database resource. It is the name its connection string arrives under (ConnectionStrings:<connectionName>), and the service key a keyed DbDataSource would be registered with — so naming the resource once in the AppHost names it everywhere.

The call is additive and order-independent. The store is contributed through ConfigureAllQuartzSchedulers, so it may be written before or after AddQuartz and the container comes out the same. AddQuartz still reads the Quartz configuration section and still configures the scheduler; nothing here replaces it. What this call decides is only what an Aspire connection is evidence of.

One ordering does matter, and only one: a client integration that registers a DbDataSource has to be called first. Where connections come from is decided at this call site, against the service collection as it stands, rather than inside the per-scheduler callback — because when that callback runs depends on whether AddQuartz has already been called, which is exactly the ordering everything else here is indifferent to.

Settings

QuartzAspireSettings is deliberately small. It is not a second spelling of Quartz's own configuration: Quartz:Scheduler, Quartz:JobStore and the rest still say what they always said, and AddQuartz still reads them. What these settings decide is the handful of things that follow from an Aspire connection.

SettingTypeDefaultWhat it decides
ConnectionStringstring?ConnectionStrings:<name>The connection string, when it is 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 hadThe prefix on the Quartz table names
ProvisionSchemabool?unset — creates under Development, validates everywhere elseWhether the store creates whatever its schema is missing as it starts
ClusteredboolfalseWhether this scheduler joins a cluster on the database, deriving an instance id to do it with
DisableHealthChecksboolfalseLeaves AddQuartzHealthChecks() unregistered
DisableTracingboolfalseLeaves the Quartz activity source unsubscribed
DisableMetricsboolfalseLeaves the Quartz meter unsubscribed

The three flags are spelled Disable* rather than Enable* because Aspire's rule for a settings type is that a fresh instance — which is what binding an absent section produces — must already hold the recommended values. A bool bound from nothing is false, so the useful default has to be the false one. Every first-party integration has been spelled this way since Aspire 8.0.

ProvisionSchema is the one bool?, for the same rule read the other way: the recommended value is not the same in every environment, so no bool could hold it. Unset is a third answer rather than a missing one — ask the environment — and true or false is how an application that knows better says so.

Every setting here says something or says nothing; none of them says no. TablePrefix left unset keeps whatever Quartz:JobStore:TablePrefix or an earlier ConfigureStore said, rather than resetting it, and Clustered = false means "this call is not what makes it a cluster" rather than "un-cluster it" — a scheduler that Quartz:JobStore:Clustering:Enabled or a UseClustering() call already clustered stays clustered.

Where the settings come from

Four sources, each more specific than the one before it:

  1. Aspire:Quartz — what is true of every Quartz connection in the application.
  2. Aspire:Quartz:<connectionName> — bound over it, for that connection alone.
  3. ConnectionStrings:<connectionName> — supplies ConnectionString when there is one.
  4. The configureSettings callback, which has the last word.

This is the order every first-party client integration uses, and step 3 sitting where it does is deliberate: ConnectionStrings:<name> is what the AppHost's WithReference actually injected, so a stale ConnectionString left in an appsettings.json should not beat it.

An application with a single database never writes the inner section:

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

An application with two writes both, and the inner one 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; the billing one alone reads BILLING_QRTZ_ tables.

Code beats 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 in appsettings.json. That file is written by hand and held to the settings type by a test: the generator Aspire uses for its own integrations (microsoft/aspire#3309) has never shipped.

Which database the connection string is for

Left unset, Provider is inferred from the connection string's keywords — parsed with DbConnectionStringBuilder, never substring-matched, and compared with spaces and underscores removed so that User ID, userid and User_Id are one keyword.

ProviderInferred when the connection string
Npgsqlhas Host and Database, and no Uid
SqlServerhas Server, Data Source, Address, Addr or Network Address, together with Initial Catalog, Database, Integrated Security or Trusted_Connection — and has no Port and no Host, neither of which Microsoft.Data.SqlClient accepts
MySqlConnectorhas Server, Data Source, Address, Addr or Network Address but not Host, together with Port and one of Uid, User Id, Username or User
SQLite-Microsofthas Data Source whose value is :memory: or ends in .db, .db3, .sqlite or .sqlite3, or has Mode=Memory
OracleODPManagedhas Data Source holding a TNS descriptor or an EZ-connect host:port/service string, and no Host or Port

An ambiguous or unrecognised string throws

Zero matches and two matches are both a SchedulerConfigException at startup, naming QuartzAspireSettings.Provider and DataSourceOptions.Providers. The failure a guess produces instead is a scheduler that starts, connects, and then issues SQL the database cannot run — discovered at the first trigger acquisition rather than at startup, and not obviously about the connection string when it is.

Three of the eight shipped provider names are never inferred, because nothing in a connection string distinguishes them: MySql and SQLite accept the same strings as MySqlConnector and SQLite-Microsoft above them, so which of each pair to use is the application's choice rather than the string's, and a Firebird string looks like several of the others. Naming one in Provider is how an application chooses.

A name Quartz ships no description for is still usable. It reaches UseGenericDatabase, which selects the generic SQL dialect and leaves the description to whatever DbMetadataFactory the application registered — so a driver Quartz has never heard of is a configuration this supports rather than an error it reports. A Driver Delegate for a New Database is the rest of that story.

Two blind spots are worth knowing, and both are refusals rather than wrong answers: a MySQL connection string written with Host= and Username= matches nothing, and a bare Oracle TNS alias (Data Source=orcl) is indistinguishable from a SQL Server instance name and so is not recognised. Set Provider in either case.

Where connections come from

Once the database is known, the store still needs a connection. The package sets one of the settings on DataSourceOptions, choosing by what the service collection already holds. The rows are tried in order, and the first that matches wins:

ConditionThe store is configured withWhy
The provider is SqlServerConnectionString and ConnectionStringNameSee below — the probe is skipped entirely
A keyed DbDataSource under connectionNameDataSourceServiceKey = connectionNameTwo databases cannot both be the container's one unkeyed data source
An unkeyed DbDataSourceUseRegisteredDataSource = trueThe application registered exactly one, and this is it
Anything elseConnectionString and ConnectionStringNameNothing to take a connection from, so open one

builder.AddKeyedNpgsqlDataSource("quartz") produces the second row and builder.AddNpgsqlDataSource("quartz") the third — both register a singleton System.Data.Common.DbDataSource, which is the service type this probes for, keyed in the first case and unkeyed in the second.

A data source is preferred because whatever it was built with is then in play for Quartz's own statements — its type mappers, its logging, its connection multiplexing — since commands are made by the connection rather than from a driver description. It is also the answer that keeps a trimmed application honest: the connection-string path is what makes AddQuartzPersistentStore carry [RequiresUnreferencedCode], because that path names the driver's connection, command and parameter types as strings.

SQL Server never takes the data-source path. Microsoft.Data.SqlClient ships no DbDataSource implementation at all, and Aspire's SQL Server client integration registers a scoped SqlConnection instead — so probing for an unkeyed DbDataSource would find some other database's, or nothing, and would fail at first use rather than at startup.

What happens to the schema

The store is configured with SchemaProvisioning.CreateIfMissing when the application is running in Development, and left at the Validate default everywhere else. builder.Environment is read at the AddQuartzPersistentStore call rather than when the scheduler starts, so the answer is the environment the container was built in.

ProvisionSchemaAdoJobStoreOptions.SchemaProvisioning becomes
unset (the default)CreateIfMissing under Development, Validate in every other environment
trueCreateIfMissing, whatever the environment
falseValidate, whatever the environment — which is what it already is, so nothing is set

An AppHost's database container comes up empty whenever its volume is new, which makes a first run that fails schema validation the ordinary outcome rather than an edge case; a production account, on the other hand, usually holds no DDL permission and is right not to. Neither is a fact about this application, which is why the environment answers rather than a default that would be wrong on one side.

The store keeps its own word. A SchemaProvisioning the application set — through ConfigureStore, or through Quartz:JobStore:SchemaProvisioning — is read as a decision about this store and left alone, because this call runs from ConfigureAllQuartzSchedulers and would otherwise win merely by being last. Validate is the exception, being what an unconfigured store already holds and so indistinguishable from silence: ProvisionSchema = false is how an application in Development says it.

Everything else about provisioning is the store's, not this package's: Creating the schema covers what it runs, why it is safe under a cluster starting at once, and the two configurations that cannot provision or would provision the wrong schema. Running Quartz under Aspire has the production recipe.

Clustering

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

The second half is not a convenience. A cluster's nodes recognise their own check-in row and their own fired triggers by InstanceId; every scheduler starts life carrying QuartzSchedulerOptions.DefaultInstanceId, which is NON_CLUSTERED; and under Aspire a replica set is one call — WithReplicas(2) — with no identity of its own to borrow. A cluster whose nodes all answer to one id is the worst failure this area has, so the setting supplies both halves rather than documenting the trap beside one of them.

It fills a gap and never overrides. An application that already set GenerateInstanceId, or that named its nodes by setting InstanceId — from code or from Quartz:Scheduler:InstanceId — keeps what it said.

Health and telemetry

The health check is AddQuartzHealthChecks() from the core Quartz package, registered on the same IHealthChecksBuilder an Aspire ServiceDefaults project put its own self check on, so MapDefaultEndpoints() serves both. It is registered per scheduler, so two schedulers get two checks under two names. What the check reports, and what survives an HTTP probe, is the how-to's Health section.

Telemetry is AddSource("Quartz") and AddMeter("Quartz") on the application's existing AddOpenTelemetry() builder. No exporter is ever added, and that is not tidiness: AddServiceDefaults() calls UseOtlpExporter() whenever OTEL_EXPORTER_OTLP_ENDPOINT is set, the AppHost sets it, and OpenTelemetry's UseOtlpExporter may be called only once and cannot be combined with a signal-specific AddOtlpExporter() — either mistake throws NotSupportedException. Aspire says the same thing more generally: defining exporters is outside a client integration's scope.

Turn any of the three off individually:

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

Observability lists every span, instrument and attribute.

More than one scheduler

Left unset, SchedulerName gives the store to every scheduler in the container — right for the single scheduler an application normally has, and wrong the moment two of them talk to two databases. Naming it scopes the call to one scheduler, by 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");

See Multiple Schedulers for what a named scheduler is and how its parts are keyed.

What this package deliberately does not do

  • There is no Quartz.Aspire.Hosting. A hosting integration would add resources to the AppHost — an AddQuartz() resource, a WithQuartzDashboard() — and there is nothing for one to orchestrate: Quartz runs inside an existing project resource rather than as a process of its own. The AppHost declares the database and hands it over, which it can already do.
  • There is no AddKeyedQuartzPersistentStore. Every other client integration has a keyed form, and it exists so an application can hold two of a thing. Quartz already has an axis for that which is not the container's — a second scheduler, registered by name — so SchedulerName is how a second call says which one it means, and two databases end up on two schedulers rather than on two keyed copies of one. Aspire's own guidance makes the keyed form a "consider, if applicable" rather than a requirement.
  • It maps no health-check status codes. HealthCheckOptions.ResultStatusCodes is an ASP.NET Core type and a decision about this application's probe, not about Quartz; the how-to explains why the default mapping loses a standby scheduler and what to write instead.
  • It creates no tables in production. The store provisions its own schema under Development and validates it everywhere else, because that is what an AppHost's empty container and a production account's permissions respectively call for — and it migrates a schema nowhere, because nothing in a Quartz schema records which version it is. The how-to has the migration-service recipe that answers the production half.
  • It adds no OpenTelemetry exporter, for the reason above.

One convention this package knowingly diverges from: Aspire's contributor guidance asks a client integration to support every supported .NET version at the time of the Aspire release it targets, which for 13.x means net8.0. Quartz.Aspire targets net10.0 alone, because every Quartz 4.0 package does.

See also

  • Running Quartz under Aspire — the AppHost, the worker, the dashboard, and every line of this call written out by hand
  • Observability — the spans, instruments and attributes the package subscribes
  • Hosted Services Integration — AddQuartzHostedService, and the health check outside Aspire
  • Job Stores and Configuration Reference — every store setting this call sets, and the ones it does not touch
Help us by improving this page!
Last Updated: 8/31/26, 5:28 AM
Contributors: Marko Lahma, Claude Fable 5
Next
ASP.NET Core Integration