Skip to content
Back to skills

Dotnet Cqrs

ASecurity

Reviews CQRS/MediatR command-query separation as its own architectural discipline, distinct from dotnet-validation's pipeline-behavior focus. Flags query handlers that mutate state, commands that return large read models instead of an identifier/minimal result, fat handlers mixing orchestration with business logic that belongs in the domain layer, and commands/queries missing a consistent cross-cutting pipeline (logging, validation, transaction) applied uniformly. Outputs findings with pilot-...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
developmentgoapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill dotnet-cqrs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dotnet Cqrs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dotnet Cqrs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-dotnet-cqrs/badge)](https://www.skillsdirectory.com/skills/agenticpawan-dotnet-cqrs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dotnet-cqrs
description: Reviews CQRS/MediatR command-query separation as its own architectural discipline, distinct from dotnet-validation's pipeline-behavior focus. Flags query handlers that mutate state, commands that return large read models instead of an identifier/minimal result, fat handlers mixing orchestration with business logic that belongs in the domain layer, and commands/queries missing a consistent cross-cutting pipeline (logging, validation, transaction) applied uniformly. Outputs findings with pilot-dotnet cqrs standard IDs.
when_to_use: CQRS, MediatR, IRequest, IRequestHandler, command handler, query handler, command query separation, fat handler, pipeline behavior, read model, write model, thin controller
---

## Standard IDs

| ID | Severity | What it checks |
|----|----------|-----------------|
| CQR-001 | P0 | Query handler mutates state (writes to the database, publishes events) |
| CQR-002 | P2 | Command returns a large read model instead of an identifier/minimal result |
| CQR-003 | P1 | Fat handler mixes orchestration with business logic that belongs in the domain layer |
| CQR-004 | P2 | Commands/queries don't share a consistent cross-cutting pipeline (logging, validation, transaction) |

---

## Check A — Query handler mutates state (CQR-001)

### Detection

Grep handlers implementing `IRequestHandler<TQuery, TResult>` where the query's name/intent
is read-only (`GetOrderQuery`, `SearchCustomersQuery`) for any `_db.SaveChangesAsync()`,
`Add`/`Update`/`Remove` call, or event publish inside the handler body. A "query" that
mutates breaks the core CQRS guarantee that reads are side-effect-free and safely
repeatable/cacheable — a caller (or a retry, or a prefetch) can trigger writes just by
reading.

### BAD — a query silently updates a "last viewed" timestamp

```csharp
public class GetOrderQueryHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
    public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)
    {
        var order = await _db.Orders.FindAsync(request.OrderId);
        order!.LastViewedAt = DateTime.UtcNow; // a write, hidden inside a "query"
        await _db.SaveChangesAsync(ct);
        return _mapper.Map<OrderDto>(order);
    }
}
```

### GOOD — the query only reads; the write is its own explicit command

```csharp
public class GetOrderQueryHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
    public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct) =>
        await _mapper.ProjectTo<OrderDto>(_db.Orders.Where(o => o.Id == request.OrderId))
            .FirstOrDefaultAsync(ct) ?? throw new NotFoundException();
}

// tracking "last viewed" is an explicit, separately-dispatched command if the business actually needs it
public record RecordOrderViewedCommand(Guid OrderId) : IRequest;
```

---

## Check B — Command returns a large read model (CQR-002)

### Detection

Grep `IRequestHandler<TCommand, TResult>` where `TResult` is a full read DTO with nested
collections, rather than the created/affected identifier (or a minimal acknowledgment). A
command's job is to change state; if the caller needs the resulting read model, that's a
separate query dispatched right after — conflating the two makes the command's cost and
purpose unclear and couples the write path to read-side shaping concerns.

### BAD — command returns the full order graph, doing double duty as a query

```csharp
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderDetailDto>
{
    public async Task<OrderDetailDto> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = ...;
        await _db.SaveChangesAsync(ct);
        return await _mapper.ProjectTo<OrderDetailDto>(
            _db.Orders.Include(o => o.LineItems).Where(o => o.Id == order.Id)).FirstAsync(ct); // a query, wearing a command's clothes
    }
}
```

### GOOD — command returns the identifier; caller queries separately if it needs the read model

```csharp
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = Order.Create(request.CustomerId, request.LineItems);
        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);
        return order.Id;
    }
}

// controller, if the client needs the detail view
var orderId = await _mediator.Send(new CreateOrderCommand(...));
var detail = await _mediator.Send(new GetOrderDetailQuery(orderId));
```

---

## Check C — Fat handler mixing orchestration with business logic (CQR-003)

### Detection

Grep command handlers for business-rule logic (discount calculation, state-transition
rules, eligibility checks) implemented inline in the handler body instead of delegated to
the domain entity/domain service, per `dotnet-clean-architecture`'s layering. The handler's
job is orchestration (load aggregate, call domain method, persist, publish); business rules
belong on the aggregate so they're enforced no matter which handler/caller touches it.

### BAD — discount and eligibility rules live inside the MediatR handler

```csharp
public async Task<Guid> Handle(ApplyDiscountCommand request, CancellationToken ct)
{
    var order = await _db.Orders.FindAsync(request.OrderId);
    if (order!.Total < 100) throw new InvalidOperationException("Order too small for a discount");
    if (order.Customer.LoyaltyTier == LoyaltyTier.Gold) order.Total *= 0.9m;
    else if (order.Customer.LoyaltyTier == LoyaltyTier.Silver) order.Total *= 0.95m;
    await _db.SaveChangesAsync(ct);
    return order.Id;
}
```

### GOOD — handler orchestrates; the domain entity owns the rule

```csharp
// Domain/Order.cs
public void ApplyLoyaltyDiscount()
{
    if (Total < 100) throw new OrderTooSmallForDiscountException(Id);
    Total *= Customer.LoyaltyTier switch { LoyaltyTier.Gold => 0.9m, LoyaltyTier.Silver => 0.95m, _ => 1m };
}

// Application/ApplyDiscountCommandHandler.cs
public async Task<Guid> Handle(ApplyDiscountCommand request, CancellationToken ct)
{
    var order = await _db.Orders.Include(o => o.Customer).FirstAsync(o => o.Id == request.OrderId, ct);
    order.ApplyLoyaltyDiscount(); // rule lives on the aggregate, reusable and independently testable
    await _db.SaveChangesAsync(ct);
    return order.Id;
}
```

---

## Check D — No consistent cross-cutting pipeline (CQR-004)

### Detection

Confirm every command/query flows through the same `IPipelineBehavior<,>` stack (logging,
`dotnet-validation`'s `ValidationBehavior`, transaction wrapping for commands) rather than
individual handlers manually logging or wrapping their own `TransactionScope`. An
inconsistent pipeline means some commands are transactional and some aren't, for no reason
a reviewer can see from the handler alone.

### BAD — one handler manually wraps a transaction, another doesn't, no shared behavior

```csharp
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
{
    using var tx = await _db.Database.BeginTransactionAsync(ct); // this handler remembers to do it
    ...
    await tx.CommitAsync(ct);
}

// a different command handler, same kind of multi-write operation, no transaction at all
public async Task<Guid> Handle(CancelOrderCommand request, CancellationToken ct)
{
    _db.Orders.Remove(order);
    await _refundService.IssueAsync(order.Id); // if this throws, the removal already committed
    return order.Id;
}
```

### GOOD — a shared behavior wraps every command in a transaction uniformly

```csharp
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : ICommand<TResponse>  // marker interface distinguishes commands from queries
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        await using var tx = await _db.Database.BeginTransactionAsync(ct);
        var response = await next();
        await tx.CommitAsync(ct);
        return response;
    }
}

builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); // dotnet-validation
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>)); // every command, uniformly
```

---

## CQRS checklist

- [ ] No query handler writes to the database or publishes events
- [ ] Commands return an identifier/minimal result, not a full read DTO — read models come from a separate query
- [ ] Business rules live on the domain entity/domain service, not inline in the MediatR handler
- [ ] Every command/query passes through the same pipeline-behavior stack (logging, validation, transaction) — no handler manually reimplements cross-cutting concerns
- [ ] A marker interface (`ICommand<T>` vs `IQuery<T>`) makes the read/write distinction visible to pipeline behaviors and reviewers alike

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…