A self hosted Web API for Quartz.Net, hosting on .net 4.8 is done with OWIN and on .net6 and higher with Kestrel
var host = new SchedulerHost("http://localhost:44344", <IScheduler>, <ILogger>);
host.Start();
Where IScheduler
is your Quartz.Net scheduler and ILogger
any logger that implements the Microsoft ILogger interface (or null if you don't want any logging)
var connector = new SchedulerConnector("http://localhost:44344");
QuartzWebApi is Copyright (C) 2022 - 2024 Magic-Sessions and is licensed under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Sicos1977 (Kees van Spelde)
QuartzWebApi uses the Microsoft ILogger interface (https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger?view=dotnet-plat-ext-5.0). You can use any logging library that uses this interface.
QuartzWebApi has some build in loggers that can be found in the QuartzWebApi.Logger
namespace.
For example
var logger = !string.IsNullOrWhiteSpace(<some logfile>)
? new ChromeHtmlToPdfLib.Loggers.Stream(File.OpenWrite(<some logfile>))
: new ChromeHtmlToPdfLib.Loggers.Console();
Most of the log informartion is logged at the information
level, small answers like booleans
, DateTimeOffsets
and small strings
(like the schedulers name) are also logged at this level. All the json that is received and sent back is logged at the debug
level.
Do a POST
request to Scheduler/IsJobGroupPaused/{groupName}
, where {groupName}
is the name of the group this will return true
or false
Do a POST
request to Scheduler/IsTriggerGroupPaused/{groupName}
, where {groupName}
is the name of the group this will return true
or false
Do a GET
request to Scheduler/SchedulerName
, this will return the name of the scheduler
Do a GET
request to Scheduler/SchedulerInstanceId
, this will return the schedulers instance id
Do a GET
request to Scheduler/SchedulerContext
When the context is;
- key1 - value1
- key2 - value2
- key3 - value3
it will return
{
"key1": "value1",
"key2": "value2",
"key3": "value3"
}
Do a GET
request to Scheduler/InStandbyMode
, this will return true
or false
Do a GET
request to Scheduler/Isshutdown
, this will return true
or false
Do a GET
request to Scheduler/GetMetaData
, this will return the meta-data that will look something like this
{
"InStandbyMode": false,
"JobStoreType": "Quartz.Simpl.RAMJobStore, Quartz, Version=3.4.0.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4",
"JobStoreClustered": false,
"JobsStoreSupportsPersistence": false,
"NumbersOfJobsExecuted": 0,
"RunningSince": "2022-05-11T16:30:06.5957565+00:00",
"SchedulerInstanceId": "NON_CLUSTERED",
"SchedulerName": "The name of the scheduler",
"SchedulerRemote": false,
"SchedulerType": "Quartz.Impl.StdScheduler, Quartz, Version=3.4.0.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4",
"Shutdown": false,
"Started": true,
"ThreadPoolSize": 10,
"ThreadPoolType": "Quartz.Simpl.DefaultThreadPool, Quartz, Version=3.4.0.0, Culture=neutral, PublicKeyToken=f6b8c98a402cc8a4",
"Version": "3.4.0.0"
}
Do a GET
request to Scheduler/GetCurrentlyExecutingJobs
, this will return the currently executing jobs that will look like this
[
"JobGroup1",
"JobGroup2"
]
Do a GET
request to Scheduler/GetJobGroupNames
, this will return the job group names that will look like this
[
"JobGroup1",
"JobGroup2"
]
Do a GET
request to Scheduler/GetTriggerGroupNames
, this will return
[
"TriggerGroup1",
"TriggerGroup2"
]
Do a GET
request to Scheduler/GetPausedTriggerGroups
, this will return
[
"PausedTriggerGroup1",
"PausedTriggerGroup2"
]
Do a POST
request to Scheduler/Start
Do a POST
request to Scheduler/StartDelayed/{delay}
where {delay}
is the amount of seconds you want to delay the start
Do a GET
request to Scheduler/IsStarted
, this will return true
or false
Do a GET
request to Scheduler/Standby
, this will return true
or false
Do a POST
request to Scheduler/Shutdown
Do a POST
request to Scheduler/Shutdown/{waitForJobsToComplete}
where {waitForJobsToComplete}
is true
Do a POST
request to Scheduler/ScheduleJobWithJobDetailAndTrigger
with in the body the job detail and the trigger
{
"JobDetail": {
"JobKey": {
"Name": "Name",
"Group": "Group"
},
"Description": "description",
"JobType": "jobType",
"JobDataMap": {
"key": "value"
},
"Durable": true,
"Replace": false,
"StoreNonDurableWhileAwaitingScheduling": false
},
"Trigger": {
"TriggerKey": {
"Name": "name",
"Group": "group"
},
"Description": "description",
"StartTimeUtc": "2022-08-15T17:34:04.5786231+00:00",
"EndTimeUtc": "2022-08-15T17:34:04.5786231+00:00",
"Priority": 5,
"CronSchedule": "0 * * ? * *",
"Priority": 5,
"JobKey": {
"Name": "name",
"Group": "value"
},
"JobDataMap": {
"key": "value"
}
}
}
Do a POST
request to Scheduler/ScheduleJobIdentifiedWithTrigger
with in the body
{
"TriggerKey": {
"Name": "TriggerKeyName",
"Group": "TriggerKeyGroup"
},
"JobKey": {
"Name": "JobKeyName",
"Group": "JobKeyGroup"
},
"Description": "Description",
"CalendarName": "CalendarName",
"JobDataMap": {
"Key1": "Value1",
"Key2": "Value2"
},
"StartTimeUtc": "2022-05-12T16:16:37.7210025+00:00",
"EndTimeUtc": "2022-05-13T02:16:37.7210025+00:00",
"FinalFireTimeUtc": "2022-05-13T22:16:37.7210025+00:00",
"CronSchedule": "0 * * ? * *",
"Priority": 5
}
This will return a datetime offset about when the job will be executed, for example "2022-05-12T16:17:00+00:00"
TODO: Task ScheduleJobs(IReadOnlyDictionary<IJobDetail, IReadOnlyCollection> triggersAndJobs, bool replace);
TODO: Task ScheduleJobs(IReadOnlyDictionary<IJobDetail, IReadOnlyCollection> triggersAndJobs, bool replace);
Do a POST
request to Scheduler/ScheduleJobWithJobDetailAndTriggers
with in the body the job detail and a related set of triggers
{
"JobDetail": {
"JobKey": {
"Name": "Name",
"Group": "Group"
},
"Description": "description",
"JobType": "jobType",
"JobDataMap": {
"key": "value"
},
"Durable": true,
"Replace": false,
"StoreNonDurableWhileAwaitingScheduling": false
},
"Triggers": [
{
"TriggerKey": {
"Name": "name",
"Group": "group"
},
"Description": "description",
"CalendarName": "calenderName",
"CronSchedule": "0 * * ? * *",
"Priority": 5,
"JobKey": {
"Name": "name",
"Group": "value"
},
"JobDataMap": {
"key": "value"
}
}
],
"Replace": false
}
When you get an error like Could not find an IJob class with the type '<the job type>'
then make sure that you have added the full namespace with the JobType
Do a POST
request to Scheduler/UnscheduleJob
with in the body the trigger of the job
{
"Name": "TriggerKeyName",
"Group": "TriggerKeyGroup"
}
this will return true
when the job is unscheduled or false
when not
Do a POST
request to Scheduler/UnscheduleJobs
with in the body the triggers of the jobs to unschedule
[
{
"Name": "TriggerKeyName1",
"Group": "TriggerKeyGroup1"
},
{
"Name": "TriggerKeyName2",
"Group": "TriggerKeyGroup2"
}
]
this will return true
when the jobs are unscheduled or false
when not
Do a POST
request to Scheduler/RescheduleJob
with in the body the reschedulejob object
{
"CurrentTriggerKey": {
"Name": "CurrentTriggerKeyName",
"Group": "CurrentTriggerKeyGroup"
},
"NewTrigger": {
"TriggerKey": {
"Name": "NewTriggerKeyName",
"Group": "NewTriggerKeyGroup"
},
"JobKey": {
"Name": "NewJobKeyName",
"Group": "NewJobKeyGroup"
},
"Description": "Description",
"CalendarName": "CalendarName",
"JobDataMap": {
"Key1": "Value1",
"Key2": "Value2"
},
"StartTimeUtc": "2022-05-12T16:16:37.7210025+00:00",
"EndTimeUtc": "2022-05-13T02:16:37.7210025+00:00",
"FinalFireTimeUtc": "2022-05-13T22:16:37.7210025+00:00",
"CronSchedule": "0 * * ? * *",
"Priority": 5
}
}
Returns 'null' if a trigger with the given name and group was not found and removed from the store (and the new trigger is therefore not stored), otherwise the first fire time of the newly scheduled trigger
Do a POST
request to Scheduler/AddJob
with in the body the addjob object
{
"JobKey": {
"Name": "JobKeyName",
"Group": "JobKeyGroup"
},
"Description": "Description",
"JobType": "JobType",
"JobDataMap": {
"Key1": "Value1",
"Key2": "Value2"
},
"Durable": true,
"Replace": false,
"StoreNonDurableWhileAwaitingScheduling": true
}
Do a POST
request to Scheduler/DeleteJob
with in the body the key of the job
{
"Name": "JobKeyName",
"Group": "JobKeyGroup"
}
this will return true
when the job is deleted or false
when not
Do a DELETE
request to Scheduler/DeleteJobs
with in the body the keys of the jobs
[
{
"Name": "JobKeyName1",
"Group": "JobKeyGroup1"
},
{
"Name": "JobKeyName2",
"Group": "JobKeyGroup2"
}
]
Do a POST
request to Scheduler/TriggerJobWithJobkey
with in the body the key of the job
{
"Name": "JobKeyName1",
"Group": "JobKeyGroup1"
}
Do a POST
request to Scheduler/TriggerJobWithDataMap
with in the body
{
"JobKey": {
"Name": "JobKeyName",
"Group": "JobKeyGroup"
},
"JobDataMap": {
"Key1": "Value1",
"Key2": "Value2"
}
}
Do a POST
request to Scheduler/PauseJob
with in the body the key of the job
{
"Name": "JobKeyName",
"Group": "JobKeyGroup"
}
Do a POST
request to Scheduler/PauseJobs
with in the body the group matching object that defined what jobs to pause
The values for Type
can be: Contains
, EndsWith
, Equals
or StartsWith
{
"Type": "Contains",
"Value": "<The job keys to match>"
}
Do a POST
request to Scheduler/PauseTrigger
with in the body the triggerkey
{
"Name": "TriggerKeyName",
"Group": "TriggerKeyGroup"
}
Do a POST
request to Scheduler/PauseTriggers
with in the body the group matching object that defined what triggers to pause
The values for Type
can be: Contains
, EndsWith
, Equals
or StartsWith
{
"Type": "Contains",
"Value": "<The trigger keys to match>"
}
Do a POST
request to Scheduler/ResumeJob
with in the body the key of the job
{
"Name": "JobKeyName",
"Group": "JobKeyGroup"
}
Do a POST
request to Scheduler/ResumeJobs
with in the body the group matching object that defined what jobs to resume
The values for Type
can be: Contains
, EndsWith
, Equals
or StartsWith
{
"Type": "Contains",
"Value": "<The job keys to match>"
}
Do a POST
request to Scheduler/ResumeTrigger
with in the body the key of the trigger
{
"Name": "TriggerKeyName",
"Group": "TriggerKeyGroup"
}
Do a POST
request to Scheduler/ResumeTriggers
with in the body the group matching object that defined what triggers to resume
The values for Type
can be: Contains
, EndsWith
, Equals
or StartsWith
{
"Type": "Contains",
"Value": "<The trigger keys to match>"
}
Do a POST
request to Scheduler/PauseAllTriggers
Do a POST
request to Scheduler/ResumeAllTriggers
Do a GET
request to Scheduler/GetJobKeys
The values for Type
can be: Contains
, EndsWith
, Equals
or StartsWith
{
"Type": "Contains",
"Value": "<The job keys to match>"
}
it will return something like this
[
{
"Name": "JobKeyName",
"Group": "JobKeyGroup"
}
]
Do a GET
request to Scheduler/GetTriggersOfJob
with in the body the key of the job
{
"Name": "JobKeyName",
"Group": "JobKeyGroup"
}
It will return something like this
[
{
"TriggerKey": {
"Name": "triggerKey",
"Group": "DEFAULT"
},
"Description": "TestTrigger",
"CalendarName": null,
"CronSchedule": null,
"NextFireTimeUtc": null,
"PreviousFireTimeUtc": "2024-01-28T11:21:14.4966492+01:00",
"StartTimeUtc": "2024-01-28T11:21:14.4966492+01:00",
"EndTimeUtc": null,
"FinalFireTimeUtc": "2024-01-28T11:21:14.4966492+01:00",
"Priority": 5,
"HasMillisecondPrecision": true,
"JobKey": null,
"JobDataMap": null
}
]
Do a GET
request to Scheduler/GetTriggerKeys
The values for Type
can be: Contains
, EndsWith
, Equals
or StartsWith
{
"Type": "Contains",
"Value": "<The group keys to match>"
}
it will return something like this
[
{
"Name": "triggerKey",
"Group": "DEFAULT"
}
]
Do a GET
request to Scheduler/GetJobDetail
with in the body the key of the job
{
"Name": "JobKeyName",
"Group": "JobKeyGroup"
}
It will return something like this
{
"JobKey": {
"Name": "JobKeyName",
"Group": "JobKeyGroup"
},
"Description": "Test",
"JobType": "QuartzWebApi.TestJob",
"JobDataMap": {},
"Durable": false,
"Replace": false,
"StoreNonDurableWhileAwaitingScheduling": false
}
Do a GET
request to Scheduler/GetTrigger
with in the body the key of the trigger
{
"Name": "TriggerKeyName",
"Group": "TriggerKeyGroup"
}
It will return something like this
{
"TriggerKey": {
"Name": "triggerKey",
"Group": "DEFAULT"
},
"Description": "TestTrigger",
"CalendarName": null,
"CronSchedule": null,
"NextFireTimeUtc": null,
"PreviousFireTimeUtc": "2024-01-28T14:09:21.9475007+01:00",
"StartTimeUtc": "2024-01-28T14:09:21.9475007+01:00",
"EndTimeUtc": null,
"FinalFireTimeUtc": "2024-01-28T14:09:21.9475007+01:00",
"Priority": 5,
"HasMillisecondPrecision": true,
"JobKey": null,
"JobDataMap": null
}
Do a GET
request to Scheduler/GetTriggerState
with in the body the key of the trigger
{
"Name": "TriggerKeyName",
"Group": "TriggerKeyGroup"
}
It will return something like this
"Normal"
Do a POST
request to Scheduler/AddCalendar
with in the body the body the calendar you want to add, for example
{
"Name" : "My new CRON calendar",
"Type": "Cron"
"CronExpression": "0 0-5 14 * * ?",
"Description": "my description"
"Replace": true
"UpdateTriggers": true
}
Do a DELETE
request to Scheduler/DeleteCalendar/{calName}
where calName
is het name of the calendar to delete, this will return true
or false
Do a GET
request to Scheduler/GetCalendar/{calName}
where calName
is het name of the calendar to get, when the calendar exists it will return something like this
{
"CronExpression": "0 0-51 4 * * ?",
"Name": null,
"Type": "Cron",
"TimeZone": {
"Id": "W. Europe Standard Time",
"DisplayName": "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
"StandardName": "W. Europe Standard Time",
"DaylightName": "W. Europe Daylight Time",
"BaseUtcOffset": "01:00:00",
"AdjustmentRules": [
{
"DateStart": "0001-01-01T00:00:00",
"DateEnd": "9999-12-31T00:00:00",
"DaylightDelta": "01:00:00",
"DaylightTransitionStart": {
"TimeOfDay": "0001-01-01T02:00:00",
"Month": 3,
"Week": 5,
"Day": 1,
"DayOfWeek": 0,
"IsFixedDateRule": false
},
"DaylightTransitionEnd": {
"TimeOfDay": "0001-01-01T03:00:00",
"Month": 10,
"Week": 5,
"Day": 1,
"DayOfWeek": 0,
"IsFixedDateRule": false
},
"BaseUtcOffsetDelta": "00:00:00"
}
],
"SupportsDaylightSavingTime": true
}
}
Do a GET
request to Scheduler/GetCalendarNames
, it will return something like this
[
"monthlyCalendar",
"MynewCRONcalendar"
]
When an error occures this is returned as a .NET exception in JSON format like this
{
"Message": "An error has occurred.",
"ExceptionMessage": "Unable to store Trigger: 'TriggerKeyGroup.TriggerKeyName', because one already exists with this identification.",
"ExceptionType": "Quartz.ObjectAlreadyExistsException",
"StackTrace": "... the .NET stack trace ..."
}