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

3

13

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.");
        }
    }
}
Withal answered 13/7, 2020 at 18:16 Comment(9)
Maybe already asked and answered: #61414544Marietta
have you tried Hangfire?Weather
@MuhammadKamranAslam that would be my last resort, im trying to use the built-in .net core libraries firstWithal
@Marietta not really, the suggest answer still fires the background task on application runWithal
@MuhammadKamranAslam Delayed Jobs in HangFire is what im trying to implement hangfire.ioWithal
what error or difficulty you are facing when using hangfire delayed jobs? Have you looked at Enqueue Jobs also?Weather
i’m trying to avoid using hanfire, seems like an overkill for just this functionWithal
why dont you just create a new thread for Email Sending?Weather
You could use Channels for that. In your API receiving the request you would publish to the channel. Your background worker would then subscribe to the channel and when a message arrives it picks it up and process it. Maybe this helps: davideguida.com/… Other way is just save the data to a table, and make your background worker poll it.. and wait 30 sec..Phenolic
B
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.

Boor answered 18/7, 2020 at 19:36 Comment(5)
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 emailWithal
How are you using scoped services injected by DI? Aren’t they disposed after the current Request has returned to the caller?Unlimber
If this is a request thread; won't the context end once the request is finished?Rotow
@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.Unbelieving
This is the simplest solution by far. Task.Run without await will execute on a separate thread and NOT the request thread.Michail
O
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;
    }
}
Ornamental answered 19/7, 2020 at 21:21 Comment(2)
@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 caseWithal
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).Singly
B
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.

Boor answered 18/7, 2020 at 19:36 Comment(5)
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 emailWithal
How are you using scoped services injected by DI? Aren’t they disposed after the current Request has returned to the caller?Unlimber
If this is a request thread; won't the context end once the request is finished?Rotow
@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.Unbelieving
This is the simplest solution by far. Task.Run without await will execute on a separate thread and NOT the request thread.Michail
C
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

Clydeclydebank answered 16/7, 2020 at 13:31 Comment(4)
that would be my last resort, im trying to use the built-in .net core libraries firstWithal
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?Clintonclintonia
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 hangfireGreatest
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: docs.hangfire.io/en/latest/extensibility/using-job-filters.htmlClydeclydebank

© 2022 - 2024 — McMap. All rights reserved.