Skip to the content.

Part 9: CommandR

Stl.CommandR is MediatR-like library helping to implement CQRS-style command handlers. Together with a set of other abstractions it enables you to get the pipeline described in the previous section with almost no extra code.

This part of the tutorial will cover CommandR itself. The next one will show how to use it together with other Fusion services to implement a robust CQRS pipeline.

Even though CommandR solves the same problem as MediatR, it offers a few new features:

Since many of you are familiar with MediatR, here is the map of its terms to CommandR terms:

You might notice the API offered by CommandR is somewhat simpler - at least while you don’t use some of its unique features mentioned earlier.

Hello, CommandR!

Let’s declare our first command and its MediatR-style handler:

``` cs –region Part09_PrintCommandSession –source-file Part09.cs –session “Hello, CommandR!” public class PrintCommand : ICommand { public string Message { get; set; } = ""; }

// Interface-based command handler public class PrintCommandHandler : ICommandHandler, IDisposable { public PrintCommandHandler() => WriteLine("Creating PrintCommandHandler."); public void Dispose() => WriteLine("Disposing PrintCommandHandler");

public async Task OnCommand(PrintCommand command, CommandContext context, CancellationToken cancellationToken)
{
    WriteLine(command.Message);
    WriteLine("Sir, yes, sir!");
} } ```

Using CommandR and MediatR is quite similar:

``` cs –region Part09_PrintCommandSession2 –source-file Part09.cs –session “Hello, CommandR!” // Building IoC container var serviceBuilder = new ServiceCollection() .AddScoped(); // Try changing this to AddSingleton var commanderBuilder = serviceBuilder.AddCommander() .AddHandlers(); var services = serviceBuilder.BuildServiceProvider();

var commander = services.Commander(); // Same as .GetRequiredService() await commander.Call(new PrintCommand() { Message = "Are you operational?" }); await commander.Call(new PrintCommand() { Message = "Are you operational?" });


The output:

Creating PrintCommandHandler. Are you operational? Sir, yes, sir! Disposing PrintCommandHandler Creating PrintCommandHandler. Are you operational? Sir, yes, sir! Disposing PrintCommandHandler


Notice that:

- CommandR doesn't auto-register command handler services - it
  cares only about figuring out how to map commands to
  command handlers available in these services.
  That's why you have to register services separately.
- `Call` creates its own `IServiceScope` to resolve
  services for every command invocation.

Try changing `AddScoped` to `AddSingleton` in above example.

## Convention-based handlers, `CommandContext`, recursion

Let's write a bit more complex handler to see how
`CommandContext` works.

``` cs --region Part09_RecSumCommandSession --source-file Part09.cs --session "Convention-based handlers, CommandContext, recursion"
public class RecSumCommand : ICommand<long>
{
    public long[] Numbers { get; set; } = Array.Empty<long>();
}

public class RecSumCommandHandler
{
    public RecSumCommandHandler() => WriteLine("Creating RecSumCommandHandler.");
    public void Dispose() => WriteLine("Disposing RecSumCommandHandler");

    [CommandHandler] // Note that ICommandHandler<RecSumCommand, long> support isn't needed
    private async Task<long> RecSum(
        RecSumCommand command,
        IServiceProvider services, // Resolved via CommandContext.Services
        ICommander commander, // Resolved via CommandContext.Services
        CancellationToken cancellationToken)
    {
        var context = CommandContext.GetCurrent();
        Debug.Assert(services == context.Services); // context.Services is a scoped IServiceProvider
        Debug.Assert(commander == services.Commander()); // ICommander is singleton
        Debug.Assert(services != commander.Services); // Scoped IServiceProvider != top-level IServiceProvider
        WriteLine($"Numbers: {command.Numbers.ToDelimitedString()}");

        // Each call creates a new CommandContext
        var contextStack = new List<CommandContext>();
        var currentContext = context;
        while (currentContext != null)
        {
            contextStack.Add(currentContext);
            currentContext = currentContext.OuterContext;
        }
        WriteLine($"CommandContext stack size: {contextStack.Count}");
        Debug.Assert(contextStack[^1] == context.OutermostContext);

        // Finally, CommandContext.Items is ~ like HttpContext.Items, and similarly to
        // service scope, they are the same for all contexts in recursive call chain.
        var depth = 1 + (int)(context.Items["Depth"] ?? 0);
        context.Items["Depth"] = depth;
        WriteLine($"Depth via context.Items: {depth}");

        // Finally, the actual handler logic :)
        if (command.Numbers.Length == 0)
            return 0;
        var head = command.Numbers[0];
        var tail = command.Numbers[1..];
        var tailSum = await context.Commander.Call(
            new RecSumCommand() { Numbers = tail }, false, // Try changing it to true
            cancellationToken);
        return head + tailSum;
    }
}

