Quartz .net job is not executing - c#

I've created an ASP.NET MVC website. Then I've created a class library named Site.Scheduler where I wanted to put all my triggers and jobs.
I've created a simple job for testing purposes
public class CurrencyRatesJob : IJob
{
private readonly IBudgetsRepository budgetsRepository;
public CurrencyRatesJob(IBudgetsRepository budgetsRepository)
{
this.budgetsRepository = budgetsRepository;
}
public void Execute(IJobExecutionContext context)
{
try
{
var budgets = new BudgetsDTO();
var user = new UserDTO();
budgets.Sum = 1;
budgets.Name = "Quartz";
user.Email = "email#g.com";
budgetsRepository.InsertBudget(budgets, user);
}
catch (Exception ex)
{
throw new Quartz.JobExecutionException(ex);
}
}
}
and a Job Scheduler
public class CurrencyRatesJobScheduler
{
public static void GetCurrencyRates()
{
try
{
IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
IJobDetail job = JobBuilder.Create<CurrencyRatesJob>().Build();
ITrigger trigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule
(s =>
s.WithIntervalInSeconds(10)
.RepeatForever()
)
.Build();
scheduler.ScheduleJob(job, trigger);
}
catch (Exception ex)
{
ex.ToString();
}
}
}
To start the scheduler when application starts, I've added the following in Global.asax.cs
CurrencyRatesJobScheduler.GetCurrencyRates();
So after all that, I was expecting the job to execute every 10 seconds and insert all that info in the DB, but it doesn't do anything and I get no errors either.
Does anyone know what could be the issue?
EDIT:
So I've created all the necessary tables (executed the script from Quartz.NET) and I've added a new App.config file in my class library
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="quartz.scheduler.instanceName" value="MyQuartzScheduler" />
<add key="quartz.scheduler.instanceId" value="instance_one" />
<add key="quartz.threadPool.type" value="Quartz.Simpl.SimpleThreadPool, Quartz" />
<add key="quartz.threadPool.threadCount" value="10" />
<add key="quartz.threadPool.threadPriority" value="1" />
<add key="quartz.jobStore.type" value="Quartz.Impl.AdoJobStore.JobStoreTX, Quartz" />
<add key="quartz.jobStore.misfireThreshold" value="60000" />
<add key="quartz.jobStore.dataSource" value="default" />
<add key="quartz.jobStore.driverDelegateType" value="Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz" />
<add key="quartz.jobStore.lockHandler.type" value="Quartz.Impl.AdoJobStore.UpdateLockRowSemaphore, Quartz" />
<add key="quartz.jobStore.tablePrefix" value="QRTZ_" />
<add key="quartz.dataSource.default.connectionString" value="Server=(local);Database=My.Database;UID=User;PWD=Password" />
<add key="quartz.dataSource.default.provider" value="SqlServer-20" />
<add key="quartz.jobStore.useProperties" value="true" />
</appSettings>
</configuration>
Still no luck. Besides that, no triggers or jobs were stored in the DB.

You need to call
scheduler.Start();

First thing that stands out, you haven't started scheduler. So quartz is not running.
IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
scheduler.Start();
After starting it the server will scan the db for job details/triggers and proceed accordingly.
However, you also want to add proper identities to your job detail/trigger. These are needed for quartz to create primary keys, otherwise you will get a SchedulerException.
IJobDetail job = JobBuilder.Create<CurrencyRatesJob>()
.WithIdentity("currencyJob", "group1")
.Build();
ITrigger trigger = TriggerBuilder.Create()
.StartNow()
.WithIdentity("currencyJob", "group1")
.WithSimpleSchedule(s => s.WithIntervalInSeconds(10).RepeatForever())
.Build();
Regarding your config file, you need to declare an appropriate quartz section and add the settings there (by default this is where StdSchedulerFactory looks to create properties)
<configuration>
<configSections>
<section name="quartz" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</configSections>
<quartz>
<add key="quartz.scheduler.instanceName" value="MyQuartzScheduler" />
<add key="quartz.scheduler.instanceId" value="AUTO" />
...
</quartz>
</configuration>

