Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 703
grokky Avatar asked Dec 18 '16 08:12

grokky


2 Answers

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.

like image 99
grokky Avatar answered Oct 21 '22 05:10

grokky


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.

like image 11
Martin Liversage Avatar answered Oct 21 '22 07:10

Martin Liversage