Как безопасно вызвать метод async из EF non-async SaveChanges?
Я использую ASP.NET Core и EF Core, у которых есть SaveChanges
и SaveChangesAsync
.
Перед сохранением в базе данных, в моем DbContext
, я выполняю некоторые проверки/протоколирование:
public async Task LogAndAuditAsync() {
// do async stuff
}
public override int SaveChanges {
/*await*/ LogAndAuditAsync(); // what do I do here???
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync {
await LogAndAuditAsync();
return await base.SaveChanges();
}
Проблема заключается в синхронном SaveChanges()
.
Я всегда делаю "асинхронный путь вниз", но здесь это невозможно. Я мог бы перепроектировать, чтобы иметь LogAndAudit()
и LogAndAuditAsync()
, но это не СУХОЙ, и мне нужно будет изменить дюжину других основных фрагментов кода, которые не принадлежат мне.
Есть много других вопросов по этой теме, и все они являются общими, сложными и полными дебатов. Мне нужно знать самый безопасный подход в этом конкретном случае.
Итак, в SaveChanges()
, как мне безопасно и синхронно вызывать метод async без взаимоблокировок?
Ответы
Ответ 1
Есть много способов сделать sync-over-async, и каждый из них имеет haschas. Но мне нужно было знать, что является безопасным для этого конкретного случая использования.
Ответ заключается в том, чтобы использовать Stephen Cleary "Hack Thread Pool Hack" :
Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();
Причина в том, что в рамках метода выполняется только больше работы с базой данных, ничего больше. Исходный контекст синхронизации не нужен - в EF Core DbContext
вам не нужен доступ к ядру ASP.NET Core HttpContext
!
Следовательно, лучше всего разгрузить операцию в пул потоков и избежать взаимоблокировок.
Ответ 2
Самый простой способ вызова асинхронного метода из неасинхронного метода - использовать GetAwaiter().GetResult()
:
public override int SaveChanges {
LogAndAuditAsync().GetAwaiter().GetResult();
return base.SaveChanges();
}
Это гарантирует, что исключение, созданное в LogAndAuditAsync
, не отображается как AggregateException
в SaveChanges
. Вместо этого распространяется исходное исключение.
Однако, если код выполняется в специальном контексте синхронизации, который может блокироваться при выполнении sync-over-async (например, ASP.NET, Winforms и WPF), тогда вам нужно быть более осторожным.
Каждый раз, когда код в LogAndAuditAsync
использует await
, он будет ждать завершения задачи. Если эта задача должна выполняться в контексте синхронизации, который в настоящий момент заблокирован вызовом LogAndAuditAsync().GetAwaiter().GetResult()
, у вас есть тупик.
Чтобы этого избежать, вам нужно добавить .ConfigureAwait(false)
ко всем вызовам await
в LogAndAuditAsync
. Например.
await file.WriteLineAsync(...).ConfigureAwait(false);
Обратите внимание, что после этого await
код продолжит выполнение вне контекста синхронизации (в планировщике пула задач).
Если это невозможно, последним вариантом является запуск новой задачи в планировщике пула задач:
Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();
Это все равно блокирует контекст синхронизации, но LogAndAuditAsync
будет выполняться в планировщике пула задач, а не в тупике, потому что ему не нужно вводить заблокированный контекст синхронизации.