Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to run BackgroundService on a timer in ASP.NET Core 2.1

I want to run a background job in ASP.NET Core 2.1. It has to run every 2 hours and it will need to access my DI Container because it will perform some cleanups in the database. It will need to be async and it should run independently of my ASP.NET Core 2.1 application.

I saw that there was an IHostedService, but ASP.NET Core 2.1 also introduced an abstract class called BackgroundService that does some more work for me. Seems good, I want to use that!

I have not been able to figure out how run a service that derived from BackgroundService on a timer, though.

Do I need to configure this in the ExecuteAsync(token) by remembering the last time it ran and figuring out if this was 2 hours, or is there a better/cleaner way to just say somewhere that it has to run every 2 hours?

Also, is my approach to my problem correct with an BackgroundService?

Thank you!

Edit:

Posted this on the MS extensions repo

like image 774
S. ten Brinke Avatar asked Dec 11 '18 15:12

S. ten Brinke


1 Answers

Updated 03-2022, read it on the bottom!

Updated 04-2020, read it on the bottom!

@Panagiotis Kanavos gave an answer in the comments of my question but it did not post it as an actual answer; this answer is dedicated to him/her.

I used a Timed background service like the one from Microsoft docs to create the service.

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

In my case I made the _timer call async by doing new Timer(async () => await DoWorkAsync(), ...).

In the future, an extension could be written that makes a class like this available in the Extensions repo because I think this is quite useful. I posted the github issue link in the description.

A tip, if you plan on reusing this class for multiple hosted services, consider creating a base class that contains the timer and an abstract PerformWork() or something so the "time" logic is only in one place.

Thank you for your answers! I hope this helps someone in the future.

Update 04-2020:

Injecting a scoped service in here is not possible with the normal Core service collection DI container, out of the box. I was using autofac which made it possible to use scoped services like IClassRepository in the constructor because of wrong registration, but when I started working on a different project that used only AddScoped<>(), AddSingleton<>(), AddTransient<>() we figured out that injecting scoped things do not work because you are not in a scoped context.

In order to use your scoped services, inject a IServiceScopeFactory (Easier to test with) and use CreateScope() which allows you to use scope.GetService() with a using statement :)

Update 03-2022: This post has gotten LOTS of views and attention, but I have to say I am no longer a big fan of my solution. I would propose different solutions:

  • Use hangfire or quartz instead if you want the code to just run in backgroundservice
  • take a look at kubernetes cronjobs if you run in a kubernetes environment
    • This has the benefit of only running your code when required, saving resources compared to running a project 24/7 and only executing a job every day at 3 AM, for example
  • take a look at Azure Functions/AWS Lambda on a timer
    • this is probably cheaper and easier to maintain than making your own timed hosted services. It might be more difficult to integrate into a k8s environment, though.

The downsides of the solution posted in this answer are:

  • You need to manage a lot of things yourself that the other options do for free. For example:
    • What if your app was down when it should have ran the job?
    • What if your job takes too long and another one starts?
    • Logging and monitoring
  • I am still unsure about the async support in this solution. I never really figured out if this solution is "correct"
  • I also do not like that DI is not supported out of the box. Quartz.Net does support this.
  • It isn't flexible compared to quartz.
like image 187
S. ten Brinke Avatar answered Oct 21 '22 21:10

S. ten Brinke