Implementing service worker in existing ASP.NET Core MVC application - asp.net-core

I'm developing an ASP.NET Core MVC web application where I have these two tasks that should be running as background services:
Set the user status as "Expired" if EndOfSubscription date is == DateTime.Now
Before 1 month of EndOfSubscription date send a reminder e-mail to this user
After searching, I found that I can use service worker to implement this. But I'm totally confused how to use this service worker in existing ASP.NET Core MVC web application where I need to access my models and database.
Should I isolate these tasks in a separate service worker project? But in this case should I share the same database for both projects?
Can someone guide me with main steps in this kind of situations?
Thank you in advance.

Service worker or Worker service?
A Service Worker is a way to run background tasks in a browser and definitely unsuitable if you want to execute something on the server.
A Worker service is essentially a template with the (few) calls needed to run a BackgroundService/IHostedService in a console application and (optionally, through extensions) as a Linux daemon or Windows service. You don't need that template to create and run a BackgroundService.
The tutorial Background tasks with hosted services in ASP.NET Core shows how to create and use a BackgroundService but is a bit ... overengineered. The article tries to show too many things at the same time and ends up missing some essential things.
A better introduction is Steve Gordon's What are Worker Services?.
The background service
All that's needed to create a background service, is a class that implements the IHostedService interface. Instead of implementing all the interface methods, it's easier to inherit from the BackgroundService base class and override just the ExecuteAsync method.
The article's example shows this method doesn't need to be anything fancy:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
That's just a loop with a delay. This will run until the web app terminates and signals the stoppingToken. This service will be created by the DI container, so it can have service dependencies like ILogger or any other singleton service.
Registering the service
The background service needs to be registered as a service in ConfigureServices, the same way any other service is registered. If you have a console application, you configure it in the host's ConfigureServices call. If you have a web application, you need to register it in Startup.ConfigureServices:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<OrdersContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
...
//Register the service
services.AddHostedService<Worker>();
services.AddRazorPages();
}
This registers Worker as a service that can be constructed by the DI container and adds it to the list of hosted services that will start once .Run() is called in the web app's Main :
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
Using DbContext and other scoped services
Adding a DbContext as a dependency is trickier, since DbContext is a scoped service. We can't just inject a DbContext instance and store it in a field - a DbContext is meant to be used as a Unit-of-Work, something that collects all changes made for a single scenario and either commit all of them to the database or discard them. It's meant to be used inside a using block. If we dispose the single DbContext instance we injected though, where do we get a new one?
To solve this, we have to inject the DI service, IServiceProvider, create a scope explicitly and get our DbContext from this scope:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IServiceProvider _services;
//Inject IServiceProvider
public Worker(IServiceProvider services, ILogger<Worker> logger)
{
_logger = logger;
_services=services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
//Create the scope
using (var scope = _services.CreateScope())
{
//Create OrdersContext in the scope
var ctx = scope.ServiceProvider.GetRequiredService<OrdersContext>();
var latestOrders = await ctx.Orders
.Where(o=>o.Created>=DateTime.Today)
.ToListAsync();
//Make some changes
if (allOK)
{
await ctx.SaveChangesAsync();
}
}
//OrdersContext will be disposed when exiting the scope
...
}
}
}
The OrdersContext will be disposed when the scope exits and any unsaved changes will be discarded.
Nothing says the entire code needs to be inside ExecuteAsync. Once the code starts getting too long, we can easily extract the important code into a separate method :
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
using (var scope = _services.CreateScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<OrdersContext>();
await DoWorkAsync(ctx,stoppingToken);
}
await Task.Delay(1000, stoppingToken);
}
}
private async Task DoWorkAsync(OrdersContext ctx,CancellationToken stoppingToken)
{
var latestOrders = await ctx.Orders
.Where(o=>o.Created>=DateTime.Today)
.ToListAsync();
//Make some changes
if (allOK)
{
await ctx.SaveChangesAsync();
}
}

Related

How to start an ASP.NET Core BackgroundService on demand?

