488 lines
34 KiB
HTML
488 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" itemscope itemtype="https://schema.org/Article">
|
|
<head>
|
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-G9G5HP50VK"></script>
|
|
<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-G9G5HP50VK');</script>
|
|
<meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>CQRS + MediatR in VSA: Make Your Code Not Spaghetti Code | Indotalent Blog</title>
|
|
<meta name="description" content="How CQRS and MediatR inside Vertical Slice Architecture eliminate spaghetti code. See the before-and-after transformation of a tangled controller into clean VSA slices." />
|
|
<meta name="robots" content="index, follow" />
|
|
<link rel="canonical" href="https://www.indotalent.com/blog/cqrs-mediatr-no-spaghetti-code.html" />
|
|
<link rel="icon" type="image/x-icon" href="../favicon.ico" />
|
|
<script type="application/ld+json">{ "@context":"https://schema.org","@type":"Article","headline":"CQRS + MediatR in VSA: Make Your Code Not Spaghetti Code","author":{"@type":"Person","name":"go2ismail"},"datePublished":"2026-07-01","publisher":{"@type":"Organization","name":"Indotalent"}}</script>
|
|
<script type="application/ld+json">{ "@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{ "@type":"ListItem","position":1,"name":"Home","item":"https://www.indotalent.com/" },{ "@type":"ListItem","position":2,"name":"Blog","item":"https://www.indotalent.com/blog/" },{ "@type":"ListItem","position":3,"name":"CQRS + MediatR: No Spaghetti Code","item":"https://www.indotalent.com/blog/cqrs-mediatr-no-spaghetti-code.html" }]}</script>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
<script>tailwind.config={theme:{extend:{colors:{brand:{50:'#fff7ed',100:'#ffedd5',200:'#fed7aa',300:'#fdba74',400:'#fb923c',500:'#f97316',600:'#ea580c',700:'#c2410c'}},fontFamily:{sans:['Inter','system-ui','sans-serif']}}}}</script>
|
|
<style>.nav-blur{backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}.gradient-text{background:linear-gradient(135deg,#f97316,#ea580c);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.btn-primary{display:inline-flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#f97316,#ea580c);color:#fff;font-weight:600;border-radius:12px;padding:12px 24px;transition:all 0.2s;box-shadow:0 4px 14px -4px rgba(249,115,22,0.35);text-decoration:none}.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 20px -4px rgba(249,115,22,0.45)}article h2{font-size:1.5rem;font-weight:700;margin:1.75rem 0 0.75rem;color:#111827}article h3{font-size:1.15rem;font-weight:600;margin:1.5rem 0 0.5rem}article p{font-size:0.95rem;line-height:1.8;color:#4b5563;margin-bottom:1rem}article code{background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:0.85rem;color:#ea580c}article pre{background:#1f2937;color:#f9fafb;padding:16px;border-radius:10px;overflow-x:auto;margin:1rem 0;font-size:0.85rem;line-height:1.6}article ul{list-style:disc;padding-left:1.5rem;margin-bottom:1rem}article ul li{font-size:0.95rem;line-height:1.8;color:#4b5563}article .spaghetti-box{background:#fef2f2;border:1px solid #fecaca;border-left:5px solid #ef4444;padding:14px 18px;border-radius:0 10px 10px 0;margin:1.2rem 0}article .clean-box{background:#f0fdf4;border:1px solid #bbf7d0;border-left:5px solid #22c55e;padding:14px 18px;border-radius:0 10px 10px 0;margin:1.2rem 0}article .label-spaghetti{display:inline-block;background:#fee2e2;color:#dc2626;font-weight:800;font-size:0.75rem;padding:3px 12px;border-radius:6px;border:1px solid #fecaca;margin-bottom:0.5rem;letter-spacing:0.5px}article .label-clean{display:inline-block;background:#dcfce7;color:#16a34a;font-weight:800;font-size:0.75rem;padding:3px 12px;border-radius:6px;border:1px solid #bbf7d0;margin-bottom:0.5rem;letter-spacing:0.5px}article .vsa-glow{background:linear-gradient(135deg,rgba(249,115,22,0.06),rgba(234,88,12,0.02));border:1px solid rgba(249,115,22,0.15);border-radius:12px;padding:16px 20px;margin:1.2rem 0}</style>
|
|
</head>
|
|
<body class="bg-white text-gray-900 antialiased">
|
|
<nav aria-label="Breadcrumb" class="pt-20 bg-white border-b border-gray-100"><div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5"><ol class="flex items-center gap-1.5 text-xs text-gray-400"><li><a href="../index.html" class="hover:text-brand-500">Home</a></li><li><span class="text-gray-300">/</span></li><li><a href="index.html" class="hover:text-brand-500">Blog</a></li><li><span class="text-gray-300">/</span></li><li><span class="text-gray-600 font-medium">CQRS + MediatR: No Spaghetti Code</span></li></ol></div></nav>
|
|
<nav class="fixed top-0 left-0 right-0 z-50 nav-blur bg-white/85 border-b border-gray-200/60"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"><div class="flex items-center justify-between h-16"><a href="../index.html" class="flex items-center gap-2.5"><img src="../img/logo-indotalent-dark.svg" alt="Indotalent" class="h-8 w-auto" /></a><div class="hidden lg:flex items-center gap-6"><a href="../index.html#products" class="text-sm font-medium text-gray-600 hover:text-brand-500">Products</a><a href="../for-beginners.html" class="text-sm font-medium text-gray-600 hover:text-brand-500">Beginners</a><a href="../for-freelancers.html" class="text-sm font-medium text-gray-600 hover:text-brand-500">Freelancers</a><a href="index.html" class="text-sm font-medium text-brand-600">Blog</a></div></div></div></nav>
|
|
<section class="pt-24 pb-10 lg:pt-32 lg:pb-12"><div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex items-center gap-2 mb-3"><span class="text-xs font-semibold bg-green-50 text-green-700 border border-green-200 rounded-full px-2.5 py-0.5">CQRS</span><span class="text-xs font-semibold bg-amber-50 text-amber-700 border border-amber-200 rounded-full px-2.5 py-0.5">VSA</span><span class="text-xs font-semibold bg-purple-50 text-purple-700 border border-purple-200 rounded-full px-2.5 py-0.5">MediatR</span><span class="text-xs text-gray-400">July 2026 · 14 min read</span></div>
|
|
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 mb-4">CQRS + MediatR in VSA: Make Your Code Not Spaghetti Code</h1>
|
|
<div class="bg-red-50 border border-red-200 rounded-xl p-4 mb-8"><p class="text-sm text-red-800 font-semibold mb-1">🍝 Spaghetti Code Alert</p><p class="text-sm text-red-700">We're exposing real spaghetti code — the kind that lives inside traditional layered architecture controllers. Then we'll show you how CQRS + MediatR, powered by Vertical Slice Architecture, eliminates it completely.</p></div>
|
|
<article>
|
|
|
|
<p>Let's call it what it is: spaghetti code. Every .NET developer has seen it. Every developer has probably written it at 11 PM on a Friday, promising to refactor later. That controller with 700 lines of business logic, validation, database access, email sending, and who-knows-what-else — all tangled together into one unmaintainable mess. That is spaghetti code. And in a traditional layered architecture, spaghetti code is not the exception. It is the inevitable destination.</p>
|
|
|
|
<p>Why does layered architecture breed spaghetti code? Because the architecture itself scatters related logic across multiple projects and layers. An order creation feature touches 7 files across 4 projects: <code>OrderController.cs</code>, <code>IOrderService.cs</code>, <code>OrderService.cs</code>, <code>IOrderRepository.cs</code>, <code>OrderRepository.cs</code>, <code>Order.cs</code>, <code>CreateOrderDto.cs</code>. To understand how an order is created, you must mentally stitch together code from all these files — like following individual strands of spaghetti across the entire plate. The architecture itself creates the tangle. Spaghetti code isn't a developer problem in layered architecture; it's an architectural problem.</p>
|
|
|
|
<p>But there is a way out. CQRS (Command Query Responsibility Segregation) combined with MediatR, inside a Vertical Slice Architecture (VSA), eliminates spaghetti code at the architectural level. Instead of organizing code by technical layer — all controllers together, all services together, all repositories together — VSA organizes code by business feature. Each feature slice contains everything that feature needs: the command, the handler, the validator, and any domain logic. One folder. One feature. Zero spaghetti code.</p>
|
|
|
|
<h2>The Problem: How Layered Architecture Creates Spaghetti Code</h2>
|
|
|
|
<p>Before we dive into the VSA solution, let's understand exactly how layered architecture produces spaghetti code. The fundamental issue is that layered architecture groups code by <em>what things are</em> technically — controllers, services, repositories — rather than <em>what they do</em> for the business. This means that any single business operation is spread across multiple layers and multiple projects. When you need to trace the flow of "creating an order," you jump from the presentation layer to the application layer to the domain layer to the infrastructure layer. Each jump is an opportunity for spaghetti code to creep in.</p>
|
|
|
|
<p>Adding a new feature in layered architecture means touching files in every layer. Adding validation? That goes in the controller, or maybe the service, or maybe a FluentValidation validator injected into the service — the decision is arbitrary, and that arbitrariness is what spawns spaghetti code. Need to send an email after order creation? Do you put it in the service? The controller? A domain event? There's no clear answer in layered architecture, so developers just stuff it wherever it fits, and the spaghetti code grows thicker with every commit.</p>
|
|
|
|
<h2>Before: Spaghetti Code in a Layered Architecture</h2>
|
|
|
|
<p>Here's what spaghetti code looks like in a typical layered .NET application. This is a single <code>OrdersController</code> that handles creating an order. Notice how validation, data access, discount calculation, tax logic, email sending, and logging are all mashed together. This is textbook spaghetti code — every concern entangled with every other concern.</p>
|
|
|
|
<div class="spaghetti-box">
|
|
<span class="label-spaghetti">🍝 SPAGHETTI CODE — Layered Architecture (BEFORE)</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">// Controllers/OrdersController.cs
|
|
// Spaghetti code: 7 concerns in 1 method across 4+ projects
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class OrdersController : ControllerBase
|
|
{
|
|
private readonly AppDbContext _context;
|
|
private readonly IEmailService _emailService;
|
|
private readonly ILogger<OrdersController> _logger;
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> Create(CreateOrderDto dto)
|
|
{
|
|
// Spaghetti concern #1: Validation mixed inline
|
|
if (string.IsNullOrWhiteSpace(dto.CustomerEmail))
|
|
return BadRequest("Email required");
|
|
if (dto.Items == null || dto.Items.Count == 0)
|
|
return BadRequest("No items");
|
|
|
|
// Spaghetti concern #2: Direct data access in controller
|
|
var customer = await _context.Customers
|
|
.FirstOrDefaultAsync(c => c.Email == dto.CustomerEmail);
|
|
if (customer == null) return BadRequest("Customer not found");
|
|
|
|
decimal subtotal = 0;
|
|
var orderItems = new List<OrderItem>();
|
|
|
|
foreach (var item in dto.Items)
|
|
{
|
|
var product = await _context.Products.FindAsync(item.ProductId);
|
|
if (product == null)
|
|
return BadRequest($"Product {item.ProductId} not found");
|
|
|
|
// Spaghetti concern #3: Business rules tangled with DB
|
|
if (product.Stock < item.Quantity)
|
|
return BadRequest($"Out of stock: {product.Name}");
|
|
|
|
// Spaghetti concern #4: Discount logic hardcoded
|
|
decimal discount = 0;
|
|
if (customer.IsPremium && product.Category == "Electronics")
|
|
discount = product.Price * 0.10m;
|
|
else if (customer.IsPremium)
|
|
discount = product.Price * 0.05m;
|
|
else if (item.Quantity > 10)
|
|
discount = product.Price * 0.02m;
|
|
|
|
var lineTotal = (product.Price - discount) * item.Quantity;
|
|
subtotal += lineTotal;
|
|
|
|
orderItems.Add(new OrderItem
|
|
{
|
|
ProductId = product.Id,
|
|
Quantity = item.Quantity,
|
|
UnitPrice = product.Price,
|
|
Discount = discount,
|
|
LineTotal = lineTotal
|
|
});
|
|
|
|
product.Stock -= item.Quantity;
|
|
}
|
|
|
|
// Spaghetti concern #5: Tax logic mixed in
|
|
decimal taxRate = customer.Country switch
|
|
{
|
|
"US" => 0.07m,
|
|
"UK" => 0.20m,
|
|
"AU" => 0.10m,
|
|
_ => 0.10m
|
|
};
|
|
decimal tax = subtotal * taxRate;
|
|
decimal total = subtotal + tax;
|
|
|
|
var order = new Order
|
|
{
|
|
CustomerId = customer.Id,
|
|
Subtotal = subtotal,
|
|
Tax = tax,
|
|
Total = total,
|
|
Status = "Pending",
|
|
CreatedAt = DateTime.UtcNow,
|
|
Items = orderItems
|
|
};
|
|
|
|
_context.Orders.Add(order);
|
|
await _context.SaveChangesAsync();
|
|
|
|
// Spaghetti concern #6: Email sending inside controller
|
|
await _emailService.SendAsync(customer.Email,
|
|
"Order Confirmed",
|
|
$"Dear {customer.Name}, order #{order.Id} total: {total:C}");
|
|
|
|
// Spaghetti concern #7: Logging embedded
|
|
_logger.LogInformation("Order {Id} created for {Email}",
|
|
order.Id, customer.Email);
|
|
|
|
return Ok(new { order.Id, order.Total });
|
|
}
|
|
}</pre>
|
|
</div>
|
|
|
|
<p>Look at this spaghetti code carefully. Seven distinct concerns — validation, data access, business rules, discount calculation, tax calculation, email notification, and logging — are all entangled in a single method. This is what layered architecture produces over time. Each new requirement adds another strand of spaghetti to the plate. Need to support a new country's tax rate? You're editing the same method that also handles email sending. Want to change how premium discounts work? You're touching code that also validates email addresses. The spaghetti code entanglement means you cannot change one thing without risking everything else.</p>
|
|
|
|
<p>And testing this spaghetti code? Forget about it. The discount logic is buried inside a loop that's inside a method that directly instantiates <code>AppDbContext</code>. You can't unit-test the discount calculation without spinning up a database. You can't test the tax logic without mocking an email service — which has nothing to do with tax. Spaghetti code doesn't just look bad. It actively prevents you from writing tests, which means you can't refactor, which means the spaghetti code gets worse. It's a death spiral.</p>
|
|
|
|
<h2>The Solution: VSA + CQRS + MediatR = No Spaghetti Code</h2>
|
|
|
|
<p>Vertical Slice Architecture flips the script entirely. Instead of organizing code by technical layer, VSA organizes by business feature. Every feature lives in its own folder. Inside that folder, you'll find the command/query, the handler, the validator, and any domain logic — all in one place. CQRS, implemented through MediatR, provides the messaging backbone. Commands (writes) and Queries (reads) are clearly separated. Cross-cutting concerns like validation and logging are handled by MediatR pipeline behaviors — written once, applied everywhere.</p>
|
|
|
|
<p>In VSA, the folder structure for our order creation feature looks like this:</p>
|
|
|
|
<pre style="background:#1f2937;color:#f9fafb;padding:16px;border-radius:10px;overflow-x:auto;margin:1rem 0;font-size:0.85rem;line-height:1.6">Features/
|
|
└── Orders/
|
|
├── CreateOrder/
|
|
│ ├── CreateOrderCommand.cs ← The command (DTO)
|
|
│ ├── CreateOrderHandler.cs ← Business logic only
|
|
│ ├── CreateOrderValidator.cs ← Validation rules
|
|
│ └── CreateOrderEndpoint.cs ← Minimal API endpoint
|
|
├── GetOrderById/
|
|
│ ├── GetOrderByIdQuery.cs
|
|
│ └── GetOrderByIdHandler.cs
|
|
└── Shared/
|
|
├── Order.cs ← Domain entity
|
|
└── OrderItem.cs</pre>
|
|
|
|
<p>One feature, one folder. No jumping between projects. No hunting through layers. No spaghetti code. When you need to understand how an order is created, you open the <code>CreateOrder</code> folder and everything is right there. When you need to add a new field to orders, you change files in one folder — not across four projects. This is the architectural antidote to spaghetti code.</p>
|
|
|
|
<h2>After: Clean Code with VSA, CQRS, and MediatR</h2>
|
|
|
|
<p>Now let's see the same order creation feature rebuilt with CQRS + MediatR inside a Vertical Slice Architecture. Every concern is separated. Every class has a single responsibility. There is not a trace of spaghetti code anywhere.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ CLEAN CODE — VSA + CQRS + MediatR (AFTER)</span>
|
|
</div>
|
|
|
|
<h3>1. The Command — A Simple, Focused DTO</h3>
|
|
<p>The command is a plain record. It carries data. Nothing else. This is not spaghetti code; it's a single-purpose data structure.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Features/Orders/CreateOrder/CreateOrderCommand.cs</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public record CreateOrderCommand(
|
|
string CustomerEmail,
|
|
List<OrderItemDto> Items
|
|
) : IRequest<CreateOrderResult>;
|
|
|
|
public record OrderItemDto(Guid ProductId, int Quantity);
|
|
public record CreateOrderResult(Guid OrderId, decimal Total);</pre>
|
|
</div>
|
|
|
|
<h3>2. The Validator — Validation Lives in Its Own Class</h3>
|
|
<p>Validation is extracted into a dedicated FluentValidation validator. No validation logic pollutes the handler or the controller. This is how you prevent spaghetti code: one class, one job.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Features/Orders/CreateOrder/CreateOrderValidator.cs</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public class CreateOrderValidator
|
|
: AbstractValidator<CreateOrderCommand>
|
|
{
|
|
public CreateOrderValidator()
|
|
{
|
|
RuleFor(x => x.CustomerEmail)
|
|
.NotEmpty()
|
|
.EmailAddress();
|
|
RuleFor(x => x.Items)
|
|
.NotEmpty()
|
|
.Must(items => items.Count > 0);
|
|
RuleForEach(x => x.Items).ChildRules(item =>
|
|
{
|
|
item.RuleFor(i => i.ProductId)
|
|
.NotEqual(Guid.Empty);
|
|
item.RuleFor(i => i.Quantity)
|
|
.InclusiveBetween(1, 1000);
|
|
});
|
|
}
|
|
}</pre>
|
|
</div>
|
|
|
|
<h3>3. The Handler — Business Logic Only, Zero Spaghetti Code</h3>
|
|
<p>The handler is the heart of the feature slice. It contains <strong>only</strong> business logic. No validation (that's already done by the pipeline). No logging (also in the pipeline). No email sending (that's a domain event). Just pure, focused business logic — the polar opposite of spaghetti code.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Features/Orders/CreateOrder/CreateOrderHandler.cs</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public class CreateOrderHandler
|
|
: IRequestHandler<CreateOrderCommand, CreateOrderResult>
|
|
{
|
|
private readonly AppDbContext _context;
|
|
|
|
public CreateOrderHandler(AppDbContext context)
|
|
=> _context = context;
|
|
|
|
public async Task<CreateOrderResult> Handle(
|
|
CreateOrderCommand cmd, CancellationToken ct)
|
|
{
|
|
var customer = await _context.Customers
|
|
.Where(c => c.Email == cmd.CustomerEmail)
|
|
.FirstOrDefaultAsync(ct);
|
|
|
|
if (customer is null)
|
|
throw new NotFoundException("Customer not found");
|
|
|
|
var order = Order.Create(customer.Id);
|
|
|
|
foreach (var item in cmd.Items)
|
|
{
|
|
var product = await _context.Products
|
|
.FindAsync([item.ProductId], ct);
|
|
|
|
if (product is null)
|
|
throw new NotFoundException(
|
|
$"Product {item.ProductId}");
|
|
|
|
product.ReserveStock(item.Quantity);
|
|
|
|
var discount = customer.CalculateDiscount(
|
|
product, item.Quantity);
|
|
|
|
order.AddItem(product, item.Quantity, discount);
|
|
}
|
|
|
|
order.CalculateTotals(customer.Country);
|
|
|
|
_context.Orders.Add(order);
|
|
await _context.SaveChangesAsync(ct);
|
|
|
|
order.AddDomainEvent(
|
|
new OrderCreatedDomainEvent(order.Id, customer.Email));
|
|
|
|
return new CreateOrderResult(order.Id, order.Total);
|
|
}
|
|
}</pre>
|
|
</div>
|
|
|
|
<h3>4. Domain Logic — Rich, Testable, No Spaghetti Code</h3>
|
|
<p>Business rules live in the domain entities themselves — where they belong. The discount calculation and stock reservation are methods on the entity, not buried in a controller. This is how you eliminate spaghetti code: put logic where it's discoverable.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Features/Orders/Shared/Order.cs (Domain Entity)</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public class Order
|
|
{
|
|
public Guid Id { get; private set; }
|
|
public Guid CustomerId { get; private set; }
|
|
public decimal Subtotal { get; private set; }
|
|
public decimal Tax { get; private set; }
|
|
public decimal Total { get; private set; }
|
|
public OrderStatus Status { get; private set; }
|
|
public List<OrderItem> Items { get; private set; } = [];
|
|
|
|
public static Order Create(Guid customerId)
|
|
=> new() { Id = Guid.NewGuid(), CustomerId = customerId,
|
|
Status = OrderStatus.Pending };
|
|
|
|
public void AddItem(Product product, int quantity, decimal discount)
|
|
{
|
|
var lineTotal = (product.Price - discount) * quantity;
|
|
Items.Add(new OrderItem
|
|
{
|
|
ProductId = product.Id,
|
|
Quantity = quantity,
|
|
UnitPrice = product.Price,
|
|
Discount = discount,
|
|
LineTotal = lineTotal
|
|
});
|
|
Subtotal += lineTotal;
|
|
}
|
|
|
|
public void CalculateTotals(string country)
|
|
{
|
|
Tax = Subtotal * TaxRate.For(country);
|
|
Total = Subtotal + Tax;
|
|
}
|
|
|
|
// Rich domain behavior — no spaghetti code here
|
|
public void MarkAsShipped() =>
|
|
Status = OrderStatus.Shipped;
|
|
public void Cancel() =>
|
|
Status = OrderStatus.Cancelled;
|
|
}</pre>
|
|
</div>
|
|
|
|
<h3>5. Pipeline Behaviors — Cross-Cutting Concerns, Applied Once</h3>
|
|
<p>MediatR pipeline behaviors are the secret weapon against spaghetti code. Instead of copying validation and logging code into every handler (which is how spaghetti code starts), you write it once as a behavior and it applies to every handler automatically. This is the DRY principle at the architectural level.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Pipeline/ValidationBehavior.cs — applied to ALL handlers</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public class ValidationBehavior<TRequest, TResponse>
|
|
: IPipelineBehavior<TRequest, TResponse>
|
|
where TRequest : IRequest<TResponse>
|
|
{
|
|
private readonly IServiceProvider _sp;
|
|
|
|
public async Task<TResponse> Handle(TRequest request,
|
|
RequestHandlerDelegate<TResponse> next,
|
|
CancellationToken ct)
|
|
{
|
|
var validator = _sp.GetService<
|
|
IValidator<TRequest>>();
|
|
|
|
if (validator is not null)
|
|
{
|
|
var result = await validator
|
|
.ValidateAsync(request, ct);
|
|
if (!result.IsValid)
|
|
throw new ValidationException(
|
|
result.Errors);
|
|
}
|
|
|
|
return await next();
|
|
}
|
|
}</pre>
|
|
</div>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Pipeline/LoggingBehavior.cs — applied to ALL handlers</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public class LoggingBehavior<TRequest, TResponse>
|
|
: IPipelineBehavior<TRequest, TResponse>
|
|
where TRequest : IRequest<TResponse>
|
|
{
|
|
private readonly ILogger<LoggingBehavior
|
|
<TRequest, TResponse>> _logger;
|
|
|
|
public async Task<TResponse> Handle(TRequest request,
|
|
RequestHandlerDelegate<TResponse> next,
|
|
CancellationToken ct)
|
|
{
|
|
var name = typeof(TRequest).Name;
|
|
_logger.LogInformation("➡️ {Name} started", name);
|
|
var sw = Stopwatch.StartNew();
|
|
var response = await next();
|
|
sw.Stop();
|
|
_logger.LogInformation(
|
|
"✅ {Name} completed in {Ms}ms", name, sw.ElapsedMilliseconds);
|
|
return response;
|
|
}
|
|
}</pre>
|
|
</div>
|
|
|
|
<h3>6. Domain Events — Side Effects, Not Spaghetti Code</h3>
|
|
<p>Email sending after order creation is a side effect — it doesn't belong in the handler. With domain events, the handler raises an event, and a separate handler sends the email. No spaghetti code entanglement between order creation and email sending.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Features/Orders/CreateOrder/OrderCreatedDomainEventHandler.cs</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public class OrderCreatedDomainEventHandler
|
|
: INotificationHandler<OrderCreatedDomainEvent>
|
|
{
|
|
private readonly IEmailService _email;
|
|
private readonly AppDbContext _context;
|
|
|
|
public async Task Handle(
|
|
OrderCreatedDomainEvent evt, CancellationToken ct)
|
|
{
|
|
var customer = await _context.Customers
|
|
.FindAsync([evt.CustomerId], ct);
|
|
|
|
await _email.SendAsync(customer!.Email,
|
|
"Order Confirmed ✅",
|
|
$"Your order #{evt.OrderId} is being processed.");
|
|
}
|
|
}</pre>
|
|
</div>
|
|
|
|
<h3>7. The Endpoint — Thin, Clean, No Spaghetti Code</h3>
|
|
<p>In VSA, the endpoint (whether a controller action or Minimal API) is razor-thin. It does exactly one thing: send the command through MediatR. That's it. This is what "no spaghetti code" looks like at the API surface.</p>
|
|
|
|
<div class="clean-box">
|
|
<span class="label-clean">✅ Features/Orders/CreateOrder/CreateOrderEndpoint.cs</span>
|
|
<pre style="margin:0.5rem 0 0 0;background:#1f2937">public static class CreateOrderEndpoint
|
|
{
|
|
public static void Map(IEndpointRouteBuilder app)
|
|
{
|
|
app.MapPost("/api/orders", async (
|
|
CreateOrderCommand cmd,
|
|
IMediator mediator,
|
|
CancellationToken ct) =>
|
|
{
|
|
var result = await mediator.Send(cmd, ct);
|
|
return Results.Created(
|
|
$"/api/orders/{result.OrderId}", result);
|
|
})
|
|
.WithName("CreateOrder")
|
|
.WithTags("Orders");
|
|
}
|
|
}</pre>
|
|
</div>
|
|
|
|
<h2>The Spaghetti Code Comparison: Side by Side</h2>
|
|
|
|
<p>Let's put it side by side so the difference is undeniable. On the left: the spaghetti code layered approach. On the right: the clean VSA + CQRS + MediatR approach.</p>
|
|
|
|
<div class="vsa-glow">
|
|
<table style="width:100%;font-size:0.85rem;border-collapse:collapse">
|
|
<thead>
|
|
<tr style="background:#fef2f2"><th style="padding:10px;text-align:left;color:#dc2626;width:50%;border:1px solid #fecaca">🍝 Layered Spaghetti Code</th><th style="padding:10px;text-align:left;color:#16a34a;width:50%;border:1px solid #bbf7d0;background:#f0fdf4">✅ VSA + CQRS + MediatR</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">7 files across 4+ projects per feature</td><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">3-4 files in 1 folder per feature</td></tr>
|
|
<tr><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Validation scattered across controllers and services</td><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Single FluentValidation validator per command</td></tr>
|
|
<tr><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Business logic mixed with data access</td><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Handler = business logic only</td></tr>
|
|
<tr><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Logging copied into every method</td><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">One LoggingBehavior for all handlers</td></tr>
|
|
<tr><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Email sending embedded in controller</td><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Decoupled domain event handler</td></tr>
|
|
<tr><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Impossible to unit test individual concerns</td><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Every class is independently testable</td></tr>
|
|
<tr><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">One change touches unrelated code</td><td style="padding:10px;border:1px solid #e5e7eb;vertical-align:top">Changes stay within the feature folder</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h2>Why CQRS + MediatR + VSA Is the Spaghetti Code Antidote</h2>
|
|
|
|
<p>The combination of these three patterns doesn't just reduce spaghetti code — it makes it architecturally impossible. Here's why each piece matters:</p>
|
|
|
|
<p><strong>Vertical Slice Architecture</strong> groups code by feature, not by technical layer. Since all the code for "create an order" lives in one folder, there's no temptation to scatter logic across projects. The architecture itself enforces cohesion. You cannot create spaghetti code when everything for a feature is co-located.</p>
|
|
|
|
<p><strong>CQRS</strong> forces you to separate reads from writes. A command handler only handles commands. A query handler only handles queries. You can't accidentally mix read logic into a write operation because the interfaces are different: <code>IRequest<TResponse></code> for commands, <code>IRequest<TResult></code> for queries. The type system itself prevents spaghetti code.</p>
|
|
|
|
<p><strong>MediatR</strong> provides the pipeline that handles cross-cutting concerns. Validation, logging, transaction management, authorization — these are all behaviors that execute before and after your handler. Your handler doesn't know about them. They don't know about your handler. This is the separation of concerns that spaghetti code violates, enforced by the framework.</p>
|
|
|
|
<div class="vsa-glow">
|
|
<p style="margin-bottom:0.5rem;font-weight:700;color:#c2410c">🔥 Indotalent Insight</p>
|
|
<p style="margin-bottom:0">Every Indotalent product — from the CRM to the WMS, from the HRM to the CMS — is built with exactly this architecture: VSA + CQRS + MediatR. Not a single line of spaghetti code. You get the complete source code for $21, and you can see for yourself how a production-grade .NET 10 application looks when it's clean from the ground up.</p>
|
|
</div>
|
|
|
|
<h2>Key Takeaways</h2>
|
|
<ul>
|
|
<li><strong>Layered architecture breeds spaghetti code</strong> by scattering related logic across multiple projects and layers — 7 files in 4 projects for one feature.</li>
|
|
<li><strong>VSA eliminates spaghetti code</strong> by grouping all code for a feature in a single folder — 3-4 files, one place, full cohesion.</li>
|
|
<li><strong>CQRS prevents concerns from mixing</strong> — commands and queries are different types, so you can't accidentally blend read and write logic.</li>
|
|
<li><strong>MediatR pipeline behaviors</strong> handle cross-cutting concerns once, stopping the copy-paste spaghetti code cycle dead in its tracks.</li>
|
|
<li><strong>Domain events decouple side effects</strong> — email sending, audit logging, and cache invalidation live in their own handlers, not tangled into business logic.</li>
|
|
<li><strong>The result is fully testable code</strong> — every class has one responsibility, making unit testing trivial compared to the spaghetti code alternative.</li>
|
|
</ul>
|
|
|
|
<h2>Stop Writing Spaghetti Code. Start with VSA.</h2>
|
|
|
|
<p>Spaghetti code isn't a personality trait. It's not something you're doomed to write because you're a "messy" developer. Spaghetti code is a symptom of the wrong architecture. When your architecture scatters related code across layers and projects, it rewards tangling. When your architecture groups code by feature, it rewards cleanliness. VSA + CQRS + MediatR is that clean architecture — and once you try it, you'll wonder how you ever tolerated spaghetti code.</p>
|
|
|
|
<p>The next time you open a controller and find 700 lines of intertwined logic, remember: it's not your fault. But it is your responsibility to fix it. And the fix has a name: CQRS + MediatR, inside a Vertical Slice Architecture. No more spaghetti code. Just clean, maintainable, testable .NET applications that make you proud to call yourself a developer.</p>
|
|
|
|
</article>
|
|
<div class="mt-10 p-6 bg-gradient-to-r from-orange-50 to-amber-50 border border-orange-200 rounded-2xl"><p class="text-sm text-gray-700 font-semibold mb-2">Explore VSA + CQRS + MediatR in Production</p><p class="text-sm text-gray-600 mb-4">See how Indotalent products implement this exact architecture. Zero spaghetti code. Complete source — $21 each.</p><a href="../index.html#products" class="btn-primary text-sm">Explore Products</a></div>
|
|
<div class="mt-6"><a href="index.html" class="text-sm text-brand-600 hover:text-brand-700">← Back to Blog</a></div>
|
|
</div></section>
|
|
<footer class="bg-white border-t border-gray-200"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10"><div class="flex flex-col md:flex-row items-center justify-between gap-4"><a href="../index.html" class="flex items-center gap-2"><img src="../img/logo-indotalent-dark.svg" alt="Indotalent" class="h-5 w-auto" /></a><p class="text-xs text-gray-500">© 2024-2026 Indotalent. <span class="text-brand-500">♥</span> by go2ismail</p></div></div></footer>
|
|
</body>
|
|
</html></content>
|
|
<content>Write file to 'blog/cqrs-mediatr-no-spaghetti-code.html'. |