i am also facing the same issue. i fixed the issue by placing config information in the app.config in the consuming application. As Class Library does not capable of running independently. It should be referenced by another app maybe console or web site. we need to place all config items in the consuming application this may be help. cheers. happy sharing

Related

How to configure App.config to remove Serilog.Sinks.MSSqlServer standard columns?

I use Serilog for logging. I need to configure App.config as to remove 2 standard columns from Logs table (StandardColumn.Properties and StandardColumn.MessageTemplate). I've searched for it in Serilog docs and other resources but can't find how to do it. This is what I got so far:
<configuration>
<appSettings>
<add key="serilog:using:MSSqlServer" value="Serilog.Sinks.MSSqlServer" />
<add key="serilog:write-to:MSSqlServer.connectionString" value="Data Source=SomeServer;initial catalog=DB; integrated security=True" />
<add key="serilog:write-to:MSSqlServer.tableName" value="Logs" />
<add key="serilog:minimum-level" value="Debug" />
</appSettings>
</configuration>
I guess I would have to add this line:
<add key="serilog:write-to:MSSqlServer.columnOptions" value="someColOptions" />
but how should I define someColOptions? For example, I removed columns using this kind of code:
var colOptions = new Serilog.Sinks.MSSqlServer.ColumnOptions();
colOptions.Store.Remove(StandardColumn.Properties);
colOptions.Store.Remove(StandardColumn.MessageTemplate);
Log.Logger = new LoggerConfiguration()
.WriteTo
.MSSqlServer(
connectionString: conn_string,
sinkOptions: new MSSqlServerSinkOptions { TableName = "Logs" },
columnOptions: colOptions)
.MinimumLevel.Debug()
.CreateLogger();
and now I want to do the same in App.config
I had a similar question, I've been able to find out that by adding the following, the configSections has to be first section after
that this will:
create a Logs table in the specified Database (from the connection string)
Use the Serilog database account (from the connection string)
Remove the column called Properties from the Logs table
Add a new column called RunId that declared as an int
<configuration>
<configSections>
<section name="MSSqlServerSettingsSection" type="Serilog.Configuration.MSSqlServerConfigurationSection, Serilog.Sinks.MSSqlServer"/>
</configSections>
<MSSqlServerSettingsSection DisableTriggers="false" ClusteredColumnstoreIndex="false" PrimaryKeyColumnName="Id">
<!-- SinkOptions parameters -->
<TableName Value="Logs"/>
<BatchPeriod Value="00:00:15"/>
<RemoveStandardColumns>
<remove Name="Properties"/>
</RemoveStandardColumns>
<Columns>
<add ColumnName="RunId" DataType="int"/>
</Columns>
</MSSqlServerSettingsSection>
<appSettings>
<!-- Serilog Settings -->
<add key="serilog:minimum-level" value="Verbose" />
<add key="serilog:using:Console" value="Serilog.Sinks.Console"/>
<add key="serilog:write-to:Console"/>
<add key="serilog:using:File" value="Serilog.Sinks.File" />
<add key="serilog:write-to:File"/>
<add key="serilog:write-to:File.path" value="{PATH}" />
<add key="serilog:write-to:File.rollingInterval" value="Day"/>
<add key="serilog:using:MSSqlServer" value="Serilog.Sinks.MSSqlServer" />
<add key="serilog:write-to:MSSqlServer.connectionString" value="Server=[SERVER];Database=[DATABASE];User Id=Serilog;Password=[PASSWORD];"/>
<add key="serilog:write-to:MSSqlServer.tableName" value="Logs"/>
<add key="serilog:write-to:MSSqlServer.autoCreateSqlTable" value="true"/>
</appSettings>
</configuration>
Calling Log.("{RunId}{Message}", _runId, _messageToShow) causes my File, Console AND MSSqlServer sinks to execute writing to these, if it doesn't exist the Logs table will be created, removing the Properties column and adding the RunId column. (If the table exists it doesn't do anything but write the data, still trying to work out how to handle that).
I'm still playing around with this (my next thing is to work out how to write {RunId} but not have it appear in the log, only written to the corresponding database field).
HTH,
Phil