I want to be able to start fire-and-forget jobs in ASP.NET Core 2.2. I have tried the following:
services.AddHostedService<TestHostedService>();
public class TestHostedService : BackgroundService
{
private readonly ILogger _logger;
public TestHostedService(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<TestHostedService>();
}
public IBackgroundTaskQueue TaskQueue { get; }
protected async override Task ExecuteAsync(
CancellationToken cancellationToken)
{
_logger.LogInformation("TestHostedService is starting.");
_logger.LogInformation("TestHostedService is stopping.");
}
}
However, this automatically starts and I want to be able to start it on demand, similarly to how Hangfire allows:
BackgroundJob.Enqueue<TestJob>(x => x.DoWork());
This also allows the job to naturally use ASP.NET CORE DI.
Question: How to start an ASP.NET Core BackgroundService on demand?
###Background information
I am dealing with an application that needs to fire-and-forget various methods. The already written code looks like this:
Task.Run(() => RunSomething(_serviceScopeFactory));
This means that each method must explicitly deal with getting a scope and retrieving the dependencies which is quite ugly.
If you want to run the BackgroundService in the MVC controller or other service. You could try to inject the IServiceProvider to that class and then loop all the hosted service and find the background service, at last you could call the startasync method.
More details, you could refer to below codes:
Register the service in Startup.cs
services.AddHostedService<TestHostedService>();
Execute the background service in the controller:
public class HomeController : Controller
{
private readonly IServiceProvider _serviceProdiver;
public HomeController(IServiceProvider serviceProdiver) {
_serviceProdiver = serviceProdiver;
}
public async Task<IActionResult> Index()
{
var allBackgroundServices = _serviceProdiver.GetServices<IHostedService>();
foreach (var hostedService in allBackgroundServices)
{
if (hostedService.GetType() == typeof(TestHostedService))
{
await hostedService.StartAsync(CancellationToken.None);
}
}
return View();
}
}
Result:

Background task in asp.net core mvc 3.1

I want to run a background task in an asp.net core mvc application.
Here is what i've done:
in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHostedService<MyTask>();
}
in MyTask.cs:
public class MyTask: BackgroundService
{
...
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("StartAsync");
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("StopAsync");
}
...
}
Here what i've notice:
- When i am deploying my website on IIS, i need to hit a page in order to start service
- I have notice the Stop is called if i do nothing.
My question is: How to keep alive my application ?
I need to run task each minute...
Thanks
In ASP.NET Core, background tasks can be implemented as hosted services. A hosted service is a class with background task logic that implements the IHostedService interface.
A timed background task makes use of the System.Threading.Timer class. The timer triggers the task's DoWork method. The timer is disabled on StopAsync and disposed when the service container is disposed on Dispose:
public class TimedHostedService : IHostedService, IDisposable
{
private int executionCount = 0;
private readonly ILogger<TimedHostedService> _logger;
private Timer _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service running.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object state)
{
var count = Interlocked.Increment(ref executionCount);
_logger.LogInformation(
"Timed Hosted Service is working. Count: {Count}", count);
}
public Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
The Timer doesn't wait for previous executions of DoWork to finish, so the approach shown might not be suitable for every scenario. Interlocked.Increment is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update executionCount concurrently.
The service is registered in IHostBuilder.ConfigureServices (Program.cs) with the AddHostedService extension method:
services.AddHostedService<TimedHostedService>();
To see more detail click below link : Microsoft Documentation
This seems to be default behaviour for asp.net. You need to make asp.net app 'always running'. You can do so from the IIS manager aswell as some config files i believe.
There're lots of articles out there on this like:
https://www.taithienbo.com/how-to-auto-start-and-keep-an-asp-net-core-web-application-and-keep-it-running-on-iis/
Additionaly i've had success with Hangfire; https://www.hangfire.io/ you can even move the processing of background threads to a different process - even on a different host.
Also see:
https://docs.hangfire.io/en/latest/deployment-to-production/making-aspnet-app-always-running.html
Instead of running the background task in you MVC application, perhaps you should run it as a windows service. It is understandable that IIS isn't starting you background task until you hit the first page - that's when it starts your app. Check out https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio

