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
Works with any application type
Web, console, background services, etc., with or without read/write segregation.Contextual Container Pattern
Commands carry data, operations, context, and state in one cohesive unit.GatewayService Replaces MediatR
Generic Execute() method eliminates MediatR’s reflection overhead, assembly scanning, runtime errors, and framework lock-in.Built-In Exception Handling
Automatic exception handling. Zero try-catch statements required.Automatic logging, profiling and audit trails
Every request is automatically logged together with all context information.Command Replay for Time-Travel Debugging
Commands are serializable audit records. Replay production issues locally with exact state. No Event Sourcing complexity required.Silent Exception Pattern
cmd.Raise() eliminates if-else chains and result propagation noise while terminating execution cleanly.Zero Framework Lock-In
~300 lines of code. No NuGet dependencies. You own the architecture.Reduced cognitive load
Less classes, less interfaces, simplified signatures, less boilerplate.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)
}
They are part of a 3-layer 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.");
}
}
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 {…}
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);
}
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)