DEV Community

Florian Necula
Florian Necula

Posted on • Originally published at Medium

CommandFlow: A Pragmatic Alternative to CQRS and MediatR

This is a shortened version of an article originally published on Medium.

The problem with traditional .NET Architecture

If you’ve built ASP.NET applications, you’ve probably encountered this scenario:
You start with a clean service layer. Six months later, your IArticleService interface has 50 methods. Your controllers are filled with custom logic. Your tests require complex mocking setups. And every time you add a new feature, you’re touching multiple layers of your application.

The industry’s answer? CQRS + MediatR.

Using three key patterns — Command/Query separation, Command Handlers and Parameter Objects — CQRS solved most of the problems of traditional service-based architecture.
Command Handlers were not invented by CQRS but, for the last years, they are almost synonym with CQRS.
Now, many authors and articles present CQRS as just CQS — Command Query Separation. That is: having separate classes and handlers for Command and Query.
But full CQRS is much more than this. It requires implementing separate code paths, separate data models, in many cases separate databases, eventual consistency and so on. In some cases, even different projects or different solutions (see CQRS microservices).
And this goes well beyond Command/Query separation, it’s all about read/write segregation.
On the other hand, all of this adds a lot of complexity. For this reason, most experts advise: “Don’t use CQRS unless you have a genuine need for read/write separation”.
And that’s a shame because most applications don’t need separate reads/writes. They just need a clean way to organize business logic. And even if they need read/write segregation, it can be done without Command/Query separation but focusing on the real separation in the other layers.
The real, universal value of CQRS, that can benefit almost all applications, is the use of CommandHandler architecture. Here is why.

But what if there was a simpler and better approach? One that can be used in any application and gives you all the benefits of handler-based architecture without the full ceremony of CQRS or the framework dependency of MediatR, while eliminating common pain points in traditional layered architecture.

Introducing CommandFlow

CommandFlow is a lightweight, handler-based architecture that uses Commands and CommandHandlers as its foundation and can be used in any kind of application while removing unnecessary constraints: mandatory command/query separation and dependency on mediator frameworks.
Think of it as the handler pattern done right — with compile-time safety, simplified business logic, reduced number of classes and interfaces, no need for MediatR or other external mediator library, reduced boilerplate, maximal code reuse, automation of common tasks.
It’s not CQRS. It’s not MediatR. It’s a universal handler orchestration pattern that creates self-documenting, replay-ready operations with zero framework dependencies.

Core Features Summary

  1. Works with any application type
    Web, console, background services, etc., with or without read/write segregation.

  2. Contextual Container Pattern
    Commands carry data, operations, context, and state in one cohesive unit.

  3. GatewayService Replaces MediatR
    Generic Execute() method eliminates MediatR’s reflection overhead, assembly scanning, runtime errors, and framework lock-in.

  4. Built-In Exception Handling
    Automatic exception handling. Zero try-catch statements required.

  5. Automatic logging, profiling and audit trails
    Every request is automatically logged together with all context information.

  6. Command Replay for Time-Travel Debugging
    Commands are serializable audit records. Replay production issues locally with exact state. No Event Sourcing complexity required.

  7. Silent Exception Pattern
    cmd.Raise() eliminates if-else chains and result propagation noise while terminating execution cleanly.

  8. Zero Framework Lock-In
    ~300 lines of code. No NuGet dependencies. You own the architecture.

  9. Reduced cognitive load
    Less classes, less interfaces, simplified signatures, less boilerplate.

  10. Simplified Testing
    Mock only what commands exposed. Uniform testing for all handlers.

Three main classes:

Command, CommandHandler and GatewayService

1.Command

Commands in CommandFlow are classes that combine data, operations, and execution context into a single cohesive unit.

public class CreateArticleCommand : ArticleCommandBase
{
    // OPERATIONS
    public IArticleRepo ArticleRepo => Hub.ArticleRepo;
    public IEmailService EmailService => Hub.EmailService;

    // INPUT DATA
    public required ArticleModel Article { get; init; }

    // OUTPUT DATA
    public int CreatedArticleId { get; set; }

    // EXECUTION CONTEXT AND STATE (inherited from CommandBase)
    // - CommandId (correlation ID)
    // - CurrentDateTime (timestamp)
    // - UserId (who's executing)
    // - UserEmail (user identity)
    // - Errors (what went wrong)
    // - Warnings (non-fatal issues)
}
Enter fullscreen mode Exit fullscreen mode

They are part of a 3-layer hierarchy:

Command hierarchy

2.CommandHandler

CommandHandlers in CommandFlow are remarkably simple:

public class CreateArticleHandler : ICommandHandler<CreateArticleCommand>
{
    public async Task Execute(CreateArticleCommand cmd)
    {
        var article = cmd.Article.ToEntity();

        if (ArticleUtils.HasWrongContent(article.Body))
            cmd.Raise("Article body contains forbidden words");

        article.CreatedAt = DateTime.UtcNow;
        article.UpdatedAt = DateTime.UtcNow;

        cmd.Validate(article);

        cmd.ArticleRepo.Add(article);
        await cmd.ArticleRepo.SaveChanges();
        cmd.CreatedArticleId = article.ArticleId;

        await cmd.EmailService.SendEmail($"Article '{article.Title}' has been created.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice what’s missing:

  • No try-catch bloc
  • No if (!result.IsSuccess) return; noise
  • No exception throwing with throw new BusinessRuleException()
  • No manual error state management
  • No constructor
  • No field assignments
  • No return type

3.GatewayService

GatewayService completely replaces MediatR with one simple generic service method:

    public async Task Execute<THandler, TCommand>(TCommand cmd)
        where THandler : ICommandHandler<TCommand>, new()
        where TCommand : ArticleCommandBase {}
Enter fullscreen mode Exit fullscreen mode

The Three-Line Controller

If you have done ASP.NET applications using traditional architecture, you can see what makes CommandFlow immediately recognizable. Every endpoint looks like this:

[HttpPost("articles")]
public async Task<IActionResult> CreateArticle(ArticleModel article)
{
    var cmd = new CreateArticleCommand { Article = article };
    await gatewayService.Execute<CreateArticleHandler, CreateArticleCommand>(cmd);
    return this.GetResponse(cmd, cmd.CreatedArticleId);
}
Enter fullscreen mode Exit fullscreen mode

Three lines. Always. No exceptions.

Command Replay — Time Travel Debugging

Because commands carry all inputs, outputs, operations, and context, they’re perfectly serializable. This enables something most architectures can’t do: complete operation replay.

Read the full article here

Source Code

Full source code with working examples and unit tests is available here.

Top comments (0)