Фоновая задача записи в базу данных по таймеру

Как записать в базу данных по таймеру в фоновом режиме. Например, проверьте почту и добавьте новые письма в базу данных. В этом примере я упростил код непосредственно перед записью в базу данных.

Имена классов из примера в Microsoft. Сам класс записи:

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

Класс таймера:

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
}

Запускать:

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

Вроде все сделано как в примере, но ничего не добавлено в базу данных, что не так?

2 ответа

Решение

Это довольно интересный вопрос, который сводится к "Как вы правильно обрабатываете обратный вызов асинхронного таймера?"

Непосредственная проблема заключается в том, что SaveChangesAsync не ждет DbContext почти наверняка будет ликвидирован раньше SaveChangesAsync есть шанс бежать. Ждать этого, DoWork должен стать async Task метод (никогда не асинхронный 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();
    }
}

Теперь проблема в том, как позвонить DoWorkAsync из таймера обратного вызова. Если мы просто позвоним без ожидания, мы получим ту же проблему, что и у нас. Обратный вызов таймера не может обрабатывать методы, которые возвращают Task. Мы не можем сделать это async void либо, потому что это приведет к той же самой проблеме - метод вернется до того, как любая асинхронная операция сможет завершиться.

Дэвид Фаулер объясняет, как правильно обрабатывать асинхронные обратные вызовы таймера в разделе "Обратные вызовы таймера " в своей статье "Асинхронное руководство":

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");
}

Фактический метод должен быть async Task но возвращаемое задание нужно только назначить, а не ожидать, чтобы оно работало должным образом.

Применение этого к вопросу приводит к чему-то вроде этого:

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

Дэвид Фаулер объясняет, почему async void ВСЕГДА ПЛОХО в ASP.NET Core - не только ожидаются асинхронные действия, исключения приводят к сбою приложения.

Он также объясняет, почему мы не можем использовать Timer(async state=>DoWorkAsync(state)) - это async void делегировать.

Вместо того, чтобы изобретать велосипед, id предлагает взглянуть на HangFire. Со своей страницы:

Простой способ выполнения фоновой обработки в приложениях.NET и.NET Core. Служба Windows или отдельный процесс не требуются.

Поддержанный постоянным хранением. Открытый и бесплатный для коммерческого использования.

Другие вопросы по тегам