FreeAwait 0.5.1

dotnet add package FreeAwait --version 0.5.1
NuGet\Install-Package FreeAwait -Version 0.5.1
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FreeAwait" Version="0.5.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add FreeAwait --version 0.5.1
#r "nuget: FreeAwait, 0.5.1"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install FreeAwait as a Cake Addin
#addin nuget:?package=FreeAwait&version=0.5.1

// Install FreeAwait as a Cake Tool
#tool nuget:?package=FreeAwait&version=0.5.1

FreeAwait

.NET Nuget GitHub

Await anything for free!

Purpose

FreeAwait is a tiny .NET library implementing a free monad-like pattern with C# async/await. It can be used as a more functional alternative to dependency injection, that comes without the need to give up on the good old idiomatic C# code style.

Reasons to use

  • ✔️ control over side effects, loose coupling and better testability;
  • 📜 code looks more idiomatic than composed LINQ expressions suggested by other good libraries;
  • ⌛ it's free, freedom is worth a wait (terrible pun, sorry).

Quick example

Lets start with installing the FreeAwait package from NuGet and adding to our using directives:

using FreeAwait;
using Void = FreeAwait.Void;

The second line gives us an actually useful Void type, instead of the fake one, but feel free to substitute it with any other alternative, or just roll your own, and contribute to the eventual heat death of the universe like everybody else does.

Anyway, now we can declare the following types:

record ReadLine: IStep<ReadLine, string?>;
record WriteLine(string? Text): IStep<WriteLine, Void>;

These are our program "steps" (instructions) that we want to be processed by some external "runner" (interpreter).

So, with all that in place, our program could look like this:

async IStep<string?> Greet()
{
    await new WriteLine("What's your name, stranger?");
    var name = await new ReadLine();
    await new WriteLine($"Greetings, {name}!");
    return name;
}

Remember, all these ReadLine() and WriteLine() are some inanimate data structures we just declared, so they won't do anything on their own, but they look like the real thing, right? To bring the entire construct to life, what we need now is a runner that knows how to handle our program steps. Well, let's implement one:

class ConsoleIO:
    IRun<ReadLine, string?>,
    IRun<WriteLine, Void>
{
    public string? Run(ReadLine command) => Console.ReadLine();
   
    public Void Run(WriteLine command)
    {
        Console.WriteLine(command.Text);
        return default;
    }
}

With all that, we are now able to run our program:

var name = await new ConsoleIO().Run(Greet());

You can find more demo code in samples.

More Features

  • Asyncronous step runners are implemented via IRunAsync<TStep, TResult> interface like this:
    record ReadTextFile(string FileName): IStep<ReadTextFile, string>;
    
    class AsyncIO: IRunAsync<ReadTextFile, string>
    {
        public Task<string> RunAsync(ReadTextFile step) => 
            File.ReadAllTextAsync(step.FileName);
    }
    
  • Recursive step runners are supported via IRunStep<TStep, TResult>, for example here is a recursive factorial step:
    record Factor(int N): IStep<Factor, int>;
    
    class FactorRunner: IRunStep<Factor, int>
    {
        public async IStep<int> RunStep(Factor step) => 
            step.N <= 1 ? 1 : await new Factor(step.N - 1) * step.N;
    }
    
  • Tail recursion is automagically trampolined (i.e. translated into a loop), so the following recursive fibonacci computation will not blow up the stack, even if called with very large N:
    
    record Fib(int N, long Current = 1, long Previous = 0) : IStep<Fib, long>;
    
    class RecursiveRunner: IRunStep<Fib, long>
    {
        public IStep<long> RunStep(Fib step) => step.N <= 1
            ? Step.Result(step.Current)
            : new Fib(step.N - 1, step.Current + step.Previous, step.Current);
    }
    
  • Some static and extension methods that might come in handy. When needed, you can utilize them to
    • turn any value into an IStep
      IStep<TResult> Step.Result<TResult>(TResult value)
      
    • pass step result into a function:
      IStep<TNext> IStep<TResult>.PassTo<TResult, TNext>( 
              Func<TResult, IStep<TNext>> next)
      
    • turn an IEnumerable<IStep<T>> into an IStep<IAsyncEnumerable<T>>:
      IStep<IAsyncEnumerable<T>> IEnumerable<IStep<T>>.Sequence<T>()
      

ASP.NET extensions

After adding a reference to FreeAwait.Extensions.AspNetCore from NuGet and a usual using FreeAwait directive, add the following line to your dependency registration code in Program.cs, like this:

builder.Services.AddFreeAwait();

or if you are using traditional Startup class, add this to the ConfigureServices() method:

services.AddFreeAwait();

This registers all exisiting runner classes and makes them available via a universal IServiceRunner interface which you can inject into your classes wherever you need it.

It also registers a global MVC action filter allowing you to return IStep<IActionResult> from your controller actions, pretty neat, huh? Take a look at a full MVC Todo backend example.

If you are a fan of the new ASP.NET Core minimal web API approach, great news for you: it is fully supported too.

The only thing you need to do in order to use IStep returning methods as an endpoint handlers, is to pass it through Results.Extensions.Run() helper method. But why is it need it, you ask? Because unit testing minimal web APIs in isolation is difficult! Well, you can see yourself how FreeAwait makes it a piece of cake: take look at a full unit test for the minimal Todo API and don't worry about WebApplicationFactory, because you might not need it.

Futher info

If you have a question, or think you found a bug, or have a good idea for a feature and don't mind sharing it, please open an issue and I would be happy to discuss it.

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net5.0

    • No dependencies.
  • net6.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on FreeAwait:

Package Downloads
FreeAwait.Extensions.AspNetCore

FreeAwait extensions for ASP.NET Core

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.5.1 420 12/8/2021
0.5.0 306 12/5/2021