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

Extending Quartz: what is open, what is closed, and how to ask

Extend Quartz by implementing an interface or deriving from an open base class, never by reflection over its internals.

The open seams

You want toSeamPage
Keep scheduling data somewhere newIJobStoreA Job Store of Your Own
Add behaviour around an existing storeDelegatingJobStore — every member virtualDecorating a store
Support a database with no shipped dialectsubclass StdAdoDelegateA Driver Delegate for a New Database
Store a trigger family of your ownITriggerPersistenceDelegatePersisting a Custom Trigger Type
Replace the QRTZ_LOCKS rowILockHandlerA Lock Handler of Your Own
Add a trigger or calendar typeTriggerBase / BaseCalendar, plus a serializer in each JSON packageSystem.Text.Json Serialization
Wrap or proxy a schedulerDelegatingScheduler — every member virtualTesting
Run code around every jobIJobExecutionMiddlewareJob Execution Middleware
React to scheduler eventsIJobListener, ITriggerListener, ISchedulerListener, ISchedulerPluginTrigger and Job Listeners
Change how jobs are constructed, types are loaded, work is scheduledIJobFactory, ITypeLoader, IThreadPoolConfiguration Reference
Keep what a scheduler has run and missedIExecutionHistoryStoreExecution history
Serve the dashboard from somewhere elseIQuartzApiClientDashboard

Register each through a Use* or Add* method on IQuartzBuilder or IPersistentStoreBuilder. Registration is first-wins (TryAdd): register yours instead of the shipped one, and for the persistent store before UseSqlServer, UsePostgres or UseGenericDatabase, each of which names its own driver delegate.

src/Quartz.Documentation.Samples is not a friend assembly, yet compiles a StdAdoDelegate subclass and a complete ITriggerPersistenceDelegate, so the build fails if the public Quartz.Impl.AdoJobStore kit loses a type.

Two promises that make a seam safe to extend

Both hold across 4.x, so later releases can give a collaborator more without breaking yours:

  • A collaborator is handed a context object, never a parameter list. DriverDelegateContext, LockHandlerContext, TriggerPersistenceDelegateContext, TriggerAcquisitionRequest, TriggerAcquisitionCriteria, TriggerFiredBundle and SchedulerIdentity have a public parameterless constructor and init properties. A new datum is a new non-required property: source- and binary-compatible.
  • A member added to a public interface arrives as a default interface member (DIM). IJobStore has nine, IScheduler one, ILockHandler and ITriggerPersistenceDelegate one each. Your implementation keeps compiling and gets the default. The public API baselines mark every DIM.

A forwarder must declare every DIM of its contract. A DIM is not part of a class's members, so it is callable only through the interface unless the class declares it. A forwarder that omits one runs the default body on the forwarder, asking the inner instance whatever the default decomposes into rather than the original question. DelegatingJobStore and DelegatingScheduler declare every member, checked by a reflection sweep in the tests; do the same.

What is closed, and why

  • RAMJobStore is sealed; the ADO.NET stores are internal. They lock across several index mutations in a fixed order no override could preserve. Decorate instead.
  • StdAdoConstants, the SQL text, is internal. The schema is the contract, public in AdoConstants. Override the delegate method that issues a statement; every IDriverDelegate member on StdAdoDelegate is virtual.
  • Instant and duration storage is fixed. GetDbDateTimeValue, GetDateTimeFromDbValue, GetDbTimeSpanValue and GetTimeSpanFromDbValue are not virtual: UTC ticks and whole milliseconds are schema contract, and the preferred-node liveness SQL does arithmetic on them. The boolean pair is virtual, because Oracle has no boolean column type.
  • Read-replica routing is not expressible: IDbProvider.CreateConnection() takes no argument. If it is ever opened, it will be a DIM, DbConnection CreateReadConnection() => CreateConnection();, never a parameter on CreateConnection, which would break both public IDbProvider implementations.
  • The HTTP wire DTOs, the health-check predicate and the dashboard's default services are internal, each behind a public interface or options object. Write your own endpoint, check or service.
  • The XML and JSON scheduling files know four schedule types. Schedule a custom trigger in code or through the API.

How to ask for a seam

Open an issue that describes the integration, not the member: what you are building, what you tried, what stopped you. "Make X public" cannot be judged; "our store shards QRTZ_TRIGGERS by tenant and we need the acquisition predicate to carry a tenant id" can. Good cases are usually granted, in a shape chosen from the problem rather than the workaround.

See also

  • A Job Store of Your Own — the largest seam, and the one with the most rules
  • Migration Guide — what moved, was sealed or was internalized in 4.0
Help us by improving this page!
Last Updated: 10/18/26, 9:21 AM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Publishing Trimmed and Native AOT
Next
A Job Store of Your Own