Serilog ReadFrom.Configuration is not working in .net framework web Api with configuration stored in web.config

I am using serilog in a web api in .net framework with the serilog config added in the Web.Config file as below
<appSettings>
<add key="serilog:minimum-level" value="Information" />
<add key="serilog:using:File" value="Serilog.Sinks.File" />
<add key="serilog:write-to:File.path" value="C:\api\logs\api_log.log" />
<add key="serilog:write-to:File.formatter" value="Serilog.Formatting.Json.JsonFormatter, Serilog" />
<add key="serilog:write-to:File.rollingInterval" value="Day" />
<add key="serilog:write-to:File.fileSizeLimitBytes" value="5242880" />
<add key="serilog:write-to:File.rollOnFileSizeLimit" value="true" />
<add key="serilog:write-to:File.shared" value="true" />
</appSettings>
I have registered the logger in the startup file as below
private static ILogger ConfigureSerilog()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.Add(new XmlConfigurationProvider())
.Build();
LoggerConfiguration loggerConfiguration = new LoggerConfiguration().Enrich.With(new SerilogClaimValueEnricher())
.Enrich.WithDemystifiedStackTraces()
.ReadFrom.Configuration(configuration);
Logger rootLogger = loggerConfiguration.CreateLogger();
Log.Logger = rootLogger;
return rootLogger;
}
The configuration object is getting all the values that we are given in the web.config but the logging is not working. But instead of ReadFrom.Configuration(configuration) if I use ReadFrom.AppSettings() the logging is working corectly. But I need make use of IConfigurationRoot here. Did I missed anything?
Please not XmlConfigurationProvider is a custom class to read keys from web.config
Below is the screenshot of debug mode states of the configuration.

Microsoft B2C Azure Demo Not Authorized Error

