Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using async await when implementing a library with both synchronous and asynchronous API for the same functionality

I've got a few questions about how to provide both synchronous and asynchronous implementation of the same functionality in a library. I am gonna ask them first and then provide the example code below (which is actually quite a bit, but in fact it's quite simple).

  1. Is there an approach to avoid violating the DRY principle? Consider the implementations of JsonStreamReader.Read, JsonStreamWriter.Write, JsonStreamWriter.Flush, ProtocolMessenger.Send, ProtocolMessenger.Receive and their asynchronous versions.

  2. Is there an approach to avoid violating the DRY principle when unit testing both synchronous and asynchronous versions of the same method? I am using NUnit, although I guess all frameworks should be the same in this regard.

  3. How should be implemented a method returning Task or Task<Something> considering the Take 1 and Take 2 variants of ComplexClass.Send and ComplexClass.Receive? Which one is correct and why?

  4. Is it correct to always include .ConfigureAwait(false) after await in a library considering it is not known where the library will be used (Console application, Windows Forms, WPF, ASP.NET)?

And here follows the code I am referring to in the first questions.

IWriter and JsonStreamWriter:

public interface IWriter
{
    void Write(object obj);
    Task WriteAsync(object obj);
    void Flush();
    Task FlushAsync();
}

public class JsonStreamWriter : IWriter
{
    private readonly Stream _stream;

    public JsonStreamWriter(Stream stream)
    {
        _stream = stream;
    }

    public void Write(object obj)
    {
        string json = JsonConvert.SerializeObject(obj);
        byte[] bytes = Encoding.UTF8.GetBytes(json);
        _stream.Write(bytes, 0, bytes.Length);
    }

    public async Task WriteAsync(object obj)
    {
        string json = JsonConvert.SerializeObject(obj);
        byte[] bytes = Encoding.UTF8.GetBytes(json);
        await _stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
    }

    public void Flush()
    {
        _stream.Flush();
    }

    public async Task FlushAsync()
    {
        await _stream.FlushAsync().ConfigureAwait(false);
    }
}

IReader and JsonStreamReader:

public interface IReader
{
    object Read(Type objectType);
    Task<object> ReadAsync(Type objectType);
}

public class JsonStreamReader : IReader
{
    private readonly Stream _stream;

    public JsonStreamReader(Stream stream)
    {
        _stream = stream;
    }

    public object Read(Type objectType)
    {
        byte[] bytes = new byte[1024];
        int bytesRead = _stream.Read(bytes, 0, bytes.Length);
        string json = Encoding.UTF8.GetString(bytes, 0, bytesRead);
        object obj = JsonConvert.DeserializeObject(json, objectType);
        return obj;
    }

    public async Task<object> ReadAsync(Type objectType)
    {
        byte[] bytes = new byte[1024];
        int bytesRead = await _stream.ReadAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
        string json = Encoding.UTF8.GetString(bytes, 0, bytesRead);
        object obj = JsonConvert.DeserializeObject(json, objectType);
        return obj;
    }
}

IMessenger and ProtocolMessenger:

public interface IMessenger
{
    void Send(object message);
    Task SendAsync(object message);
    object Receive();
    Task<object> ReceiveAsync();
}

public interface IMessageDescriptor
{
    string GetMessageName(Type messageType);
    Type GetMessageType(string messageName);
}

public class Header
{
    public string MessageName { get; set; }
}

public class ProtocolMessenger : IMessenger
{
    private readonly IMessageDescriptor _messageDescriptor;
    private readonly IWriter _writer;
    private readonly IReader _reader;

    public ProtocolMessenger(IMessageDescriptor messageDescriptor, IWriter writer, IReader reader)
    {
        _messageDescriptor = messageDescriptor;
        _writer = writer;
        _reader = reader;
    }

    public void Send(object message)
    {
        Header header = new Header();
        header.MessageName = _messageDescriptor.GetMessageName(message.GetType());

        _writer.Write(header);
        _writer.Write(message);
        _writer.Flush();
    }

    public async Task SendAsync(object message)
    {
        Header header = new Header();
        header.MessageName = _messageDescriptor.GetMessageName(message.GetType());

        await _writer.WriteAsync(header).ConfigureAwait(false);
        await _writer.WriteAsync(message).ConfigureAwait(false);
        await _writer.FlushAsync().ConfigureAwait(false);
    }

