Atmoos.Commands 0.1.1

dotnet add package Atmoos.Commands --version 0.1.1
NuGet\Install-Package Atmoos.Commands -Version 0.1.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="Atmoos.Commands" Version="0.1.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Atmoos.Commands --version 0.1.1
#r "nuget: Atmoos.Commands, 0.1.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 Atmoos.Commands as a Cake Addin
#addin nuget:?package=Atmoos.Commands&version=0.1.1

// Install Atmoos.Commands as a Cake Tool
#tool nuget:?package=Atmoos.Commands&version=0.1.1

Commands

Executing a sequence of commands may seem quite a trivial task at first. This holds true for very short sequences that don't exchange much data. When chains of sequences become a bit longer and execute non-trivial tasks, the problem becomes a bit involved.

This library sets out at solving this set of problems:

  • Passing data form one command to the next.
    • using C# strong type safety.
    • making it obvious for all involved in a project how data is passed around.
  • Building the chain of commands.
  • Cancelling the execution of the chain of commands.
  • Reporting progress during execution of the chain of commands.

Concept

In this libraries abstraction of a command, there are five principal properties a command must fulfil:

  • Every command can be executed.
  • Execution of every command can be cancelled.
  • Progress of individual commands or a group of commands can be easily observed.
  • Obvious data dependencies form one command to the next.
  • Failure is reported using standard C# semantics with Exceptions.

Four Interfaces

The library hinges on four distinct ICommand interfaces that each define a single Execute method, reflecting the stated properties above:

Task Execute(CancellationToken cancellationToken, Progress progress);

Task Execute(TArgument argument, CancellationToken cancellationToken, Progress progress);

Task<TResult> Execute(CancellationToken cancellationToken, Progress progress);

Task<TResult> Execute(TArgument argument, CancellationToken cancellationToken, Progress progress);
  • The first three properties (execute, cancel, progress) are obvious, as they are each an explicit part of the signature.
  • The fourth property (data dependency) is made obvious by defining a distinct interface for each variant of data exchange (none, in, out, both).
  • The fifth property (failure) is the least obvious property. It is "implemented" by just using standard C# error semantics.

Extension Methods

This library aims at making it as easy as possible for developers to implement any sequence of commands. For this reason, there is a large set of extension methods that take over most of the plumbing required to integrate arbitrary existing code into a chain of commands.

--> It ought to be able to integrate existing codebases to use this library without having to implement any of the four ICommand interfaces.

Command Builder

To easily chain up commands, there is a pre-implemented builder pattern. It allows for obvious chaining of commands reflecting their data dependencies in a type safe manner.

As with the commands, most of this functionality is exposed using extension methods.

A Note on Asynchronicity

For now the pattern is implemented using the task asynchronous pattern (TAP). This does not mean that all commands are executed in parallel or out of order. The standard way of chaining commands is in sequential order.

Concurrent execution is just an option.

Distinction from the GoF Command Pattern

It is a "forward only" command pattern without an "undo" operation. This greatly simplifies dealing with a chain of commands. However, it limits the usefulness of this library should "undo" be required.

The focus is on easily chaining up commands.

Examples

Method to ICommand

public ICommand<Byte[], SomeDomainObject> CreateCommandFormMethod()
{
    static SomeDomainObject SerializeMethod(Byte[] json)
    {
        return JsonSerializer.Deserialize<SomeDomainObject>(json);
    }

    return FuncExtensions.ToCommand<Byte[], SomeDomainObject>(SerializeMethod);
}

Integrate a Method using Builder

Note how the builders reflect which input output type combination is expected for the command (or sequence of commands).

  • the input builder expects to receive a command or method that takes an array of bytes as input.
  • The method we add takes that array of bytes and serializes it into some domain object.
  • Then in the next step in the sequence of commands, we expect a command that takes that serialized domain object as input.
public IBuilder<SomeDomainObject> AddCommandToBuilder(IBuilder<Byte[]> builder)
{
    static SomeDomainObject SerializeMethod(Byte[] json)
    {
        return JsonSerializer.Deserialize<SomeDomainObject>(json);
    }

    return builder.Add(SerializeMethod);
}

Building a Chain of Commands

This example focusses on how commands can be chained together in a type safe way. The example focusses on the chaining and, hence, does not show the actual implementations of the methods themselves.

// Create an opaque command given some parameters.
public static ICommand EvaluateFromFileSystem(String fileToRead, String pathToPersistTo)
{
    // Read form file -> do some computation -> persist back to file.
    IBuilder<FileInfo> builder = ((Func<FileInfo>)(() => GetFileToProcess(fileToRead))).StartBuilder();
    IBuilder completedBuilder = builder.Add(ReadFile)
                                    .AddComputationEngine()
                                    .Add(content => PersistToFile(pathToPersistTo, content));
    return completedBuilder.Build();
}

// Add "computation engine" that is oblivious from where the input bytes come from,
// or where the resulting output bytes are persisted to.
public static IBuilder<Byte[]> AddComputationEngine(this IBuilder<Byte[]> builder)
{
    return builder.Add(Deserialize).Add(Parse).Add(Compute).Add(Serialize);

    /* The same chain with explicit type arguments: 
    IBuilder<Addition> deserialize = builder.Add(Deserialize);
    IBuilder<(Addition, BinaryOperation)> parse = deserialize.Add(Parse);
    IBuilder<Result> compute = parse.Add(Compute);
    IBuilder<Byte> serialize = compute.Add(Serialize);
    return serialize;
    */
}

// Use the example in a console application.
public static async Task Main(String[] _)
{
    ICommand command = EvaluateFromFileSystem("input.json", "output.json");
    await command.Execute(CancellationToken.None, Progress.Empty).ConfigureAwait(false);
}
Product Compatible and additional computed target framework versions.
.NET 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.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.1.1 288 12/12/2021

First version