Context: We need to build an improved version of our chat system. The old system worked so well that we need to scale it up. The mediator was of help to us, so we kept that part, and we picked the CQS pattern to help us with this new, improved design. A participant was limited to a single chatroom in the past, but now a participant must be able to chat in multiple rooms simultaneously.The new system is composed of three commands and two queries:

  • A participant must be able to join a chatroom.
  • A participant must be able to leave a chatroom.
  • A participant must be able to send a message into a chatroom.
  • A participant must be able to obtain the list of participants that joined a chatroom.
  • A participant must be able to retrieve the existing messages from a chatroom.

The first three are commands, and the last two are queries. The system is backed by the following mediator that makes heavy use of C# generics:

public interface IMediator
{
    TReturn Send<TQuery, TReturn>(TQuery query)
        where TQuery : IQuery<TReturn>;
    void Send<TCommand>(TCommand command)
        where TCommand : ICommand;
    void Register<TCommand>(ICommandHandler<TCommand> commandHandler)
        where TCommand : ICommand;
    void Register<TQuery, TReturn>(IQueryHandler<TQuery, TReturn> commandHandler)
        where TQuery : IQuery<TReturn>;
}

If you are not familiar with generics, this might look daunting, but that code is way simpler than it looks. Next, the ICommand interface is empty, which we could have avoided, but it helps describe our intent. The ICommandHandler interface defines the contract a class must implement to handle a command. That interface defines a Handle method that takes the command as a parameter. The generic parameter TCommand represents the type of command the class implementing the interface can handle. Here’s the code:

public interface ICommand { }
public interface ICommandHandler<TCommand>
    where TCommand : ICommand
{
    void Handle(TCommand command);
}

The IQuery<TReturn> interface is similar to the ICommand interface but has a TReturn generic parameter indicating the query’s return type. The IQueryHandler interface is also very similar, but its Handle method takes an object of type TQuery as a parameter and returns a TReturn type. Here’s the code:

public interface IQuery<TReturn> { }
public interface IQueryHandler<TQuery, TReturn>
    where TQuery : IQuery<TReturn>
{
    TReturn Handle(TQuery query);
}

The IMediator interface allows registering command and query handlers using its Register methods. It also supports sending commands and queries through its Send methods. Then we have the ChatMessage class, which is similar to the last two samples (with an added creation date):

public record class ChatMessage(IParticipant Sender, string Message)
{
    public DateTime Date { get; } = DateTime.UtcNow;
}

Next is the updated IParticipant interface:

public interface IParticipant
{
    string Name { get; }
    void Join(IChatRoom chatRoom);
    void Leave(IChatRoom chatRoom);
    void SendMessageTo(IChatRoom chatRoom, string message);
    void NewMessageReceivedFrom(IChatRoom chatRoom, ChatMessage message);
    IEnumerable<IParticipant> ListParticipantsOf(IChatRoom chatRoom);
    IEnumerable<ChatMessage> ListMessagesOf(IChatRoom chatRoom);
}

All methods of the IParticipant interface accept an IChatRoom parameter to support multiple chatrooms. The updated IChatRoom interface has a name and a few basic operations to meet the requirement of a chatroom, like adding and removing participants:

public interface IChatRoom
{
    string Name { get; }
    void Add(IParticipant participant);
    void Remove(IParticipant participant);
    IEnumerable<IParticipant> ListParticipants();
    void Add(ChatMessage message);
    IEnumerable<ChatMessage> ListMessages();
}

Before going into commands and the chat itself, let’s take a peek at the Mediator class:

public class Mediator : IMediator
{
    private readonly HandlerDictionary _handlers = new();
    public void Register<TCommand>(ICommandHandler<TCommand> commandHandler)
        where TCommand : ICommand
    {
        _handlers.AddHandler(commandHandler);
    }
    public void Register<TQuery, TReturn> (IQueryHandler<TQuery, TReturn> commandHandler)
        where TQuery : IQuery<TReturn>
    {
        _handlers.AddHandler(commandHandler);
    }
    public TReturn Send<TQuery, TReturn>(TQuery query)
        where TQuery : IQuery<TReturn>
    {
        var handler = _handlers.Find<TQuery, TReturn>();
        return handler.Handle(query);
    }
    public void Send<TCommand>(TCommand command)
        where TCommand : ICommand
    {
        var handlers = _handlers.FindAll<TCommand>();
        foreach (var handler in handlers)
        {
            handler.Handle(command);
        }
    }
}

The Mediator class supports registering commands and queries as well as sending a query to a handler or sending a command to zero or more handlers.

I omitted the implementation of HandlerDictionary because it does not add value to the example, it is just an implementation detail, but it would have added unnecessary complexity. It is available on GitHub: https://adpg.link/2Lsm.

Now to the commands. I grouped the commands and the handlers together to keep it organized and readable, but you could use another way to organize yours. Moreover, since this is a small project, all the commands are in the same file, which would not be viable for something bigger. Remember, we are playing LEGO blocks, this chapter covers the CQS pieces, but you can always use them with bigger pieces like Clean Architecture or other types of architecture.