63

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);
    }
}
janw
  • 8,758
  • 11
  • 40
  • 62
Waxren
  • 2,002
  • 4
  • 30
  • 43
  • 2
    Use a third party tool like Hangifre, but there must be thousands of similar questions to this on here. – DavidG Apr 13 '18 at 09:31
  • Thanks for the comment, I ended up using Hangfire, it's very powerful. Consider writing an answer so I can accept it. – Waxren Jun 06 '18 at 18:14

4 Answers4

47

You still can use IHostedService as base for background tasks in combination with BlockingCollection.

Create a wrapper for BlockingCollection so we can inject it as singleton.
BlockingCollection.Take will not consume processor time when collection is empty. Passing cancellation token to the .Take method will gracefully exit when token is cancelled.

public class TasksToRun
{
    private readonly BlockingCollection<SingleTaskData> _tasks;

    public TasksToRun() => _tasks = new BlockingCollection<SingleTaskData>(new ConcurrentQueue<SingleTaskData>());

    public void Enqueue(SingleTaskData taskData) => _tasks.Add(settings);

    public TaskSettings Dequeue(CancellationToken token) => _tasks.Take(token);
}

For background process we can use "built-in" implementation of IHostedService - Microsoft.Extensions.Hosting.BackgroundService.
This service will consume tasks extracted from the "queue".

public class TaskProcessor : BackgroundService
{
    private readonly TasksToRun _tasks;

    public TaskProcessor(TasksToRun tasks) => _tasks = tasks;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Yield(); // This will prevent background service from blocking start up of application

        while (cancellationToken.IsCancellationRequested == false)
        {
            try
            {
                var taskToRun = _tasks.Dequeue(_tokenSource.Token);


                await ExecuteTask(taskToRun);               
            }
            catch (OperationCanceledException)
            {
                // execution cancelled
            }
            catch (Exception e)
            {
                // Catch and log all exceptions,
                // So we can continue processing other tasks
            }
        }
    }
}

Then we can add new tasks from the controller without waiting for them to complete

public class JobController : Controller
{
    private readonly TasksToRun _tasks;

    public JobController(TasksToRun tasks) => _tasks = tasks;

    public IActionResult PostJob()
    {
        var taskData = CreateSingleTaskData();

        _tasks.Enqueue(taskData);

        return Ok();
    }
}

Wrapper for blocking collection should be registered for dependency injection as singleton

services.AddSingleton<TasksToRun, TasksToRun>();

Register background service

services.AddHostedService<TaskProcessor>();
Fabio
  • 31,528
  • 4
  • 33
  • 72
  • How can you use IHostedService to run jobs on databases etc? The Singleton blocks using Scoped or Transient resources. I am not having any luck finding a way to do a background task in a web app that is long running and will not cause the UI to timeout. – Tyler Durden May 23 '18 at 07:51
  • Transient resources should be allowed to use. Personally I use `ContextFactory` which create new context every time it asked for. – Fabio May 23 '18 at 08:04
  • Thanks @Fabio. Just tried finding an example of that, but am not having any joy. Do you have an example? – Tyler Durden May 23 '18 at 08:36
  • 1
    @TylerDurden, I think you can ask a question about your problem and pretty sure you will get correct answer – Fabio May 23 '18 at 08:48
  • I followed this patter (attempted to) and public async Task StopAsync(CancellationToken cancellationToken) is never called. So I have to manually halt my process. Anything I should be looking for? service.AddSingleton should be services.AddSingleton right? So both the singleton and the hosted service are registered in Startup? – Timothy John Laird Mar 06 '19 at 17:37
  • When you run locally, CTRL + C should properly stop your server and will call Stop method. – Fabio Mar 06 '19 at 17:40
  • When I press CTRL + C it starts to shut down but StopAsync is never called. Oddly enough. If I don't register the hosted service and the singleton there is no issue. I verfied with breakpoints and logging that StartAsync is called but something is preventing StopAsync from being called. – Timothy John Laird Mar 06 '19 at 19:18
  • 1
    @Fabio the problem was that Deque was blocking forever. Passing a cancellation token into TasksToRun.Deque from _tokenSource fixes the problem. And now it shuts down. – Timothy John Laird Mar 06 '19 at 20:00
  • How to implement cancel of dequeued (in progress) task? – Adam Cox Sep 11 '19 at 01:34
  • 8
    Does this actually work? I attempted to implement and quickly noticed that the `BlockingCollection`'s `Take` method in the Hosted Service's `StartAsync` method prevents the service from starting. This also blocks the ASP.NET runtime from starting because the service is never completed registration. – Justin Skiles Jun 23 '20 at 13:13
  • 2
    @Justin, no it does not work. We need to return ``Task.CompletedTask`` in `StartAsync` in order for the web app to start. The actual work could be wrapped in a ``Task``. But then one could just use a task without `IHostedService`. I am really wondering why this answer gained so many votes. – Jack Miller Dec 17 '20 at 15:48
  • The code in the answer is pseudo code, the main point of the answer is that in `IHostedService.StartAsync` you will start a task which will "poll" blocking collection without waiting for this task to complete and cancel the task in `IHostedService.StopAsync`. Blocking collection is exposed to other parts of the application where consumer can add "requests" to execute in background. – Fabio Dec 18 '20 at 00:47
  • @Fabio Is it possible (and how?) to extend the above example so there is multiple (dynamic) tasks consumers from a BlockingCollection? – Maciej Pszczolinski Dec 26 '20 at 10:13
  • 3
    @Fabio your answer doesn't indicate that it's pseudocode. Further, it looks like completely valid runtime code as written so it is obviously misleading. I would suggest you edit your answer to clarify. – Justin Skiles Dec 28 '20 at 18:47
  • What was TaskSettings, where does this come from? – Andrew Jan 24 '23 at 15:01
  • @Fabio, is there a way to secure the queue content if the app is shutting down? There is a StopAsync() in BackgroundService where we might do something but it also has a limitation to 15 or 30 seconds I guess – Tanmay Sharma Jul 11 '23 at 15:32
  • @TanmaySharma, can you use persisted queue? For example: save items to the datastore and background service will retrieve it, process it and mark as processed – Fabio Jul 12 '23 at 05:33
  • yes, I think I can go with database as a data store. Thanks. Since every event in a queue is a new isolated process, will creating new threads in net core web api project make sense, say each thread runs atleast for an hour? and also thinking of whether we can handle exception generated in this background threads. – Tanmay Sharma Jul 16 '23 at 19:54
