12

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.");
        }
    }
}
Yehia A.Salam
  • 1,987
  • 7
  • 44
  • 93

3 Answers3

15

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;
    }
}
Martin Schneider
  • 14,263
  • 7
  • 55
  • 58
Federico Dipuma
  • 17,655
  • 4
  • 39
  • 56
  • @ch_g answer worked for me better, because my task in transactional, and introducing a queue system, even if basic, with a loop to dequeue the items seemed like an overkill for my use case – Yehia A.Salam Jul 22 '20 at 12:46
  • I use exactly the same code as Federico Dipuma to process small "mini jobs" (resize images, fire & forget HTTP-POST Api calls, etc.). From time to time, however, this queue hangs. Then only elements are added to the queue, but no more elements are processed. Do you have any idea what could be the reason for this? At such times, I can only perform an IIS reset to restart the queue (which of course means losing current jobs). – Grimm Mar 22 '22 at 10:42
5

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.

ch_g
  • 1,394
  • 8
  • 12
  • i know in most cases we should try to avoid the Task.Run approach, but it worked in my use case, a simple fire forget, with delay to fire the email – Yehia A.Salam Jul 22 '20 at 12:43
  • 1
    How are you using scoped services injected by DI? Aren’t they disposed after the current Request has returned to the caller? – Coastpear Sep 21 '20 at 18:58
  • If this is a request thread; won't the context end once the request is finished? – FlyingV Nov 09 '20 at 00:13
  • @Yehia, was Task.Run sufficient for you or did you have to choose something else, once the system got bigger? I face the same issue, because I need to implement some sort of process manager in a (small) CQRS project and I don't want to introduce too many frameworks. – Joerg Feb 13 '21 at 21:32
  • This is the simplest solution by far. Task.Run without await will execute on a separate thread and NOT the request thread. – TheLegendaryCopyCoder Jul 03 '21 at 09:33
1

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

Luke Storry
  • 6,032
  • 1
  • 9
  • 22
  • that would be my last resort, im trying to use the built-in .net core libraries first – Yehia A.Salam Jul 16 '20 at 14:07
  • 3
    Hangfire is a great library, and has great support for .net core. Why would you want to use .net core libraries first when this is readily available and battle tested? – Kush Jul 17 '20 at 00:46
  • Just Wanted to ask how does Hangfire notifies back to the User that a particular background job has finished or there was any error.... is there some promise or callback or something in hangfire – Rohit Kumar Jul 19 '20 at 18:14
  • It adds a `/hangfire` endpoint to your API, which visualise all past jobs, with graphs of success/failure. If you want email notification, you can add a filter that will run code upon failure, using a job filter: https://docs.hangfire.io/en/latest/extensibility/using-job-filters.html – Luke Storry Jul 19 '20 at 19:56