I am developing a web application with a REST API using C# with ASP.NET Core 2.0.
What I want to achieve is when the client send a request to an endpoint I will run a background task separated from the client request context which will be ended if the task started successfully.
I know there is HostedService
but the problem is that the HostedService
starts when the server starts, and as far as I know there is no way to start the HostedService
manually from a controller.
Here is a simple code that demonstrates the question.
[Authorize(AuthenticationSchemes = "UsersScheme")] public class UsersController : Controller { [HttpPost] public async Task<JsonResult> StartJob([FromForm] string UserId, [FromServices] IBackgroundJobService backgroundService) { // check user account (bool isStarted, string data) result = backgroundService.Start(); return JsonResult(result); } }
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. This article provides three hosted service examples: Background task that runs on a timer. Hosted service that activates a scoped service.
The IHostedService interface provides a convenient way to start background tasks in an ASP.NET Core web application (in . NET Core 2.0 and later versions) or in any process/host (starting in . NET Core 2.1 with IHost ).
Background task that runs on a timer. Hosted service that activates a scoped service. The scoped service can use dependency injection (DI). Queued background tasks that run sequentially. The ASP.NET Core Worker Service template provides a starting point for writing long running service apps.
In order to make them to handle some sort of background task, you need to implement some sort of "global" queue system in your app for the controllers to store the data/events.
Once Visual Studio opens up, I will select the menu File -> New -> Project. This will open the Create a new Project project popup window. Secondly, in the Create a new Project popup window, I will select ASP.NET Core Web Application from the project template and click on the Next button.
Create a method that returns a Task, pass in a IServiceProvider to that method and create a new Scope in there to make sure ASP.NET would not kill the task when the controller Action completes. Something like
You still can use IHostedService
as base for background tasks in combination with BlockingCollection
.
Create wrapper for BlockingCollection
so you can inject it as singleton.
public class TasksToRun { private readonly BlockingCollection<TaskSettings> _tasks; public TasksToRun() => _tasks = new BlockingCollection<TaskSettings>(); public void Enqueue(TaskSettings settings) => _tasks.Add(settings); public TaskSettings Dequeue(CancellationToken token) => _tasks.Take(token); }
Then in implementation of IHostedService
"listen" for tasks and when tasks "arrive" execute it.BlockingCollection
will stop execution if collection is empty - so your while
loop will not consume processor time..Take
method accept cancellationToken
as argument. With token you can cancel "waiting" for next task when application stops.
public class BackgroundService : IHostedService { private readonly TasksToRun _tasks; private CancellationTokenSource _tokenSource; private Task _currentTask; public BackgroundService(TasksToRun tasks) => _tasks = tasks; public async Task StartAsync(CancellationToken cancellationToken) { _tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); while (cancellationToken.IsCancellationRequested == false) { try { var taskToRun = _tasks.Dequeue(_tokenSource.Token); // We need to save executable task, // so we can gratefully wait for it's completion in Stop method _currentTask = ExecuteTask(taskToRun); await _currentTask; } catch (OperationCanceledException) { // execution cancelled } } } public async Task StopAsync(CancellationToken cancellationToken) { _tokenSource.Cancel(); // cancel "waiting" for task in blocking collection if (_currentTask == null) return; // wait when _currentTask is complete await Task.WhenAny(_currentTask, Task.Delay(-1, cancellationToken)); } }
And in the controller you simply add task you want to run to our collection
public class JobController : Controller { private readonly TasksToRun _tasks; public JobController(TasksToRun tasks) => _tasks = tasks; public IActionResult PostJob() { var settings = CreateTaskSettings(); _tasks.Enqueue(settings); return Ok(); } }
Wrapper for blocking collection should be registered for dependency injection as singleton
services.AddSingleton<TasksToRun, TasksToRun>();
Register background service
services.AddHostedService<BackgroundService>();
Microsoft has documented the same at https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1
It accomplishes using BackgroundTaskQueue, which gets work assigned from Controller and the work is performed by QueueHostedService which derives from BackgroundService.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With