23

This is heavily inspired from the documentation linked in skjagini's answer, with a few improvements.

I figured that it may help to reiterate the entire example here, in case the link breaks at some point. I have made some adjustments; most notably, I inject an IServiceScopeFactory, to allow the background processes to safely request services themselves. I explain my reasoning at the end of this answer.


The core idea is creating a task queue, which the user can inject into their controller and then assign tasks to. The same task queue is present in a long-running hosted service, which dequeues one task at a time and executes it.

Task queue:

public interface IBackgroundTaskQueue
{
    // Enqueues the given task.
    void EnqueueTask(Func<IServiceScopeFactory, CancellationToken, Task> task);

    // Dequeues and returns one task. This method blocks until a task becomes available.
    Task<Func<IServiceScopeFactory, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly ConcurrentQueue<Func<IServiceScopeFactory, CancellationToken, Task>> _items = new();

    // Holds the current count of tasks in the queue.
    private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void EnqueueTask(Func<IServiceScopeFactory, CancellationToken, Task> task)
    {
        if(task == null)
            throw new ArgumentNullException(nameof(task));

        _items.Enqueue(task);
        _signal.Release();
    }

    public async Task<Func<IServiceScopeFactory, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
    {
        // Wait for task to become available
        await _signal.WaitAsync(cancellationToken);

        _items.TryDequeue(out var task);
        return task;
    }
}

At the heart of the task queue, we have a thread-safe ConcurrentQueue<>. Since we don't want to poll the queue until a new task becomes available, we use a SemaphoreSlim object to keep track of the current number of tasks in the queue. Each time we call Release, the internal counter is incremented. The WaitAsync method blocks until the internal counter becomes greater than 0, and subsequently decrements it.

For dequeuing and executing the tasks, we create a background service:

public class BackgroundQueueHostedService : BackgroundService
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly IServiceScopeFactory _serviceScopeFactory;
    private readonly ILogger<BackgroundQueueHostedService> _logger;

    public BackgroundQueueHostedService(IBackgroundTaskQueue taskQueue, IServiceScopeFactory serviceScopeFactory, ILogger<BackgroundQueueHostedService> logger)
    {
        _taskQueue = taskQueue ?? throw new ArgumentNullException(nameof(taskQueue));
        _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Dequeue and execute tasks until the application is stopped
        while(!stoppingToken.IsCancellationRequested)
        {
            // Get next task
            // This blocks until a task becomes available
            var task = await _taskQueue.DequeueAsync(stoppingToken);

            try
            {
                // Run task
                await task(_serviceScopeFactory, stoppingToken);
            }
            catch(Exception ex)
            {
                _logger.LogError(ex, "An error occured during execution of a background task");
            }
        }
    }
}

Finally, we need to make our task queue available for dependency injection, and start our background service:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    
    services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
    services.AddHostedService<BackgroundQueueHostedService>();
    
    // ...
}

We can now inject the background task queue into our controller and enqueue tasks:

public class ExampleController : Controller
{
    private readonly IBackgroundTaskQueue _backgroundTaskQueue;

    public ExampleController(IBackgroundTaskQueue backgroundTaskQueue)
    {
        _backgroundTaskQueue = backgroundTaskQueue ?? throw new ArgumentNullException(nameof(backgroundTaskQueue));
    }

