The tech stack I used and why (updated, with Repository and Unit of Work)
This project is small, but I wanted it to feel like a real API you can extend. The repository is here:
https://github.com/stevsharp/ChainOfResponsibilityWithMediatR
Carter for endpoints
I used Carter because I like minimal APIs, but I still want structure. Carter lets you group endpoints by feature (modules) without big controllers, so the API layer stays clean. (Microsoft Learn)
CQRS is the separation of:
- Commands (write, change state)
- Queries (read, return data)
With MediatR, the endpoint just does mediator.Send(...) and the logic lives in a handler. This keeps endpoints thin and makes features easy to follow.
ICommand and IQuery marker interfaces
I added ICommand and IQuery as simple marker interfaces on top of MediatR requests. This is not a “framework”, it is only a clean way to tag what is a write and what is a read.
The reason is pipeline behaviors. With these markers, I can run transactions only for commands, and avoid wrapping queries.
Pipeline behaviors are like middleware around every handler (IPipelineBehavior<TRequest, TResponse>). (
In this project I use:
- LoggingBehavior, so I see execution time and request names
- ValidationBehavior, so invalid requests never reach business logic
- TransactionBehavior, so commands run inside a transaction
This keeps handlers focused, no repeated boilerplate.
FluentValidation
I used FluentValidation because it keeps validation clean and testable. It also supports assembly scanning registration, so validators are discovered automatically.
Repository pattern (why it exists here)
The Repository is used as a boundary between the application and persistence. In simple words, the application talks to an interface like IPurchaseOrderRepository, and Infrastructure decides how the data is stored (EF Core, SQLite, later maybe something else).
Conceptually, a repository “looks like” an in-memory collection for your domain objects, but it hides the database mapping details.
This is the classic definition from Fowler. (martinfowler.com)
In this project, repositories help me keep handlers free from EF Core code, so handlers read more like business use cases.
Unit of Work (and how it connects to DbContext)
A Unit of Work groups changes and commits them together. Fowler describes it as tracking everything you do in a business transaction and coordinating writing changes to the database. (martinfowler.com)
Important detail: in EF Core, DbContext is already designed to be used as a unit of work. Microsoft docs even explain that a DbContext instance is typically used for a single unit of work, then you call SaveChanges and dispose it. (Microsoft Learn)
So why still have IUnitOfWork in the Application layer?
Because it is not only about “needing” it, it is about controlling dependencies. Sometimes you want:
- handlers to never call EF directly
- one
SaveChangesAsync per command (enforced in one place)
- the possibility to swap persistence later without touching application code
So IUnitOfWork is an Application abstraction, Infrastructure implements it by calling DbContext.SaveChangesAsync. (Microsoft Learn)
EF Core + SQLite
EF Core is the persistence technology, SQLite is the lightweight database so the project runs locally with minimal setup. For a demo project, it is perfect.
Also, EF Core saving is straightforward: you change tracked entities, then call SaveChanges or SaveChangesAsync to persist. (Microsoft Learn)
Swagger (Swashbuckle)
Swagger is here so you can test endpoints fast without Postman. It gives visibility to request and response contracts, which is very useful in demos and early development.
Domain patterns, Chain of Responsibility and Specification
The business part is the approval flow. I used Chain of Responsibility to avoid one big if/else if method, and I used Specification style rules so conditions stay readable and composable. This makes it easy to add new approval levels without rewriting old logic. (Coder Legion)
References
https://github.com/stevsharp/ChainOfResponsibilityWithMediatR
- Original article on CoderLegion:
https://coderlegion.com/8906/chain-of-responsibility-inside-cqrs-with-mediatr-a-small-practical-example
- How to use Carter in a CQRS API, advantages and alternatives :
https://coderlegion.com/9342/carter-in-a-cqrs-api-advantages-and-alternatives
- MediatR pipeline behaviors (
IPipelineBehavior):
- FluentValidation DI scanning:
- Repository pattern (Fowler): (martinfowler.com)
- Unit of Work pattern (Fowler): (martinfowler.com)
- EF Core: DbContext as unit of work + SaveChanges lifecycle: (Microsoft Learn)