``` cs –region Part09_RecSumCommandSession2 –source-file Part09.cs –session “Convention-based handlers, CommandContext, recursion” // Building IoC container var serviceBuilder = new ServiceCollection() .AddScoped(); var commanderBuilder = serviceBuilder.AddCommander() .AddHandlers(); var services = serviceBuilder.BuildServiceProvider();

var commander = services.Commander(); // Same as .GetRequiredService() WriteLine(await commander.Call(new RecSumCommand() { Numbers = new[] { 1L, 2, 3 } }));


The output:

Creating RecSumCommandHandler. Numbers: 1, 2, 3 CommandContext stack size: 1 Depth via context.Items: 1 Numbers: 2, 3 CommandContext stack size: 2 Depth via context.Items: 2 Numbers: 3 CommandContext stack size: 3 Depth via context.Items: 3 Numbers: CommandContext stack size: 4 Depth via context.Items: 4 6


A few things are interesting here:

1. You can use convention-based command handlers:
   all you need is to decorate a method with `[CommandHandler]`
   instead of implementing `ICommandHandler<TCommand>`.
2. Such handlers are more flexible with the arguments:
   - The first argument should always be the command
   - The last one should always be the `CancellationToken`
   - `CommandContext` arguments are resolved via `CommandContext.GetCurrent()`
   - Everything else is resolved via `CommandContext.Services`,
     i.e. a scoped service provider.

But the most complex part of this example covers `CommandContext`.
Contexts are "typed" - even though they all are inherited from
`CommandContext`, their actual type is `CommandContext<TResult>`.

Command context allows to:

- Find currently running command
- Set or read its result. Usually you don't have to set the result manually -
  the code invoking command handlers ensures the result is set once
  the "deepest" handler exist, but you may want to read it
  in some handlers in your pipeline.
- Manage `IServiceScope`
- Access its `Items`. It's an `OptionSet`, ~ a thread-safe dictionary-like structure
  helping to store any info associated with the current command.

Finally, `CommandContext` is a class, but there is also
[`ICommandContext` - an internal interface defining its API](https://github.com/servicetitan/Stl.Fusion/blob/master/src/Stl.CommandR/Internal/ICommandContext.cs) - check it out.
And if you're looking for details, check out
[`CommandContext` itself](https://github.com/servicetitan/Stl.Fusion/blob/master/src/Stl.CommandR/CommandContext.cs).

So when you call a command, a new `CommandContext` is created.
But what about the service scope? The code from `CommandContext<TResult>`
constructor explains this better than a few sentences:

```cs
// PreviousContext here is CommandContext.Current, 
// which will be replaced with `this` soon after.
if (PreviousContext?.Commander != commander) {
    OuterContext = null;
    OutermostContext = this;
    ServiceScope = Commander.Services.CreateScope();
    Items = new OptionSet();
}
else {
    OuterContext = PreviousContext;
    OutermostContext = PreviousContext!.OutermostContext;
    ServiceScope = OutermostContext.ServiceScope;
    Items = OutermostContext.Items;
}

