Как безопасно вызвать метод 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 будет выполняться в планировщике пула задач, а не в тупике, потому что ему не нужно вводить заблокированный контекст синхронизации.