Command Query Responsibility Segregation (CQRS) is a prominent architectural approach for modern.NET applications that separates read and write actions. By separating commands and queries, CQRS may increase maintainability, scalability, and code structure. Any developers use CQRS libraries like MediatR. While MediatR is a fantastic tool, it is not required for deploying CQRS. In reality, for many systems, adding another abstraction layer might increase complexity while delivering little benefits.
A lightweight CQRS implementation enables developers to take use of the pattern while keeping the coding simple, transparent, and easy to debug.
This post will explain how CQRS works, why MediatR is not always required, and how to build CQRS directly in ASP.NET Core apps using built-in dependency injection.
Understanding CQRS
CQRS separates application operations into two categories:
Commands
Commands modify application state.
Examples include:
- Creating a user
- Updating a product
- Deleting an order
- Processing a payment
Commands typically do not return data beyond success or failure information.
Queries
Queries retrieve data without modifying application state.
Examples include:
- Getting product details
- Listing customers
- Viewing order history
- Retrieving dashboard metrics
Queries should never change data.
Traditional Approach
1 2 3 4 5 | Controller ↓ Service ↓ Database |
CQRS Approach
1 2 3 4 5 6 7 8 9 10 11 | Controller ↓ Command Handler ↓ Database Controller ↓ Query Handler ↓ Database |
This separation creates clearer responsibilities and improves maintainability.
Why Developers Use MediatR
MediatR is commonly used to implement CQRS because it provides a mediator pattern that decouples controllers from handlers.
Typical flow:
1 2 3 4 5 6 7 | Controller ↓ Mediator ↓ Handler ↓ Database |
Example:
1 2 | <span class="token keyword keyword-await">await</span> _mediator<span class="token punctuation">.</span><span class="token function">Send</span><span class="token punctuation">(</span> <span class="token keyword keyword-new">new</span> <span class="token constructor-invocation class-name">CreateProductCommand</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
While convenient, this introduces an additional dependency and abstraction layer.
For smaller and medium-sized applications, direct handler invocation can often be simpler.
Implementing Lightweight CQRS
Instead of routing requests through a mediator, controllers can directly depend on command and query handlers.
The architecture becomes:
1 2 3 4 5 6 7 | Controller ↓ Handler ↓ Repository ↓ Database |
This reduces indirection while preserving CQRS principles.
Creating a Command
Let’s create a command for adding a product.
1 2 3 4 5 | <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-class">class</span> <span class="token class-name">CreateProductCommand</span> <span class="token punctuation">{</span> <span class="token keyword keyword-public">public</span> <span class="token return-type class-name"><span class="token keyword keyword-string">string</span></span> Name <span class="token punctuation">{</span> <span class="token keyword keyword-get">get</span><span class="token punctuation">;</span> <span class="token keyword keyword-set">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword keyword-string">string</span><span class="token punctuation">.</span>Empty<span class="token punctuation">;</span> <span class="token keyword keyword-public">public</span> <span class="token return-type class-name"><span class="token keyword keyword-decimal">decimal</span></span> Price <span class="token punctuation">{</span> <span class="token keyword keyword-get">get</span><span class="token punctuation">;</span> <span class="token keyword keyword-set">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
The command contains only the data required for the operation.
Creating a Command Handler
Next, implement a dedicated handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-class">class</span> <span class="token class-name">CreateProductHandler</span> <span class="token punctuation">{</span> <span class="token keyword keyword-private">private</span> <span class="token keyword keyword-readonly">readonly</span> <span class="token class-name">AppDbContext</span> _context<span class="token punctuation">;</span> <span class="token keyword keyword-public">public</span> <span class="token function">CreateProductHandler</span><span class="token punctuation">(</span> <span class="token class-name">AppDbContext</span> context<span class="token punctuation">)</span> <span class="token punctuation">{</span> _context <span class="token operator">=</span> context<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-async">async</span> <span class="token return-type class-name">Task<span class="token punctuation"><</span><span class="token keyword keyword-int">int</span><span class="token punctuation">></span></span> <span class="token function">HandleAsync</span><span class="token punctuation">(</span> <span class="token class-name">CreateProductCommand</span> command<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name"><span class="token keyword keyword-var">var</span></span> product <span class="token operator">=</span> <span class="token keyword keyword-new">new</span> <span class="token constructor-invocation class-name">Product</span> <span class="token punctuation">{</span> Name <span class="token operator">=</span> command<span class="token punctuation">.</span>Name<span class="token punctuation">,</span> Price <span class="token operator">=</span> command<span class="token punctuation">.</span>Price <span class="token punctuation">}</span><span class="token punctuation">;</span> _context<span class="token punctuation">.</span>Products<span class="token punctuation">.</span><span class="token function">Add</span><span class="token punctuation">(</span>product<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword keyword-await">await</span> _context<span class="token punctuation">.</span><span class="token function">SaveChangesAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword keyword-return">return</span> product<span class="token punctuation">.</span>Id<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
The handler contains all business logic related to product creation.
Registering the Handler
Register the handler using ASP.NET Core’s built-in dependency injection.
1 2 | builder<span class="token punctuation">.</span>Services<span class="token punctuation">.</span><span class="token generic-method"><span class="token function">AddScoped</span><span class="token generic class-name"><span class="token punctuation"><</span> CreateProductHandler<span class="token punctuation">></span></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
No mediator library is required.
Using the Command Handler
The controller directly invokes the handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">ApiController</span></span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">Route</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"api/products"</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span> <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-class">class</span> <span class="token class-name">ProductsController</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">ControllerBase</span></span> <span class="token punctuation">{</span> <span class="token keyword keyword-private">private</span> <span class="token keyword keyword-readonly">readonly</span> <span class="token class-name">CreateProductHandler</span> _handler<span class="token punctuation">;</span> <span class="token keyword keyword-public">public</span> <span class="token function">ProductsController</span><span class="token punctuation">(</span> <span class="token class-name">CreateProductHandler</span> handler<span class="token punctuation">)</span> <span class="token punctuation">{</span> _handler <span class="token operator">=</span> handler<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">HttpPost</span></span><span class="token punctuation">]</span> <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-async">async</span> <span class="token return-type class-name">Task<span class="token punctuation"><</span>IActionResult<span class="token punctuation">></span></span> <span class="token function">Create</span><span class="token punctuation">(</span> <span class="token class-name">CreateProductCommand</span> command<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name"><span class="token keyword keyword-int">int</span></span> id <span class="token operator">=</span> <span class="token keyword keyword-await">await</span> _handler<span class="token punctuation">.</span><span class="token function">HandleAsync</span><span class="token punctuation">(</span>command<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword keyword-return">return</span> <span class="token function">Ok</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
The request flow remains simple and easy to follow.
Creating a Query
Queries retrieve data without changing state.
Example:
1 2 3 4 | <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-class">class</span> <span class="token class-name">GetProductByIdQuery</span> <span class="token punctuation">{</span> <span class="token keyword keyword-public">public</span> <span class="token return-type class-name"><span class="token keyword keyword-int">int</span></span> Id <span class="token punctuation">{</span> <span class="token keyword keyword-get">get</span><span class="token punctuation">;</span> <span class="token keyword keyword-set">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Creating a Query Handler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-class">class</span> <span class="token class-name">GetProductByIdHandler</span> <span class="token punctuation">{</span> <span class="token keyword keyword-private">private</span> <span class="token keyword keyword-readonly">readonly</span> <span class="token class-name">AppDbContext</span> _context<span class="token punctuation">;</span> <span class="token keyword keyword-public">public</span> <span class="token function">GetProductByIdHandler</span><span class="token punctuation">(</span> <span class="token class-name">AppDbContext</span> context<span class="token punctuation">)</span> <span class="token punctuation">{</span> _context <span class="token operator">=</span> context<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-async">async</span> <span class="token return-type class-name">Task<span class="token punctuation"><</span>ProductDto<span class="token punctuation">?</span><span class="token punctuation">></span></span> <span class="token function">HandleAsync</span><span class="token punctuation">(</span> <span class="token class-name">GetProductByIdQuery</span> query<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword keyword-return">return</span> <span class="token keyword keyword-await">await</span> _context<span class="token punctuation">.</span>Products <span class="token punctuation">.</span><span class="token function">Where</span><span class="token punctuation">(</span>p <span class="token operator">=></span> p<span class="token punctuation">.</span>Id <span class="token operator">==</span> query<span class="token punctuation">.</span>Id<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">Select</span><span class="token punctuation">(</span>p <span class="token operator">=></span> <span class="token keyword keyword-new">new</span> <span class="token constructor-invocation class-name">ProductDto</span> <span class="token punctuation">{</span> Id <span class="token operator">=</span> p<span class="token punctuation">.</span>Id<span class="token punctuation">,</span> Name <span class="token operator">=</span> p<span class="token punctuation">.</span>Name<span class="token punctuation">,</span> Price <span class="token operator">=</span> p<span class="token punctuation">.</span>Price <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">FirstOrDefaultAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Notice that the query returns a DTO instead of an entity.
This improves API design and prevents unnecessary data exposure.
Using Separate Read Models
One of the advantages of CQRS is the ability to optimize read models independently.
Instead of returning entities:
1 2 | <span class="token keyword keyword-return">return</span> <span class="token keyword keyword-await">await</span> _context<span class="token punctuation">.</span>Products <span class="token punctuation">.</span><span class="token function">FirstOrDefaultAsync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Use specialized DTOs:
1 2 3 4 5 6 7 | <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-class">class</span> <span class="token class-name">ProductSummaryDto</span> <span class="token punctuation">{</span> <span class="token keyword keyword-public">public</span> <span class="token return-type class-name"><span class="token keyword keyword-int">int</span></span> Id <span class="token punctuation">{</span> <span class="token keyword keyword-get">get</span><span class="token punctuation">;</span> <span class="token keyword keyword-set">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword keyword-public">public</span> <span class="token return-type class-name"><span class="token keyword keyword-string">string</span></span> Name <span class="token punctuation">{</span> <span class="token keyword keyword-get">get</span><span class="token punctuation">;</span> <span class="token keyword keyword-set">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword keyword-string">string</span><span class="token punctuation">.</span>Empty<span class="token punctuation">;</span> <span class="token punctuation">}</span> |
This improves performance and reduces payload size.
Organizing Project Structure
A common folder structure looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Features │ ├── Products │ ├── Commands │ │ ├── CreateProductCommand.cs │ │ └── CreateProductHandler.cs │ │ │ ├── Queries │ │ ├── GetProductByIdQuery.cs │ │ └── GetProductByIdHandler.cs │ │ │ └── DTOs │ ├── Orders │ └── Customers |
This feature-based organization scales well as applications grow.
Benefits of CQRS Without MediatR
Simpler Debugging
The execution flow is explicit.
Developers can easily trace:
1 2 3 4 5 | Controller ↓ Handler ↓ Database |
No hidden middleware or mediator pipeline exists between components.
Reduced Complexity
Fewer abstractions mean:
- Easier onboarding
- Simpler maintenance
- Less boilerplate code
Fewer Dependencies
The application relies primarily on:
- ASP.NET Core
- Dependency Injection
- Entity Framework Core
This reduces external package management overhead.
Better Performance
Although the performance difference is usually small, direct handler invocation avoids mediator dispatch overhead.
For high-throughput systems, reducing unnecessary layers can be beneficial.
When MediatR May Still Be Useful
Despite the advantages of a lightweight approach, MediatR remains valuable in some scenarios.
Examples include:
- Large enterprise systems
- Extensive pipeline behaviors
- Cross-cutting concerns
- Request validation pipelines
- Centralized logging
- Notification-based workflows
If your application heavily depends on these patterns, MediatR may still be the right choice.
Common Mistakes
Mixing Commands and Queries
Avoid code like:
1 | <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-async">async</span> <span class="token return-type class-name">Task<span class="token punctuation"><</span>Product<span class="token punctuation">></span></span> <span class="token function">UpdateAndGetProduct</span><span class="token punctuation">(</span><span class="token punctuation">)</span> |
Commands should modify data.
Queries should retrieve data.
Keep these responsibilities separate.
Business Logic in Controllers
Controllers should remain thin.
Avoid:
1 2 3 4 5 | <span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">HttpPost</span></span><span class="token punctuation">]</span> <span class="token keyword keyword-public">public</span> <span class="token keyword keyword-async">async</span> <span class="token return-type class-name">Task<span class="token punctuation"><</span>IActionResult<span class="token punctuation">></span></span> <span class="token function">Create</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Business logic here</span> <span class="token punctuation">}</span> |
Business rules belong in handlers.
Returning Entities Directly
Prefer DTOs over EF Core entities to improve API stability and security.
Best Practices
When implementing lightweight CQRS:
- Keep commands focused on a single action.
- Keep queries read-only.
- Use dedicated handlers for each operation.
- Return DTOs instead of entities.
- Organize code by feature rather than technical layer.
- Keep controllers thin.
- Use dependency injection for handler registration.
- Apply validation before executing handlers.
- Use asynchronous database operations.
- Avoid introducing abstractions that do not provide clear value.
Conclusion
CQRS is a strong architectural pattern for separating read and write responsibilities, resulting in cleaner and more maintainable programs. MediatR is typically connected with CQRS in the.NET ecosystem, however it is not required.
By explicitly implementing command and query handlers with ASP.NET Core’s built-in dependency injection, developers can reap the benefits of CQRS while keeping their architecture simple and visible. This lightweight technique lowers dependencies, simplifies debugging, and avoids unneeded complexity.
For many modern.NET applications, particularly small and medium-sized systems, CQRS without MediatR provides a useful combination of architectural discipline and development simplicity.
Recommendation for ASP.NET 10.0 Hosting
A solid base for developing online services and applications is ASP.NET. Before creating an ASP.NET web application, you must be proficient in JavaScript, HTML, CSS, and C#. There are thousands of web hosting providers offering ASP.NET hosting on the market. However, there are relatively few web hosting providers that offer top-notch ASP.NET hosting.
ASP.NET is the best development language in Windows platform, which is released by Microsoft and widely used to build all types of dynamic Web sites and XML Web services. With this article, we’re going to help you to find the best ASP.NET Hosting solution in Europe based on reliability, features, price, performance and technical support. After we reviewed about 30+ ASP.NET hosting providers in Europe, our Best ASP.NET Hosting Award in Europe goes to HostForLIFE.eu, one of the fastest growing private companies and one of the most reliable hosting providers in Europe.
