Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Design Fluent Async Operations?

Async operations do not seem to play well with fluent interfaces which I prefer to code in. How can Asynchrony be combined with Fluent?


Sample: I have two methods that previously returned a MyEntity but do not play well when change to Async. After I asyncfy them I have to await the result of the tasks, but I have to do that for each step added:

MyEntity Xx = await(await FirstStepAsync()).SecondStepAsync();

There has to be a better way.

like image 615
k.c. Avatar asked Aug 20 '15 07:08

k.c.


4 Answers

A better way would be to have deferred execution similar to LINQ.

You can have many methods that don't actually do anything, they just set some option or store some logic. And at the end have a few methods that actually execute all the other logic that was stored previously.

That way only a few methods need to be async and only a single one is used at the end of each chain.

Something like this:

var myEntity = await StartChain().StoreSomeLogic().StoreSomeOtherLogic().ExecuteAsync()

That's how, for example, the new async MongoDB C# driver works:

var results = await collection.Find(...).Project(...).Skip(...).Sort(...).ToListAsync();
like image 92
i3arnon Avatar answered Nov 09 '22 07:11

i3arnon


Some of the answers that deal with continuations are forgetting that fluent works on concrete instances that are returned from each method.

I have written a sample implementation for you. The asynchronous work will start immediately on calling any of the DoX methods.

public class AsyncFluent
{
    /// Gets the task representing the fluent work.
    public Task Task { get; private set; }

    public AsyncFluent()
    {
        // The entry point for the async work.
        // Spin up a completed task to start with so that we dont have to do null checks    
        this.Task = Task.FromResult<int>(0);
    }

    /// Does A and returns the `this` current fluent instance.
    public AsyncFluent DoA()
    {
        QueueWork(DoAInternal);
        return this;
    }

    /// Does B and returns the `this` current fluent instance.
    public AsyncFluent DoB(bool flag)
    {
        QueueWork(() => DoBInternal(flag));
        return this;
    }

    /// Synchronously perform the work for method A.
    private void DoAInternal()
    {
        // do the work for method A
    }

    /// Synchronously perform the work for method B.
    private void DoBInternal(bool flag)
    {
        // do the work for method B
    }

    /// Queues up asynchronous work by an `Action`.
    private void QueueWork(Action work)
    {
        // queue up the work
        this.Task = this.Task.ContinueWith<AsyncFluent>(task =>
            {
                work();
                return this;
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
    }
}
like image 43
Gusdor Avatar answered Nov 09 '22 08:11

Gusdor


You could add an extension method overload which takes a Task or Task<T> to any method that you want to be chainable.

public static async Task<MyEntity> SecondStepAsync(this Task<MyEntity> entityTask)
{
    return (await entityTask).SecondStepAsync();
}

So you can just call await FirstStepAsync().SecondStepAsync()

like image 10
Jamie Humphries Avatar answered Nov 09 '22 08:11

Jamie Humphries


One of the options is to declare and use the following generic extension methods:

public static TR Pipe<T, TR>(this T target, Func<T, TR> func) =>
    func(target);

public static async Task<TR> PipeAsync<T, TR>(this Task<T> target, Func<T, TR> func) =>
    func(await target);

public static async Task<TR> PipeAsync<T, TR>(this Task<T> target, Func<T, Task<TR>> func) =>
    await func(await target);

These utilities allow to represent the chain of asynchronous calls in such way:

MyEntity Xx = await FirstStepAsync()
    .PipeAsync(async firstResult => await firstResult.SecondStepAsync())
    .PipeAsync(async secondResult => await secondResult.ThirdStepAsync());

Resulting code may look more verbose, however it is easier to expand the chain, since there are no nested brackets.

like image 7
Gennadii Saltyshchak Avatar answered Nov 09 '22 07:11

Gennadii Saltyshchak