Autofac, thread and ISErviceScopeFactory

I have a problem: the service provider is disposed when arrives in the method.
Is this an issue or it is my fault?
My service
public class BomService
{
private readonly IServiceScopeFactory _scope;
public BomService(IServiceScopeFactory scope)
{
_scope = scope;
}
public void ImportAsync(ImportRequestDto importSettings)
{
Task.Run(async () => await ImportFile.ImportAsync<Bom, CatalogContext>(_scope));
}
}
Method
public static async Task ImportAsync<T, TContext>(IServiceScopeFactory parentScope) where T : class where TContext : DbContext
{
using var scope = parentScope.CreateScope();
var repo = scope.ServiceProvider.GetService<IGenericRepository<T, TContext>>();
}
The error:
Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed
Do not run long running tasks in an HTTP request, these should be done in a separate process while returning a response to the client immediately.
Refer to the answer of this question
As also suggested there you can use something like Hangfire to run background processes.
Update
It is not good practice to inject IServiceScopeFactory. Like that you are implementing the Service Locator anti pattern. Instead inject the repository directly and let the DI figure out the resolution and scope.

Service Injected on startup is null in Extension Service Configuration aspnet core

Service Injected on startup is null in Extension Service Configuration ASP.NET Core
We have one service for userservice to save user profiles and it is injected as scoped on startup.
In our extension, we add another service for students as singleton to insert update delete users' transactions. We want student info from userservice but in our student service it showing null.
In Startup.
services.AddMemoryCache();
services.AddSingleton<ITempDataProvider, CookieTempDataProvider>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IUserService, UserService>();
In Extension
public void Execute(IServiceCollection serviceCollection, IServiceProvider serviceProvider)
{
serviceCollection.AddMvc();
serviceCollection.AddSingleton<IStudenService, StudenService>();
}
In Student Service
public class StudentSerivce : IStudentSerivce
{
private readonly IUserService _userService;
public StudentSerivce(IUserService userService)
{
_userService = userService; // is null
}
}
You could try this:
public StudentService(IServiceProvider services)
{
Services = services;
}
public IServiceProvider Services { get; }
public void SomeMethod()
{
using (var scope = Services.CreateScope())
{
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
// Do Something
}
}
It would probably be better to make them both scoped or both singletons (then it would work without manually creating a scope).
It's dangerous to resolve a scoped service from a singleton. It may cause the service to have incorrect state when processing subsequent requests.
Reference: Dependency injection in ASP.NET Core

How to start Quartz in ASP.NET Core?