    public object Receive()
    {
        Header header = (Header)_reader.Read(typeof(Header));
        Type messageType = _messageDescriptor.GetMessageType(header.MessageName);
        object message = _reader.Read(messageType);
        return message;
    }

    public async Task<object> ReceiveAsync()
    {
        Header header = (Header)await _reader.ReadAsync(typeof(Header)).ConfigureAwait(false);
        Type messageType = _messageDescriptor.GetMessageType(header.MessageName);
        object message = await _reader.ReadAsync(messageType).ConfigureAwait(false);
        return message;
    }
}

ComplexClass:

public interface ISomeOtherInterface
{
    void DoSomething();
}

public class ComplexClass : IMessenger, ISomeOtherInterface
{
    private readonly IMessenger _messenger;
    private readonly ISomeOtherInterface _someOtherInterface;

    public ComplexClass(IMessenger messenger, ISomeOtherInterface someOtherInterface)
    {
        _messenger = messenger;
        _someOtherInterface = someOtherInterface;
    }

    public void DoSomething()
    {
        _someOtherInterface.DoSomething();
    }

    public void Send(object message)
    {
        _messenger.Send(message);
    }

    // Take 1
    public Task SendAsync(object message)
    {
        return _messenger.SendAsync(message);
    }

    // Take 2
    public async Task SendAsync(object message)
    {
        await _messenger.SendAsync(message).ConfigureAwait(false);
    }

    public object Receive()
    {
        return _messenger.Receive();
    }

    // Take 1
    public Task<object> ReceiveAsync()
    {
        return _messenger.ReceiveAsync();
    }

    // Take 2
    public async Task<object> ReceiveAsync()
    {
        return await _messenger.ReceiveAsync().ConfigureAwait(false);
    }
}
like image 347
Ceco Avatar asked Jan 09 '15 14:01

Ceco


People also ask

Is async await asynchronous or synchronous?

Top-level code, up to and including the first await expression (if there is one), is run synchronously. In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously.

Can one async function have multiple awaits?

Using one try/catch block containing multiple await operations is fine when waiting for promises created on the right hand side of the await unary operator: The await operator stores its parent async functions' execution context and returns to the event loop.

Is await the same as synchronous?

Your async code block is waiting for the await call to return to continue, however the rest of your application isn't waiting and can still continue like normal. In contrast, a synchronous call would make your entire application or thread wait until the code finished executing to continue on with anything else.

What is async await and how does it work?

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method.


2 Answers

The general answer here is that making both truly async and sync versions of the same functionality requires 2 different (maybe similar, maybe not) implementations. You can try and find duplicate parts and reuse them using a base class (or a utility class) but the implementations would mostly be different.

In many cases, people choose to only supply one version of the API, be it asynchronous or not. For example the .Net client library for YouTube API v3 is entirely async all the way through. If you can afford that (many can't) that would be my recommendation.

About your specific questions:

  1. Not really, other than finding similar parts and abstracting them away.
  2. Not really, synchronous methods need to be tested in a synchronous context while async ones in an async context.
  3. Take 1 (i.e. returning a task directly) is preferable in 2 ways:
    • It lacks the overhead of creating the whole unneeded async state machine which adds a very slight performance boost.
    • ConfigureAwait in this case affects only the code that comes after it, which in this case is none at all. It doesn't affect the caller's code whether it uses ConfigureAwait or not.
  4. Definitely yes (finally, positivity). async code in libraries should use ConfigureAwait(false) by default, and remove it only when there's a specific need to.
like image 70
i3arnon Avatar answered Oct 24 '22 04:10

i3arnon


Generally speaking, APIs should be either asynchronous or synchronous. E.g., if your implementation includes I/O, it should be asynchronous.

That said, there are scenarios where you do want to have both synchronous and asynchronous APIs. E.g., if the work is naturally asynchronous, but the synchronous APIs need to be kept for backwards compatibility.

If you're in that situation, I recommend using the boolean argument hack to minimize the amount of duplicated code. Asynchronous wrappers over synchronous methods and synchronous wrappers over asynchronous methods are both antipatterns.

like image 38
Stephen Cleary Avatar answered Oct 24 '22 05:10

Stephen Cleary