Overview
A deliberate learning project that mirrors real production evolution.
Built as a mentored progression through 10 increasingly complex business scenarios — from simple product search to explicit transactions with savepoints — this API demonstrates how architectural decisions compound. Starting score: 3.7/10. Final architecture: 9.3/10.
This is not a tutorial follow-along. Each task was submitted, reviewed, and iterated. The codebase captures the exact journey from "controllers talking directly to the database" to a fully layered system with the Dependency Rule, Result<T> patterns, global exception handling, and automated validation.
Architecture Evolution
The project evolved in two distinct phases, both preserved in the commit history.
Phase 1 — EF Core Mastery (Tasks 01–10)
Controllers called DbContext directly while I learned how EF Core actually translates LINQ to SQL. The focus was query performance, data integrity, and understanding the ORM at the expression-tree level.
Deferred execution with conditional
IQueryablechainingAsNoTracking()+Select()projection for read-only endpoints (65% fewer columns over the wire vsInclude())AsSplitQuery()to eliminate Cartesian explosion on multi-collection loadsExecuteUpdateAsync/ExecuteDeleteAsyncfor true bulk operations without loading entitiesCompiled queries (
EF.CompileAsyncQuery) to skip LINQ-to-SQL translation on hot pathsSavepoints inside explicit transactions to preserve critical work when audit trails fail
Phase 2 — Production Architecture (Direction A)
The same business logic was refactored into a layered architecture to prove the patterns transfer to real systems.
Concern | Before | After |
|---|---|---|
Controller size | 50–150 lines | 5–15 lines (HTTP only) |
Business logic | Scattered in controllers | Service classes |
Validation | Manual | FluentValidation (automatic) |
Error handling | Try/catch everywhere | One middleware, consistent JSON |
Testability | Requires a real database | Fully mockable services |
Dependency Rule held throughout:Store.API → Store.Application → Store.Domain ← Store.Infrastructure
Featured Code Snippets
1. The Result<T> Pattern — Replacing Exceptions with Contracts
Services communicate failures without throwing. The controller maps to HTTP in one line.
Why this matters: Exceptions are expensive and carry no HTTP semantics. Result<T> makes failure a first-class value that is explicit, testable, and cheap.
2. Select() Projection vs Include() — The 65% Reduction
For read scenarios, Select() fetches only the columns you name. Include() loads full entities including every column you never use.
The rule I now apply in production:
Writing data →
Include()→ modify entity →SaveChangesAsync()Reading data →
Select()projection →AsNoTracking()→ neverInclude()
3. Explicit Transactions with Savepoints — Protecting Critical Work
When confirming an order, stock reduction is critical. Audit trail creation is recoverable. A savepoint preserves the critical work if the audit fails.
Why this matters: In high-throughput systems, failing an entire transaction because an audit log insert failed is unacceptable. Savepoints let you define which operations are essential and which are best-effort.
4. Bulk Operations — ExecuteUpdateAsync Without Loading Entities
Applying a discount across an entire category in a single SQL statement. No entities loaded. No SaveChanges. No change tracker.
The trap I learned to avoid: Pre-computing decimal newPrice = product.Price * 0.9 in C# forces EF to generate one UPDATE per row. Referencing the column inside the lambda (p.Price * (1 - discount)) generates a single bulk UPDATE with SQL arithmetic.
5. FluentValidation — Validation as Infrastructure, Not Code
Validation rules live in dedicated classes and run automatically before the controller action is ever invoked.
Impact: Services no longer contain defensive if checks against malformed input. The boundary is enforced at the edge, keeping business logic clean and focused.
Key Metrics & Growth
Task | First Attempt | Final Score | Concept |
|---|---|---|---|
Product Search | 3.7 | 8.7 | Deferred execution, pagination |
Payment Processing | 4.7 | 8.3 | Aggregate validation, balance guards |
Bulk Stock Adjustment | 6.3 | 9.3 | Batch loading, partial failure |
Customer Sales Report | 7.0 | 9.0 | Navigation aggregates in SQL |
Order Confirmation (Transactions) | 7.3 | 9.3 | Savepoints, critical vs recoverable |
Architecture Refactor | 7.0 | 9.3 | Layered architecture, DI |
Total progression: 3.7 → consistent 9.0+ across all new tasks.
What I Would Do Next
This project established the foundation. The next iteration would add:
Unit & integration test suite (xUnit + Moq + FluentAssertions) — roadmap already planned
Soft-delete safety audit on
ExecuteDeleteAsyncto prevent accidental hard deletesCQRS split — separate read models with Dapper for report-heavy endpoints
Redis caching on compiled query results for hot product/catalog data
