Question

I have a C# (WPF) application which consumes a particular 3rd party API/tool (let's call this Tool A). My colleagues and I are trying to decouple that from our application, so that it is possible to switch out Tool A in the future, replacing with another vendor's API/tool.

We decided to define a set of interfaces, which would provide the main functionalities of the tool. For every 3rd party API/tool that we want to use to replace Tool A, they would need to implement an adapter (or a set of adapters) that implements the interface(s). This also allows us to use this tool for other projects/applications.

Some of the functionalities provided by Tool A are as non-async (e.g. public Foo DoSomething(Bar bar)) methods. We are considering to define interface methods that provide such functionalities as async method (e.g. Task<Foo> DoSomethingAsync(Bar bar), i.e. Task-returning methods).

That means there are three choices here:

  1. Define these methods as non-async only.
  2. Define these methods as async only.
  3. Define both non-async and async methods..

Things to consider:

  1. There are also methods which we thought would make sense to be implemented as non-async methods, but surprisingly the vendor only provided an async version of it.

  2. When we force an async method to be synchronous (by Task.Result or Task.Wait()), we are likely to get deadlocks.

  3. Non-async methods can be easily made async.

  4. Some of the methods are likely to take some time to execute (likely CPU-bound), but that's likely to be between 100-2000ms (i.e. not terribly slow either). Actual result might depend on how the vendor implements it. Implementations of the adapter/interface can choose to:

    a. Call the tool's methods synchronously. (e.g. return Task.FromResult(DoSomething());)

    b. Wrap with Task.Run() or TaskFactory.StartNew().

  5. I have read this article from Stephen Toub, but I'm not sure whether my case is similar to the Stream example.

Example:

public class MainApp
{
    private ITool Tool; // Injected in
    public Task<Foo> GetFooFromBarAsync(Bar bar)
    {
        return Tool.DoSomethingAsync(bar);
    }
}

public interface ITool
{
    Task<Foo> DoSomethingAsync(Bar bar);
}

public class ToolAdapterA : ITool
{
    // Private fields of objects of 3rd party API's namespaces
    
    public Task<Foo> DoSomethingAsync(Bar bar)
    {
        return Task.FromResult(ToolFromVendorA.DoSomething(bar));
    }
}

public class ToolAdapterB : ITool
{
    // Private fields of objects of second 3rd party API's namespaces
    
    public Task<Foo> DoSomethingAsync(Bar bar)
    {
        return ToolFromVendorB.ICanDoSomethingInAsync(bar);
    }
}

Which of the three choices are most appropriate for my case, based on the considerations I have (+ any other considerations that might be valid)?

Was it helpful?

Solution

The linked article Should I expose asynchronous wrappers for synchronous methods? would be good advice for those writing the vendor API you are trying to adapt. Under the assumption that you will be suing APIs from third parties, that helps little.


First, at broad strokes: If it is I/O bound, async. Otherwise, sync where possible.

Then the discrepancies. There will be things that are not async but you want them async, or that are async on one vendor API but not another. You can solve some cases on consensus among the vendors. If there isn't consensus, prefer async. It is easier to go from sync to async.

That is, I'm telling you that adding overload to the sync call is better than waiting on the async call. As per FromResult vs Task.Run. Use FromResult unless it becomes a problem. You would, of course, be deciding per API per vendor. In fact, to reduce the overhead, consider ValueTask, or even a custom awaitable. See How to Use the C# Await Keyword on Anything.

Then we have the blue-red argument (What Color is Your Function?). Async tends to propagate. If you are calling from the UI, you make your event handler async, and that is about it. No big deal, big gain. But, if you are calling from somewhere deeper in the call stack, you want to avoid those async methods.

For deep calls, consider adding continuations instead of converting the methods to async.

In particular, consider defining events in your components. The UI can subscribe to them. And the components can raise them in task continuations.


The following is another way to think about it.

Ascribe to the idea pure-functional-sync "core", and a impure-imperative-async "shell". That is what you get if you bundle up all pure, sync, functional code together. Why do that? Because by the blue-red argument, impure functions tends to propagate. The same way that async propagates.

The vendor adapter by definition talks to external systems. Thus, they are shell. Event handlers take input from external systems, thus they are shell too. Thus using async between them makes sense.

Aside from the shell, the async alternative for functional code is continuations.


But, you could not have a well drawn line between shell and anything else. By considering how to enforce it we will discover another approach.

I presume you will inject the vendor adapter. Your main, or whatever composition root you have, is shell also. After all, it need to be able to call both pure and impure, both async and sync.

You can have a pure-sync components, which you also inject. That way those components has no knowledge of the UI, making that line I mentioned above clear.

Let the UI deal with all the async shenanigans, and call into those components. How do you call the adapter from them? You don't!

As mentioned above, one solution is to have them raise events, and the UI subscribe to them.

Yet, it is natural to call into the components from the UI event handlers, and process what they return. This leads to the other solution I was alluding to: Command objects. That is, the component returns a value that tells the UI what to do. See also Command pattern.


For completeness I'll say that polling is another way to go about it. As you would guess, I'm not advising to do that.


For abstract, I'm saying to prefer async. If you call from UI, you can make event handlers async. To avoid propagation of async operations in other cases:

  • Use continuations, might be raising events from them.
  • Let the UI deal with the async operations and let components return command objects to the UI telling it what to do.
Licensed under: CC-BY-SA with attribution
scroll top