I'm using ASP.NET Core, and EF Core which has SaveChanges
and SaveChangesAsync
.
Before saving to the database, in my DbContext
, I perform some auditing/logging:
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();
}
The problem is the synchronous SaveChanges()
.
I always do "async all the way down", but here that isn't possible. I could redesign to have LogAndAudit()
and LogAndAuditAsync()
but that is not DRY and I'll need to change a dozen other major pieces of code which don't belong to me.
There are lots of other questions about this topic, and all are general and complex and full of debate. I need to know the safest approach in this specific case.
So, in SaveChanges()
, how do I safely and synchronously call an async method, without deadlocks?
There are many ways to do sync-over-async, and each has it's gotchas. But I needed to know which is the safest for this specific use case.
The answer is to use Stephen Cleary's "Thread Pool Hack":
Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();
The reason is that within the method, only more database work is performed, nothing else. The original sychronization context is not needed - within EF Core's DbContext
you shouldn't need access to ASP.NET Core's HttpContext
!
Hence it is best to offload the operation to the thread pool, and avoid deadlocks.
The simplest way to call an async method from a non-async method is to use GetAwaiter().GetResult()
:
public override int SaveChanges {
LogAndAuditAsync().GetAwaiter().GetResult();
return base.SaveChanges();
}
This will ensure that an exception thrown in LogAndAuditAsync
does not appear as an AggregateException
in SaveChanges
. Instead the original exception is propagated.
However, if the code is executing on a special synchronization context that may deadlock when doing sync-over-async (e.g. ASP.NET, Winforms and WPF) then you have to be more careful.
Every time the code in LogAndAuditAsync
uses await
it will wait for a task to complete. If this task has to execute on the synchronization context that currently is blocked by the call to LogAndAuditAsync().GetAwaiter().GetResult()
you have a deadlock.
To avoid this you need to add .ConfigureAwait(false)
to all await
calls in LogAndAuditAsync
. E.g.
await file.WriteLineAsync(...).ConfigureAwait(false);
Notice that after this await
the code will continue executing outside the synchronization context (on the task pool scheduler).
If that is not possible your last option is to start a new task on the task pool scheduler:
Task.Run(() => LogAndAuditAsync()).GetAwaiter().GetResult();
This will still block the synchronization context but LogAndAuditAsync
will execute on the task pool scheduler and not deadlock because it does not have to enter the synchronization context that is blocked.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With