Background task of writing to the database by timer
Asked Answered
B

1

6

How to write to the database on a timer in the background. For example, check mail and add new letters to the database. In the example, I simplified the code just before writing to the database.

The class names from the example in Microsoft. The recording class itself:

namespace EmailNews.Services
{

internal interface IScopedProcessingService
{
    void DoWork();
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private readonly ApplicationDbContext _context;
    public ScopedProcessingService(ApplicationDbContext context)
    {
        _context = context;
    }

    public void DoWork()
    {
        Mail mail = new Mail();
        mail.Date = DateTime.Now;
        mail.Note = "lala";
        mail.Tema = "lala";
        mail.Email = "lala";
        _context.Add(mail);
        _context.SaveChangesAsync();
    }
}
}

Timer class:

namespace EmailNews.Services
{
#region snippet1
internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(IServiceProvider services, ILogger<TimedHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }
    public IServiceProvider Services { get; }

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

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromMinutes(1));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService =
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            scopedProcessingService.DoWork();
        }
    }

    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();
    }
}
#endregion
}

Startup:

        services.AddHostedService<TimedHostedService>();
        services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

It seems everything is done as in the example, but nothing is added to the database, which is not so?

Brag answered 2/11, 2018 at 10:6 Comment(5)
Did you try debugging? Does the timer fire? Is the service's DoWork called? Does it complete or does it throw? Apart from debugging you should add logging to your code to log exceptions at least.Mosora
It's probably the fact that you don't await your SaveChangesAsync call, especially as the scope that produces your IScopedProcessingService and corresponding ApplicationDbContext instances is being disposed before that call completes.Oraleeoralia
The Timer class can't handle an async callback. DoWork has to be async Task to allow SaveChangesAsync to complete without blocking. This means you can't call it from a timer's callback. You could replace the timer with a a loop that contains a Task.Delay().Mosora
Check Maarten Balliauw's article Building a scheduled task in ASP.NET Core/Standard 2.0. The ExecuteAsync contains a loop that call the tasks inside a timer and then calls await Task.Delay(...);. The article goes a lot farther than that, allowing the definition of custom tasks, parsing cron schedule strings, executing multiple tasks at a time etcMosora
David Fowler created a Github repo with async gotchas and warnings. Check the Timer Callbacks section for the proper way to call asynchronous callbacksMosora
M
7

This is a rather interesting question, that boils down to "How do you correctly handle an async timer callback?"

The immediate problem is that SaveChangesAsync isn't getting awaited. The DbContext almost certainly gets disposed before SaveChangesAsync has a chance to run. To await it, DoWork must become an async Task method (never async void) :

internal interface IScheduledTask
{
    Task DoWorkAsync();
}

internal class MailTask : IScheduledTask
{
    private readonly ApplicationDbContext _context;
    public MailTask(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task DoWorkAsync()
    {
        var mail = new Mail 
                   { Date = DateTime.Now,
                     Note = "lala",
                     Tema = "lala",
                     Email = "lala" };
        _context.Add(mail);
        await _context.SaveChangesAsync();
    }
}

The problem now is how to call DoWorkAsync from the timer callback. If we just call it without awaiting, we'll get the same problem we had in the first place. A timer callback can't handle methods that return Task. We can't make it async void either, because this would result in the same problem - the method will return before any async operation has a chance to finish.

David Fowler explains how to properly handle asynchronous timer callbacks in the Timer Callbacks section of his Async Guidance article :

private readonly Timer _timer;
private readonly HttpClient _client;

public Pinger(HttpClient client)
{
    _client = new HttpClient();
    _timer = new Timer(Heartbeat, null, 1000, 1000);
}

public void Heartbeat(object state)
{
    // Discard the result
    _ = DoAsyncPing();
}

private async Task DoAsyncPing()
{
    await _client.GetAsync("http://mybackend/api/ping");
}

The actual method should be async Task but the returned task only has to be assigned, not awaited, in order for it to work properly.

Applying this to the question leads to something like this :

public Task StartAsync(CancellationToken cancellationToken)
{
    ...
    _timer = new Timer(HeartBeat, null, TimeSpan.Zero,
        TimeSpan.FromMinutes(1));

    return Task.CompletedTask;
}

private void Heartbeat(object state)
{
    _ = DoWorkAsync();
}


private async Task DoWorkAsync()
{
    using (var scope = Services.CreateScope())
    {
        var schedTask = scope.ServiceProvider
                             .GetRequiredService<IScheduledTask>();

        await schedTask.DoWorkAsync();
    }
}

David Fowler explains why async void is ALWAY BAD in ASP.NET Core - it's not only that async actions won't be awaited, exceptions will crash the application.

He also explains why we can't use Timer(async state=>DoWorkAsync(state)) - that's an async void delegate.

Mosora answered 2/11, 2018 at 11:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.