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

4

22

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

Pyromania answered 11/12, 2018 at 15:56 Comment(6)
A timed background service is one of the examples in the documentation. Check Background tasks with hosted services in ASP.NET Core.Brower
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 (?)Pyromania
@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 :)Pyromania
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?Pyromania
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.Pyromania
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.Peskoff
P
26

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.
Pyromania answered 17/1, 2019 at 15:8 Comment(3)
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 explainsUltraism
For every user who logs to application, background service will trigger first and the subsequent trigger is after mentioned time in processAdamok
Don't forget to register the service in Startup.cs with "services.AddHostedService<TimedHostedService>();".Reclinate
P
4

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

Prudhoe answered 11/12, 2018 at 16:33 Comment(4)
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.Pyromania
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.Prudhoe
"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. :)Pyromania
hangfire.io/pricing here you're - first column "Open" - Hangfire Core under LGPL 3.0, Commercial useDarius
H
3

I have found a simple Solution using .NET Core built in functionality. It uses BackgroundService rather than IHostedService and IDisposable directly. I was inspiered by a blog entry of Philipp Bauknecht: https://medium.com/medialesson/run-and-manage-periodic-background-tasks-in-asp-net-core-6-with-c-578a31f4b7a3 He made it managable to be able to pause the service.

NOTE: When hosting this app e.g. in IIS or Azure App Service make sure the app is set to Always on otherwise the hosted service will be shut down after a while

public class TimedBackgroundService : BackgroundService
{
    private async Task ExecuteTaskAsync()
    {
        // ...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        TimeSpan interval = TimeSpan.FromMinutes(60);
        // Available with .NET 6.0
        using PeriodicTimer timer = new PeriodicTimer(interval);

        while (!stoppingToken.IsCancellationRequested &&
            await timer.WaitForNextTickAsync(stoppingToken))
            {
                // Alternative to PeriodicTimer (available in .NET Core 2.1).
                // REMARK: Calls would successively be delayed a little bit this way.
                //await Task.Delay(interval, stoppingToken);

                await ExecuteTaskAsync();    
            }
    }
}

And don't forget to register it in Startup.

services.AddHostedService<TimedBackgroundService>
Hypophysis answered 7/10, 2022 at 5:49 Comment(0)
A
1

Here is an improved version based on previous responses and https://mcmap.net/q/588466/-async-timer-in-scheduler-background-service

Improvements:

  1. It will not start timer until the previous task finished execution. It will help not to get into a situation of two tasks executing at the same time.
  2. It supports async tasks
  3. It handles possible exceptions during task execution to make sure it won't prevent next tasks from executing.
  4. For each task executing a scope is created, so you can access any scoped services in RunJobAsync
  5. You can specify interval and initial task execution time in the inherited class.

Access to scoped services example

    protected override async Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    {
            DbContext context = serviceProvider.GetRequiredService<DbContext>();
    }

Source code:

public abstract class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

    IServiceProvider _services;
    public TimedHostedService(IServiceProvider services)
    {
        _services = services;
        _logger = _services.GetRequiredService<ILogger<TimedHostedService>>();
        
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _timer = new Timer(ExecuteTask, null,FirstRunAfter, TimeSpan.FromMilliseconds(-1));

        return Task.CompletedTask;
    }

    private void ExecuteTask(object state)
    {
        _timer?.Change(Timeout.Infinite, 0);
        _executingTask = ExecuteTaskAsync(_stoppingCts.Token);
    }

    private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
    {
        try
        {
            using (var scope = _services.CreateScope())
            {
                await RunJobAsync(scope.ServiceProvider, stoppingToken);
            }
        }
        catch (Exception exception)
        {
            _logger.LogError("BackgroundTask Failed", exception);
        }
        _timer.Change(Interval, TimeSpan.FromMilliseconds(-1));
    }

    /// <summary>
    /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task 
    /// </summary>
    /// <param name="serviceProvider"></param>
    /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
    /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
    protected abstract Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken);
    protected abstract TimeSpan Interval { get; }
    
    protected abstract TimeSpan FirstRunAfter { get; }
    
    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.Infinite, 0);

        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }

    }

    public void Dispose()
    {
        _stoppingCts.Cancel();
        _timer?.Dispose();
    }
}
Alatea answered 30/4, 2021 at 18:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.