Saturday 28 January 2023

CQRS - C# Style

CQRS - Command and Query Responsibility Segregation.

Quite a mouthful, but essentially is the art of seperating commands and queries with an application. A command chages state, a query retruns data and does not change state.

Most CQRS libraries that I have encountered are verbose and have a large footprint in terms of memory and disk space

I have developed a simple but usuable CQRS library. Currently the library caters for commands, queries are usually much simpler to implement.


Top

In this post...

Top

Commands

A command performs some kind of action and is expected to modify application state. Examples include writing to a database, the console, painting a control. A command typically raises an event to inform the application of state changes. A command will usually contain parameters, used to modify application state. A seperate class is typically responsible for running commands. This allows pre and post actions when acting upon commands.

Top

Events

Commands, as stated perform state changes. An application is informed of state changes using events. In my opinion it is best to keep event classes seperate from commands. This promotes loose-coupling. Consider C# control events, the control hosts the event handler. This means anything wishing to respond to an event must access the control to add the event. This results in tight coupling.

Top

CQRS library

Currently, my CQRS library, whilst simple only caters forcommand end events.

It is available as a separate project to include where requied.

The code follows, then an analysis follows...

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public static class CQRS
{
  public static readonly CommandBuilder Commands = new CommandBuilder();

  public interface IMessage { }

  public class Event : IMessage { }

  public class Command : IMessage { }

  public interface IHandleCommand<T>
    where T : Command
  {
    Task Handle(T command);
  }

  public class CommandBuilder
  {
    public CommandBuilder Handle<T>(IHandleCommand<T> handler)
      where T : Command
    {
      CommandProcessor<T>.Handler(handler);
      return this;
    }
  }

  public static void Send<T>(T command)
    where T : Command =>
    CommandProcessor<T>.Process(command);

  public static void Raise<T>(T @event)
    where T : Event =>
    EventBus<T>.Raise(@event);

  public static void EventSubscribe<T>(Action<T> handler)
    where T : Event =>
    EventBus<T>.Subscribe(handler);

  public static void EventUnsubscribe<T>(Action<T> handler)
    where T : Event =>
    EventBus<T>.Unsubscribe(handler);

  static class CommandProcessor<T>
    where T : Command
  {
    private static IHandleCommand<T> _handler;

    public static void Handler(IHandleCommand<T> handler) =>
      _handler = handler;

    public static void Process(T command)
    {
      _handler.Handle(command);
    }
  }

  static class EventBus<T>
    where T : Event
  {
    private static readonly List<Action<l;T>> _handlers = new List<Action<T>&t;();

    public static void Raise(T @event)
    {
      foreach (var handler in _handlers)
        handler(@event);
    }

    public static void Subscribe(Action<T> handler)
    {
      _handlers.Remove(handler);
      _handlers.Add(handler);
    }

    public static void Unsubscribe(Action<T> handler)
    {
      _handlers.Remove(handler);
    }
  }
}
Top

CQRS Library Analysis

The library is a .NET framework library targeted for .NET Framework 4.8. That said, the library is very simple so earlier versions might work.

Commands and events are simply messages, but, with different semantics. So, I create a message interface, IMessage, from which both commands and events will implement. Commands must derive from the Command class. Events must derive from the Event class.

Creating a command is deemed a separate concern to executing a command. That is, a command is created with state that conveys potential application state changes. However, a command is not responsible for running itself, mainly due to a lack of global context.

Running commands within a single entry point aids debugging and allows logging, tracing to be added with relative ease.

For each new Command class created a corresponding command handler needs to be present. The interface, IHandleCommand<T> helps to achieve this.

The idea is that many commands might be available, but, an application may only wish to use a subset of said commands.

Top

CQRS Library Functions

  • Send<T>(T command)
    Executes a command using the private CommandProcessor class. The command parameter must be a type derivedfrom the Command class.
  • Raise<T>(T @event)
    Raises an event, the event must be derived from the Event class.
  • EventSubscribe<T>(Action<T> handler)
    Used to register an event handler. The type parameter, T, must be derived from the Event class.
Top

No comments:

Post a Comment