Это довольно интересный вопрос, который сводится к "Как вы правильно обрабатываете обратный вызов асинхронного таймера?"
Непосредственная проблема в том, что 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();
}
}
Дэвид Фаулер объясняет, почему асинхронная пустота ВСЕГДА ПЛОХАЯ в ASP.NET Core - не только ожидаются асинхронные действия, исключения приводят к сбою приложения.
Он также объясняет, почему мы не можем использовать Timer(async state=>DoWorkAsync(state))
- это async void
делегат.