Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to chain methods in .net with async/await

I've started to learn functional programming and while chaining methods looks great (in my opinion) in normal cases, it really gets ugly when dealing with async/await

await (await (await CosmosDbRepository<ApplicationProcess>
    .GetItemAsync(param.ProcessId))
.Historize(() => _analyseFinanciereService.ProcessAsync(), 
    ProcessStepEnum.Application))
.Notify(p => p.GetLastStep());

Is there any way to remove this noise?

Edit :

public static async Task<ApplicationProcess> Historize(
this ApplicationProcess process, 
Func<Task> fn, 
ProcessStepEnum stepEnum)
{
    var dateStart = DateTime.UtcNow;
    var error = string.Empty;
    try
    {
        await fn();
        return process;
    }
    …

public static async Task Notify<TResult>(
    this ApplicationProcess process, 
    Func<ApplicationProcess, TResult> fn)
...

Edit2 : with extensions methods accepting a Task

await CosmosDbRepository<ApplicationProcess>
    .GetItemAsync(param.ProcessId)
    .HistorizeAsync(() => _analyseFinanciereService.ProcessAsync(), ProcessStepEnum.Application)
    .NotifyAsync(p => p.GetLastStep());

So that's what I was looking for even if I'm confused with the latest comments

like image 313
Xav Sc Avatar asked Oct 10 '18 09:10

Xav Sc


1 Answers

All the code presented I uploaded as a LinqPad query, so You can try it right away.

Functional programming has a concept of monad (unfamiliar C# programmers I strongly recommend to start with the link provided). A C# task may be considered a monad, and as far as I understand it is exactly what You need.

For the purpose of this answer I made a simplified example of what You have:

await (await (await A.GetNumber()).DoubleIt()).SquareIt()

where the methods are as follows (defined as static just for my convenience):

public static class A
{
    public static Task<int> GetNumber(){return Task.FromResult(3);}
    public static Task<int> DoubleIt(this int input){return Task.FromResult(2 * input);}
    public static Task<int> SquareIt(this int input){return Task.FromResult(input * input);}
}

Now You can easily chain them with just a bit of glue which can look like this:

public static async Task<TOut> AndThen<TIn, TOut>(this Task<TIn> inputTask, Func<TIn, Task<TOut>> mapping)
{
    var input = await inputTask;
    return (await mapping(input));
}

The AndThen method acts exactly like a monadic bind:

await A
     .GetNumber()
     .AndThen(A.DoubleIt)
     .AndThen(A.SquareIt)

What's more important, C# has nice syntax for working with monads: the LINQ query comprehension syntax. You just need to define a SelectMany method which works with the type You desire (Task in this case) and You're ready to go.

Below I implemented the most "hardcore" overload of SelectMany (with additional resultSelector) which gives You the most flexibility. The simple version would be almost exactly the same as AndThen (I think just renaming would do the job).

public static async Task<TOut> SelectMany<TIn, TInterm, TOut>(
   this Task<TIn> inputTask,
   Func<TIn, Task<TInterm>> mapping,
   Func<TIn, TInterm, TOut> resultSelector)
{
    var input = await inputTask;
    return resultSelector(input, await mapping(input));
}

With it You can use the syntax:

var task = 
    from num in A.GetNumber()
    from doubled in num.DoubleIt()
    from squared in num.SquareIt()
    select $"number: {num} doubled: {doubled}, squared: {squared}";
    
Console.WriteLine(await task);

And You get number: 3 doubled: 6, squared: 9.

The simple SelectMany version would allow You to use squared as the only possible expression in the final select line. The "hardcore" version lets You use any expression that uses any of the values defined after a from keyword.

like image 88
Grzegorz Sławecki Avatar answered Oct 16 '22 16:10

Grzegorz Sławecki