1

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)?

2
  • Do you plan to call the API from the UI? Do the operation require I/O?
    – Theraot
    Nov 5, 2020 at 5:46
  • @Theraot It is likely to be called from UI thread (Application Thread), but I can't be totally sure. Some of the operations do involve IO (though it's done by the API), and some operations involve the UI as well (for the current vendor's implementation, it provides a WPF control and it's written in C++). I believe some parts are internally offloaded to GPU (or OpenGL/DirectX etc) as well.
    – Jai
    Nov 5, 2020 at 5:49

1 Answer 1

1

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.
5
  • Thanks for making the effort to write a long answer. The blue-red article is definitely an interesting read for me - that is one of the reasons why we want to be careful in designing the adapter interface. The "shell" part starts to get a little too complex for me, if it's possible, could you give a quick example?
    – Jai
    Nov 5, 2020 at 10:19
  • @Jai By shell I mean anything that goes outside of your system. Imagine what would happen if you decided to do the same you are doing for the API vendors but for everything. Database? Interface, and vendor specific adapters. UI? Interface, and vendor specific adapters. And so on. All that is shell. Some of them will be input only, some output only, some input and output. These adapters should be simple, avoiding application logic, minimal branching. You, of course, don't have to do all that. Just identify what is shell and what isn't. What isn't shell is either core or infrastructure.
    – Theraot
    Nov 5, 2020 at 18:03
  • @Jai The cure is pure (as in "no side effects"), has the application logic, has no reason to have async code, and should be easy to test mocking the shell (all the external systems). Infrastructure is not application specific. Generic code that can, and usually is reused. For example, a generic collection library that does not really talk to an external system, and thus you have no reason to replace it. However, it is a dependency nonetheless. The shell should depend on the core and the infrastructure, not the other way around.
    – Theraot
    Nov 5, 2020 at 18:06
  • @Jai you will find similar concepts in the hexagon architecture, the onion architecture, and similar. There is an architectural line where dependencies goes only in one direction. They talk about ports, adapters, boundary, gateways, controllers, etc... However, they all start on the same basic idea of an architectural line that separates your system from the outside, limiting the propagation of changes on other systems into yours. Well, if we are stopping propagation, of async in this case, that is a good tool.
    – Theraot
    Nov 5, 2020 at 18:10
  • Thanks for the explanation! It took me a while to read up on some new topics which like hexagon architecture etc. I believe most part of the application coincidentally looks similar to one of those architectures.
    – Jai
    Nov 10, 2020 at 8:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.