When implementing a library/infrastructure, and the user of this API would want to use the code both synchronously & asynchronously, I read that it's not a good idea to mix sync & async (e.g. the sync implementation includes waiting for the async implementation).
So obviously the sync & async implementations should be separated.
Is there an elegant way to avoid code (or more accurately "flow") duplication for sync & async implementations, which would obviously bubble down to the whole call hierarchy?
interface IMyInterface
{
void Foo();
Task FooAsync();
}
class MyImplementation1 : IMyInterface
{
public void Foo()
{
OtherMethod1();
OtherMethod2();
OtherMethod3();
OtherMethod4();
}
public async Task FooAsync()
{
await OtherMethod1Async();
await OtherMethod2Async();
await OtherMethod3Async();
await OtherMethod4Async();
}
private void OtherMethod1() { /* may contain other sync calls */ }
private void OtherMethod2() { /* may contain other sync calls */ }
private void OtherMethod3() { /* may contain other sync calls */ }
private void OtherMethod4() { /* may contain other sync calls */ }
private async Task OtherMethod1Async() { /* may contain other async calls */ }
private async Task OtherMethod2Async() { /* may contain other async calls */ }
private async Task OtherMethod3Async() { /* may contain other async calls */ }
private async Task OtherMethod4Async() { /* may contain other async calls */ }
}
When implementing a library/infrastructure, and the user of this API would want to use the code both synchronously & asynchronously
Ideally, each API in your library should either be naturally synchronous or naturally asynchronous. I recommend exposing only the most natural API. I.e., if your library needs to do I/O, it could choose to only expose an asynchronous API.
Is there an elegant way to avoid code (or more accurately "flow") duplication for sync & async implementations, which would obviously bubble down to the whole call hierarchy?
I've not found an ideal solution for this. The closest I've come is the boolean argument hack, where you have asynchronous and synchronous APIs both forward to an internal method that takes a bool sync
argument. This internal method has an asynchronous signature (returning a Task
/Task<T>
), but if sync
is true
, it always returns a completed task.
This ends up looking like this:
interface IMyInterface
{
void Foo();
Task FooAsync();
}
class MyImplementation1 : IMyInterface
{
public void Foo() => Foo(sync: true).GetAwaiter().GetResult();
public Task FooAsync() => Foo(sync: false);
private async Task Foo(bool sync)
{
// Pass `sync` along to all methods that can be sync or async.
await OtherMethod1(sync);
await OtherMethod2(sync);
await OtherMethod3(sync);
await OtherMethod4(sync);
}
private async Task OtherMethod1(bool sync)
{
// When you have to choose sync/async APIs of other classes, then choose based on `sync`.
if (sync)
Thread.Sleep(1000); // synchronous placeholder
else
await Task.Delay(1000); // asynchronous placeholder
}
}