CQRS Handler (C#) with Domain Logic and Pipelines – Explained via Derek Comartin's Video
Command Query Responsibility Segregation (CQRS) is a powerful design pattern in .NET applications that helps maintain a clear separation between read and write operations. This separation enables better scalability, testability, and control over complex business logic. However, as your application grows, your CQRS handlers in C# can become bloated with logic, making them harder to maintain and test.
In his insightful video on "Clean Up Bloated CQRS Handlers with Domain Logic & Pipelines", Derek Comartin walks through how to clean up such CQRS command handlers by shifting logic to domain models and structuring a pipeline to manage command logic step by step. In this article, we will explore Derek’s approach in detail and learn how to build more maintainable CQRS implementations in C#.
The Problem with Bloated Handlers
Derek begins by highlighting the common scenario in many web applications: you open a CQRS handler in C#, and it's a mess. There's data validation, authorization, business logic, state transitions, event publishing, logging—all tangled together.
He illustrates this using a command object for dispatching a shipment. The handler is responsible for:
Accessing the data store to load the shipment.
Checking if the shipment status is ready.
Updating the status (i.e., updating data).
Saving changes back to the data access layer.
Sending an email.
- Publishing a domain event.
All of this happens in one place. This violates the intent of the CQRS pattern, where the goal is to separate concerns and improve performance and maintainability.
Moving Domain Logic to the Data Model
Derek’s first step is to move validation and state transition logic into the data model. He creates a Dispatch() method inside the Shipment class. This is where the domain logic now lives.
Instead of manually checking shipment status in the handler, the logic is encapsulated in this method, ensuring data integrity and consistent behavior wherever dispatching is triggered. This is key to implementing clean architecture in your CQRS-based application.
For example, any place that calls shipment.Dispatch() automatically performs all validations and state transitions. This aligns with the CQRS design pattern, helping maintain a clear separation between the handler and domain logic.
The Value of Centralizing Logic
Derek points out that this kind of change isn't about adding unnecessary abstraction. Instead, it's about centralizing logic that is used across different parts of your application code. If multiple command handlers need to dispatch a shipment, then this custom logic should live in one place—inside the domain model.
This makes your data model more robust and your CQRS handler C# implementations simpler and more maintainable.
Introducing the Pipeline Pattern
To further clean up the command handler, Derek introduces a pipeline pattern. This structure processes a command as a sequence of small, single-purpose steps, each taking a context object and calling the next step.
This is conceptually similar to ASP.NET Core middleware, and each step focuses on a specific part of the flow:
Retrieving the shipment (i.e., read data)
Dispatching it (executing write operations)
Publishing events
- Saving to the data store
These steps use a shared command object that flows through the pipeline. This creates a clean and modular implementation of command and query responsibility segregation.
Sample Implementation of the Pipeline
In his sample implementation, Derek structures the pipeline with steps like:
Loading the shipment – pulling the data from the data access layer using a repository.
Dispatching the shipment – invoking the Dispatch() method to apply the domain logic.
Adding a domain event – attaching a “ShipmentDispatched” event to the context.
Publishing events – dispatching events to notify external systems.
- Saving changes – persisting updates to the data store.
Each step represents a distinct piece of command logic, enhancing data validation and keeping responsibilities separate.
Derek also notes that email notifications are now handled separately by reacting to the domain event. This aligns with event sourcing principles and promotes eventual consistency.
Testing and Maintainability Benefits
One of the biggest benefits of this pattern is testability. With a large command handler, you may have several dependencies (e.g., repositories, mail services, loggers). But when you break the handler into pipeline steps, each one only requires a couple of dependencies.
This modular approach allows you to test individual steps easily with dependency injection, using fakes or mocks where needed. For example, if you're testing a step that calls Dispatch(), you don’t need to mock an email service or event publisher.
This separation of concerns follows the responsibility segregation CQRS pattern, making your read and write models cleaner and more focused.
Composability and Reusability
Another benefit of the pipeline approach is that it is composable. If you use something like the Outbox Pattern, you can ensure that events are published only after the write models are persisted. This level of control is essential in CQRS implementations where consistency and delivery guarantees matter.
You can also share steps across different CQRS handlers—for instance, a generic “SaveChanges” step or a “ValidateRequest” step.
Using tools like the MediatR library, which supports command and query handling, you can even register these steps through dependency injection in your .NET Core application using IServiceCollection services.
To set up this system, you might run Install-Package MediatR via Visual Studio’s Package Manager Console—a common step when implementing CQRS in C#.
The Trade-Offs
Derek doesn’t shy away from the increased complexity that comes with this approach. Pipelines introduce indirection, and when you look at a call stack, it can feel like navigating a maze.
However, for complex business logic, the trade-off is often worth it. If a handler has 10+ dependencies and hundreds of lines of logic, CQRS enables developers to better structure and maintain these flows.
Final Thoughts on When to Refactor
Derek wraps up by reminding viewers to carefully consider whether their CQRS handler C# implementation is truly bloated. Not every scenario requires a pipeline. His goal is to illustrate possibilities, and it’s up to developers to assess their own CQRS implementation and determine whether such patterns will help.
He encourages developers to look at areas in their code where separation of concerns would help maintain consistency, make code more modular, and manage read and write operations better—especially in CQRS web applications.
Conclusion
Derek Comartin’s video offers a practical guide to cleaning up CQRS handlers using domain logic encapsulation and pipelines. This approach helps address issues of code bloat, promotes data integrity, and enhances maintainability by breaking down application code into distinct models.
Whether you're handling employee data, product details, or a new user command, applying the CQRS pattern with pipelines and domain-driven design will make your codebase more scalable, testable, and robust.
By using data transfer objects, separate models, and maintaining a clear separation between read and write logic, your .NET application will be better structured and easier to evolve over time.


