I don't remember where I saw it some years ago.
Probably from this article of mine. I was introduced to the pattern by Stephen Toub when he did a technical review of that article.
Being that the implementation guarantees no async stuff is happening (No working with Tasks), does that change the understood logic of using GetAwaiter().GetResult() over .Result?
GetAwaiter().GetResult() - although verbose - is the accepted pattern. This is because of how exceptions are handled; Result will wrap exceptions in an AggregateException.
Is it right to assume calling the private method synchronously would lose all accepted concerns of "calling async method synchronously" because of the style of implementation? What else changes?
Yes. The way the pattern works is that if runSynchronously is true, then it must return a completed task. Since the task is already completed, it is safe to block on without any chance of deadlocks. Technically, "block" isn't really the right word, since the task is complete; the GetAwaiter().GetResult() is just extracting the result (return value or exception) from the task.
Or, what other patterns are common to combine async and sync methods that do exactly the same thing?
One update to my article is that ValueTask<T> is now available, which means there's no need to allocate a Task<T> for the synchronous path.
Also, Stephen Toub just published a blog post a few weeks ago that mentions a further improvement to the pattern. There's a kind of trick you can do; I don't think it has a name yet, but I think of it as "generic code generation", since logically it's similar to template code generation in C++. The idea is that you have a value type that implements an interface, and by constraining a generic argument to that interface, the compiler will generate different code for each generic argument (they're different because they're value types). Further, due to the constraint and lack of inheritance, the resulting IL has an extremely high chance of being well-optimized (i.e., inlined) by the JIT compiler.
It might be easier to explain with an example than with words:
interface IDoGetSomething
{
ValueTask<object> DoGetSomethingAsync(object input, Lol lol);
}
struct SyncDoGetSomething : IDoGetSomething
{
public ValueTask<object> DoGetSomethingAsync(object input, Lol lol) =>
new(GetSomething(input, lol));
}
struct AsyncDoGetSomething : IDoGetSomething
{
public async ValueTask<object> DoGetSomethingAsync(object input, Lol lol) =>
await GetSomethingAsync(input, lol);
}
The above is the generic code generation pattern for the parts of DoSomethingInternally that are different between sync and async code. DoSomethingInternally then participates in the generic code generation like this:
private async Task DoSomethingInternally<TDoGetSomething>(object input)
where TDoGetSomething : IDoGetSomething
{
//a dozen other lines of code
object somethingINeed = await default(TDoGetSomething).DoGetSomethingAsync(input, lol);
//a dozen other lines of code
}
And is exposed as such:
public Task DoSomethingAsync(object input)
=> DoSomethingInternally<AsyncDoGetSomething>(input);
public void DoSomething(object input)
=> DoSomethingInternally<SyncDoGetSomething>(input).GetAwaiter().GetResult();
And that's the end of the pattern. Perhaps a bit more verbose, but it does provide one nice benefit: all the synchronous/asynchronous differences are in a separate place (IDoGetSomething and its implementations) instead of being scattered through your implementation method.
Side note: Since this is using generic code generation, there will be two separate DoSomethingInternally methods generated, one bound to AsyncDoGetSomething and the other bound to SyncDoGetSomething. Previously there would only be one method that took a bool (runtime) parameter.
C# 11 introduces static interface methods, so generic code generation becomes even nicer, avoiding the awkward default(T) constructs:
interface IDoGetSomething
{
static abstract ValueTask<object> DoGetSomethingAsync(object input, Lol lol);
}
struct SyncDoGetSomething : IDoGetSomething
{
public static ValueTask<object> DoGetSomethingAsync(object input, Lol lol) =>
new(GetSomething(input, lol));
}
struct AsyncDoGetSomething : IDoGetSomething
{
public static async ValueTask<object> DoGetSomethingAsync(object input, Lol lol) =>
await GetSomethingAsync(input, lol);
}
private async Task DoSomethingInternally<TDoGetSomething>(object input)
where TDoGetSomething : IDoGetSomething
{
//a dozen other lines of code
object somethingINeed = await TDoGetSomething.DoGetSomethingAsync(input, lol);
//a dozen other lines of code
}
// (the rest is unchanged)
public Task DoSomethingAsync(object input)
=> DoSomethingInternally<AsyncDoGetSomething>(input);
public void DoSomething(object input)
=> DoSomethingInternally<SyncDoGetSomething>(input).GetAwaiter().GetResult();