    public IActionResult Index()
    {
        _backgroundTaskQueue.EnqueueTask(async (serviceScopeFactory, cancellationToken) =>
        {
            // Get services
            using var scope = serviceScopeFactory.CreateScope();
            var myService = scope.ServiceProvider.GetRequiredService<IMyService>();
            var logger = scope.ServiceProvider.GetRequiredService<ILogger<ExampleController>>();
            
            try
            {
                // Do something expensive
                await myService.DoSomethingAsync(cancellationToken);
            }
            catch(Exception ex)
            {
                logger.LogError(ex, "Could not do something expensive");
            }
        });

        return Ok();
    }
}

Why use an IServiceScopeFactory?

In theory, we could directly use the service objects which we have injected into our controller. This will probably work well with singleton services, and also with most scoped services.

However, for scoped services which implement IDisposable (e.g., DbContext), this will likely break: After enqueuing the task, the controller method returns and the request is completed. The framework then cleans up the injected services. If our background task is sufficiently slow or delayed, it may try to call a method of a disposed service, and will then run into an error.

To avoid this, our queued tasks should always create their own service scope, and should not make use of service instances from the surrounding controller.

janw
  • 8,758
  • 11
  • 40
  • 62
  • Works as advertised in net5. Thanks. –  Jul 23 '21 at 18:30
  • I have a question about this line of code: 'var task = await _taskQueue.DequeueAsync(stoppingToken);' // This blocks until a task becomes available. Is it safe to have a thread blocked like this for a long period of time? In my case a task is queued when a user wants to start a process, but he might only do this, say, once a week. – Chris Oct 28 '21 at 10:13
  • 1
    @Chris Oh, this line is a bit misleading. The comment means that the background job waits until the next task becomes available. However, it doesn't block, but uses `await`, so the thread is freed up and can be used elsewhere. Only when something is enqueued, the `_taskQueue` triggers continuation of the code in `ExecuteAsync`. See also here: [Does await completely block the thread?](https://stackoverflow.com/questions/34705703/does-await-completely-blocks-the-thread) – janw Oct 28 '21 at 10:31
  • @janw But what if during the processing of a backgroundTask a new task is added in the ExampleController ? – ChsharpNewbie Nov 26 '21 at 23:24
  • 1
    @ChsharpNewbie I don't think I fully understand your question. If you mean possible race condition issues: This is unproblematic, as the `ConcurrentQueue` takes care of the necessary locking and serialization, and the `SemaphoreSlim` allows thread-safe waiting until a new item becomes available. So the current task is finished, and then the background service takes the next task from the queue, if there is any. – janw Nov 27 '21 at 11:06
  • @janw But what if you poll regularly instead of SemaphoreSlim, can there be problems ? – ChsharpNewbie Nov 27 '21 at 16:00
  • 1
    @ChsharpNewbie Well, it depends on the respective implementation, but should work as well. `TryDequeue` will return `false` if there are no items in the queue, so this would need to be handled accordingly. The advantage of using `SemaphoreSlim` is that the execution waits until a new item becomes available, and then _immediately_ consumes it, without further overhead/delay from polling etc. The thread-safety is already guaranteed by the `ConcurrentQueue`, so there shouldn't be concurrency issues. – janw Nov 27 '21 at 16:29
  • Hello@janw , and thanks for the quick feedback But what if I don't use SemaphoreSlim and instead I poll every few seconds and while a job is being processed but a new one comes into the ConcurrentQueue, can there be problems in the backgroundservice-class ? – ChsharpNewbie Nov 27 '21 at 16:50
  • 1
    @ChsharpNewbie No, this should work just fine. – janw Nov 27 '21 at 16:56
  • @janw Ahhh okay, then you only use `SemaphoreSlim` just to avoid polling ?! – ChsharpNewbie Nov 27 '21 at 17:33
  • thanks for this. may i know how to pass parameter to the enqueuetask? – jay May 26 '22 at 21:02
  • @jay I'm not entirely sure what you mean by "parameter" - you can just capture variables in the Func<> object by using them in the lambda expression. – janw May 26 '22 at 22:24
14

Microsoft has documented the same at https://learn.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.

skjagini
  • 3,142
  • 5
  • 34
  • 63
  • 1
    The link shown is not the same as OP's question. Question asks how to start the task from a controller. Link cited shows how to start a keyboard poller and start a task when user presses a key. Not the same at all. –  Jul 23 '21 at 14:44
1

You can use another thread in ThreadPool:

Queues a method for execution. The method executes when a thread pool thread becomes available.

public class ToDoController : Controller
{
    private readonly IServiceScopeFactory _serviceScopeFactory;
    public ToDoController(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }
    public string Index(Func<IToDoDependency,Task> DoHeavyWork)
    {
        ThreadPool.QueueUserWorkItem(delegate {
            // Get services
            using var scope = _serviceScopeFactory.CreateScope();
            var dependency= scope.ServiceProvider.GetRequiredService<IToDoDependency>();
            DoHeavyWork(dependency);

            // OR 
            // Get the heavy work from ServiceProvider
            var heavyWorkSvc= scope.ServiceProvider.GetRequiredService<IHeavyWorkService>();
            heavyWorkSvc.Do(dependency);
        });
        return "Immediate Response";
    }
}
n.y
  • 3,343
  • 3
  • 35
  • 54