As you see, if you “switch” ICommander instances on such calls, the new context behaves like it’s a top-level one, i.e. it creates a new service scope, new Items, and exposes itself as OutermostContext.

Now it’s a good time to try changing false to true in this fragment above:

var tailSum = await context.Commander.Call(
    new RecSumCommand() { Numbers = tail }, false, // Try changing it to true
    cancellationToken);

Ways To Run A Command

ICommander offers just a single method to run the command, but in reality, it’s the most “low-level” option, so you’ll rarely need it.

The actual options are implemented in CommanderEx type (Ex is a shortcut for Extensions that’s used everywhere in Stl for such classes).

All these methods take up to 3 arguments:

Command Services and filtering handlers

The most interesting way to register command handlers are to declare them inside so-called Command Service:

``` cs –region Part09_RecSumCommandServiceSession –source-file Part09.cs –session “Command Services and filtering handlers” public class RecSumCommandService : ICommandService { [CommandHandler] // Note that ICommandHandler<RecSumCommand, long> support isn’t needed public virtual async Task RecSum( // Notice "public virtual"! RecSumCommand command, // You can't have any extra arguments here CancellationToken cancellationToken = default) { if (command.Numbers.Length == 0) return 0; var head = command.Numbers[0]; var tail = command.Numbers[1..]; var tailSum = await RecSum( // Note it's a direct call, but the whole pipeline still gets invoked! new RecSumCommand() { Numbers = tail }, cancellationToken); return head + tailSum; }

// This handler is associated with ANY command (ICommand)
// Priority = 10 means it runs earlier than any handler with the default priority 0
// IsFilter tells it triggers other handlers via InvokeRemainingHandlers
[CommandHandler(Priority = 10, IsFilter = true)]
protected virtual async Task DepthTracker(ICommand command, CancellationToken cancellationToken)
{
    var context = CommandContext.GetCurrent();
    var depth = 1 + (int)(context.Items["Depth"] ?? 0);
    context.Items["Depth"] = depth;
    WriteLine($"Depth via context.Items: {depth}");

    await context.InvokeRemainingHandlers(cancellationToken).ConfigureAwait(false);
}

// Another filter for RecSumCommand
[CommandHandler(Priority = 9, IsFilter = true)]
protected virtual Task ArgumentWriter(RecSumCommand command, CancellationToken cancellationToken)
{
    WriteLine($"Numbers: {command.Numbers.ToDelimitedString()}");
    var context = CommandContext.GetCurrent();
    return context.InvokeRemainingHandlers(cancellationToken);
} } ```

Such services has to be registered via AddCommandService method of the CommanderBuilder:

``` cs –region Part09_RecSumCommandServiceSession2 –source-file Part09.cs –session “Command Services and filtering handlers” // Building IoC container var serviceBuilder = new ServiceCollection(); var commanderBuilder = serviceBuilder.AddCommander() .AddCommandService(); // Such services are auto-registered as singletons var services = serviceBuilder.BuildServiceProvider();

var commander = services.Commander(); var recSumService = services.GetRequiredService(); WriteLine(recSumService.GetType()); WriteLine(await commander.Call(new RecSumCommand() { Numbers = new[] { 1L, 2 } })); WriteLine(await recSumService.RecSum(new RecSumCommand() { Numbers = new[] { 3L, 4 } }));


The output:

Castle.Proxies.RecSumCommandServiceProxy Depth via context.Items: 1 Numbers: 1, 2 Depth via context.Items: 2 Numbers: 2 Depth via context.Items: 3 Numbers: 3 Depth via context.Items: 1 Numbers: 3, 4 Depth via context.Items: 2 Numbers: 4 Depth via context.Items: 3 Numbers: 7 ```

As you see, the proxy type generated for such services routes every direct invocation of a command handler through ICommander.Call. So contrary to regular handlers, you can invoke such handlers directly - the whole CommandR pipeline gets invoked for them anyway.

Next: Multi-Host Invalidation and CQRS with Fusion + CommandR + Operations Framework » | Tutorial Home