I have the following class
public class MyEmailService
{
public async Task<bool> SendAdminEmails()
{
...
}
public async Task<bool> SendUserEmails()
{
...
}
}
public interface IMyEmailService
{
Task<bool> SendAdminEmails();
Task<bool> SendUserEmails();
}
I have installed the latest Quartz 2.4.1 Nuget package as I wanted a lightweight scheduler in my web app without a separate SQL Server database.
I need to schedule the methods
SendUserEmails to run every week on Mondays 17:00,Tuesdays 17:00 & Wednesdays 17:00
SendAdminEmails to run every week on Thursdays 09:00, Fridays 9:00
What code do I need to schedule these methods using Quartz in ASP.NET Core? I also need to know how to start Quartz in ASP.NET Core as all code samples on the internet still refer to previous versions of ASP.NET.
I can find a code sample for the previous version of ASP.NET but I don't know how to start Quartz in ASP.NET Core to start testing.
Where do I put the JobScheduler.Start(); in ASP.NET Core?
TL;DR (full answer can be found below)
Assumed tooling: Visual Studio 2017 RTM, .NET Core 1.1, .NET Core SDK 1.0, SQL Server Express 2016 LocalDB.
In web application .csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- .... existing contents .... -->
<!-- add the following ItemGroup element, it adds required packages -->
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
In the Program class (as scaffolded by Visual Studio by default):
public class Program
{
private static IScheduler _scheduler; // add this field
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
StartScheduler(); // add this line
host.Run();
}
// add this method
private static void StartScheduler()
{
var properties = new NameValueCollection {
// json serialization is the one supported under .NET Core (binary isn't)
["quartz.serializer.type"] = "json",
// the following setup of job store is just for example and it didn't change from v2
// according to your usage scenario though, you definitely need
// the ADO.NET job store and not the RAMJobStore.
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "false",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core
["quartz.dataSource.default.connectionString"] = #"Server=(localdb)\MSSQLLocalDB;Database=Quartz;Integrated Security=true"
};
var schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = schedulerFactory.GetScheduler().Result;
_scheduler.Start().Wait();
var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>()
.WithIdentity("SendUserEmails")
.Build();
var userEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("UserEmailsCron")
.StartNow()
.WithCronSchedule("0 0 17 ? * MON,TUE,WED")
.Build();
_scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait();
var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>()
.WithIdentity("SendAdminEmails")
.Build();
var adminEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("AdminEmailsCron")
.StartNow()
.WithCronSchedule("0 0 9 ? * THU,FRI")
.Build();
_scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait();
}
}
An example of a job class:
public class SendUserEmailsJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
// an instance of email service can be obtained in different ways,
// e.g. service locator, constructor injection (requires custom job factory)
IMyEmailService emailService = new MyEmailService();
// delegate the actual work to email service
return emailService.SendUserEmails();
}
}
Full answer
Quartz for .NET Core
First, you have to use v3 of Quartz, as it targets .NET Core, according to this announcement.
Currently, only alpha versions of v3 packages are available on NuGet. It looks like the team put a lot of effort into releasing 2.5.0, which does not target .NET Core. Nevertheless, in their GitHub repo, the master branch is already dedicated to v3, and basically, open issues for v3 release don't seem to be critical, mostly old wishlist items, IMHO. Since recent commit activity is quite low, I would expect v3 release in few months, or maybe half year - but no one knows.
Jobs and IIS recycling
If the web application is going to be hosted under IIS, you have to take into consideration recycling/unloading behavior of worker processes. The ASP.NET Core web app runs as a regular .NET Core process, separate from w3wp.exe - IIS only serves as a reverse proxy. Nevertheless, when an instance of w3wp.exe is recycled or unloaded, the related .NET Core app process is also signaled to exit (according to this).
Web application can also be self-hosted behind a non-IIS reverse proxy (e.g. NGINX), but I will assume that you do use IIS, and narrow my answer accordingly.
The problems that recycling/unloading introduces are explained well in the post referenced by #darin-dimitrov:
If for example, on Friday 9:00 the process is down, because several hours earlier it was unloaded by IIS due to inactivity - no admin emails will be sent until the process is up again. To avoid that, configure IIS to minimize unloads/recyclings (see this answer).
From my experience, the above configuration still doesn't give a 100% guarantee that IIS will never unload the application. For 100% guarantee that your process is up, you can setup a command that periodically sends requests to your application, and thus keeps it alive.
When the host process is recycled/unloaded, the jobs must be gracefully stopped, to avoid data corruption.
Why would you host scheduled jobs in a web app
I can think of one justification of having those email jobs hosted in a web app, despite the problems listed above. It is decision to have only one kind of application model (ASP.NET). Such approach simplifies learning curve, deployment procedure, production monitoring, etc.
If you don't want to introduce backend microservices (which would be a good place to move the email jobs to), then it makes sense to overcome IIS recycling/unloading behaviors, and run Quartz inside a web app.
Or maybe you have other reasons.
Persistent job store
In your scenario, status of job execution must be persisted out of process. Therefore, default RAMJobStore doesn't fit, and you have to use the ADO.NET Job Store.
Since you mentioned SQL Server in the question, I will provide example setup for SQL Server database.
How to start (and gracefully stop) the scheduler
I assume you use Visual Studio 2017 and latest/recent version of .NET Core tooling. Mine is .NET Core Runtime 1.1 and .NET Core SDK 1.0.
For DB setup example, I will use a database named Quartz in SQL Server 2016 Express LocalDB. DB setup scripts can be found here.
First, add required package references to web application .csproj (or do it with NuGet package manager GUI in Visual Studio):
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- .... existing contents .... -->
<!-- the following ItemGroup adds required packages -->
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
With the help of Migration Guide and the V3 Tutorial, we can figure out how to start and stop the scheduler. I prefer to encapsulate this in a separate class, let's name it QuartzStartup.
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using Quartz;
using Quartz.Impl;
namespace WebApplication1
{
// Responsible for starting and gracefully stopping the scheduler.
public class QuartzStartup
{
private IScheduler _scheduler; // after Start, and until shutdown completes, references the scheduler object
// starts the scheduler, defines the jobs and the triggers
public void Start()
{
if (_scheduler != null)
{
throw new InvalidOperationException("Already started.");
}
var properties = new NameValueCollection {
// json serialization is the one supported under .NET Core (binary isn't)
["quartz.serializer.type"] = "json",
// the following setup of job store is just for example and it didn't change from v2
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "false",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core
["quartz.dataSource.default.connectionString"] = #"Server=(localdb)\MSSQLLocalDB;Database=Quartz;Integrated Security=true"
};
var schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = schedulerFactory.GetScheduler().Result;
_scheduler.Start().Wait();
var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>()
.WithIdentity("SendUserEmails")
.Build();
var userEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("UserEmailsCron")
.StartNow()
.WithCronSchedule("0 0 17 ? * MON,TUE,WED")
.Build();
_scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait();
var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>()
.WithIdentity("SendAdminEmails")
.Build();
var adminEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("AdminEmailsCron")
.StartNow()
.WithCronSchedule("0 0 9 ? * THU,FRI")
.Build();
_scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait();
}
// initiates shutdown of the scheduler, and waits until jobs exit gracefully (within allotted timeout)
public void Stop()
{
if (_scheduler == null)
{
return;
}
// give running jobs 30 sec (for example) to stop gracefully
if (_scheduler.Shutdown(waitForJobsToComplete: true).Wait(30000))
{
_scheduler = null;
}
else
{
// jobs didn't exit in timely fashion - log a warning...
}
}
}
}
Note 1. In the above example, SendUserEmailsJob and SendAdminEmailsJob are classes that implement IJob. The IJob interface is slightly different from IMyEmailService, because it returns void Task and not Task<bool>. Both job classes should get IMyEmailService as a dependency (probably constructor injection).
Note 2. For a long-running job to be able to exit in timely fashion, in the IJob.Execute method, it should observe the status of IJobExecutionContext.CancellationToken. This may require change in IMyEmailService interface, to make its methods receive CancellationToken parameter:
public interface IMyEmailService
{
Task<bool> SendAdminEmails(CancellationToken cancellation);
Task<bool> SendUserEmails(CancellationToken cancellation);
}
When and where to start and stop the scheduler
In ASP.NET Core, application bootstrap code resides in class Program, much like in console app. The Main method is called to create web host, run it, and wait until it exits:
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
host.Run();
}
}
The simplest thing to do is just put a call to QuartzStartup.Start right in the Main method, much like as I did in TL;DR. But since we have to properly handle process shutdown as well, I prefer to hook both startup and shutdown code in a more consistent manner.
This line:
.UseStartup<Startup>()
refers to a class named Startup, which is scaffolded when creating new ASP.NET Core Web Application project in Visual Studio. The Startup class looks like this:
public class Startup
{
public Startup(IHostingEnvironment env)
{
// scaffolded code...
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// scaffolded code...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// scaffolded code...
}
}
It is clear that a call to QuartzStartup.Start should be inserted in one of methods in the Startup class. The question is, where QuartzStartup.Stop should be hooked.
In the legacy .NET Framework, ASP.NET provided IRegisteredObject interface. According to this post, and the documentation, in ASP.NET Core it was replaced with IApplicationLifetime. Bingo. An instance of IApplicationLifetime can be injected into Startup.Configure method through a parameter.
For consistency, I will hook both QuartzStartup.Start and QuartzStartup.Stop to IApplicationLifetime:
public class Startup
{
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IApplicationLifetime lifetime) // added this parameter
{
// the following 3 lines hook QuartzStartup into web host lifecycle
var quartz = new QuartzStartup();
lifetime.ApplicationStarted.Register(quartz.Start);
lifetime.ApplicationStopping.Register(quartz.Stop);
// .... original scaffolded code here ....
}
// ....the rest of the scaffolded members ....
}
Note that I have extended the signature of the Configure method with an additional IApplicationLifetime parameter. According to documentation, ApplicationStopping will block until registered callbacks are completed.
Graceful shutdown on IIS Express, and ASP.NET Core module
I was able to observe expected behavior of IApplicationLifetime.ApplicationStopping hook only on IIS, with the latest ASP.NET Core module installed. Both IIS Express (installed with Visual Studio 2017 Community RTM), and IIS with an outdated version of ASP.NET Core module didn't consistently invoke IApplicationLifetime.ApplicationStopping. I believe it is because of this bug that was fixed.
You can install latest version of ASP.NET Core module from here. Follow the instructions in the "Installing the latest ASP.NET Core Module" section.
Quartz vs. FluentScheduler
I also took a look at FluentScheduler, as it was proposed as an alternative library by #Brice Molesti. To my first impression, FluentScheduler is quite a simplistic and immature solution, compared to Quartz. For example, FluentScheduler doesn't provide such fundamental features as job status persistence and clustered execution.
In addition to #felix-b answer. Adding DI to jobs. Also QuartzStartup Start can be made async.
Based on this answer: https://stackoverflow.com/a/42158004/1235390
public class QuartzStartup
{
public QuartzStartup(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task Start()
{
// other code is same
_scheduler = await schedulerFactory.GetScheduler();
_scheduler.JobFactory = new JobFactory(_serviceProvider);
await _scheduler.Start();
var sampleJob = JobBuilder.Create<SampleJob>().Build();
var sampleTrigger = TriggerBuilder.Create().StartNow().WithCronSchedule("0 0/1 * * * ?").Build();
await _scheduler.ScheduleJob(sampleJob, sampleTrigger);
}
}
JobFactory class
public class JobFactory : IJobFactory
{
private IServiceProvider _serviceProvider;
public JobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job)
{
(job as IDisposable)?.Dispose();
}
}
Startup class:
public void ConfigureServices(IServiceCollection services)
{
// other code is removed for brevity
// need to register all JOBS by their class name
services.AddTransient<SampleJob>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
{
var quartz = new QuartzStartup(_services.BuildServiceProvider());
applicationLifetime.ApplicationStarted.Register(() => quartz.Start());
applicationLifetime.ApplicationStopping.Register(quartz.Stop);
// other code removed for brevity
}
SampleJob class with contructor dependency injection:
public class SampleJob : IJob
{
private readonly ILogger<SampleJob> _logger;
public SampleJob(ILogger<SampleJob> logger)
{
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogDebug("Execute called");
}
}
I don't know how to do it with Quartz, but i had experimented the same scenario with an other library wich works very well. Here how I dit it
Install FluentScheduler
Install-Package FluentScheduler
Use it like this
var registry = new Registry();
JobManager.Initialize(registry);
JobManager.AddJob(() => MyEmailService.SendAdminEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Monday)
.At(17, 00));
JobManager.AddJob(() => MyEmailService.SendAdminEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Wednesday)
.At(17, 00));
JobManager.AddJob(() => MyEmailService.SendUserEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Thursday)
.At(09, 00));
JobManager.AddJob(() => MyEmailService.SendUserEmails(), s => s
.ToRunEvery(1)
.Weeks()
.On(DayOfWeek.Friday)
.At(09, 00));
Documentation can be found here FluentScheduler on GitHub
What code do I need to schedule these methods using Quartz in ASP.NET Core? I also need to know how to start Quartz in ASP.NET Core as all code samples on the internet still refer to previous versions of ASP.NET.
Hi, there is now a good quartz DI to initialize and use
[DisallowConcurrentExecution]
public class Job1 : IJob
{
private readonly ILogger<Job1> _logger;
public Job1(ILogger<Job1> logger)
{
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Start job1");
await Task.Delay(2, context.CancellationToken);
_logger?.LogInformation("End job1");
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddQuartz(cfg =>
{
cfg.UseMicrosoftDependencyInjectionJobFactory(opt =>
{
opt.AllowDefaultConstructor = false;
});
cfg.AddJob<Job1>(jobCfg =>
{
jobCfg.WithIdentity("job1");
});
cfg.AddTrigger(trigger =>
{
trigger
.ForJob("job1")
.WithIdentity("trigger1")
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever());
});
});
services.AddQuartzHostedService(opt =>
{
opt.WaitForJobsToComplete = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// standart impl
}
}
The accepted answer covers the topic very well, but some things have changed with the latest Quartz version. The following is based on this article shows a quick start with Quartz 3.0.x and ASP.NET Core 2.2:
Util class
public class QuartzServicesUtilities
{
public static void StartJob<TJob>(IScheduler scheduler, TimeSpan runInterval)
where TJob : IJob
{
var jobName = typeof(TJob).FullName;
var job = JobBuilder.Create<TJob>()
.WithIdentity(jobName)
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"{jobName}.trigger")
.StartNow()
.WithSimpleSchedule(scheduleBuilder =>
scheduleBuilder
.WithInterval(runInterval)
.RepeatForever())
.Build();
scheduler.ScheduleJob(job, trigger);
}
}
Job factory
public class QuartzJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public QuartzJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
var jobDetail = bundle.JobDetail;
var job = (IJob)_serviceProvider.GetService(jobDetail.JobType);
return job;
}
public void ReturnJob(IJob job) { }
}
A job sample that also deals with exiting on application pool recycle / exit
[DisallowConcurrentExecution]
public class TestJob : IJob
{
private ILoggingService Logger { get; }
private IApplicationLifetime ApplicationLifetime { get; }
private static object lockHandle = new object();
private static bool shouldExit = false;
public TestJob(ILoggingService loggingService, IApplicationLifetime applicationLifetime)
{
Logger = loggingService;
ApplicationLifetime = applicationLifetime;
}
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
ApplicationLifetime.ApplicationStopping.Register(() =>
{
lock (lockHandle)
{
shouldExit = true;
}
});
try
{
for (int i = 0; i < 10; i ++)
{
lock (lockHandle)
{
if (shouldExit)
{
Logger.LogDebug($"TestJob detected that application is shutting down - exiting");
break;
}
}
Logger.LogDebug($"TestJob ran step {i+1}");
Thread.Sleep(3000);
}
}
catch (Exception exc)
{
Logger.LogError(exc, "An error occurred during execution of scheduled job");
}
});
}
}
Startup.cs configuration
private void ConfigureQuartz(IServiceCollection services, params Type[] jobs)
{
services.AddSingleton<IJobFactory, QuartzJobFactory>();
services.Add(jobs.Select(jobType => new ServiceDescriptor(jobType, jobType, ServiceLifetime.Singleton)));
services.AddSingleton(provider =>
{
var schedulerFactory = new StdSchedulerFactory();
var scheduler = schedulerFactory.GetScheduler().Result;
scheduler.JobFactory = provider.GetService<IJobFactory>();
scheduler.Start();
return scheduler;
});
}
protected void ConfigureJobsIoc(IServiceCollection services)
{
ConfigureQuartz(services, typeof(TestJob), /* other jobs come here */);
}
public void ConfigureServices(IServiceCollection services)
{
ConfigureJobsIoc(services);
// other stuff comes here
AddDbContext(services);
AddCors(services);
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
protected void StartJobs(IApplicationBuilder app, IApplicationLifetime lifetime)
{
var scheduler = app.ApplicationServices.GetService<IScheduler>();
//TODO: use some config
QuartzServicesUtilities.StartJob<TestJob>(scheduler, TimeSpan.FromSeconds(60));
lifetime.ApplicationStarted.Register(() => scheduler.Start());
lifetime.ApplicationStopping.Register(() => scheduler.Shutdown());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
ILoggingService logger, IApplicationLifetime lifetime)
{
StartJobs(app, lifetime);
// other stuff here
}