How do I safely call an async method from EF's non-async SaveChanges?

async-await asynchronous c# entity-framework entity-framework-core

Question

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?

1
11
12/18/2016 1:27:17 PM

Accepted Answer

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.

0
12/20/2016 11:15:19 AM

Popular Answer

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.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow