Update dependency Quartz.AspNetCore to v4 - #1373
Open
renovate[bot] wants to merge 1 commit into
Open
Conversation
renovate
Bot
force-pushed
the
renovate/major-quartznet-monorepo
branch
from
September 9, 2026 03:11
cfb06c6 to
82c6548
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
3.14.0→4.0.1Release Notes
quartznet/quartznet (Quartz.AspNetCore)
v4.0.1Quartz.NET 4.0.1 is a maintenance release about getting to 4.0: nothing about how a trigger fires changed, the public API is untouched (the baselines did not move), and the schema is 4.0's. Five days after 4.0.0 an audit of every public dependency-bot pull request that touched a Quartz package found two reasons an upgrade never got as far as compiling, both of them ours, both fixable in a patch — one gap in the migration guide for F# — and, found by the rc.1 security gate and moved forward, a cron calendar that could take a century to answer.
What changed
Microsoft.Extensions.*dependency at10.0.11, the newest patch on the day it was built, so a project pinned at 10.0.9 or 10.0.10 hitNU1605"detected package downgrade" before a line of source compiled. Every floor is now the lowest version of its major that the code compiles against and that carries no advisory:10.0.0for the framework extensions,13.0.2for Newtonsoft.Json (13.0.1 reflects overTimeOnlymember by member and a job data map loses its seconds — a test said so),1.15.3for OpenTelemetry.Extensions.Hosting (the first version whoseOpenTelemetry.Apicarries no advisory),3.0.0for StackExchange.Redis and7.0.0for TimeZoneConverter. The library is built against those floors, so the claim is checked on every build, and a test refuses a floor that creeps up. (#3717,c9ecf89)Quartz(Quartz.Extensions.DependencyInjection,Quartz.Extensions.Hosting,Quartz.Serialization.SystemTextJson, andQuartz.Serialization.Json, whose successor isQuartz.Serialization.Newtonsoft) had no 4.0.0, so a bot that groups Quartz with any of them resolved the group to the newest version every member has — 3.20.1 — and closed the 4.0.0 pull request it had already opened as superseded, with green checks. From 4.0.1 those four ids carry an empty package at every 4.x version: no assembly, one dependency on the replacement, a readme that says to remove the reference. The migration guide's instruction stands — remove them — but the upgrade is now offered.Quartz.OpenTracingandQuartz.OpenTelemetry.Instrumentationdeliberately have no such package: neither has a 4.x replacement, and an empty one would hide that. (#3717,c28f213)CronCalendar.GetNextIncludedTimeUtcno longer walks a century one second at a time — it askedCronExpression.GetNextInvalidTimeAfterfor the end of an excluded run, and that method stepped through the run one second per full cron computation; for an expression that excludes everything (* * * * * ?) the walk ran to the give-up year, roughly three billion computations, on a public member. The next non-matching instant is now read off the expression's own field sets — second, minute, hour, day, month, year — with each skip verified against the time zone's clock so a repeated fall-back hour is never stepped over. An expression that fires every second answersnull, and the calendar turns that into aSchedulerExceptionnaming the expression instead of hanging. The unchanged next-fire-time path measures the same as before; the changed method is 76–86 % faster on the benchmark corpus. (#3690,a125809,4d60904)GetNextInvalidTimeAfterreturnsnullwhere it used to return a valid instant after giving up, and aCronCalendarwhose expression excludes every instant now fails fast withSchedulerException.FS0856onIJob.Execute's arity,FS0041onScheduleJoboverload resolution,Async.AwaitTaskwith aValueTask,FS0039forStdSchedulerFactory), each quoted with its fix, and every sample is copied from a compiled example project the build keeps honest. (#3718,1b97b49,aff4fea)Upgrading
From 4.0.0:
dotnet add package Quartz --version 4.0.1; nothing else. From 3.x: the 4.x migration guide is unchanged in substance; if your bot has been landing 3.20.1 "upgrades", this is the release that lets it offer 4.x.Full changelog: quartznet/quartznet@v4.0.0...v4.0.1
v4.0.0Quartz.NET 4.0 targets
net10.0, is asynchronous and container-built throughout, and trims a public surface that had accumulated for a decade. It is a major version with extensive breaking changes and a mandatory schema migration. This page is the short form; the detail lives in the docs:Highlights
net10.0only — nonetstandard2.0build, no Full Framework, no.configsupport.Quartzpackage;StdSchedulerFactory,quartz.configdiscovery and the process-globalSchedulerRepository.Instance/DBConnectionManager.Instanceare gone. Flatquartz.*keys still work, translated to typed options by the one component that understands them, and a misspelled key is refused with a message rather than ignored. Options are validated at startup, so a bad value failsHost.Build()with every failure listed.IJob.Executetakes aCancellationToken,IJobFactoryhands out aJobScoperather than a bare instance, every publicTaskbecameValueTask, and every asynchronous member ends with a cancellation token.QueryJobs/QueryTriggersreturn aPagedResult<T>whose rows already carry what a listing needs, so a dashboard over a large schema no longer pays for the whole schema. The old call shapes remain as extension methods.TriggerState.Executingsays whether a trigger's job is running anywhere in the cluster; fire instances, cluster nodes, execution groups, misfires and history are listings that say which node they came from; the health check notices a node whose own check-in has stopped.ISchedulerclient over it (the replacement for .NET Remoting, which is gone), a dashboard, typed job input (IJob<TInput>;UsingInput(input)on a registration,ScheduleJob<TJob, TInput>(input, at)for a one-off), a retry policy a trigger carries — persisted, cluster-safe, never burning a repeat count — job execution middleware, a[JobTimeout]attribute, execution groups with per-node or cluster-wide limits, node affinity, and a firing that links back to the trace that scheduled it.0 15 10 1 * *is the 1st, not every day); a time the clocks skip fires when the gap ends; the parser refuses what it used to quietly reinterpret (1-5W,L-3in day-of-week,MON,FRI#3,MON/2, steps of zero); the five-field Unix form and the@daily-style macros work everywhere an expression is read.CalendarIntervalTriggerwithPreserveHourOfDayAcrossDaylightSavingssteps in local wall-clock time and no longer drifts in zones whose delta is not a whole hour; calendars mean the local day even where midnight itself moves. Review any schedule that crosses a transition.Quartz.Aspire:builder.AddQuartzPersistentStore("quartz")turns an Aspire connection name into a configured persistent store with its telemetry and health check, with noAspire.*dependency; a store can provision its own schema as it starts (ProvisionSchema()), safe under a cluster racing to start.MapQuartzHttpApi()andMapQuartzDashboard()refuse to start unless the endpoints carry authorization or an explicitAllowAnonymous(); a scheduler can be authorized on its own name, and every call the dashboard makes is authorized where it is made.ActivitySource("Quartz")andMeter("Quartz")(the names are public constants), nine instruments, spans on every store the same way, and every log line carrying a stable event id — 278 of them, catalogued on a generated page.QuartzdeclaresIsAotCompatible; a canary is published natively and run against a real store on three operating systems on every pull request; the remaining string-named paths are recorded and each has a documented alternative (#3341).RAMJobStore(numbers below).JobRunShellare internal, and non-public types are sealed. If something you relied on is gone, say so in an issue; these can be reopened.Breaking changes, the ten to know before the guide
net10.0only.Quartz.Extensions.DependencyInjection,Quartz.Extensions.HostingandQuartz.Serialization.SystemTextJsonare part ofQuartz;Quartz.Serialization.JsonisQuartz.Serialization.Newtonsoft;Quartz.OpenTracinghas no 4.x release, andOpenTelemetry.Instrumentation.Quartzproduces nothing on 4.x — subscribe to the source and meter directly. The first error a mixed project produces is CS0433 (a type in bothQuartz4 and a 3.x satellite): remove the three references.StdSchedulerFactory,DirectSchedulerFactoryandquartz.configare gone;AddQuartz(…)orQuartzSchedulerBuilder.Create(q => …)build a scheduler.Task→ValueTaskon nearly every member, and aCancellationTokenon every asynchronous one.Quartz.SpiisQuartz.Extensibility,Quartz.Simplmerged intoQuartz.Impl, the Newtonsoft types left the core namespaces. A string naming an old namespace still resolves, with a warning.*Supportbase classes are gone (every member has a default body); a listener with a 3.x signature is refused at registration.TimeOnlyandDateOnlyreplaceTimeOfDay;TimeProviderreplacesSystemTime; the semaphores are lock handlers.?in either day field) now parses, and two shapes fire on a different day than Cronos would — a bare digit in day-of-week (Quartz numbers Sunday1) and both day fields restricted (Quartz fires on the union). The guide's second audit finds them.Everything else — every renamed member, every sealed type, every removed constant — is in the guide, with the name you would have typed.
Fixes worth knowing about
The full list is spread over the six pre-release notes below. These change what a running cluster does without saying so, and most of them are as old as 3.x:
ResumeAllcould unpause real trigger groups: it deleted the all-groups sentinel with aLIKE, and the sentinel's four underscores are wildcards.TimeProviderthe scheduler was given (#3456).DailyCalendarcould crawl for minutes to answer months late (#3457, #3466).[DisallowConcurrentExecution]siblings behind it (#3502); a firing whose listener notification failed was listed as executing forever; a trigger with nothing left to fire was left behind when its last firing was abandoned (#3507).ACQUIREDfor good (#3673, on every 3.x version too).RAMJobStorenotified its listeners inside its own lock, so a listener that touched the store deadlocked (#3472).MySQLDelegateforced an index the misfire sweep could not use.ITypeLoaderof its own (#3705). Stopping the host twice at once — whichhost.StopAsync()duringRunAsync()does — no longer throws out ofRunAsync(#3701).Numbers
Measured on one shared machine with other work running, the same harness on both branches (
src/Quartz.Benchmark; the 3.20 half is in the tree underbaseline-3x/):MaxConcurrencyRAMJobStoreRAMJobStoreThe persistent-store fire path is batched and counts 1.27 commits per firing at the database. Two clustered nodes ran a mixed workload for thirty minutes on PostgreSQL and on SQL Server — every trigger family, a serial job behind an overlap detector, a retry policy, a job timeout, induced misfires, a node killed mid-run and recovered — with peak serial concurrency 1, exactly one recovery, nothing left behind and a flat heap, on the beta.1 build and again on the 4.0.0 commit itself. The upgrade from a running 3.20 cluster was rehearsed by hand through its mixed-version window on both engines, and the offline upgrade over rows a released 3.20 wrote runs on every dialect on every pull request.
Known limitations
Each is documented on the page that owns it; this is the list, not the explanation.
SendMailJobreads SMTP credentials out of job data when nothing is registered (Quartz.Jobs).MaxConcurrencydefaults to 10 on the shared thread pool (configuration reference).HttpScheduler.StatusandSchedulerInstanceIdblock on an HTTP round trip (HTTP client).OpenTelemetry.Instrumentation.Quartzproduces nothing on 4.x, and 4.x warns at start if it is present (OpenTelemetry).TransactionScope(job stores).Quartz.Extensions.Redis(one unit and one container fixture);Quartz.Aspireunder an AppHost was run by hand, not in CI.Upgrading a running deployment
The runbook is short and every step links the page that owns it. In one breath: retarget to
net10.0and drop the merged packages; on 3.x, turn binary blobs off and run the cron audit; runschema_30_to_40_upgrade_<database>.sqlwhile 3.x nodes are still up; configure and start the 4.0 nodes one at a time (routeAddCalendarthrough the 3.x nodes until the last one is gone); runschema_30_to_40_indexes_<database>.sqlonce it is; pause any job group you relied on being paused (3.x never persisted one). A 4.0 node against a schema you have not migrated refuses to start and names the column and the script.If you ran a 4.0 pre-release
Everything that changed between one pre-release and the next is one appendix in the guide, build by build: If you ran a 4.0 pre-release. 4.0.0 is the rc.2 commit: between the two tags, nothing changed but this page and the site.
Thanks
Task→ValueTaskacross the whole surface (#1964), Microsoft.Extensions.Logging in place of LibLog (#1480), the move ontoTimeProvider(#2286), a job's type recorded so a process that cannot load it can still work on it (#1610) — the groundwork for #3705 — and most of the cron work 4.0 is built on: both day fields restricting together (#1980),LandLWin day-of-month and an offset from the last weekday (#1956, #1975), the guards that refuse what used to be quietly reinterpreted (#1955, #2836), and the parser's rewrite and its allocation cuts (#2508, #2003).RAMJobStore(#1379, #1393, #1403),JobRunShell(#1388), listener notification (#1385, #1586, #1600, #1768) — for configuration values verified rather than assumed (#1408), and for sealing what nobody derives from (#1470).HttpSchedulerclient (#1803, #1815, #1831, #1845), which 4.0's HTTP API carries forward.DbDataSourcesupport (#2438, #2453) — which is how a trimmed application configures a store — asynchronous database transactions (#2456), and OpenTelemetry that records exceptions (#2699).QuartzRandom.Next(#1937) and the connection manager's dictionary (#1888).IJobDetailfinally works.UseJobStore<T>seam.DailyCalendar's checks; @Skimmenthal13 (#2486) forNOVandDECparsing as themselves; @philr (#2039) for extended properties surviving a change of trigger type; @OronDF343 (#2060) for blob reads on SQL Server.UsePersistentStore<T>; @GhostlyRaven (#2386, #2522) forIHostedLifecycleService; @bdovaz (#2796) for the registration overloads that take anIServiceProvider; @williamdenton (#2335) for disposing a job's scope asynchronously; @BraedonWooding (#1853) for listener registration; @saklig (#1571) for[DisallowConcurrentExecution]on the configurator; @asherber (#1592) forTryGet*and @perringaiden (#2606) forIReadOnlyDictionaryonJobDataMap; @tonyqus (#1613) for property access in place of reflection; @adamsitnik (#2312) for lessBinaryFormatter; @AmirShitrit (#2792) for the scheduler's ownTimeProviderreaching its waits; @unageek (#2788) forInterruptreaching every job it identified; @alanblack (#2004) for the interrupt monitor reading the merged map; @dima-zhemkov (#1995) for what the diagnostic listener is handed.mainwhile 4.0 was being written, and everyone who commented on the 4.0 roadmap (#988) over the years it stayed open.Full changes
v3.21.0Quartz.NET 3.21.0 carries the three fixes held back from 3.20.1 because each needed a small addition to the public surface or changed what a running scheduler does. All three were found while 4.0 was being finished; each is as old as 3.x. The public API grows by two interfaces on one class and nothing else; the schema is 3.20's. Three of the changes alter behaviour, each marked Behavior change worth noting below.
What changed
ResumeAllclears every paused trigger group, not only the ones with triggers — the persistent store resumed the groups it found inQRTZ_TRIGGERSand then deleted only its all-groups marker, so a group paused while it held no triggers kept itsQRTZ_PAUSED_TRIGGER_GRPSrow and went on pausing whatever was scheduled into it afterwards. Pausing a group before anything is scheduled into it is a documented use of the exact-name matcher, so this was a row the store wrote on purpose and could not take back. The trailing delete now takes every group, asRAMJobStorehas always done. (#3721, #3742, port off76b04a)ResumeAll.RedisSemaphoreopened aConnectionMultiplexeron the first lock and kept it, with its heartbeat, for the life of the process, because nothing on the store's shutdown path reached the lock handler andISemaphorehad no member that meant "we are done". On a branch that targetsnetstandard2.0andnet462an interface cannot gain a default member, soJobStoreSupport.Shutdownnow disposes a lock handler that implementsIAsyncDisposableorIDisposable, after the misfire handler, the cluster manager and the connection manager have stopped, logging and continuing if that throws;RedisSemaphoreimplements both and closes the multiplexer it opened. (#3721, #3742, port of #3639)ISemaphorethat implements either interface is now disposed at shutdown.Task.Delayrefuses anything longer than about 49.7 days on .NET and about 24.9 days on .NET Framework, with anArgumentOutOfRangeExceptionnaming a parameter calleddelay, and every duration Quartz waits out that way was accepted unchecked and reported later from wherever the wait happened.MisfireHandlerFrequency,MisfireThreshold(when it is also the handler's sleep),ClusterCheckinInterval,DbRetryInterval,TransientRetryInterval, the row-lock handlers'RetryPeriod,StartDelayed's argument andQuartzHostedServiceOptions.StartDelaynow name the setting, the ceiling and the value at configuration time. The ceiling is per target framework, held to whatTask.Delayactually accepts by a test. (#3721, #3742, port of #3577)Public API — additive only
Quartz.Extensions.Redis:RedisSemaphoreimplementsIAsyncDisposableandIDisposable.Upgrading
dotnet add package Quartz --version 3.21.0. Nothing to migrate. The 4.0 line is the current major; the 4.x migration guide is the way there, and 4.0.1 made the upgrade one a dependency bot can offer.Full changelog: quartznet/quartznet@v3.20.1...v3.21.0
v3.20.1Quartz.NET 3.20.1 is a maintenance release: every change is a bug fix, the public API is untouched (the baselines did not move), and the schema is 3.20's. Most of it was found while 4.0 was being finished and rehearsed — a fix that turned out to be as old as 3.x was ported here rather than left on the newer line — and one item comes from a production application's 3.19.1 → 4.0 upgrade that also read on 3.x. Eight of the fixes change what a running scheduler does, each marked Behavior change worth noting below.
What changed
Landed on the branch since 3.20.0:
DailyTimeIntervalTriggerstored through the default Newtonsoft path reads back again —TimeOfDayhas no parameterless constructor, so with the trigger converters off (the default)EndTimeOfDaythrew "Unable to find a constructor" andStartTimeOfDaysilently read back as midnight. A converter scoped toTimeOfDay-typed members reads both forms; nothing about what is written changed, so every blob a released 3.20 wrote is one this reads. (9ee33fec17, fixes #3508)StartTimeUtckept its milliseconds while the fire times are counted in whole seconds, so a start of22:50:00.68could produce a first fire at22:50:00.000. Start and end are rounded down to the second when set, asCronTriggerImplalways did. (cc051a7788, #3386)RAMJobStoreand as a permanentCOMPLETErow in the ADO store. Both stores finish it now. (0af9431d3e, #3507)[DisallowConcurrentExecution]job is neither acquired nor swept, so the completion that unblocks it is the first thing that can settle its missed fire time;RAMJobStorenow does whatJobStoreSupport.RecoverUnblockedMisfiresalways did. (c9d8658a35, #3463)RAMJobStorewrotePausedoverError, so a failed trigger vanished from every listing once its group was paused andResetTriggerFromErrorStatehad nothing to reset. It now pauses only what the ADO store pauses: waiting, acquired and blocked triggers. (a56a16ca0c)RescheduleJobadvanced a never-fired repeating simple trigger's start time past a next fire time it kept, so it fired at the stale time and again at its start. (3e086091fc, #3554)overwrite-existing-dataon, and a repeating trigger that starts now fired twice milliseconds apart. (f25080cef6, #3554)Dictionary<string, string>job-data value written by the Newtonsoft package carried a$typethe System.Text.Json reader handed back as an entry, and one written by System.Text.Json came back from Json.NET as aJObject. Both readers read both shapes; neither writer changed. (83ba80ce79, part of #3582)QRTZ_SIMPROP_TRIGGERStoo — a database missing only that table passed validation and failed on the first calendar-interval, daily-time-interval or recurrence trigger insert. (b33c70487b, #3564)4b3c43a90e); untagged builds from the branch say 3.20 (0e2f6bcf31); the XML scheduling integration test opens its own fixture's data source (4c07210199, #3573).Ported from 4.0:
JobToBeExecutedescaped as itself rather than the exception the run shell catches, soTriggeredJobCompletewas never reached: the trigger stayed acquired and, for a[DisallowConcurrentExecution]job, every sibling trigger stayed blocked; the firing was also listed as executing for the life of the process. (port of #3502)IDX_QRTZ_T_NFT_ST_MISFIRE, whose second column is compared with<>and stops the seek dead. Measured on 4.x against 100,000 triggers: the count 111 ms → 0.7 ms, the sweep 66 ms → 0.7 ms. No schema change. (port of #3608)RescheduleJobandUpdateTriggerDetailson the ADO store resolved the job's class to decide whether the new trigger could run, and failed in an administration node without the assembly. Both read the job's two attribute flags fromQRTZ_JOB_DETAILSnow, so the decision is right without the class and a placeholderITypeLoadHelper— which decided that question by whether the placeholder carried the attribute — is no longer needed. (port of #3705)40001,40P01;40002excepted) is transient. Firebird reports a write conflict that way withIsTransientfalse, andMySql.Dataits 1213 deadlock. (port of #3454)SimpleTriggerinterval finer than a millisecond was stored as0, read back as zero, and left the trigger inACQUIREDfor good behind a divide-by-zero the store logged and swallowed. It is refused on write now, naming the trigger and the column;RAMJobStorekeeps accepting it. (port of #3673)List<string>or a nested object serialized happily and threw on the next read with the blob already in the database, and every later acquisition of the trigger failed on it. A value that would be stored as a JSON array, or as an object other than aDictionary<string, string>, is refused before the first byte is written, naming the entry and its type. Anything stored as a number or a string — every numeric type,DateTime,Guid,byte[],Uri— still round-trips exactly as before. (port of #3495)JsonSerializationExceptionat store time, where it used to be a blob the next read failed on. The refusal also covers three shapes that did not throw before but never came back as themselves either — a non-genericHashtable, an object whose properties are all strings, and aJobDataMapnested inside aJobDataMap— each of which the reader handed back as aDictionary<string, string>. Store one of those as a string of your own making.EndTimeUtcfalling between two fire times of the same day let the trigger go on firing until the daily window closed, andFinalFireTimeUtcreported that close even when it was a day past the end. (port of the daily half of #3458)NativeJobno longer deadlocks a child that writes more than a pipe buffer — both streams were redirected whether or notconsumeStreamsasked for them to be read, so with the defaults a chatty process blocked on its own write and the job's synchronous wait held a worker for ever. Nothing is redirected unless consumed. (port of the rc.1 fix)EnlistConnectioninside aTransactionScopetook aMicrosoft.Data.Sqliteconnection on trust, and SQLite cannot enlist, so every statement committed on the spot and a rolled-back scope left the schedule behind.EnlistTransaction(DbTransaction)still works there. (port of the beta.1 fix)now - MisfireThresholdwas a misfire toRAMJobStoreand to the ADO store's single-trigger path but not to its periodic sweep; the sweep says<=now and the acquisition predicate moved to>in step. (port of #3462)"3,14"read as314from the floating-point accessors whileGetIntthrew. (port of the beta.1 fix)GetDoubleandGetFloatthrow aFormatExceptionfor such a string where they used to answer a number a hundred times too large.[matches literally on SQL Server — T-SQL reads[as a character class inLIKE; it is escaped on that dialect only, because the standard forbids escaping a non-wildcard elsewhere. (port of the rc.1 fix)[matches the groups it names rather than the character class T-SQL read it as, so it can list, pause, resume or delete a different set than on 3.20.DirectoryScanJobstores its previous scan as something a job store can write — it kept aList<FileInfo>under[PersistJobDataAfterExecution], which System.Text.Json cannot write, so its first firing on such a store failed to persist. A legacy list already in a running scheduler is still read. (port of the rc.1 fix)DirectoryScanJobruns at all on 3.20 — it read its optional job-data keys withGetString, which throws for a key that is not there, so a job configured without a directory provider or listener name failed on every firing withKeyNotFoundException. Found while porting the previous item; the optional keys are asked for rather than read.SelectSchedulerStateRecordsbinds its parameters in statement order — only a provider withBindByNameoff could ever have noticed. (port of the alpha.3 fix)Public API
Unchanged. No signature was added, altered or removed, and the
PublicApiTestbaselines did not move.Upgrading
A drop-in upgrade from 3.20.0: no schema change, no configuration change, no migration script. Read the Behavior change worth noting bullets above — each is a case that used to be silently wrong and is now either correct or loud.
About the 4.0 line
Quartz.NET 4.0 was released on 2026-09-03. It targets
net10.0only; 3.x remains the line fornetstandard2.0and .NET Framework, and fixes that apply to both keep landing on both, which is what most of this release is. The migration guide is the map if you are considering the move.Full changes: quartznet/quartznet@v3.20.0...v3.20.1
v3.20.0Quartz.NET 3.20.0 is a feature release: the ADO.NET job store can take part in the application's own database transaction, job instantiation failures finally carry the trigger they died on, and the daylight-saving and calendar arithmetic got a systematic pass that fixed several defects a schedule can actually hit. The public API is additive only — three new members and one new exception type, nothing changed or removed — but this is not quite a drop-in upgrade. Four things to read before you take it:
database/that used to sit flat is nowdatabase/migrations/<version>/<name>_<dialect>.sql, one runnable file per database instead of one file with five commented-out dialect blocks. Old links still resolve against release tags; the mapping is below.database/migrations/3.20/). Nothing needs it to run, but PostgreSQL users should read that bullet.Highlights
quartz.jobStore.acceptEnlistedTransactionsorAcceptEnlistedTransactions(), then hand the store a connection for the duration of a scope withIScheduler.EnlistTransaction(DbTransaction)orIScheduler.EnlistConnection(DbConnection). The application owns the commit; the job store neither commits nor rolls back, and the enlistment flows with the current asynchronous context the wayTransaction.Currentdoes — which is what makes it work whileIJobStoreis a singleton and aDbContextis scoped. Nothing about it is EF Core specific. Taking part always means handing over a connection: an ambientTransactionScopealone is not enough, because a connection the job store opens for itself would be a second connection in that transaction and would force promotion to a distributed one — which needs MSDTC (Windows only, and on modern .NET also an explicit opt-in throughTransactionManager.ImplicitDistributedTransactions) and is impossible on Npgsql, which has no distributed transaction support at all. Sharing the one connection is what keeps the transaction local. Opt-in, because joining the application's transaction changes when, and whether, scheduling commits;JobStoreCMTis untouched, since running inside a container-managed transaction is that store's whole contract. (#3204, fixes #2038)txIsolationLevelSerializableapplies only to the job store's own connections.IJobFactorycannot produce a job the trigger has already fired and been committed, but there is noIJobExecutionContextyet, so no trigger or job listener can be raised andISchedulerListener.SchedulerErroris the only notification. It carried the job key as interpolated message text and nothing else, leaving callers parsing a string to find out which firing died. The newQuartz.Core.JobInstantiationException : SchedulerExceptioncarriesTrigger,JobDetailandFireInstanceId— the same shapeJobExecutionProcessExceptionhas for execution-time failures — and both catch blocks inJobRunShell.Runraise it, so the DI and non-DI paths are enriched alike. Message text is byte-identical in both paths, including a long-standing misplaced quote, so anything parsing it today keeps working while it migrates. (#3215, closes #3213)SchedulerExceptionpath the exception handed to listeners is now aJobInstantiationExceptionwrapping the factory's exception rather than being it; the original is reachable asInnerException. The choice betweenNoInstructionandSetAllJobTriggersErrorstill tests the original exception, so cancellation and disposal races behave exactly as before.TimeZoneInfo.GetUtcOffsetreturning the pre-gap offset for positive-delta zones. It is now two internalTimeZoneUtilhelpers —ResolveLocal(ambiguous → the first/daylightConfiguration
📅 Schedule: (in timezone Asia/Shanghai)
🚦 Automerge: Enabled.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.