TransWikia.com

Running background task on demand in asp.net core 3.x

Stack Overflow Asked on November 20, 2021

I’m trying to start a background task on demand, whenever I receive a certain request from my api end point. All the task does is sending an email, delayed by 30 seconds. So I though BackgroundService would fit. But the problem is it looks like the BackgroundService is mostly for recurring tasks, and not to be executed on demand per this answer.

So what other alternatives I have, im hoping not to have to rely on 3rd parties libraries like Hangfire? I’m using asp.net core 3.1.

This is my background service.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace ProjectX.Services {
    public  class EmailOfflineService : BackgroundService {

        private readonly ILogger<EmailOfflineService> log;
        private readonly EmailService emailService;
        public EmailOfflineService(
            ILogger<EmailOfflineService> log, 
            EmailService emailService
        ) {
            this.emailService = emailService;
            this.log = log;
        }

        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {

            log.LogDebug("Email Offline Service Starting...");
            stoppingToken.Register(() => log.LogDebug("Email Offline Service is stopping."));

            while(!stoppingToken.IsCancellationRequested)
            {
                // wait for 30 seconds before sending
                await Task.Delay(1000 * 30, stoppingToken);

                await emailService.EmailOffline();
                
                // End the background service
                break;
            }
            log.LogDebug("Email Offline Service is stoped.");
        }
    }
}

3 Answers

You could try to combine an async queue with BackgroundService.

public class BackgroundEmailService : BackgroundService
{
    private readonly IBackgroundTaskQueue _queue;

    public BackgroundEmailService(IBackgroundTaskQueue queue)
    {
        _queue = queue;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var job = await _queue.DequeueAsync(stoppingToken);
            
            _ = ExecuteJobAsync(job, stoppingToken);
        }
    }

    private async Task ExecuteJobAsync(JobInfo job, CancellationToken stoppingToken)
    {
        try
        {
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
            // todo send email
        }
        catch (Exception ex)
        {
            // todo log exception
        }
    }
}

public interface IBackgroundTaskQueue
{
    void EnqueueJob(JobInfo job);

    Task<JobInfo> DequeueAsync(CancellationToken cancellationToken);
}

This way you may inject IBackgroundTaskQueue inside your controller and enqueue jobs into it while JobInfo will contain some basic information for executing the job in background, e.g.:

public class JobInfo
{
    public string EmailAddress { get; set; }
    public string Body { get; set; }
}

An example background queue (inspired by the ASP.NET Core documentation):

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private ConcurrentQueue<JobInfo> _jobs = new ConcurrentQueue<JobInfo>();
    private SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void EnqueueJob(JobInfo job)
    {
        if (job == null)
        {
            throw new ArgumentNullException(nameof(job));
        }

        _jobs.Enqueue(job);
        _signal.Release();
    }

    public async Task<JobInfo> DequeueAsync(CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _jobs.TryDequeue(out var job);

        return job;
    }
}

Answered by Federico Dipuma on November 20, 2021

I think the simplest approach is to make a fire-and-forget call in the code of handling the request to send a email, like this -

//all done, time to send email
Task.Run(async () => 
{
    await emailService.EmailOffline(emailInfo).ConfigureAwait(false); //assume all necessary info to send email is saved in emailInfo
});

This will fire up a thread to send email. The code will return immediately to the caller. In your EmailOffline method, you can include time-delay logic as needed. Make sure to include error logging logic in it also, otherwise exceptions from EmailOffline may be silently swallowed.

P.S. - Answer to Coastpear and FlyingV -

No need to concern the end of calling context. The job will be done on a separate thread, which is totally independent of the calling context.

I have used similar mechanism in production for a couple of years, zero problem so far.

If your site is not supper busy, and the work is not critical, this is the easiest solution. Just make sure you catch and log error inside your worker (EmailOffline, in this example).

If you need more reliable solution, I'd suggest using a mature queue product like AWS SQS, do not bother to create one by yourself. It is not an easy job to create a really good queue system.

Answered by ch_g on November 20, 2021

Use Hangfire, it's Background Methods functionality is great, and provides you with a nice dashboard for free: https://docs.hangfire.io/en/latest/background-methods/index.html

Answered by Luke Storry on November 20, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP