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

34,957

Solution 1

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.

Solution 2

One way to achieve this is to use HangFire.io, this will handle scheduled background tasks, manage balancing across servers and is pretty scalable.

See Recurring Jobs at https://www.hangfire.io

Share:
34,957

Related videos on Youtube

S. ten Brinke
Author by

S. ten Brinke

Updated on March 25, 2022

Comments

  • S. ten Brinke
    S. ten Brinke about 2 years

    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

    • Panagiotis Kanavos
      Panagiotis Kanavos over 5 years
      A timed background service is one of the examples in the documentation. Check Background tasks with hosted services in ASP.NET Core.
    • S. ten Brinke
      S. ten Brinke over 5 years
      Hmm, I see. The thing is, I see that DoWork() is not async. I could mark DoWork async, but that is not really the correct way because it won't be awaited (?)
    • S. ten Brinke
      S. ten Brinke over 5 years
      @PanagiotisKanavos If you have an answer, please write it as an actual answer so I can mark it as completed when your answer helps me figure out this question :)
    • S. ten Brinke
      S. ten Brinke over 5 years
      Could you tell me why implementing IHostedService and then using a timer would be better than using BackgroundService and checking if you want to run your work in the ExecuteAsync by doing a timer check? (Again,post your answer + reasons why this is better than this approach as an answer) I get that my approach would lead to ExecuteAsync being called if it is not going to be executed, but then my question becomes: What is the point of BackgroundService if you can't put it on a timer? Followed up by: Why is there not a TimedBackgroundService then?
    • S. ten Brinke
      S. ten Brinke over 5 years
      My apologies for this comment chain, but I feel like what @PanagiotisKanavos is saying could work. This comment says you can do the following for async Timer: var timer = new System.Threading.Timer(async (e) => { await Task.Delay(500); Console.WriteLine("Tick"); }, null, 0, 5000); That thread says you'd need to do a try/catch to manage exceptions though.If an unhandled one occurs, the timer could stop working. Again, this all should be brought as an actual answer to the question so it's easier to discuss.
    • Alexein1
      Alexein1 about 5 years
      Be carrefull with Hosted service with timer, because of IIS recycling every 20 min, your hosted service will be stopped in the same time. So you'll need to set your application pool to always on which can cause leak or memories issues.
  • S. ten Brinke
    S. ten Brinke over 5 years
    I'd like to add that .NET Core offers a completely free solution. HangFire will cost money depending on your usecase. As I mentioned in my post, I'd like to use ASP.NET Core's solution because it exists; I just do not know how to use it. To use a 3rd party solution for something like this seems a bit odd since what I want to do is not complex.
  • Mark Redman
    Mark Redman over 5 years
    Sure. HangFire is free if you use SQL server, and offers a complete solution that you can implement and then continue with you dev. (note: I am not affiliated) but sure I understand your requirement, just trying to help.
  • S. ten Brinke
    S. ten Brinke over 5 years
    "HangFire is free if you use SQL server". Could you provide a link for this? Furthermore, I do value your response, it's just that it's not part of the ASP.NET Core functionality that I want to use and thus I do not feel like it deserves to be the answer of the post. :)
  • jkosmala
    jkosmala over 5 years
    hangfire.io/pricing here you're - first column "Open" - Hangfire Core under LGPL 3.0, Commercial use
  • johnny 5
    johnny 5 over 5 years
    this does not make the timer async, new Timer(async () => await DoWorkAsync() the timer by default will execute your function on a timed schedule regardless of whether the other function is still executing. Additionally there is no gaurentee your timer will execute if no requests are made see this question I had the same misconception about timers the answer explains
  • Sagar K
    Sagar K almost 5 years
    For every user who logs to application, background service will trigger first and the subsequent trigger is after mentioned time in process
  • IngoB
    IngoB about 3 years
    Don't forget to register the service in Startup.cs with "services.AddHostedService<TimedHostedService>();".