I am trying to learn ASP.Net with Azure AD B2C Login / Register flow. I am running the demo here:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/tutorial-web-api-dotnet?tabs=app-reg-ga
The C# code for the demo can be downloaded from https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/archive/master.zip
I have gone all the way through the demo from the beginning and have completed all of the pre-requisites.
I am at the point now where I have signed in successfully and when I click the To-Do List link while debugging the application, I get a User Not Authorized (404) error.
I apologize in advance if I am not explaining what I think I am seeing very well, as I am very new to Azure and web programming. I am most comfortable with Windows Desktop applications interfacing with SQL Server, but I am trying to expand my knowledge, so please bear with me.
As I stated before, I can successfully log-in to the application, which I believe happens in the TaskWebApp project.
Here is the code where the error is happening, which is in the TasksController.cs in the TaskWebApp project:
namespace TaskWebApp.Controllers
{
[Authorize]
public class TasksController : Controller
{
private readonly string apiEndpoint = Globals.ServiceUrl + "/api/tasks/";
// GET: Makes a call to the API and retrieves the list of tasks
public async Task<ActionResult> Index()
{
try
{
// Retrieve the token with the specified scopes
var scope = new string[] { Globals.ReadTasksScope };
IConfidentialClientApplication cca = MsalAppBuilder.BuildConfidentialClientApplication();
var accounts = await cca.GetAccountsAsync();
AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault()).ExecuteAsync();
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiEndpoint);
// Add token to the Authorization header and make the request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
// Handle the response
switch (response.StatusCode)
{
case HttpStatusCode.OK:
string responseString = await response.Content.ReadAsStringAsync();
JArray tasks = JArray.Parse(responseString);
ViewBag.Tasks = tasks;
return View();
case HttpStatusCode.Unauthorized:
return ErrorAction("Please sign in again. " + response.ReasonPhrase);
default:
return ErrorAction("Error. Status code = " + response.StatusCode + ": " + response.ReasonPhrase);
}
}
catch (MsalUiRequiredException ex)
{
/*
If the tokens have expired or become invalid for any reason, ask the user to sign in again.
Another cause of this exception is when you restart the app using InMemory cache.
It will get wiped out while the user will be authenticated still because of their cookies, requiring the TokenCache to be initialized again
through the sign in flow.
*/
return new RedirectResult("/Account/SignUpSignIn?redirectUrl=/Tasks");
}
catch (Exception ex)
{
return ErrorAction("Error reading to do list: " + ex.Message);
}
}
The response status code in the Switch statement is 404.
When I debug, here is what I see:
var scope returns https://ShoppingCartB2C.onmicrosoft.com/tasks/demo.read
cca returns (I am questioning the format of the Authority property):
accounts returns nothing. A count of 0.
I believe 0 accounts is the problem.
When I try to get result, it goes to the catch block.
Here is the Web.config for the TaskWebApp project:
<configuration>
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="ida:Tenant" value="ShoppingCartB2C.onmicrosoft.com" />
<!--MSAL cache needsĀ a tenantId along with the user's objectId to function. It retrieves these two from the claims returned in the id_token.
As tenantId is not guaranteed to be present in id_tokens issued by B2C unless the steps listed in this
document (https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/AAD-B2C-specifics#caching-with-b2c-in-msalnet).
If you are following the workarounds listed in the doc and tenantId claim (tid) is available in the user's token, then please change the
code in <ClaimsPrincipalsExtension.cs GetB2CMsalAccountId()> to let MSAL pick this from the claims instead -->
<add key="ida:TenantId" value="db1b052a-415c-4604-887c-e27b59860001" />
<add key="ida:ClientId" value="975f1457-e3e2-4cb8-b069-6b0b6b46611d" />
<add key="ida:ClientSecret" value="Gw4.3o-DRDr.j_828H-JMfsk_Jd1d-jQ5p" />
<add key="ida:AadInstance" value="https://ShoppingCartB2C.b2clogin.com/tfp/{0}/{1}" />
<add key="ida:RedirectUri" value="https://localhost:44316/" />
<add key="ida:SignUpSignInPolicyId" value="B2C_1_signupsignin1" />
<add key="ida:EditProfilePolicyId" value="b2c_1_profileediting1" />
<add key="ida:ResetPasswordPolicyId" value="b2c_1_passwordreset1" />
<add key="api:TaskServiceUrl" value="https://localhost:44332/" />
<!-- The following settings is used for requesting access tokens -->
<add key="api:ApiIdentifier" value="https://ShoppingCartB2C.onmicrosoft.com/tasks/" />
<add key="api:ReadScope" value="demo.read" />
<add key="api:WriteScope" value="demo.write" />
</appSettings>
And for the TaskService project:
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="ida:AadInstance" value="https://ShoppingCartB2C.b2clogin.com/{0}/{1}/v2.0/.well-known/openid-configuration" />
<add key="ida:Tenant" value="ShoppingCartB2C.onmicrosoft.com" />
<add key="ida:ClientId" value="975f1457-e3e2-4cb8-b069-6b0b6b46611d" />
<add key="ida:SignUpSignInPolicyId" value="B2C_1_signupsignin1" />
<!-- The following settings is used for requesting access tokens -->
<add key="api:ReadScope" value="demo.read" />
<add key="api:WriteScope" value="demo.write" />
</appSettings>
If you would like screen shots from Azure, or have questions about how that is configured, feel free to ask.
I am not concerned about exposing client secrets or AppId's because I am just following a demo. This is never going to be a production app.
I have not made any code modifications to the demo. Thanks for your help.
Edit: Showing API Permissions

Quartz.net version 2.3.3 - setting up AdoJobStore

I am using an ASP.Net website as a sort of back-end control panel, and I want to set it up so that when the user adds something to my database, it will create/schedule a job to send out a reminder to everybody on some date.
I have the job and trigger parts down and working, however I want to set it up to use AdoJobStore so that these jobs won't be lost (in case there is an instance where the reminder doesn't have to be sent out for a whole month or two).
I've tried using their official tutorial, some relevant posts here, and other guides I've found through google, but I can't figure out how to set this up at all. Most provide code that needs to be added to some kind of configuration file, but I can't seem to find any - I have seen them saying to edit quartz.config, web.config, or quartz.properties. I can only find the web.config in ASP.net my project, but I can't seem to get any of the examples working inside this file.
You need to define an ADOJobStore in your app.config/web.config and then some quartz.config. I call mine QuartzDataStoreSettingsDatabase.config
<configSections>
<section name="quartz" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<quartz configSource="QuartzDataStoreSettingsDatabase.config" />
Then the file for quartz specific stuff
QuartzDataStoreSettingsDatabase.config
<quartz>
<add key="quartz.scheduler.instanceName" value="ExampleDefaultQuartzSchedulerFromConfigFileSqlServer"/>
<add key="quartz.scheduler.instanceId" value="instance_one"/>
<add key="quartz.threadPool.threadCount" value="10"/>
<add key="quartz.threadPool.threadPriority" value="Normal"/>
<!--
org.quartz.scheduler.idleWaitTime
Is the amount of time in milliseconds that the scheduler will wait before re-queries for available triggers when the scheduler is otherwise idle. Normally you should not have to 'tune' this parameter, unless you're using XA transactions, and are having problems with delayed firings of triggers that should fire immediately.
It defaults to every 30 seconds until it finds a trigger. Once it finds any triggers, it gets the time of the next trigger to fire and stops checking until then, unless a trigger changes. -->
<add key="quartz.scheduler.idleWaitTime" value ="5000"/>
<!-- Misfire : see http://nurkiewicz.blogspot.com/2012/04/quartz-scheduler-misfire-instructions.html -->
<add key="quartz.jobStore.misfireThreshold" value="60000"/>
<add key="quartz.jobStore.type" value="Quartz.Impl.AdoJobStore.JobStoreTX, Quartz"/>
<add key="quartz.jobStore.tablePrefix" value="QRTZ_"/>
<add key="quartz.jobStore.clustered" value="false"/>
<add key="quartz.jobStore.driverDelegateType" value="Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz"/>
<add key="quartz.jobStore.dataSource" value="MySqlServerFullVersion"/>
<add key="quartz.jobStore.useProperties" value="false"/>
<add key="quartz.dataSource.MySqlServerFullVersion.connectionString" value="SuperSecret!!"/>
<add key="quartz.dataSource.MySqlServerFullVersion.provider" value="SqlServer-20"/>
</quartz>
See my answer here:
Unable to save anything to the Quartz.net ado store
Then when an "event" happens in your website world.......you need to schedule a job programatically.
NameValueCollection config = (NameValueCollection)ConfigurationManager.GetSection("quartz");
ShowConfiguration(config, logger);
ISchedulerFactory factory = new StdSchedulerFactory(config);
IScheduler sched = factory.GetScheduler();
/* the below code has to be tweaked for YOUR Job */
IJobDetail textFilePoppingJobJobDetail = JobBuilder.Create<TextFilePoppingNonConcurrentJob>()
.WithIdentity("textFilePoppingJobJobDetail001", "groupName007")
.UsingJobData("JobDetailParameter001", "Abcd1234")
.Build();
ITrigger textFilePoppingJobJobDetailTrigger001 = TriggerBuilder.Create()
.WithIdentity("textFilePoppingJobJobDetailTrigger001", "groupName007")
.UsingJobData("TriggerParameter001", "Bcde2345")
.UsingJobData("TempDirectorySubFolderName", "MyTempDirectorySubFolderName")
.UsingJobData("DestinationFullFolderName", #"C:\SomeFolder")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever()
/* .WithRepeatCount(1) */
)
.Build();
sched.ScheduleJob(textFilePoppingJobJobDetail, textFilePoppingJobJobDetailTrigger001);

