Фоновая задача записи в базу данных по таймеру
Как записать в базу данных по таймеру в фоновом режиме. Например, проверьте почту и добавьте новые письма в базу данных. В этом примере я упростил код непосредственно перед записью в базу данных.
Имена классов из примера в 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 или отдельный процесс не требуются.
Поддержанный постоянным хранением. Открытый и бесплатный для коммерческого использования.