22

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.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
k.c.
  • 1,755
  • 1
  • 29
  • 53

7 Answers7

18

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();
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • @i3amon This solution looks great if you can pass som settings DTO all the way to the final Async method. But that is not realy the nature of my methods. – k.c. Aug 20 '15 at 08:32
  • @k.c. This is the nature of a fluent API. You have a single interface (e.g. `IEnumerable`) with all the extension methods and you have some class that implements that interface that must be returned from all these methods (except the last one) to enable the next fluent step. That class is your DTO. – i3arnon Aug 20 '15 at 08:35
  • @i3arnon nice idea, in my case, the caller method can not be marked as `async`, so `await` is not there (at the beginning of the chain), is there any way to use your idea in this case or I should use alternative pattern? – Mehdi Dehghani Mar 11 '19 at 13:09
  • How would this solution handle if some of the methods in chain requiere to call async methods themselves? .Wait() on every such call? – wondra Jan 12 '21 at 12:31
14

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);
    }
}
ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
Gusdor
  • 14,001
  • 2
  • 52
  • 64
  • your answer seems to come closest to what I'm trying to achieve: Invoke a varying number of methods on some class instance. Normally each method would return the instance, so that an other action can be invoked on it. This works kind of like that. It gives quite a bit of overhead though. Thanks – k.c. Aug 20 '15 at 11:22
  • Seem pretty complex. – Jerry Nixon Mar 03 '20 at 07:04
  • @JerryNixon absolutely. Fluent interfaces are designed to remove imperative complexity from consuming code and replace it with lots of structural complexity behind the interface. I'm tempted to update the answer to use local functions as a simplification. – Gusdor Mar 03 '20 at 14:31
  • I'm not understand your example, you don't have any async / async. – Guilherme Flores May 14 '20 at 16:46
  • @GuilhermeFlores it is the `Task` that enables async/await because it implements `GetAwaiter` – Gusdor May 19 '20 at 14:02
  • If DoA wanted to access a DB asynchronously how would that work? – Tod Jul 05 '22 at 07:45
12

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

Jamie Humphries
  • 3,368
  • 2
  • 18
  • 21
  • this is the best answer IMHO. Essentially for each `StepAsync(this T)` you need a `StepAsync(this Task)`. Then you can chain them as normal – Jonesopolis May 27 '21 at 18:08
7

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.

3

Rewrite your fluent methods to this form - i.e. it makes little sense for them to be extension methods:

        public static async Task<ResultType> Transform(SourceType original)
        {
            // some async work 
            var transformed = await DoSomething(original)
            return transformed;
        }

Provide these generic extension methods (pretty much the same as @Gennadii Saltyshchak's PipeSync methods, but I find the "Then" moniker much more succinct):

        public static async Task<T2> Then<T1, T2>(this T1 first, Func<T1, Task<T2>> second)
        {
            return await second(first).ConfigureAwait(false);
        }

        public static async Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, Task<T2>> second)
        {
            return await second(await first.ConfigureAwait(false)).ConfigureAwait(false);
        }

        public static async Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, T2> second)
        {
            return second(await first.ConfigureAwait(false));
        }

        public static Task<T2> Then<T1, T2>(this T1 first, Func<T1, T2> second)
        {
            return Task.FromResult(second(first));
        }

You can then construct a fluent chain like this:

        var processed = await Transform1(source)
            .Then(Transform1)
            .Then(Transform2)
            .Then(Transform3);

Thanks to the Then() overloads, this works with any permutation of async/sync methods.

If e.g. Transform2 takes arguments, you need to expand to a lambda:

        var processed = await Transform1(source)
            .Then(Transform1)
            .Then(x => Transform2(x, arg1, arg2))
            .Then(Transform3);

I think this is as close to a fluent async chain as you can get with the current language!

Combinations of async/sync should be optimized with ValueTask where possible. An exercise for the reader! :-)

Robert Schmidt
  • 699
  • 4
  • 14
2

await is basically shorthand for ContinueWith, a method on a Task object (I'm simplifying here, but that's the basic concept). If you're trying to build a fluent syntax, consider using extension methods that receive Tasks and use ContinueWith to chain them:

public Task FirstStep()
{
     return Task.Run(/* whatever */);
}

public static Task SecondStep (this Task previousStep)
{
    return previousStep.ContinueWith(task => { /* whatver */  };
}

Now you can call await FirstStep().SecondStep(), and await the final result. Each method essentially adds another ContinueWith step.

If you want to make it more type safe, inherit a MyFluentTask from Task, and return that instead of a regular Task.

Avner Shahar-Kashtan
  • 14,492
  • 3
  • 37
  • 63
0

Another option is to implement the basic LINQ operators to allow LINQ syntax. Then you can do this:

MyEntity Xx = await
    from f in FirstStepAsync()
    from e in f.SecondStepAsync()
    select e;

The code you need is this:

public static class Ex
{
    public static Task<TResult> SelectMany<TSource, TResult>(this Task<TSource> source, Func<TSource, Task<TResult>> collectionSelector)
        => source.ContinueWith(t => collectionSelector(t.Result)).Unwrap();

    public static Task<TResult> SelectMany<TSource, TCollection, TResult>(this Task<TSource> source, Func<TSource, Task<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)
        => source.ContinueWith(t =>
        {
            Task<TCollection> ct = collectionSelector(t.Result);
            return ct.ContinueWith(_ => resultSelector(t.Result, ct.Result));
        }).Unwrap();
}
Enigmativity
  • 113,464
  • 11
  • 89
  • 172