Quartz.net JobStoreTX Job Save issue

I'm trying to run Quartz.net server for my website maintance tasks.
I create Jobs and triggers in my WCF application (hosted on IIS). So they can be stored in database (SQL Server).
Now i can't understand ADO.NET Job Store.
Here is my web.config part for Quartz.net:
<configSections>
<section name="quartz" type="System.Configuration.NameValueSectionHandler" />
</configSections>
<quartz>
<add key="quartz.threadPool.type" value="Quartz.Simpl.SimpleThreadPool, Quartz" />
<add key="quartz.threadPool.threadCount" value="10" />
<add key="quartz.threadPool.threadPriority" value="Normal" />
<add key="quartz.jobStore.misfireThreshold" value="60000" />
<add key="quartz.jobStore.type" value="Quartz.Impl.AdoJobStore.JobStoreTX, Quartz" />
<add key="quartz.jobStore.driverDelegateType" value="Quartz.Impl.AdoJobStore.**SqlServerDelegate**, Quartz" />
<add key="quartz.jobStore.tablePrefix" value="QRTZ_" />
<add key="quartz.jobStore.dataSource" value="ConnectionString" />
<add key="quartz.dataSource.ConnectionString.connectionString" value="Server=*;Database=*;Uid=*;Pwd=*" />
<add key="quartz.dataSource.ConnectionString.provider" value="SqlServer-20" />
<add key="quartz.scheduler.instanceName" value="PaymentService" />
<add key="quartz.jobStore.useProperties" value="true" />
</quartz>
And here is my global.asax:
public class Global : System.Web.HttpApplication
{
public static ISchedulerFactory Factory;
public static IScheduler Scheduler;
protected void Application_Start(Object sender, EventArgs e)
{
Factory = new StdSchedulerFactory();
Scheduler = Factory.GetScheduler();
JobKey JobKey = new JobKey("GetOrderInfoJob", "Project");
if (Scheduler.CheckExists(JobKey))
Scheduler.DeleteJob(JobKey);
IJobDetail job = JobBuilder.Create<PaymentServiceLogic>()
.WithIdentity(JobKey)
.StoreDurably()
.RequestRecovery()
.Build();
TriggerKey triggerKey = new TriggerKey("GetOrderInfoTrigger", "Project");
TriggerBuilder tb = TriggerBuilder.Create();
tb.WithIdentity(triggerKey );
tb.WithSimpleSchedule(a => a.WithIntervalInMinutes(1));
tb.StartNow();
tb.ForJob(job);
ITrigger trigger = tb.Build();
Scheduler.ScheduleJob(trigger);
Scheduler.Start();
}
}
Once i've started WCF service on localhost, QRTZ_JOB_DETAILS table in SQL Server for jobs had 1 entry, but no triggers.
I've tested this code a couple times and now no jobs are storing and therefore i have this exception:
Couldn't store trigger job: The job referenced by the trigger does not exist.
Is there some bug with job's building or AdoJobStore?
And the second question is about how to do correct shutdown in global.asax. Now i've decided this method:
protected void Application_End(object sender, EventArgs e)
{
ICollection<IScheduler> all = Factory.AllSchedulers;
foreach (IScheduler item in all)
{
item.Shutdown(true);
}
}
and implementing my own logging in Application_Error.
Note, sure whether it is a good idea to ask two questions in one, but the answer to your first question is you need to change
Scheduler.ScheduleJob(trigger);
to
Scheduler.ScheduleJob(job, trigger);
The former is when the job has previously been added to the scheduler, whereas the latter adds both the job and trigger at the same time.

Categories

Resources