Zero to Hero: Learn .NET Backend Development

A complete beginner's course — using a real project as your training ground


Zero to Architect: The Complete .NET Backend Course

From "what is a backend?" to designing production, distributed systems — using a real project as your training ground


Your journey map

This course is two books. Book 1 takes you from zero to a working, secured API — a real backend developer. Book 2 takes you from there to the deeper skills senior and staff-level engineers are expected to have: language mastery, architecture, distributed systems, and production operations.

 START HERE
    │
    ▼
┌─────────────┐     ┌──────────────┐     ┌───────────────┐     ┌──────────────┐
│  PART 1     │     │  PART 2      │     │  PART 3       │     │  PART 4      │
│  Foundations│ ──> │  Talking to  │ ──> │  Building the │ ──> │  Securing    │
│  (the web,  │     │  a Database  │     │  API itself   │     │  the API     │
│  APIs, code │     │  (DI, Dapper,│     │  (routes,     │     │  (auth, keys,│
│  structure) │     │  EF, LINQ)   │     │  validation)  │     │  rate limits)│
└─────────────┘     └──────────────┘     └───────────────┘     └──────────────┘
                                                                        │
    ┌───────────────────────────────────────────────────────────────┘
    ▼
┌─────────────┐     ┌──────────────────────┐
│  PART 5     │     │  CHECKPOINT:         │
│  Polish &   │ ──> │  BACKEND DEVELOPER 🎓│
│  Production │     │  End of Book 1 —      │
│  (logging,  │     │  you can build,       │
│  Swagger,   │     │  secure, and ship     │
│  deploy)    │     │  a real backend API   │
└─────────────┘     └──────────────────────┘
                                │
   ┌────────────────────────────┘
   ▼
┌──────────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  BOOK 2          │  │  Architecture│  │  Resilience &│  │  Distributed │
│  Language        │─>│  & Design    │─>│  Performance │─>│  Systems &   │
│  Mastery, HTTP,   │  │  (SOLID,     │  │  (caching,   │  │  Ops (Docker,│
│  ASP.NET Core     │  │  CQRS, DDD)  │  │  resilience, │  │  K8s, CI/CD, │
│  internals, SQL   │  │              │  │  messaging)  │  │  microserv.) │
└──────────────────┘  └──────────────┘  └──────────────┘  └──────────────┘
                                                                    │
   ┌────────────────────────────────────────────────────────────────┘
   ▼
┌──────────────────────┐
│  YOU ARE HERE:        │
│  ARCHITECT 🏆         │
│  You can design,      │
│  scale, and operate   │
│  production distributed│
│  systems               │
└──────────────────────┘

How this course works: every lesson explains a concept in plain English first, using a real-life comparison. Then we look at how that exact concept is used in a real, working project — a REST API that reads product data from a retail database (RetailPro9) — or, for Book 2 topics that go beyond what our project needs, how you'd apply it in a larger real-world system.

The 📖 boxes are your glossary — short, simple definitions of terms, so you always have a plain-English anchor to come back to.


PART 1 — FOUNDATIONS

Lesson 1: What is "Backend Development"?

🎯 The problem

You open a food delivery app, tap "Order Pizza," and two seconds later you see "Order confirmed, arriving in 30 minutes." Where did that confirmation come from? Who checked if the restaurant is open? Who saved your order so it doesn't get lost?

💡 The idea

Every app has two halves:

  • Frontend — what you see and tap: buttons, screens, colors. It's the restaurant's dining room.
  • Backend — what happens behind the scenes: checking your order is valid, saving it, telling the kitchen, replying "confirmed." It's the restaurant's kitchen and staff — the customer never sees it, but nothing works without it.
   YOU (the customer)
        │
        │ "I'd like a pizza"
        ▼
  ┌───────────┐        ┌─────────────────────┐        ┌──────────┐
  │ FRONTEND  │ ─────> │      BACKEND         │ ─────> │ DATABASE │
  │ (the app  │ <───── │ (checks, decides,    │ <───── │ (stores  │
  │  screen)  │        │  saves, replies)     │        │  data)   │
  └───────────┘        └─────────────────────┘        └──────────┘

📖 In simple words:

  • Backend = the part of a program that runs on a server, handles logic, and talks to a database — the part users never see directly.
  • Server = a computer whose job is to sit ready, waiting for requests, and respond to them.
  • Database = organized, permanent storage for data — like a filing cabinet the backend can search and update.

🏗 In our project

Throughout this course we'll build (and study) the RetailPro9 Product API — a backend that answers one question really well: "What products does this store have, what do they cost, and are they in stock?" Point-of-sale systems, mobile apps, and websites can all ask it that question instead of talking to the messy internal retail database directly.

✅ Key takeaway

The backend is the invisible worker: it receives a request, makes a decision (often by checking a database), and sends back an answer.


Lesson 2: How the web actually talks — HTTP

🎯 The problem

Your phone and a server on the other side of the planet need to agree on exactly how to ask for and receive information — like two strangers needing a shared language.

💡 The idea

That shared language is called HTTP (HyperText Transfer Protocol). Every web request follows the same simple shape:

 CLIENT                                    SERVER
   │                                          │
   │   REQUEST                                │
   │   "GET /products?siteCode=ST001"         │
   │ ───────────────────────────────────────> │
   │                                          │  (server thinks,
   │                                          │   checks database)
   │   RESPONSE                                │
   │   Status: 200 OK                          │
   │   Body: [ {...}, {...} ]                  │
   │ <─────────────────────────────────────── │
   │                                          │

Every request has a method — a verb saying what you want to do:

Method Real-life meaning Example
GET "Show me something" Get the list of products
POST "Create something new" Place a new order
PUT/PATCH "Update something" Change a product's price
DELETE "Remove something" Cancel an order

Every response has a status code — a 3-digit number that instantly tells you what happened:

Code Meaning Real-life comparison
200 OK Success "Here's your pizza"
400 Bad Request You asked wrong "That's not a valid order"
401 Unauthorized You didn't prove who you are "Show your ID first"
404 Not Found Doesn't exist "We don't sell that"
500 Internal Server Error The kitchen caught fire "Something broke on our end"

📖 In simple words:

  • HTTP = the standard "language" web browsers, apps, and servers use to talk to each other.
  • Request = a message asking the server to do something.
  • Response = the server's reply.
  • Status code = a short number telling you if the request succeeded, failed, or was invalid.

🏗 In our project

Every call to our API is an HTTP GET request, like:

GET https://api.example.com/api/v1/products?siteCode=ST001

And it always replies with a status code — 200 with real data, 401 if you forgot your API key, or 500 if something broke internally.

✅ Key takeaway

HTTP is just a structured way of asking a question (request) and getting an answer (response), with a method (verb) and a status code (result) every single time.


Lesson 3: What is a REST API?

🎯 The problem

"API" gets thrown around constantly. What actually is one?

💡 The idea

📖 API (Application Programming Interface) = a defined way for one piece of software to ask another piece of software for something — like a restaurant menu: you don't need to know how the kitchen cooks, you just need to know what you can order and how to ask for it.

📖 REST = a popular style of building APIs, where you organize everything around "things" (called resources) and use HTTP methods to act on them.

 Resource: "products"

 GET    /products          → list all products
 GET    /products/123       → get one specific product
 POST   /products           → create a new product
 PUT    /products/123       → update product 123
 DELETE /products/123       → delete product 123

The URL says what thing you're working with. The HTTP method says what you're doing to it.

🏗 In our project

Our whole API is built around one resource: products.

GET /api/v1/products                → list products (with filters)
GET /api/v1/products/{plu}          → get one specific product

Notice we only ever GET — because this API's whole job is reading store data, never changing it. That's a deliberate design choice, not an accident.

✅ Key takeaway

A REST API organizes itself around "things" (resources) and uses standard HTTP verbs to act on them — this predictability is exactly why REST became so popular.


Lesson 4: The anatomy of a .NET backend project

🎯 The problem

Open any real backend project and you'll see a dozen folders. Where does anything even start?

💡 The idea

A well-organized backend is split into layers — like a company's org chart. Each layer has one job and only talks to the layer next to it.

┌─────────────────────────────────────────────────────┐
│  CONTROLLERS  ── "receives requests, sends replies"  │   <- talks to the outside world
├─────────────────────────────────────────────────────┤
│  SERVICES / REPOSITORIES ── "the actual logic"       │   <- the "brain"
├─────────────────────────────────────────────────────┤
│  DATA ACCESS ── "talks to the database"              │   <- the "hands"
├─────────────────────────────────────────────────────┤
│  DATABASE ── "where everything is stored"            │
└─────────────────────────────────────────────────────┘

Why split it up? Imagine a restaurant where the same person takes your order, cooks it, AND washes dishes. It works for one table. It falls apart at scale. Splitting responsibilities means each "layer" can be understood, tested, and changed on its own.

📖 In simple words:

  • Controller = the "front desk" — receives a request, decides who should handle it, and sends back the reply.
  • Service / Repository = the "expert" that actually does the work (in our case, decides what data to fetch).
  • DTO (Data Transfer Object) = a simple object whose only job is to carry data between layers — like a delivery box, not the product itself.
  • Model / Entity = a class that represents something in your system (often matching a database table).

🏗 In our project

RetailPro9.ProductApi/
├── Controllers/     <- "front desk": ProductsController.cs
├── Services/        <- "the expert": ProductRepository.cs
├── Data/            <- "the hands": OracleDbConnectionFactory.cs
├── Models/          <- the boxes: ProductDto.cs
├── Config/          <- settings: Options.cs
├── Middleware/       <- security guards checking everyone who walks in
├── Validation/       <- the bouncer checking your ID is filled out correctly
└── Program.cs        <- the manager who sets everything up

Notice ProductsController never writes a single line of SQL — that's ProductRepository's job. This is the layering principle in action.

✅ Key takeaway

A backend project is organized into layers so each part has one clear job — this makes it possible to change how something works (like switching databases) without rewriting everything.


PART 2 — TALKING TO A DATABASE

Lesson 5: Dependency Injection (DI) — the concept everyone finds confusing at first

🎯 The problem

Imagine every lamp in your house had its own built-in, non-removable battery. When the battery dies, you throw away the whole lamp. That's what code looks like when every class creates its own dependencies internally — rigid, and impossible to swap out or test.

💡 The idea

Instead, plug your lamp into a wall outlet. The outlet doesn't care what's plugged in — a lamp, a phone charger, a fan — it just provides power on demand. This is Dependency Injection (DI): instead of a class creating the things it needs, those things are handed to it from outside.

   ❌ WITHOUT DI                        ✅ WITH DI

  class ProductsController {           class ProductsController {
    var repo = new ProductRepo();        private IRepo _repo;
    // stuck with this ONE repo           
  }                                       // constructor injection:
                                          ProductsController(IRepo repo) {
                                            _repo = repo;
                                          }
                                          // <- someone ELSE decides
                                          //    which repo to hand in

📖 In simple words:

  • Dependency Injection (DI) = giving a class the tools it needs from the outside, instead of letting it build those tools itself.
  • DI Container = the "manager" (built into ASP.NET Core) that knows how to build and hand out every registered class automatically.
  • Interface = a contract — a list of "things this must be able to do" — without saying how. IProductRepository says "you must be able to fetch products," not how to fetch them.

The three "types" of DI in .NET (service lifetimes)

When you register a service, you choose how long one instance of it should live:

Lifetime Real-life comparison Lives for... Good for
AddSingleton The building's one shared water tank The whole app's lifetime Things with no per-request state (a connection factory, a config object)
AddScoped A fresh order ticket per customer One HTTP request Things that should be consistent within one request (a repository)
AddTransient A disposable paper cup Every single time it's asked for Lightweight, stateless helpers
builder.Services.AddSingleton<IDbConnectionFactory, OracleDbConnectionFactory>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

Getting this wrong causes real bugs: if something holding per-request data were registered as Singleton, data from one user's request could leak into another user's response.

🏗 In our project

ProductsController never writes new ProductRepository(). Instead:

public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;
    public ProductsController(IProductRepository repository) // <- injected automatically
        => _repository = repository;
}

ASP.NET Core's built-in DI container sees the constructor needs an IProductRepository, looks up what we registered for that interface, builds it, and hands it in. We never wrote the "wiring" code ourselves.

✅ Key takeaway

DI means classes receive what they need instead of creating it — making code flexible, testable, and centrally controlled.


Lesson 6: Talking to a database — your three main options in .NET

🎯 The problem

Your backend needs to fetch rows from a database and turn them into C# objects. .NET gives you three very different ways to do this.

💡 The idea

  LOW-LEVEL, MAX CONTROL                          HIGH-LEVEL, MAX CONVENIENCE
  ─────────────────────────────────────────────────────────────────────────>

    ADO.NET                    Dapper                    Entity Framework Core
    (raw, manual)          (micro-ORM, you                (full ORM, generates
                            write SQL, it maps              SQL for you)
                            results to objects)

📖 In simple words:

  • ORM (Object-Relational Mapper) = a tool that translates between database rows and C# objects, so you can work with product.Price instead of raw table cells.
  • ADO.NET = the most basic, built-in way to talk to a database in .NET — you write everything by hand (connections, commands, reading rows one by one).
  • Dapper = a lightweight helper that still has you write real SQL, but automatically maps the results into C# objects for you. Nicknamed a "micro-ORM."
  • Entity Framework Core (EF Core) = a full ORM — you describe your data as C# classes, and EF Core generates the SQL for you (including complex joins, migrations, and change tracking).

We'll go deeper into Dapper and EF Core in the next two lessons, then compare them directly.

✅ Key takeaway

There's no single "correct" way to access a database — it's a trade-off between how much control you want and how much boilerplate you're willing to write.


Lesson 7: Dapper — the lightweight data mapper

🎯 The problem

Writing raw ADO.NET means manually opening connections, building commands, and reading each column out of a result set by hand — for every single query. That's a lot of repetitive, error-prone code.

💡 The idea

Dapper does one thing extremely well: you write the SQL yourself (so you stay in full control of performance), and Dapper automatically converts the rows that come back into C# objects.

var sql = "SELECT ITEM_SID AS Plu, PRICE AS Price FROM ITEMS WHERE ITEM_SID = :plu";
var product = await connection.QueryFirstAsync<ProductDto>(sql, new { plu = "123" });
// Dapper matches "Plu" and "Price" columns to ProductDto.Plu and ProductDto.Price automatically

That's it. No manual while (reader.Read()) loop, no manual type conversion — but the SQL text itself is 100% yours to write and optimize.

📖 In simple words:

  • Dapper = a small library that runs your own SQL and automatically turns the results into objects — "you write the what, Dapper handles the mapping."

🏗 In our project

Every query in ProductRepository is Dapper:

var rows = await connection.QueryAsync<ProductRow>(sql, parameters);

We chose Dapper here specifically because our SQL needed to be very precise — joining across seven real database views with exact business rules (like "only count an item as in-stock if QTY + SO_ORD_QTY - SO_SENT_QTY > 0"). That's much easier to get exactly right when you write the SQL yourself.

✅ Key takeaway

Dapper is the right tool when you know exactly what SQL you want to run and just want help turning the results into objects.


Lesson 8: Entity Framework Core — the full ORM

🎯 The problem

For a lot of everyday CRUD (Create, Read, Update, Delete) work, hand-writing SQL for every single operation is repetitive — insert, update, delete often look almost identical across dozens of tables.

💡 The idea

EF Core flips the approach: you describe your data as C# classes, and EF Core generates the SQL for you.

public class Product
{
    public int Id { get; set; }
    public string Plu { get; set; }
    public decimal Price { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
}

// EF Core writes the SQL for you:
var product = await db.Products.FirstOrDefaultAsync(p => p.Plu == "123");
db.Products.Add(new Product { Plu = "456", Price = 9.99m });
await db.SaveChangesAsync();   // EF Core figures out the INSERT statement

📖 In simple words:

  • Entity Framework Core (EF Core) = a full ORM where you work with C# classes and let the framework generate SQL, track changes, and manage the database schema for you.
  • DbContext = the "session" object that represents your connection to the database and knows about all your entity classes.
  • Migration = a versioned, trackable change to your database schema, generated from changes to your C# classes.

The trade-off

EF Core is faster to build with for standard CRUD, but you give up some control — the generated SQL isn't always the most efficient for complex, custom queries. This is exactly why our project uses Dapper instead: the queries needed precise control over joins and business logic that would be awkward to express through EF Core's query translation.

✅ Key takeaway

EF Core trades some control for a lot of convenience — excellent for typical CRUD apps, less ideal when you need to hand-tune complex, performance-critical queries.


Lesson 9: Dapper vs. EF Core — choosing the right tool

Dapper Entity Framework Core
You write SQL? Yes, always Rarely — EF generates it
Learning curve Small Bigger (LINQ, change tracking, migrations)
Performance control Very high Good, but less direct
Speed to build simple CRUD Slower (more manual) Very fast
Best for Complex, custom, performance-sensitive queries Standard CRUD apps, rapid development
Used in our project? ✅ Yes ❌ No

📖 In simple words: Dapper is a scalpel — precise, but you do the cutting. EF Core is a food processor — faster for common tasks, less precise control over exactly how each cut is made.

✅ Key takeaway

Neither tool is "better" — the right choice depends on whether your priority is query precision (Dapper) or development speed on standard patterns (EF Core). Many real companies use both, for different parts of the same system.


Lesson 10: LINQ — querying data the C# way

🎯 The problem

You have a List<Product> in memory and you want "only the ones in stock, sorted by price." Writing manual loops for this every time is tedious.

💡 The idea

📖 LINQ (Language Integrated Query) = a set of built-in C# features that let you filter, sort, and transform collections using readable, SQL-like syntax — directly in C#.

var cheapInStockItems = products
    .Where(p => p.Status == 1)          // filter
    .OrderBy(p => p.Price)              // sort
    .Select(p => p.Plu)                 // transform (pick just one field)
    .ToList();

Read it left to right, almost like English: "take products, keep only the in-stock ones, order by price, select just their PLU, and turn it into a list."

LINQ works two ways:

  • In-memory (like the example above) — filtering a list already loaded into your app.
  • Translated to SQL — when used with EF Core, LINQ expressions get converted into real SQL and run on the database itself, not in your app's memory.

🏗 In our project

var items = rows.Select(MapToDto).ToList();

This takes the raw database rows Dapper gave us and transforms each one into a clean ProductDto using our own mapping function — that's LINQ's .Select() doing exactly what it's for: transforming a collection, one item at a time.

✅ Key takeaway

LINQ lets you filter, sort, and transform collections using clear, chainable C# syntax instead of manual loops — and it's a core skill used constantly in .NET, with or without EF Core.


PART 3 — BUILDING THE API

Lesson 11: Controllers, routes, and DTOs

🎯 The problem

You have data access working. Now you need a way for the outside world to actually ask for it over HTTP.

💡 The idea

A controller is a class whose job is to receive HTTP requests and send back responses. Routing is how ASP.NET Core decides which controller method should handle a given URL.

[Route("api/v1/products")]     // <- base URL for everything in this controller
public class ProductsController : ControllerBase
{
    [HttpGet]                   // <- responds to GET requests
    public ActionResult<List<ProductDto>> Get()
    {
        return Ok(myData);      // <- 200 OK, with myData as the JSON body
    }
}

📖 In simple words:

  • Controller = a class that handles incoming web requests for a specific area (like "everything about products").
  • Route = the URL pattern that maps to a controller method.
  • Model binding = ASP.NET Core automatically converting the incoming URL/query string/body into a C# object for you.
  • DTO (Data Transfer Object) = a plain object designed purely to carry data in or out of your API — deliberately separate from your internal database structure.

Why DTOs matter: never expose your database directly

   ❌ RISKY                                ✅ SAFE

  Database columns  ─────────>  API      Database columns  ──> DTO ──> API
  response directly                       (you CHOOSE what
                                            to expose, and
                                            can rename fields)

If your API returns database rows directly, any internal column, any internal naming quirk, becomes permanently part of your public contract. A DTO is a deliberate boundary — you decide exactly what the outside world sees.

🏗 In our project

public class ProductDto
{
    public string SiteCode { get; set; }   // <- NOT the same name as the DB column (STORE_CODE)
    public string Plu { get; set; }
    public decimal Price { get; set; }
}

SiteCode doesn't match any single database column name by coincidence — it's a deliberately chosen name, decided by what the API's consumer actually needs to call it. The database can be renamed, restructured, even migrated to a different system entirely, and as long as ProductRepository still fills in the same DTO shape, nothing about the public API needs to change.

✅ Key takeaway

Controllers receive requests and route them; DTOs are the deliberate, controlled shape of data crossing that boundary — never expose your raw database structure directly.


Lesson 12: Validation — rejecting bad input before it causes trouble

🎯 The problem

A caller sends pageSize=999999999. If nothing stops it, your database might try to process an enormous, expensive query.

💡 The idea

📖 Validation = checking that incoming data is well-formed and within acceptable limits, before it reaches your real logic.

  Request comes in
        │
        ▼
  ┌─────────────┐     ❌ invalid?  ──> reply 400 Bad Request immediately
  │  VALIDATION │                      (database never even touched)
  └─────────────┘
        │ ✅ valid
        ▼
  Business logic runs

This is called "failing fast, at the edge." The earlier you catch a problem, the cheaper and clearer the failure is — both for you and for whoever's calling your API.

public class ProductQueryValidator : AbstractValidator<ProductQuery>
{
    public ProductQueryValidator(int maxPageSize)
    {
        RuleFor(q => q.PageSize).InclusiveBetween(0, maxPageSize);
        RuleFor(q => q.SbsNo).NotNull();
    }
}

🏗 In our project

Every request to /api/v1/products passes through a validator before the repository ever runs a single query. If sbsNo is missing, the caller gets an immediate, clear 400 — not a confusing database error five layers deep.

✅ Key takeaway

Validate at the boundary — the earlier a bad request is rejected, the less damage it can do and the clearer the error message can be.


Lesson 13: Pagination — handling data that doesn't fit in one response

🎯 The problem

A store has 50,000 products. Returning all of them in one response is slow, wastes bandwidth, and can overwhelm both your server and the caller.

💡 The idea

📖 Pagination = splitting a large result into smaller "pages" that get requested one at a time.

  Full result: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ... 50,000]

  Page 1 (size 10): [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  Page 2 (size 10): [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
  ...and so on

Two common approaches:

  • Offset pagination — "skip the first N, give me the next page-size." Simple, but gets slower the deeper you page, because the database still has to count through everything it's skipping.
  • Keyset (cursor) pagination — "give me everything after item #X." Stays fast no matter how deep you go, because it uses an indexed lookup instead of counting and skipping.

🏗 In our project

We support both — offset for simple page-by-page browsing, and keyset (afterPlu) for bulk exports of the full catalog, where performance at scale actually matters.

✅ Key takeaway

Pagination isn't just a UI nicety — the strategy you choose has real, measurable performance consequences at scale.


PART 4 — SECURING THE API

Lesson 14: Authentication vs. Authorization — two different questions

🎯 The problem

These two words get mixed up constantly, even by experienced developers.

💡 The idea

Think of entering a members-only building:

   🚪 FRONT DOOR                        🔑 SPECIFIC ROOM
   "Who are you?"                       "Are you allowed in HERE?"
   = AUTHENTICATION                     = AUTHORIZATION

📖 In simple words:

  • Authentication = proving who you are (or what system you are).
  • Authorization = once known, deciding what you're allowed to do.

You can be authenticated (you showed a valid ID) but not authorized (your ID doesn't grant access to the server room). They're always two separate checks, even though they often happen back-to-back.

🏗 In our project

[Authorize]   // <- this attribute checks BOTH, in order:
public class ProductsController : ControllerBase { ... }

First: is there a valid API key at all? (authentication) Then: is this authenticated caller allowed to reach this endpoint? (authorization) If either fails, the request never reaches our business logic.

✅ Key takeaway

Always keep these separate in your head: authentication = identity, authorization = permission.


Lesson 15: API keys — a simple way to authenticate

🎯 The problem

You need a simple, reliable way for known systems (like a POS terminal or an e-commerce backend) to identify themselves to your API.

💡 The idea

📖 API key = a long, secret string that acts like a password for a system, not a person. The caller includes it with every request, usually in a header.

GET /api/v1/products
X-API-Key: 87A4C2DC909DB8877A92886FE9E13F7FAF7EF734D2B91238081576F8DC237E96
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
    if (!Request.Headers.TryGetValue("X-API-Key", out var key))
        return Task.FromResult(AuthenticateResult.Fail("Missing X-API-Key header"));
    // ... compare against known valid keys ...
}

Two important security details:

  1. Never store keys in plain text — hash them (with something like SHA-256) and compare hashes instead. That way, even a leaked configuration file doesn't expose the real key.
  2. Use a constant-time comparison when checking the key, not a normal ==. A normal comparison can leak how much of the key is correct through subtle timing differences — a real attack called a timing attack.

🏗 In our project

Every API key is hashed at startup and compared using CryptographicOperations.FixedTimeEquals — never a plain string comparison. Each consuming system (POS middleware, e-commerce backend) gets its own unique key, so any one of them can be revoked individually if it's ever compromised.

✅ Key takeaway

API keys are a simple, effective way to identify systems calling your API — but only if you hash them, compare them safely, and issue a unique one per consumer.


Lesson 16: Common security threats and how to defend against them

🎯 The problem

Backend APIs are a favorite target for attackers, because they often sit directly in front of valuable data.

💡 The idea — three threats every backend developer must know

1. SQL Injection

❌ DANGEROUS:  "SELECT * FROM Items WHERE Plu = '" + userInput + "'"

If userInput = ' OR '1'='1  →  the query returns EVERY row, bypassing the filter entirely.

📖 SQL Injection = tricking a database into running unintended commands by sneaking SQL syntax into user input.

The fix — parameterized queries:

// ✅ SAFE: the value is sent SEPARATELY from the SQL text
"SELECT * FROM Items WHERE Plu = :plu", new { plu = userInput }

The database engine treats :plu purely as a value, never as code — no matter what's inside it.

2. Missing HTTPS (unencrypted traffic) 📖 HTTPS = HTTP, but encrypted, so data traveling between client and server can't be read or altered by anyone intercepting it in between.

  Without HTTPS: 🕵️ anyone on the network can read your API key, passwords, data
  With HTTPS:    🔒 traffic is scrambled — only the two ends can read it

3. Leaking internal errors

❌ DANGEROUS: returning the full exception (stack trace, SQL text, server paths) to the caller
✅ SAFE: log the full detail server-side; return only a generic message + a request ID to the caller

🏗 In our project

  • Every query is parameterized (Lesson 16.1 above), no exceptions.
  • HTTPS is enforced (or handled by a reverse proxy in front of the app).
  • Our ExceptionHandlingMiddleware always returns a generic {"error": "internal_error", "requestId": "..."} to callers — the real Oracle error, with full detail, only ever appears in server-side logs.

✅ Key takeaway

Three habits prevent the vast majority of real-world backend breaches: always parameterize queries, always use HTTPS, and never leak internal error detail to the caller.


Lesson 17: CORS — a rule that only applies to browsers

🎯 The problem

This one confuses almost every beginner: "why does my API work in Postman but not from my website's JavaScript?"

💡 The idea

📖 CORS (Cross-Origin Resource Sharing) = a browser security rule that blocks a website's JavaScript from calling a different website's API, unless that API explicitly allows it.

   Browser on "shopping-site.com"
        │
        │  JavaScript tries to call "api.example.com"
        ▼
   🛑 Browser blocks it UNLESS api.example.com says
      "yes, shopping-site.com is allowed to call me"

The key insight: CORS is enforced by the browser, not the server. A curl command, a mobile app, or one server calling another server is completely unaffected by CORS — there's no browser involved to enforce anything.

🏗 In our project

CORS is deliberately locked down to allow no browser-based origins by default, because our real consumers (POS middleware, backend systems) call this API server-to-server — CORS simply doesn't apply to them.

✅ Key takeaway

CORS only matters if browser JavaScript on a different domain needs to call your API directly — server-to-server calls are never affected by it.


Lesson 18: Rate limiting — protecting your API from overload

🎯 The problem

What stops one buggy integration (or a malicious caller) from sending 10,000 requests a second and overwhelming your database?

💡 The idea

📖 Rate limiting = capping how many requests a single caller can make within a time window, and rejecting the rest.

  Caller's budget: 120 requests per 60 seconds

  Requests 1-120  →  ✅ processed normally
  Request 121     →  🛑 429 Too Many Requests
  (budget resets after the 60-second window)
options.AddPolicy("api", context => {
    var key = context.Request.Headers["X-API-Key"].ToString();
    return RateLimitPartition.GetFixedWindowLimiter(key, _ => new FixedWindowRateLimiterOptions {
        PermitLimit = 120, Window = TimeSpan.FromSeconds(60)
    });
});

🏗 In our project

We partition the limit per API key, not globally. This means one consumer having a bug (like a retry loop gone wrong) can't degrade service for every other consumer — each system gets its own fair budget.

✅ Key takeaway

Rate limiting protects your system's stability by capping how much any single caller can consume — and partitioning by identity (not just by IP) keeps one bad actor from affecting everyone else.


PART 5 — POLISH & PRODUCTION

Lesson 19: Error handling and logging

🎯 The problem

Something WILL go wrong in production eventually — a network blip, a bad value, a database hiccup. What happens then decides whether you fix it in five minutes or five hours.

💡 The idea

📖 Middleware = a piece of code that every request passes through on its way in (and every response passes through on its way out) — like security checkpoints in an airport, each doing one job.

 Request  ──> [Exception Handler] ──> [Auth] ──> [Your Controller] ──> Response
                     ▲                                    │
                     └────── catches ANY error from here ─┘

An error-handling middleware wraps everything: if any layer throws an exception, it's caught in one place, logged in full detail, and turned into a safe, generic response.

📖 Logging = recording what happened while your app runs, so you can investigate later — like a flight recorder.

_logger.LogError(ex, "Unhandled exception. RequestId={RequestId}", requestId);

This isn't just text — {RequestId} is a structured field, meaning you can later search your logs for "everything that happened for this exact request," not just scroll through a wall of text.

🏗 In our project

Every error gets a unique requestId. The caller only ever sees:

{"error": "internal_error", "message": "...", "requestId": "0HNN1NDQCJ7F4:0000000B"}

But server-side, that exact ID unlocks the full Oracle exception, stack trace, and everything needed to diagnose it — safely separated from what the outside world can see.

✅ Key takeaway

Good error handling means the caller gets a safe, generic message, while you — server-side — get everything you need to actually fix the problem.


Lesson 20: Swagger / OpenAPI — documenting your API automatically

🎯 The problem

How does another developer (or future-you) know what endpoints exist, what parameters they take, and what they return — without reading all your source code?

💡 The idea

📖 Swagger / OpenAPI = a standard that generates interactive, always-up-to-date API documentation directly from your code.

  Your C# code (controllers, DTOs, comments)
                │
                ▼
     Swagger generates a live, clickable webpage
                │
                ▼
  Other developers can READ and TEST your API
  without ever opening your source code

The key benefit: because it's generated from your actual code, it cannot silently go out of date the way a hand-written wiki page can.

builder.Services.AddSwaggerGen(...);
if (app.Environment.IsDevelopment())   // <- usually only shown in dev, not production
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

🏗 In our project

Anyone opening /swagger sees every endpoint, every parameter, and can even test a real request right there in the browser (after entering their API key) — no source code access required.

✅ Key takeaway

Swagger turns your code itself into living documentation — always accurate, because it's generated from the real thing, not written and maintained separately.


Lesson 21: Deployment — taking your API live

🎯 The problem

Your API works perfectly on your own laptop. Now it needs to run somewhere real, reliably, for other systems to depend on.

💡 The idea

The golden rule: build once, configure per-environment. The actual compiled code should be identical between development, testing, and production — only configuration (connection strings, API keys, log levels) should differ, always through environment variables or a secrets manager, never hardcoded.

   Same compiled app
        │
        ├──> Dev environment      (dev database, verbose logs)
        ├──> Staging environment   (test database, medium logs)
        └──> Production environment (real database, minimal logs, HTTPS enforced)

📖 In simple words:

  • Environment = a distinct place your app runs (development, staging, production), each usually with its own configuration.
  • Environment variable = a value set outside your code, at the operating-system level, that your app reads at startup — the standard way to keep secrets and per-environment settings out of your source code.

🏗 In our project

The same published .dll runs identically whether it's a developer's laptop or the production server — only environment variables like RP9_DB_HOST and RP9_API_KEYS change. Nothing in the code itself needs editing between environments.

✅ Key takeaway

Never hardcode environment-specific values — a properly built app should be deployable anywhere just by changing its configuration, never its code.


PART 6 — THE CAPSTONE: PUTTING IT ALL TOGETHER

The full journey of one request

Here's every lesson from this course, working together, for a single real request: GET /api/v1/products?sbsNo=1&siteCode=ST001

 1. Request arrives at the server                         (Lesson 2 — HTTP)
 2. Error-handling middleware wraps everything below        (Lesson 19)
 3. HTTPS/security checks                                    (Lesson 16)
 4. CORS checked (only matters for browser callers)           (Lesson 17)
 5. Authentication: valid X-API-Key?                           (Lesson 15)
 6. Authorization: [Authorize] requirement met?                 (Lesson 14)
 7. Rate limiter: within this key's budget?                      (Lesson 18)
 8. Routing sends it to ProductsController.Get()                  (Lesson 11)
 9. Model binding turns the URL into a ProductQuery object          (Lesson 11)
10. Validation checks sbsNo, pageSize, etc.                          (Lesson 12)
11. DI hands the controller a ready-made IProductRepository           (Lesson 5)
12. Repository builds safe, parameterized SQL                          (Lesson 16)
13. Dapper runs it and maps rows to ProductDto objects                  (Lesson 7)
14. Pagination info attached to the response                             (Lesson 13)
15. Response sent back as JSON, 200 OK                                    (Lesson 2, 11)
16. The whole thing gets logged                                            (Lesson 19)

Every single arrow in that chain is something you now understand.

You've completed Book 1: you're a Backend Developer 🎓

You started knowing what "backend" meant, roughly. You now know:

  • How the web actually communicates (HTTP)
  • How to structure a real project into layers
  • How Dependency Injection makes code flexible and testable
  • Three different ways to talk to a database, and when to use each
  • How to filter and transform data with LINQ
  • How to build safe, validated, well-documented API endpoints
  • How to prove identity (authentication) and enforce permission (authorization)
  • How to defend against the most common real-world attacks
  • How to protect your system's stability under load
  • How to deploy the same code safely across different environments

That's a real, hireable skill set. Plenty of working backend developers stop right here and have long, successful careers.

But if you want to go further — to the level where you're making architecture decisions, designing systems that span multiple services, and operating things at production scale — Book 2 continues below. It builds on everything above; nothing here gets un-learned, only extended.


BOOK 2: FROM HERO TO ARCHITECT

Book 1 taught you to build and secure one API well. Book 2 teaches you the deeper language skills, architectural thinking, and operational knowledge to build and run systems — the skills that separate a mid-level developer from a senior or staff engineer.

 BOOK 2 MAP

 PART 7          PART 8           PART 9          PART 10
 C# Language  →  HTTP/ASP.NET  →  Data Layer   →  Architecture &
 Mastery         Core Internals    Mastery         Design Patterns
    │
    └──────────────────────────────────────────────────────┐
                                                              ▼
 PART 11         PART 12          PART 13         PART 14
 Background   →  Advanced      →  Performance  →  Testing &
 Work & Events    Security         & Resilience    Observability
    │
    └──────────────────────────────────────────────────────┐
                                                              ▼
 PART 15                          PART 16
 DevOps & Infrastructure      →   Distributed Systems &
 at Scale                         Modern API Styles
                                        │
                                        ▼
                                   ARCHITECT 🏆

PART 7 — MODERN C# LANGUAGE MASTERY

Everything in Book 1 used C# without stopping to explain the language features deeply. Book 2 starts here because everything else — generics-based repositories, async database calls, records-as-DTOs — leans on these language fundamentals.

Lesson 22: Modern C# fundamentals

🎯 The problem

C# has evolved a lot. Code you see in a modern .NET 8 project looks noticeably leaner than C# from ten years ago — if you don't know the modern syntax, real code looks unfamiliar even when the underlying idea is simple.

💡 The idea — the features you'll see constantly

Top-level statements — no class Program { static void Main() } ceremony needed:

// This is a complete, valid Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Run();

Pattern matching — checking a value's "shape" concisely:

if (query.Status is 0 or 1) { ... }              // "is this 0 or 1?"
var label = status switch {
    1 => "In Stock",
    0 => "Out of Stock",
    _ => "Unknown"                                  // "_" = default case
};

String interpolation:

var sql = $"SELECT * FROM {tableName} WHERE ID = :id";   // $"..." embeds expressions in {}

Target-typed new:

ProductDto dto = new();   // instead of "new ProductDto()" - the type is already known

📖 In simple words:

  • Syntactic sugar = a shorter, easier-to-read way to write something the compiler could already do the long way — it doesn't add new capability, just readability.

🏗 In our project

Program.cs is entirely top-level statements — no Main method visible at all. Every SQL string in ProductRepository uses $"..." interpolation to insert table/column names from configuration.

✅ Key takeaway

Modern C# syntax isn't a different language — it's the same concepts, written with far less ceremony. Learning to read it fluently is what makes real-world codebases feel approachable instead of intimidating.


Lesson 23: Generics, delegates, and records

🎯 The problem

You need a "list of products," a "list of orders," a "list of stores" — writing a separate class for each would be absurd duplication.

💡 The idea

Generics — write a class or method once, for any type:

public class PagedResult<T>          // <- T is a placeholder for "whatever type you need"
{
    public IReadOnlyList<T> Items { get; set; }
}

PagedResult<ProductDto> productPage;   // used with ProductDto
PagedResult<OrderDto> orderPage;       // same class, used with OrderDto

Delegates — a variable that holds a reference to a method, not a value:

Func<ProductRow, ProductDto> mapper = MapToDto;   // "mapper" now points at a method
var items = rows.Select(mapper).ToList();          // pass behavior around like data

This is the mechanism that makes LINQ's .Where(x => ...) and .Select(x => ...) possible — the lambda (x => ...) is a small, inline delegate.

Records — a concise way to define immutable, value-based data:

public record ProductSummary(string Plu, decimal Price);
// automatically gets equality comparison, a readable ToString(), and immutability

📖 Record = a type designed for holding data whose identity is its value — two records with the same data are considered equal, unlike normal classes.

🏗 In our project

PagedResult<ProductDto> is a generic class — the same pagination wrapper works no matter what you're paginating. rows.Select(MapToDto).ToList() passes the MapToDto method as a delegate, exactly like the example above.

✅ Key takeaway

Generics avoid duplicating code across types; delegates let you treat behavior as a value you can pass around; records are the modern, concise choice for simple immutable data.


Lesson 24: Nullable reference types — catching "null" bugs at compile time

🎯 The problem

NullReferenceException is famously one of the most common runtime crashes in C# history — a variable you assumed had a value turned out to be empty.

💡 The idea

Modern C# (with <Nullable>enable</Nullable> in your .csproj) makes null-ness part of the type itself:

string name;        // the compiler now assumes this can NEVER be null
string? nickname;    // the "?" means "this CAN be null - you must check before using it"

The compiler now warns you at build time if you use a string? without checking it first — catching a whole category of bugs before your code ever runs.

public string? Barcode { get; set; }   // <- honestly says "this might not have a value"
public string Plu { get; set; } = string.Empty;  // <- this one always has SOME value

📖 In simple words:

  • Nullable reference types = a compiler feature where string? means "might be null, check first" and string means "guaranteed to have a value" — turning a runtime crash risk into a compile-time warning.

🏗 In our project

Look at ProductDto: Barcode is string? (a product genuinely might not have one), while Plu is string with a default of string.Empty (every product must have a PLU — it's mandatory by the API's own contract). This isn't accidental — it's a deliberate, type-level statement of which fields are guaranteed and which aren't.

✅ Key takeaway

Nullable reference types turn "I hope this isn't null" into a compiler-enforced guarantee — one of the highest-value features for reducing real production crashes.


Lesson 25: async/await, Task, and CancellationToken

🎯 The problem

While your API waits for a database to respond (which can take hundreds of milliseconds), should the entire server just... freeze, unable to handle any other request?

💡 The idea

📖 Asynchronous code = code that can pause while waiting for something slow (like a database or network call), without blocking the thread — freeing it up to handle other work in the meantime.

  SYNCHRONOUS (blocking)              ASYNCHRONOUS (non-blocking)

  Thread waits idle for DB  🚫         Thread handles OTHER requests
  ...............2 seconds...          while DB call is in flight  ✅
  Thread resumes                       Thread resumes when DB replies
public async Task<List<ProductDto>> GetProductsAsync()   // "async" + returns a Task
{
    var rows = await connection.QueryAsync<ProductDto>(sql);  // "await" = pause HERE,
    return rows.ToList();                                       //  free the thread, resume later
}

📖 In simple words:

  • Task = represents "work that will finish eventually" — like a receipt for food you ordered, not the food itself yet.
  • async = marks a method as containing pausable, asynchronous work.
  • await = "pause here until this Task finishes, but don't block the thread while waiting."
  • CancellationToken = a signal you can pass into long-running work meaning "stop early if the caller gave up waiting."
public async Task<ProductDto> GetAsync(CancellationToken ct)
{
    // if the HTTP caller disconnects, "ct" lets the database query stop early too,
    // instead of wasting resources finishing work nobody will receive
    return await connection.QueryFirstAsync<ProductDto>(sql, cancellationToken: ct);
}

🏗 In our project

Every single repository and controller method is async and accepts a CancellationToken ct, threaded all the way down to the actual Dapper call. If a caller's HTTP connection drops mid-request, that cancellation genuinely propagates down to Oracle, rather than the query running to completion for nobody.

✅ Key takeaway

async/await lets one server handle thousands of concurrent slow operations efficiently, by freeing up threads while waiting instead of blocking them — essential for any real-world API's scalability.


Lesson 26: Memory, Garbage Collection, and IDisposable

🎯 The problem

Your app opens a database connection. If nobody ever closes it, connections pile up until the database refuses new ones.

💡 The idea

📖 Garbage Collector (GC) = .NET's automatic memory manager — it frees memory used by objects you're no longer referencing, so you (mostly) never manually manage memory like in C or C++.

But the GC only manages memory. Some resources — open database connections, open files, network sockets — are not just memory, and need explicit, prompt cleanup. That's what IDisposable is for.

using var conn = _connectionFactory.CreateConnection();
// conn.Dispose() is called AUTOMATICALLY when this block ends,
// even if an exception is thrown inside it

📖 In simple words:

  • IDisposable = an interface marking "this object holds a resource that must be explicitly released" (a connection, a file handle).
  • using = a keyword that guarantees Dispose() gets called automatically once you're done — even if an error happens in between.

🏗 In our project

using var conn = _connectionFactory.CreateConnection();
var command = new CommandDefinition(finalSql, ...);
var rows = await conn.QueryAsync<ProductRow>(command);
// connection is automatically returned to the pool here, guaranteed

Combined with connection pooling (Lesson 44 territory, but worth previewing: Min Pool Size=2;Max Pool Size=20 in our connection string), this using pattern is why our API can handle many concurrent requests without exhausting Oracle's connection limit — every connection is reliably given back the moment it's done being used.

✅ Key takeaway

The GC handles memory for you automatically, but anything implementing IDisposable (connections, files, sockets) needs using to guarantee timely cleanup — skipping this is one of the most common causes of production resource exhaustion.


Lesson 27: Threads, concurrency, and race conditions

🎯 The problem

Two requests arrive at the exact same moment and both try to update the same in-memory counter. The result is sometimes wrong — and it's not consistent, which makes it terrifying to debug.

💡 The idea

📖 Thread = an independent path of execution — your app can run multiple threads "at once" (or interleaved) to do work concurrently.

📖 Race condition = a bug that occurs when the outcome depends on the unpredictable timing of two or more threads accessing the same data at the same time.

  Thread A: read counter (=5)
  Thread B: read counter (=5)          <- both read the SAME starting value
  Thread A: write counter = 5 + 1 = 6
  Thread B: write counter = 5 + 1 = 6   <- should have been 7!

  One increment was silently LOST.

Common fixes:

  • Avoid shared mutable state entirely (favor immutable data, like records from Lesson 23)
  • Use lock for small critical sections
  • Use thread-safe collections (ConcurrentDictionary, etc.)
  • Push the problem to the database, which has its own concurrency controls (Lesson 31)

🏗 In our project

Our API deliberately has almost no shared mutable state — each request gets its own scoped repository instance (Lesson 5's AddScoped), and the actual "who wins" concurrency problem (two people changing the same price at once) is handled by Oracle itself, not by our application code. This is a common, good practice: let the database be the single source of truth for concurrent data changes, rather than trying to coordinate it yourself in application memory.

✅ Key takeaway

Race conditions are bugs that only show up under specific timing — the safest fix is usually to avoid shared mutable state altogether, rather than trying to perfectly synchronize access to it.


PART 8 — DEEP HTTP, REST & ASP.NET CORE INTERNALS

Book 1 covered HTTP basics. Here we go deeper into how ASP.NET Core actually processes a request internally — knowledge that separates "I can build an endpoint" from "I can debug why the pipeline behaved unexpectedly."

Lesson 28: Deep HTTP and REST

🎯 The problem

"It works" isn't the same as "it follows the conventions every experienced developer expects" — and violating those conventions makes your API confusing to integrate with.

💡 The idea — concepts beyond the basics

Idempotency (we'll go deeper in Lesson 47, but the HTTP-level concept starts here): some methods are defined to be safe to repeat.

Method Idempotent? Meaning
GET ✅ Yes Calling it 100 times has the same effect as calling it once
PUT ✅ Yes "Set this to X" — repeating doesn't change the outcome
DELETE ✅ Yes Already deleted stays deleted
POST ❌ No Calling "create an order" twice creates TWO orders

Content negotiation — the client and server agree on data format:

Accept: application/json      <- client: "reply in JSON please"
Content-Type: application/json <- server: "here's JSON"

Caching headers — telling clients (and browsers, and proxies) how long a response can be reused without asking again:

Cache-Control: max-age=300     <- "this is valid for 5 minutes, don't re-ask"
ETag: "abc123"                  <- a fingerprint of this exact response version

📖 In simple words:

  • Idempotent = doing it twice has the same effect as doing it once.
  • ETag = a short fingerprint of a response's content, used to detect if it changed.

🏗 In our project

Every endpoint is GET — deliberately, because this API only reads data, and GET is naturally idempotent and cacheable. If we ever added an endpoint to update stock, it would need careful thought about whether it should be PUT (idempotent — "set stock to 50") or POST (not idempotent — "add a stock adjustment record").

✅ Key takeaway

REST isn't just "use HTTP" — it's a set of conventions (idempotency, correct verb choice, content negotiation) that make your API predictable to anyone who already knows HTTP.


Lesson 29: ASP.NET Core middleware, filters, and configuration — the deep version

🎯 The problem

Book 1 introduced middleware as "a security checkpoint." Now let's understand exactly how the pipeline is built, and where filters fit in versus middleware.

💡 The idea — the exact pipeline, in order

 Request
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│  MIDDLEWARE PIPELINE (runs for EVERY request, app-wide)       │
│                                                                │
│  ExceptionHandling → ForwardedHeaders → HttpsRedirect →       │
│  CORS → Authentication → Authorization → RateLimiter →         │
│  Routing → [ENDPOINT EXECUTION] → (response flows back up)     │
└─────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────────────────────┐
│  FILTERS (run only for MVC controller actions, more granular) │
│                                                                │
│  Authorization Filter → Resource Filter → Action Filter →      │
│  [YOUR ACTION METHOD RUNS] → Result Filter → Exception Filter   │
└─────────────────────────────────────────────────────────────┘

📖 In simple words:

  • Middleware = app-wide request/response pipeline steps — runs for literally every request, controller or not.
  • Filter = a more fine-grained hook specifically around MVC controller action execution — can run before/after a specific action, or only on actions with a specific attribute.

Order matters, critically. app.UseAuthentication() must come before app.UseAuthorization() — you can't check permissions for an identity you haven't established yet. This is exactly why middleware is registered as an ordered list, not an unordered set.

Configuration providersIConfiguration isn't just appsettings.json. It's a layered system:

1. appsettings.json                    (lowest priority)
2. appsettings.{Environment}.json
3. Environment variables
4. Command-line arguments               (highest priority - overrides everything below)

Later sources override earlier ones for the same key — this is exactly the mechanism that lets RP9_DB_CONNECTION (an environment variable) override whatever's in appsettings.json.

🏗 In our project

Our middleware order in Program.cs is deliberate: exception handling wraps everything (registered first), then forwarded headers, then HTTPS, then CORS, then auth, then rate limiting — each one depends on the ones before it having already run.

✅ Key takeaway

Middleware = app-wide, ordered pipeline steps. Filters = more granular hooks around controller actions. Configuration is layered, with later sources overriding earlier ones — this is how environment-specific values win over defaults.


Lesson 30: Problem Details — a standard shape for API errors

🎯 The problem

Every developer invents their own JSON error shape: {"error": "..."}, {"message": "..."}, {"errors": [...]}. Every API consumer has to learn each one individually.

💡 The idea

📖 Problem Details (RFC 7807) = a standardized JSON shape for representing errors from an HTTP API, so any client can parse errors from any compliant API the same way.

{
  "type": "https://example.com/errors/validation",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "detail": "sbsNo is required.",
  "instance": "/api/v1/products",
  "traceId": "0HNN1NDQCJ7F4:0000000B"
}

ASP.NET Core has this built in:

return ValidationProblem(ModelState);   // automatically produces RFC 7807 shape

🏗 In our project

if (!validationResult.IsValid)
{
    foreach (var error in validationResult.Errors)
        ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
    return ValidationProblem(ModelState);   // <- standard Problem Details shape
}

For our unhandled-exception case, we chose a custom (non-standard) shape instead, deliberately kept minimal (error, message, requestId) — a reasonable choice for an internal API, though a public-facing API would typically standardize fully on Problem Details for every error path, not just validation.

✅ Key takeaway

Problem Details gives your API errors a predictable, standard shape — reducing the guesswork for anyone integrating with your API for the first time.


PART 9 — DATA LAYER MASTERY

Lesson 31: SQL deep dive — indexes, transactions, and concurrency

🎯 The problem

A query that's instant on your test data with 100 rows can take minutes on production data with 10 million rows. And two people editing the same record at once can silently overwrite each other's changes.

💡 The idea

Indexes — a lookup structure that lets the database find rows without scanning the whole table:

  WITHOUT an index on ITEM_SID:
  "find ITEM_SID = 12345" → database checks EVERY row, one by one  🐌

  WITH an index on ITEM_SID:
  "find ITEM_SID = 12345" → database jumps almost straight there    ⚡

📖 Index = a separate, sorted structure the database maintains alongside a table, so it can find matching rows quickly instead of scanning everything.

The trade-off: indexes speed up reads but slow down writes slightly (the index itself must be updated too) and use extra storage. You index columns you filter/sort/join on frequently — not every column.

Transactions — grouping multiple operations so they either all succeed or all fail together:

using var transaction = connection.BeginTransaction();
try {
    await connection.ExecuteAsync(deductStockSql, ..., transaction);
    await connection.ExecuteAsync(createOrderSql, ..., transaction);
    transaction.Commit();     // both succeed together
} catch {
    transaction.Rollback();   // both undone together - never a half-finished state
}

📖 Transaction = a group of database operations treated as a single, all-or-nothing unit.

Concurrency control — handling two people editing the same row at once:

Strategy How it works Trade-off
Pessimistic locking Lock the row the moment you read it for editing; others must wait Safe, but can cause waiting/deadlocks
Optimistic concurrency Everyone reads freely; on save, check "has this changed since I read it?" (often via a version column) — reject if so Better throughput, but the loser has to retry

📖 Isolation level = how strictly the database prevents one transaction from seeing another's in-progress, uncommitted changes — a spectrum from fast-but-loose to slow-but-strict.

🏗 In our project

Our SQL joins on ITEM_SID, SBS_NO, and STORE_NO constantly — in a real production tuning pass, confirming indexes exist on these join/filter columns in INVENTORY_V, INVN_SBS_QTY_V, etc. would be one of the first performance checks to make. Since our API is read-only, we don't need transactions or concurrency control ourselves — but the RP9 POS system writing to these same tables absolutely does, behind the scenes.

✅ Key takeaway

Indexes make reads fast at the cost of slightly slower writes; transactions guarantee multi-step operations don't leave data half-changed; concurrency control decides how conflicting simultaneous edits are resolved.


Lesson 32: Advanced Entity Framework Core

🎯 The problem

Basic EF Core (db.Products.Add(...)) is easy — but real applications hit performance and correctness issues that basic usage doesn't prepare you for.

💡 The idea — key advanced concepts

Change tracking — EF Core watches every entity you load, so SaveChanges() knows exactly what changed:

var product = await db.Products.FindAsync(1);
product.Price = 19.99m;          // EF Core silently notices this change
await db.SaveChangesAsync();      // generates UPDATE ... SET Price = 19.99 WHERE Id = 1

This is powerful, but costs memory/CPU — for read-only queries, disable it:

var products = await db.Products.AsNoTracking().ToListAsync();  // faster, read-only

Migrations — versioned, trackable schema changes:

dotnet ef migrations add AddPriceColumn
dotnet ef database update

Each migration is a C# file describing exactly what changed — reviewable in a pull request, just like any other code change.

The N+1 query problem — a classic EF Core trap:

// ❌ 1 query for orders, then N MORE queries (one per order) for each order's items
var orders = await db.Orders.ToListAsync();
foreach (var o in orders) { var items = o.Items; }   // triggers a query EACH iteration

// ✅ Eager loading - ONE query total, using a JOIN
var orders = await db.Orders.Include(o => o.Items).ToListAsync();

📖 N+1 problem = accidentally running one query per item in a loop, instead of one combined query — a very common, very expensive EF Core mistake.

Split queries — for very wide Include chains, sometimes multiple simpler queries outperform one giant join:

.AsSplitQuery()

🏗 In our project

We didn't use EF Core here — precisely because our real query needed exact control over seven joined views with specific business logic (Lesson 9's trade-off, in practice). This is a genuine, common real-world decision: EF Core for your typical CRUD entities, Dapper for the handful of complex, performance-critical queries — often in the same application.

✅ Key takeaway

EF Core's convenience comes with real performance traps (especially N+1 queries) that every serious EF Core developer must learn to recognize and avoid.


Lesson 33: Advanced Dapper

🎯 The problem

Basic Dapper (QueryAsync<T>) covers simple cases — real applications need multi-table mapping, multiple result sets, and bulk operations.

💡 The idea

Multi-mapping — map a single joined row into two related objects at once:

var sql = "SELECT o.*, c.* FROM Orders o JOIN Customers c ON o.CustomerId = c.Id";
var orders = await connection.QueryAsync<Order, Customer, Order>(
    sql,
    (order, customer) => { order.Customer = customer; return order; },
    splitOn: "Id"   // <- tells Dapper where the Customer columns begin
);

QueryMultiple — run several queries in one round trip to the database:

using var multi = await connection.QueryMultipleAsync(
    "SELECT * FROM Orders WHERE Id = :id; SELECT * FROM OrderItems WHERE OrderId = :id;",
    new { id });
var order = await multi.ReadFirstAsync<Order>();
var items = await multi.ReadAsync<OrderItem>();

Buffered vs. unbufferedQueryAsync loads all results into memory before returning; for huge result sets, streaming avoids that:

var reader = await connection.ExecuteReaderAsync(sql);   // process rows one at a time

Bulk operations — Dapper itself doesn't include bulk insert, but pairs well with libraries designed for it (e.g., Dapper.Contrib or dedicated bulk-copy tools) when inserting thousands of rows at once — a naive loop of single-row INSERTs doesn't scale.

🏗 In our project

Our repository uses a single QueryAsync<ProductRow> call per request — appropriate here because we return one shape from one (complex, but single) query. If we later added an endpoint returning "a store, plus all its recent price changes" as two related result sets, QueryMultiple would be the right next tool.

✅ Key takeaway

Dapper scales well beyond simple single-table queries — multi-mapping and QueryMultiple handle relational and multi-result scenarios without giving up its core "you write the SQL" philosophy.


PART 10 — ARCHITECTURE & DESIGN

Lesson 34: SOLID principles

🎯 The problem

Code that works today becomes impossible to change safely six months from now, as a project grows — unless you deliberately design for change from the start.

💡 The idea — five principles, one letter each

S — Single Responsibility: a class should have one reason to change.

❌ ProductService that fetches data AND sends emails AND logs to a file
✅ ProductRepository (fetches data), EmailService (sends emails), Logger (logs)

O — Open/Closed: open for extension, closed for modification — add new behavior without editing existing, tested code.

✅ Add a new IProductRepository implementation for a new database,
   without touching ProductsController at all

L — Liskov Substitution: a subtype must be usable anywhere its base type is expected, without breaking things.

I — Interface Segregation: many small, focused interfaces beat one giant one.

❌ IRepository { Get(); Save(); Delete(); SendEmail(); GenerateReport(); }
✅ IProductRepository { GetProductsAsync(); }   <- does one job

D — Dependency Inversion: depend on abstractions (interfaces), not concrete implementations — this is Lesson 5's DI, formalized as a principle.

public ProductsController(IProductRepository repository)  // <- depends on the INTERFACE

📖 In simple words: SOLID = five habits that keep code flexible enough to change safely as requirements evolve, instead of becoming fragile and risky to touch.

🏗 In our project

Every one of these shows up: ProductRepository only fetches product data (S). IProductRepository is a small, focused interface (I). The controller depends on that interface, never the concrete class (D). If RP9 were ever replaced by a different retail system, only a new class implementing IProductRepository would be needed — the controller, validator, and DTOs wouldn't change at all (O).

✅ Key takeaway

SOLID isn't academic theory — it's the concrete set of habits that made every "swap this one piece without breaking everything else" moment in this course possible.


Lesson 35: Clean Architecture

🎯 The problem

If your business logic directly references Oracle-specific code, you can't test that logic without a real database — and you can't easily swap databases later.

💡 The idea

📖 Clean Architecture = organizing your code in concentric layers, where dependencies only point inward, toward your core business logic — the center of the circle knows nothing about databases, web frameworks, or any outside technology.

        ┌─────────────────────────────────────┐
        │   Infrastructure (DB, web, APIs)     │   outermost - depends on everything inside
        │  ┌─────────────────────────────┐    │
        │  │   Application (use cases)    │    │
        │  │  ┌───────────────────────┐  │    │
        │  │  │   Domain (core rules)  │  │    │   innermost - depends on NOTHING outside itself
        │  │  └───────────────────────┘  │    │
        │  └─────────────────────────────┘    │
        └─────────────────────────────────────┘

           Dependencies point INWARD only ──►

The Domain layer defines interfaces (like IProductRepository); the Infrastructure layer implements them (with actual Oracle code). The domain never references Oracle — Oracle-specific code references the domain's interface.

🏗 In our project

IProductRepository (the interface) conceptually belongs to an inner layer; ProductRepository (the Oracle-specific implementation, in Services/) belongs to an outer layer. Our project is small enough that we didn't need separate class libraries for this — but the principle is already there: the controller depends on the interface, never on OracleConnection directly.

✅ Key takeaway

Clean Architecture's core rule — dependencies point inward, toward business logic, never outward toward specific technologies — is what lets you test business rules without a real database, and swap infrastructure without rewriting logic.


Lesson 36: Vertical Slice Architecture

🎯 The problem

Clean Architecture organizes code horizontally, by technical layer (Controllers/, Services/, Data/). For a large app with many unrelated features, this means every single feature is scattered across five different folders — touching "add a new feature" requires editing files in five places.

💡 The idea

📖 Vertical Slice Architecture = organizing code by feature, not by technical layer — everything needed for one feature (its request, handler, validation, response) lives together in one folder.

  HORIZONTAL (by layer)              VERTICAL SLICE (by feature)

  Controllers/                        Features/
    ProductsController.cs               GetProducts/
    OrdersController.cs                    GetProductsHandler.cs
  Services/                                GetProductsValidator.cs
    ProductService.cs                      GetProductsResponse.cs
    OrderService.cs                     CreateOrder/
  Validation/                              CreateOrderHandler.cs
    ProductValidator.cs                    CreateOrderValidator.cs
    OrderValidator.cs                      CreateOrderResponse.cs

The trade-off: horizontal layering makes it easy to see "all my validators in one place," but vertical slices make it easy to see "everything about this ONE feature in one place" — and reduces the risk that changing one feature accidentally affects an unrelated one, since there's less shared code between slices.

🏗 In our project

Our project uses horizontal layering (Controllers/, Services/, Validation/) because it only really has one feature ("get products"). If we added five more unrelated features (orders, customers, inventory adjustments...), a real team would likely reconsider organizing by feature instead, especially if different people work on different features independently.

✅ Key takeaway

Neither horizontal nor vertical organization is universally "correct" — horizontal suits small, cohesive APIs; vertical slices shine as the number of unrelated features grows and team members need to work independently without stepping on each other.


Lesson 37: Domain-Driven Design (DDD) fundamentals

🎯 The problem

A "Store" means something different to your accounting system, your POS system, and your marketing system — but if your code has one giant Store class trying to serve all three, it becomes an unmanageable mess of unrelated concerns.

💡 The idea

📖 Domain-Driven Design (DDD) = an approach to software design centered on deeply modeling the real-world business domain, using the same language the business itself uses — and drawing deliberate boundaries around different parts of that domain.

Key concepts:

  • Ubiquitous language — the code uses the exact same terms the business does. If the business says "subsidiary" and "PLU," your code says SbsNo and Plu too — not generic names like ParentId and Code.
  • Bounded context — a boundary within which a specific model applies. "Store" in the inventory context (has stock, has prices) is a different model than "Store" in the HR context (has employees, has schedules) — even though it's "the same store" in real life.
  • Entity — an object defined by its identity, not its attributes (a specific product, even if its price changes, is still "the same" product).
  • Value Object — an object defined purely by its value, with no identity (a price of $9.99 — two $9.99s are simply equal, there's no "which one" question).
  • Aggregate — a cluster of entities/value objects treated as one consistency unit, with a single "root" controlling access to the whole cluster.

🏗 In our project

Our whole API only cares about the inventory-and-pricing bounded context of a store — it deliberately knows nothing about, say, HR or scheduling data that might also live somewhere in the RP9 database. ProductDto.SiteCode uses RP9's own real term (confirmed with the actual business), not a generic renamed field — that's the ubiquitous language principle in direct practice, and it's exactly why we had those back-and-forth conversations confirming what "site code" really meant to the consuming system.

✅ Key takeaway

DDD's biggest practical lesson: use the business's real vocabulary in your code, and deliberately scope what "an entity" means within a specific boundary — don't try to build one model that serves every possible interpretation at once.


Lesson 38: CQRS (Command Query Responsibility Segregation)

🎯 The problem

A single model trying to handle both "read this efficiently" and "write this safely, with all business rules enforced" often ends up compromised at both jobs.

💡 The idea

📖 CQRS = separating the read side (Queries) from the write side (Commands) of your application, often with entirely different models for each.

  ┌───────────────┐              ┌───────────────┐
  │   COMMANDS     │              │    QUERIES     │
  │  (writes)      │              │   (reads)       │
  │                │              │                 │
  │  "CreateOrder"  │              │  "GetProducts"  │
  │  "UpdatePrice"  │              │  "GetOrderById" │
  │                │              │                 │
  │  Goes through   │              │  Can skip       │
  │  full validation│              │  business rules,│
  │  and business   │              │  read directly, │
  │  rules          │              │  even from a     │
  │                │              │  separate,       │
  │                │              │  read-optimized  │
  │                │              │  data store      │
  └───────────────┘              └───────────────┘

The read side can be simpler, faster, and even use a completely different (denormalized, cache-friendly) data shape than the write side, since it doesn't need to enforce write-time business rules.

🏗 In our project

Our entire API is, in a sense, the "query" half of a CQRS-shaped system — it never writes anything, only reads. The actual RP9 POS terminals handle the "command" side (creating sales, adjusting stock) through their own, completely separate path. We benefit from CQRS's core insight without needing to formally implement the pattern: our read model (ProductDto) is shaped purely for the reader's convenience, with zero obligation to also support writes.

✅ Key takeaway

CQRS's core insight — reads and writes often have fundamentally different needs — is useful even when you don't build a full formal CQRS system: it's the reasoning behind why a read-only API's model can look completely different from its underlying write-side data.


PART 11 — BACKGROUND WORK & DISTRIBUTED DATA

Lesson 39: Background workers

🎯 The problem

Some work shouldn't happen inside a request/response cycle at all — like "sync inventory from RP9 every 5 minutes," which has no HTTP caller waiting for a reply.

💡 The idea

📖 Background worker = a long-running process inside your app that runs independently of any HTTP request — started when the app starts, stopped when it stops.

public class InventorySyncWorker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await SyncInventoryAsync();
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}
builder.Services.AddHostedService<InventorySyncWorker>();  // starts automatically with the app

📖 In simple words:

  • IHostedService/BackgroundService = the built-in .NET pattern for running code that starts alongside your app and runs independently of any individual request.

🏗 In our project

Our API is purely request-driven — every unit of work is triggered by an incoming HTTP call. If we ever needed to, say, pre-warm a cache of frequently-requested products every few minutes, or push a nightly full-catalog export to another system, a BackgroundService would be the natural tool — running inside the same app process, no separate infrastructure required.

✅ Key takeaway

Not all backend work is triggered by a request — background workers handle scheduled or continuous work that runs on its own timeline, hosted inside your same application.


Lesson 40: Messaging and event-driven architecture

🎯 The problem

When "Order Created" needs to trigger five different things (send email, update inventory, notify shipping, update analytics, charge payment), should the order-creation code call all five directly? What happens when a sixth thing needs to react later?

💡 The idea

📖 Message/event-driven architecture = instead of one service directly calling another, a service publishes an event ("this happened") to a message broker, and any number of other services can independently subscribe and react — without the publisher knowing or caring who's listening.

                      ┌──────────────┐
                      │ Email Service │  (subscribes)
                      └──────────────┘
                             ▲
  ┌──────────┐    "Order    │      ┌──────────────┐
  │  Order    │──Created"───┼─────>│ Message Broker│
  │  Service  │   event      │      │  (e.g. RabbitMQ,│
  └──────────┘              │      │   Azure Service │
                             │      │   Bus, Kafka)    │
                             ▼      └──────────────┘
                      ┌──────────────┐
                      │ Inventory     │  (subscribes)
                      │ Service       │
                      └──────────────┘

The Order Service publishes once. Adding a sixth subscriber later requires zero changes to the Order Service — this is the big win: services become decoupled from each other's existence.

📖 In simple words:

  • Message broker = middleware software whose job is reliably delivering messages from publishers to subscribers (RabbitMQ, Kafka, Azure Service Bus are common examples).
  • Event = a message describing "something that already happened" (past tense) — as opposed to a command, which says "please do this."

🏗 In our project

Our API doesn't publish or consume events — it's a simple, synchronous request/response system. But it's worth recognizing: if RP9's own POS system publishes a "SaleCompleted" event whenever a sale happens, a background worker (Lesson 39) could subscribe to that event and use it to know precisely when to refresh cached data (Lesson 44), rather than polling on a fixed timer.

✅ Key takeaway

Event-driven architecture decouples services from directly knowing about each other — a publisher doesn't need to know who (or how many) subscribers exist, making it easy to add new reactions to an event without touching the original code.


Lesson 41: Outbox and Saga patterns

🎯 The problem

Your code saves an order to the database, then tries to publish an "OrderCreated" event. What happens if the app crashes in between those two steps? The order exists, but nobody was ever told.

💡 The idea

The Outbox Pattern — solve "save data + publish event" atomically:

  ❌ RISKY: two separate operations, either can fail independently
     1. INSERT order into database
     2. Publish "OrderCreated" event
     (crash between 1 and 2 = order exists, nobody notified)

  ✅ OUTBOX PATTERN: one atomic transaction
     1. INSERT order  ┐
     2. INSERT into an "Outbox" table  ├─ same DB transaction (Lesson 31) - both or neither
        (a record saying "publish this event")  ┘
     3. A SEPARATE background worker (Lesson 39) reads the Outbox table
        and reliably publishes events, retrying until it succeeds

📖 Outbox pattern = writing "I need to send this event" into the same database transaction as your actual data change, then having a separate process reliably deliver it — guaranteeing the event is never silently lost, even if the app crashes right after saving.

The Saga Pattern — coordinating a multi-step process across multiple services, where a traditional single database transaction isn't possible:

  Book a trip = Reserve Flight + Reserve Hotel + Charge Payment
  (three different services, three different databases)

  If "Charge Payment" fails AFTER flight and hotel were reserved:
  A Saga runs COMPENSATING actions in reverse:
    → Cancel hotel reservation
    → Cancel flight reservation
  (undoing what succeeded, since we can't "rollback" across separate databases)

📖 Saga pattern = managing a multi-step process across multiple independent services by defining a compensating (undo) action for each step, since a single all-or-nothing transaction isn't possible across separate systems.

🏗 In our project

We don't need either pattern — we have one database and no cross-service writes. But this is exactly the kind of problem RP9's own POS system likely has to solve internally: "record a sale" probably needs to atomically update inventory, record the transaction, and eventually sync to a central system — the Outbox pattern is the standard, battle-tested answer to that exact category of problem.

✅ Key takeaway

The Outbox pattern guarantees an event is never lost between "save data" and "notify others," even across a crash. The Saga pattern extends that guarantee across multiple independent services, using compensating actions instead of a single transaction.


PART 12 — ADVANCED SECURITY

Lesson 42: JWT, OAuth 2.0, and OpenID Connect

🎯 The problem

API keys (Lesson 15) work great for identifying systems. But what about identifying an actual human user, who needs to log in, and whose access might need to expire, carry specific permissions, or be verified by a separate identity provider (like "Sign in with Google")?

💡 The idea — three related but distinct things

JWT (JSON Web Token) — a compact, self-contained token that carries claims (facts about the user) and is cryptographically signed:

  header.payload.signature

  Decoded payload might contain:
  { "sub": "user123", "role": "admin", "exp": 1735689600 }

📖 JWT = a signed token containing claims about a user, which a server can verify without needing to look anything up in a database — the signature alone proves it hasn't been tampered with.

OAuth 2.0 — a framework for delegated authorization: "let App X access my data on Service Y, without giving App X my Service Y password."

  You → "Sign in with Google" → Google asks "allow this app to see your email?"
  → You approve → App receives a token, NEVER sees your Google password

📖 OAuth 2.0 = a standard protocol letting one application access resources on your behalf, on another service, without ever handling your password for that service.

OpenID Connect (OIDC) — a thin authentication layer built on top of OAuth 2.0 (which is really an authorization framework):

  OAuth 2.0 alone:  "this app CAN access your data" (authorization)
  OpenID Connect:   "...AND here's proof of WHO you are" (authentication, added on top)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.Authority = "https://your-identity-provider.com";
        options.Audience = "your-api";
    });

🏗 In our project

We deliberately used API keys, not JWT/OAuth — because our consumers are systems (POS middleware, an e-commerce backend), not individual human users needing to log in. Our .csproj actually already references Microsoft.AspNetCore.Authentication.JwtBearer, anticipating that a future version of this API might need to authenticate individual human users (say, a store manager's dashboard) — at which point JWT/OIDC would be the right addition, likely running alongside the existing API key scheme rather than replacing it.

✅ Key takeaway

API keys authenticate systems; JWT/OAuth/OIDC authenticate — and delegate access for — actual human users, especially when a separate identity provider should be the one verifying who they are.


Lesson 43: Advanced API security

🎯 The problem

Beyond "use API keys and parameterize your SQL" (Book 1's security lessons), production APIs face a wider range of real threats.

💡 The idea — key concepts

Scopes and least privilege — an authenticated caller shouldn't automatically get access to everything:

  API Key A: scope = "read:products"           <- can only read products
  API Key B: scope = "read:products write:orders"  <- can also create orders

📖 Scope = a specific, named permission a token or key carries — letting you grant fine-grained access instead of all-or-nothing.

Secrets rotation — credentials shouldn't live forever unchanged:

  Old key issued  →  New key issued (both valid briefly)  →  Old key revoked
  (a "grace period" avoids breaking consumers mid-rotation)

mTLS (mutual TLS) — beyond normal HTTPS (which proves the server's identity to the client), mTLS also proves the client's identity to the server, using a client certificate — common in high-security service-to-service communication.

The OWASP API Security Top 10 — a well-known, regularly-updated list of the most common real-world API vulnerabilities, worth knowing by name:

  • Broken Object Level Authorization (a user can access another user's data just by changing an ID in the URL)
  • Broken Authentication
  • Excessive Data Exposure (returning more fields than the consumer actually needs — exactly why DTOs, Lesson 11, matter)
  • Lack of Rate Limiting (Book 1, Lesson 18)
  • Security Misconfiguration (leaving debug endpoints, verbose errors, or default credentials exposed in production)

🏗 In our project

We don't yet implement scopes — every valid API key has identical access to every endpoint. In a larger, multi-consumer system, giving the e-commerce backend "read-only, products-only" access while a future inventory-management tool gets broader access would be the natural next step, directly applying the least-privilege principle. Our ExceptionHandlingMiddleware (Book 1, Lesson 19) also directly defends against "Excessive Data Exposure" — by design, it never leaks internal detail to callers.

✅ Key takeaway

Real API security goes beyond "add authentication" — least privilege (scopes), credential rotation, and awareness of common vulnerability categories (like OWASP's API Top 10) are what separate a functional API from a genuinely production-hardened one.


PART 13 — PERFORMANCE & RESILIENCE

Lesson 44: Caching and Redis

🎯 The problem

The same "list of products for store ST001" gets requested a thousand times a minute, and each time you're hitting Oracle for an answer that hasn't actually changed in hours.

💡 The idea

📖 Cache = a fast, temporary storage layer that holds a copy of data so repeat requests can be answered without redoing the expensive work.

  WITHOUT cache:                       WITH cache:

  Request → Database (slow, every time) Request → Cache? ─Yes─> instant reply
                                                     │
                                                    No
                                                     ▼
                                              Database (slow) → save to cache → reply

In-memory cache — fastest, but lives only inside one server instance:

builder.Services.AddMemoryCache();
// ...
if (!_cache.TryGetValue(key, out var products)) {
    products = await _repository.GetProductsAsync(query);
    _cache.Set(key, products, TimeSpan.FromMinutes(5));
}

Redis — a shared, external cache, so multiple server instances all see the same cached data:

  Server A ──┐
             ├──> Redis (shared cache) ──> all servers see the SAME cached data
  Server B ──┘

📖 Redis = a popular, extremely fast in-memory data store, commonly used as a shared cache across multiple application instances.

The hard part: cache invalidation. When underlying data changes, stale cached data must be cleared or updated — famously one of the trickiest problems in software ("there are only two hard things in computer science: cache invalidation and naming things").

🏗 In our project

We don't cache — every request goes straight to Oracle. This was a reasonable choice for this stage: the query isn't yet a proven bottleneck, and stock/price data changes frequently enough that caching would need careful invalidation (probably a short TTL, like 30–60 seconds, rather than caching indefinitely) to avoid serving stale stock counts. If load testing later showed the database becoming a bottleneck, a short-TTL cache on GetProductsAsync results would be a natural, high-value addition.

✅ Key takeaway

Caching trades a small risk of slightly-stale data for a large performance win — the key design decision is always "how stale is acceptable, and how do I invalidate it when it isn't?"


Lesson 45: HttpClientFactory

🎯 The problem

Your API needs to call another API (maybe a payment gateway, or a shipping service). Creating a new HttpClient() for every request is a famous, well-documented way to exhaust your server's network sockets.

💡 The idea

📖 HttpClientFactory = a built-in .NET service that manages the creation and pooling of HttpClient instances correctly, avoiding the socket-exhaustion problem of manually new-ing them up.

// Program.cs (build phase)
builder.Services.AddHttpClient("PaymentGateway", client => {
    client.BaseAddress = new Uri("https://payments.example.com");
    client.Timeout = TimeSpan.FromSeconds(10);
});
// Usage - injected, not manually created
public class PaymentService
{
    private readonly HttpClient _client;
    public PaymentService(IHttpClientFactory factory) => _client = factory.CreateClient("PaymentGateway");
}

🏗 In our project

We don't call any external HTTP APIs — our only external dependency is Oracle, via a database driver, not HTTP. But this is exactly the tool you'd reach for the moment your API needs to call, say, a shipping-rate API or a tax-calculation service — and it pairs directly with the next lesson.

✅ Key takeaway

Always use HttpClientFactory (never manually new HttpClient() per request) when your API calls other HTTP services — it's the standard, correct way to manage outbound HTTP connections in .NET.


Lesson 46: Resilience — retry and circuit breaker

🎯 The problem

A downstream service you depend on has a brief hiccup. Should your API immediately fail every request that touches it? Should it retry forever, making the hiccup worse by hammering an already-struggling service?

💡 The idea

Retry — automatically try again on transient (temporary) failures:

// Using Polly, the standard .NET resilience library
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
    // retry 3 times, waiting 2s, then 4s, then 8s ("exponential backoff")

📖 Exponential backoff = waiting progressively longer between each retry attempt, so repeated retries don't hammer an already-struggling service.

Circuit breaker — stop trying entirely, for a while, once failures cross a threshold:

   CLOSED (normal)  ──── too many failures ────>  OPEN (stop trying entirely)
       ▲                                                  │
       │                                            after a cooldown
       │                                                  ▼
       └──────── success ────── HALF-OPEN (try one test request) 

📖 Circuit breaker = a pattern that stops sending requests to a failing dependency for a cooldown period, instead of retrying uselessly — giving the failing service room to recover, and failing fast for your own callers instead of hanging.

var circuitBreaker = Policy
    .Handle<HttpRequestException>()
    .CircuitBreakerAsync(handledEventsAllowedBeforeBreaking: 5, durationOfBreak: TimeSpan.FromSeconds(30));

🏗 In our project

Our own database connection pooling (Min Pool Size/Max Pool Size in the connection string) provides a basic layer of resilience against connection exhaustion, but we don't currently implement retry or circuit breaker logic around the Oracle calls themselves. A production-hardened version would likely wrap the Dapper calls in a retry policy for transient network blips (like the ORA-12541 errors we debugged earlier in this project!) — while being careful not to retry on errors that won't resolve by retrying, like ORA-00942 (missing table) or ORA-01017 (bad credentials).

✅ Key takeaway

Retry handles brief, transient failures gracefully; a circuit breaker prevents making a struggling dependency worse by stopping requests entirely for a cooldown period — knowing which failures are worth retrying (network blips) versus not (bad credentials) is the key judgment call.


Lesson 47: Idempotency

🎯 The problem

A client sends "create this order," the response times out (but the order was actually created), and the client — not knowing it succeeded — retries. Now there are two orders.

💡 The idea

📖 Idempotency (revisited from Lesson 28, now at the design level) = ensuring an operation produces the same result no matter how many times it's repeated — critical for safely handling retries.

Idempotency keys — the standard pattern for making non-idempotent operations (like POST) safely retryable:

  Client generates a unique key BEFORE sending:
  POST /orders
  Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

  Server remembers: "I already processed this exact key"
  → if the SAME key arrives again, return the ORIGINAL response,
    without creating a second order
if (await _idempotencyStore.HasProcessedAsync(idempotencyKey))
    return await _idempotencyStore.GetStoredResponseAsync(idempotencyKey);

var result = await ProcessOrderAsync(request);
await _idempotencyStore.StoreAsync(idempotencyKey, result);
return result;

🏗 In our project

Every endpoint we have is GET, which is naturally idempotent — repeating a GET never creates duplicate data, so we don't need this pattern at all right now. This becomes essential the moment this API (or a sibling API) adds any POST endpoint — like "record a stock adjustment" — where a network retry must not silently double-apply the change.

✅ Key takeaway

Idempotency keys are how you make "unsafe to repeat" operations (like POST) safely retryable — an essential pattern the moment your API does anything beyond reading data.


PART 14 — TESTING & OBSERVABILITY

Lesson 48: Unit, integration, and contract testing

🎯 The problem

How do you know a code change didn't silently break something, without manually re-testing your entire API by hand every single time?

💡 The idea — three levels of testing, each answering a different question

       ▲  Fewer, slower, broader
       │
       │  ┌─────────────────────┐
       │  │  Contract tests       │  "does my API still match what
       │  │                       │   consumers expect?"
       │  ├─────────────────────┤
       │  │  Integration tests    │  "do my layers work together
       │  │                       │   correctly (real DB, real HTTP)?"
       │  ├─────────────────────┤
       │  │  Unit tests           │  "does this ONE piece of logic
       │  │                       │   work correctly, in isolation?"
       │  └─────────────────────┘
       │
       ▼  More, faster, narrower

Unit tests — test one piece of logic in complete isolation, with all dependencies faked:

[Fact]
public void Status_ShouldBeInStock_WhenAvailableQtyPositive()
{
    var result = StockCalculator.GetStatus(availableQty: 5);
    Assert.Equal(1, result);
}

Integration tests — test multiple real layers working together (often a real, disposable test database):

[Fact]
public async Task GetProductsAsync_ReturnsRealData_FromTestDatabase()
{
    var repository = new ProductRepository(realTestConnectionFactory, ...);
    var result = await repository.GetProductsAsync(new ProductQuery { SbsNo = 1 });
    Assert.NotEmpty(result.Items);
}

Contract tests — verify your API's shape (the actual JSON contract) hasn't accidentally changed in a way that breaks consumers — often automated by comparing against a saved schema or recorded example.

📖 In simple words:

  • Mock/fake = a fake stand-in for a real dependency (like a fake IProductRepository that returns known test data instead of hitting a real database), used to isolate what you're actually testing.
  • Test pyramid = the general guidance that you should have many fast unit tests, fewer integration tests, and fewer still contract/end-to-end tests — because slower, broader tests cost more to run and maintain.

🏗 In our project

This exact project is genuinely IProductRepository-shaped specifically because that interface makes unit testing ProductsController possible without a real Oracle database — you could inject a fake repository returning known ProductDto objects and test the controller's validation/error-handling logic in complete isolation, in milliseconds, with zero database dependency.

✅ Key takeaway

Unit tests check logic in isolation (fast, many); integration tests check real layers working together (slower, fewer); contract tests guard your API's public shape against accidental breaking changes.


Lesson 49: Metrics, tracing, and OpenTelemetry

🎯 The problem

Your API is slow somewhere, in production, right now — but "somewhere" isn't good enough to fix it. Is it the database? A specific endpoint? A specific downstream call?

💡 The idea — the three pillars of observability

Logs (Book 1, Lesson 19) — discrete events: "this happened, at this time."

Metrics — numbers tracked over time, good for dashboards and alerts:

  requests_per_second, average_response_time_ms, error_rate_percent, active_db_connections

📖 Metric = a numeric measurement tracked over time — answers "how much/how many/how fast," in aggregate, rather than describing one specific event.

Traces — following a single request's full journey across every layer (and every service, in a distributed system):

  Request abc123
  ├─ ProductsController.Get()              [2ms]
  │  └─ ProductRepository.GetProductsAsync() [340ms]
  │     └─ Oracle query execution           [335ms]  <- THIS is where the time went

📖 Trace = a detailed record of one request's full path and timing across every component it touched — the tool that turns "it's slow" into "it's slow specifically HERE."

OpenTelemetry — the modern, vendor-neutral standard for collecting all three (logs, metrics, traces) consistently, so you can send them to any compatible backend (Grafana, Application Insights, Datadog, etc.) without rewriting your instrumentation:

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddAspNetCoreInstrumentation().AddSource("RetailPro9.ProductApi"))
    .WithMetrics(m => m.AddAspNetCoreInstrumentation());

🏗 In our project

We have logs (Serilog), but not yet metrics or distributed tracing. If this API were under real production load, adding OpenTelemetry tracing would immediately answer questions our current logs can only hint at — like "is our slow response time coming from the Oracle query itself, or from something else in the pipeline?" (exactly the kind of question we had to answer manually, by reading exception stack traces, throughout this project's real debugging sessions).

✅ Key takeaway

Logs tell you what happened; metrics tell you how much/how often, in aggregate; traces tell you exactly where time was spent for one specific request — OpenTelemetry is the modern standard for capturing all three consistently.


Lesson 50: Health checks — a deeper look

🎯 The problem

Book 1 introduced /health as "can this instance do its job." At scale, health checks power real, automated decisions.

💡 The idea

A liveness check ("is the process even running?") is different from a readiness check ("is it ready to accept real traffic right now?") — a distinction that matters a lot in orchestrated environments (Lesson 54).

builder.Services.AddHealthChecks()
    .AddOracle(connectionString, name: "oracle-rp9", tags: new[] { "ready" })
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" });

app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = check => check.Tags.Contains("live") });
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") });

A load balancer or orchestrator uses readiness to decide "should I send this instance traffic right now?" — and uses liveness to decide "should I restart this instance entirely?" These are genuinely different questions with different consequences if answered wrong.

🏗 In our project

Our single /health endpoint (checking real Oracle connectivity) currently blends both concerns. In a larger deployment — especially behind a load balancer or in Kubernetes (Lesson 54) — splitting it into separate liveness (/health/live: "is the process alive?") and readiness (/health/ready: "can it reach Oracle right now?") checks would let infrastructure make more precise decisions, like temporarily routing traffic away from an instance with a flaky DB connection without restarting the whole process.

✅ Key takeaway

Liveness answers "should this be restarted?"; readiness answers "should this receive traffic right now?" — conflating them means infrastructure can't make the right decision for the right problem.


PART 15 — DEVOPS & INFRASTRUCTURE AT SCALE

Lesson 51: Docker

🎯 The problem

"It works on my machine" is the most infamous phrase in software — differences in installed runtime versions, OS, or configuration between machines cause real, painful bugs.

💡 The idea

📖 Docker / Container = a way to package your app and everything it needs to run (runtime, dependencies, configuration) into one portable unit that runs identically anywhere Docker is installed.

  ┌─────────────────────────────────┐
  │  Container image                 │
  │  ┌────────────────────────────┐ │
  │  │  Your app (.dll)            │ │
  │  │  .NET 8 runtime               │ │
  │  │  OS libraries                 │ │
  │  └────────────────────────────┘ │
  └─────────────────────────────────┘
       runs IDENTICALLY on your laptop, a colleague's laptop,
       a test server, and production — same image, every time
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
COPY . .
RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY --from=build /app .
ENTRYPOINT ["dotnet", "RetailPro9.ProductApi.dll"]
docker build -t my-api .
docker run -p 8080:8080 my-api

📖 In simple words:

  • Image = the packaged, reusable blueprint (built once).
  • Container = a running instance of an image (you can run many containers from one image).

🏗 In our project

We actually built a Docker deployment path for this exact project earlier — then deliberately moved away from it when the real deployment need turned out to be "run directly on the same Windows machine as RetailPro9, via IIS/Windows Service" instead. This is a genuine, common real-world decision: Docker is extremely valuable for consistency across environments, but it's not automatically the right choice for every single deployment scenario — sometimes a simpler, more direct deployment matches the actual infrastructure better.

✅ Key takeaway

Docker solves "works on my machine" by packaging the app with everything it needs — but like every tool in this course, it's a deliberate choice, not a default you apply everywhere without thinking about the actual deployment target.


Lesson 52: Load balancing

🎯 The problem

One server instance can only handle so much traffic. And if that one instance goes down, your entire API goes down with it.

💡 The idea

📖 Load balancer = a component that sits in front of multiple identical server instances and distributes incoming requests across them.

                          ┌──> Instance A
  Clients ──> Load        │
              Balancer ───┼──> Instance B
                          │
                          └──> Instance C

  If Instance B fails its health check (Lesson 50),
  the load balancer stops sending it traffic automatically.

This gives you two things at once: horizontal scaling (handle more traffic by adding more instances) and high availability (one instance failing doesn't take down the whole service).

📖 In simple words:

  • Horizontal scaling = handling more load by adding more machines/instances, as opposed to vertical scaling (making one machine bigger/more powerful).

🏗 In our project

Right now we run as a single instance. If this API needed to handle significantly more traffic (or needed to survive one server going down), the natural next step is running multiple identical instances behind a load balancer — which is exactly why we made ProductRepository stateless and used AddScoped/connection pooling correctly (Lessons 5, 26): a stateless app is what makes horizontal scaling behind a load balancer actually safe. If we'd stored any per-user state in memory, adding a second instance would silently break things, since requests could land on either instance unpredictably.

✅ Key takeaway

Load balancing enables both handling more traffic and surviving individual instance failures — but it only works safely if your app is stateless, which is why "avoid shared mutable state" (Lesson 27) matters even more once you're scaling horizontally.


Lesson 53: CI/CD

🎯 The problem

Manually building, testing, and deploying code every time is slow, error-prone, and inconsistent between team members.

💡 The idea

📖 CI (Continuous Integration) = automatically building and testing your code every time someone pushes a change, catching problems immediately instead of days later.

📖 CD (Continuous Deployment/Delivery) = automatically deploying code that passes CI, either straight to production (Deployment) or to a staging environment awaiting approval (Delivery).

  Developer pushes code
        │
        ▼
  ┌───────────────────────────────────────────────┐
  │  CI PIPELINE (runs automatically)               │
  │  1. Build the code   →  fails? stop here.       │
  │  2. Run unit tests    →  fails? stop here.       │
  │  3. Run integration tests → fails? stop here.     │
  └───────────────────────────────────────────────┘
        │  all passed
        ▼
  ┌───────────────────────────────────────────────┐
  │  CD PIPELINE                                    │
  │  4. Build Docker image / publish artifact        │
  │  5. Deploy to staging                             │
  │  6. (optional manual approval)                     │
  │  7. Deploy to production                            │
  └───────────────────────────────────────────────┘

A simple GitHub Actions example:

on: [push]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: dotnet build
      - run: dotnet test

🏗 In our project

Every fix throughout this actual project — the InvariantGlobalization bug, the Swagger security requirement fix, the siteCode mapping change — was manually rebuilt and manually redeployed by you, in Visual Studio, tested by hand against a real database. A CI/CD pipeline would have automatically caught the compile error the moment it was pushed, and (with proper integration tests) could have caught some of these issues before they ever reached a manual test session.

✅ Key takeaway

CI catches problems immediately, automatically, on every change; CD automates getting validated code safely into production — together they replace slow, manual, inconsistent deployment with a fast, repeatable, trustworthy process.


Lesson 54: Kubernetes concepts

🎯 The problem

You have many containers (Lesson 51), across many machines, that need to be started, restarted on failure, scaled up and down, and load-balanced — doing this by hand doesn't work past a handful of instances.

💡 The idea

📖 Kubernetes (K8s) = a system for automatically deploying, scaling, healing, and managing containerized applications across a cluster of machines.

Key building blocks:

  Pod        = the smallest deployable unit - one or more containers running together
  Deployment = "I want 3 replicas of this Pod, always" - K8s keeps that promise,
               automatically replacing any Pod that dies
  Service    = a stable network address that load-balances across a Deployment's Pods,
               even as individual Pods are replaced
  ConfigMap / Secret = externalized configuration and secrets, injected into Pods
               as environment variables (exactly like our RP9_DB_HOST pattern!)
  ┌─────────────────────────── Kubernetes Cluster ───────────────────────────┐
  │                                                                          │
  │   Deployment: "retailpro9-product-api, 3 replicas"                       │
  │   ┌────────┐   ┌────────┐   ┌────────┐                                  │
  │   │ Pod 1  │   │ Pod 2  │   │ Pod 3  │   <- K8s automatically restarts    │
  │   └────────┘   └────────┘   └────────┘      any Pod that fails health    │
  │        ▲             ▲            ▲          checks (Lesson 50!)          │
  │        └─────────────┼────────────┘                                     │
  │                 Service (stable internal address,                        │
  │                  load-balances across all 3 Pods)                        │
  └──────────────────────────────────────────────────────────────────────────┘

🏗 In our project

This project deliberately runs as a plain Windows Service on a single machine — appropriately simple for its actual scale and requirements. But notice how much of what we already built maps directly onto Kubernetes concepts if it ever needed to grow: our /health endpoint is exactly what K8s uses to decide whether to restart a Pod; our environment-variable-driven configuration (RP9_DB_HOST, RP9_API_KEYS) is exactly the pattern K8s ConfigMaps/Secrets are designed to inject.

✅ Key takeaway

Kubernetes automates what you'd otherwise do by hand across many servers — restarting failed instances, load balancing, and injecting configuration — and it rewards apps that are already stateless and configuration-driven, exactly like the habits this whole course has been building toward.


PART 16 — DISTRIBUTED SYSTEMS & MODERN API STYLES

Lesson 55: Microservices

🎯 The problem

One giant application ("monolith") handling products, orders, payments, shipping, and customers together becomes slow to build on, risky to deploy (one bug anywhere can break everything), and hard for large teams to work on simultaneously.

💡 The idea

📖 Microservices = an architecture where an application is split into multiple small, independently deployable services, each owning one specific business capability.

   MONOLITH                          MICROSERVICES

  ┌───────────────────┐            ┌──────────┐ ┌──────────┐
  │  One big app        │            │ Products  │ │ Orders    │
  │  (products, orders,  │            │ Service   │ │ Service    │
  │  payments, shipping) │    vs.     └──────────┘ └──────────┘
  │                      │            ┌──────────┐ ┌──────────┐
  │  ONE deployment,     │            │ Payments  │ │ Shipping  │
  │  ONE database          │            │ Service   │ │ Service    │
  └───────────────────┘            └──────────┘ └──────────┘
                                     each with its OWN database,
                                     deployed and scaled independently

The trade-off is real: microservices trade simplicity for independence. You gain independent deployment and scaling, but you now have network calls (and everything from Part 13 — retries, circuit breakers) where you used to have a simple in-process method call, plus the data-consistency challenges from Lesson 41 (Saga pattern).

🏗 In our project

Our RetailPro9 Product API is, itself, a microservice — a small, independently deployable service owning exactly one responsibility ("answer questions about product availability and pricing"), separate from RP9's own POS logic, separate from any e-commerce backend, separate from any future orders service. It doesn't try to do everything — that focus is the whole point.

✅ Key takeaway

Microservices trade a monolith's simplicity for independent deployability and scaling — worthwhile once a system and team have grown large enough that the coordination overhead is worth it, and not a default you reach for on day one.


Lesson 56: API Gateway

🎯 The problem

If you have ten microservices, should every client need to know all ten different addresses, and separately implement auth/rate-limiting/logging for each one?

💡 The idea

📖 API Gateway = a single entry point that sits in front of multiple backend services, routing requests to the right one — often also handling cross-cutting concerns (auth, rate limiting, logging) in one place instead of duplicating them in every service.

                              ┌──> Products Service
  Clients ──> API Gateway ────┼──> Orders Service
              (auth, rate      │
               limiting,        └──> Payments Service
               routing, all
               in one place)

🏗 In our project

Right now, consumers call our Product API directly — appropriate while it's the only (or one of very few) services involved. If a real e-commerce platform eventually had ten backend services (products, orders, customers, shipping...), an API Gateway sitting in front of all of them would let a mobile app talk to one single, stable address, with the gateway handling auth centrally (rather than every one of the ten services separately implementing our ApiKeyAuthHandler from Lesson 15) and routing requests to whichever service actually owns that data.

✅ Key takeaway

An API Gateway centralizes cross-cutting concerns (auth, routing, rate limiting) across multiple services, so clients deal with one stable entry point instead of needing to know about every individual service.


Lesson 57: gRPC

🎯 The problem

REST/JSON over HTTP is great for broad compatibility (any client, any language, human-readable) — but for high-performance, service-to-service communication where every millisecond counts, its text-based format and lack of a strict contract have real costs.

💡 The idea

📖 gRPC = a high-performance framework for service-to-service communication, using a compact binary format (Protocol Buffers) and a strict, code-generated contract, instead of loose JSON.

  REST/JSON                              gRPC

  Human-readable text                     Compact binary format (smaller, faster)
  { "plu": "123", "price": 9.99 }         (binary bytes - not human-readable)

  Contract: loosely implied by            Contract: STRICTLY defined in a .proto
  your DTOs (nothing stops drift)          file, and code is GENERATED from it -
                                            client and server literally cannot
                                            disagree about the shape
// products.proto - the strict contract, shared by client and server
service ProductService {
  rpc GetProduct (GetProductRequest) returns (ProductReply);
}
message ProductReply {
  string plu = 1;
  double price = 2;
}

When to choose which: REST/JSON for public-facing APIs (broad compatibility, human-debuggable, works everywhere including a browser). gRPC for internal, service-to-service calls where performance matters and both ends are under your control (so you can regenerate client/server code together when the contract changes).

🏗 In our project

We use REST/JSON — the right choice here, since our consumers include external, possibly browser-facing systems (an e-commerce backend) where broad compatibility and human-debuggability (you could literally read our error responses in a browser throughout this whole project) mattered more than shaving milliseconds off response time.

✅ Key takeaway

gRPC trades REST/JSON's universal compatibility and readability for raw performance and a strictly enforced contract — the right tool specifically for internal, high-throughput service-to-service communication, not general-purpose public APIs.


Lesson 58: SignalR

🎯 The problem

A stock dashboard needs to update the moment inventory changes — but the client shouldn't have to repeatedly ask "has anything changed yet?" (polling) every few seconds.

💡 The idea

📖 SignalR = a .NET library for real-time, bidirectional communication between server and client — the server can push updates to connected clients the instant something happens, instead of the client having to keep asking.

  WITHOUT SignalR (polling):              WITH SignalR (push):

  Client: "anything new?" → No             Server: [stock changes]
  Client: "anything new?" → No                    ↓ pushes update immediately
  Client: "anything new?" → Yes!  (delay!)  Client: receives update INSTANTLY
  (wastes requests, and is slow to notice)
public class InventoryHub : Hub
{
    public async Task NotifyStockChanged(string plu, int newQty)
        => await Clients.All.SendAsync("StockChanged", plu, newQty);
}

🏗 In our project

Our API is a classic request/response model — a caller asks, we answer, done. If we later built a live "store manager dashboard" that needed to show stock levels updating in real time as sales happen, SignalR (likely combined with the event-driven pattern from Lesson 40 — an "InventoryChanged" event triggering a SignalR push) would be the natural tool, rather than having the dashboard poll our /api/v1/products endpoint every few seconds.

✅ Key takeaway

SignalR enables the server to push updates to clients instantly, replacing inefficient, laggy polling — the standard .NET tool for real-time features like live dashboards or notifications.


Lesson 59: Webhooks

🎯 The problem

A third-party payment provider needs to tell your system "this payment succeeded" — but they can't call a method in your running process; they need an HTTP-based way to notify you.

💡 The idea

📖 Webhook = a way for one system to notify another about an event by making an HTTP POST request to a URL you provide — essentially "a callback, over HTTP." It's the inverse direction of a normal API call: instead of you asking them, they tell you.

  Normal API call:                  Webhook:

  You → "any updates?" → Them        Them → "here's an update!" → You
  (you have to keep asking)          (they tell you the moment it happens)
[HttpPost("webhooks/payment-completed")]
public async Task<IActionResult> HandlePaymentWebhook([FromBody] PaymentWebhookPayload payload)
{
    // IMPORTANT: verify the request genuinely came from the real provider
    // (usually via a signature header), never trust a webhook body blindly
    if (!VerifySignature(Request, payload)) return Unauthorized();

    await ProcessPaymentAsync(payload);
    return Ok();
}

🏗 In our project

We don't receive or send webhooks — our API is purely a request/response service. But this is the natural next tool the moment an external system (a payment provider, a shipping carrier) needs to notify us about something asynchronously — and it's worth connecting back to Lesson 47 (Idempotency): webhook providers commonly retry delivery if they don't get a fast 200 response, so a webhook receiver needs the exact same "don't double-process a repeated delivery" thinking we discussed there.

✅ Key takeaway

Webhooks flip the normal API direction — instead of polling for updates, external systems push events to a URL you provide — always verify the sender's signature, and always handle the fact that webhook deliveries can be retried.


THE FINAL CAPSTONE: YOU ARE NOW AN ARCHITECT 🏆

Everything, together

You started this course not knowing what "backend" meant. Here's the full map of what you now understand — 59 lessons, two books:

BOOK 1: BACKEND DEVELOPER                BOOK 2: ARCHITECT
─────────────────────────                ─────────────────────────
✅ HTTP & REST basics                     ✅ Modern C# (generics, async, GC)
✅ Project structure & layers              ✅ Deep HTTP, middleware internals
✅ Dependency Injection                    ✅ SQL depth, advanced EF Core/Dapper
✅ Dapper, EF Core, LINQ                   ✅ SOLID, Clean/Vertical/DDD, CQRS
✅ Controllers, DTOs, validation           ✅ Background workers, messaging, Sagas
✅ Pagination                              ✅ JWT/OAuth/OIDC, advanced security
✅ Authentication & authorization          ✅ Caching, resilience, idempotency
✅ API keys, common security threats       ✅ Testing pyramid, OpenTelemetry
✅ CORS, rate limiting                     ✅ Docker, load balancing, CI/CD, K8s
✅ Error handling, logging, Swagger        ✅ Microservices, gateways, gRPC,
✅ Deployment                                 SignalR, webhooks

How a real, larger system uses ALL of this at once

Imagine RetailPro9's product data needs to power a full e-commerce platform. Here's how everything in this course composes:

                         [ API Gateway ]  (L56)
                                │
          ┌─────────────────────┼─────────────────────┐
          ▼                     ▼                     ▼
   [Products Service]    [Orders Service]      [Payments Service]
   (OUR API — this        (CQRS: commands       (calls external
    exact project,          write orders,         payment gRPC
    a microservice, L55)    queries read them,     service, L57,
                            L38)                    with retry +
          │                      │                  circuit breaker,
          ▼                      ▼                  L46)
   [Oracle: RP9 data]     [Outbox table]                 │
   (indexed, L31)         (L41) ──> [Message Broker] <────┘
          │                              │        (L40)
          ▼                              ▼
   [Redis cache]                 [Background Worker]  (L39)
   (L44)                          subscribes, updates
                                   [SignalR Hub] (L58)
                                        │
                                        ▼
                              Live dashboard updates
                              pushed to connected clients

  Running across:  Docker containers (L51) → Kubernetes (L54) →
  load balanced (L52) → deployed via CI/CD (L53) →
  observed with OpenTelemetry traces/metrics (L49) →
  tested at every level before deployment (L48)

Every box in that diagram is a lesson you've now completed. That's not a coincidence — it's the actual shape of how real, large .NET systems are built, and you can now recognize, explain, and reason about every single piece of it.

Where to go from here

  1. Pick ONE Book 2 topic and build a tiny, real proof-of-concept — a background worker, a Redis cache, a Dockerized version of an API. Reading explains the shape; building is what makes it stick.
  2. Read this project's ProductRepository.cs again, now with Book 2 eyes — you'll notice things (the connection pooling settings, the using statement, the async/await threading) that were invisible to you before.
  3. When you join or study a real production system, use this course's structure as a checklist: "where's their DI? Their caching? Their resilience policies? Their observability?" You now know exactly what questions to ask.
  4. Keep both glossaries (Book 1 and Book 2) as your personal reference. Every 📖 term across these 59 lessons is something you'll hear in real system-design interviews and real architecture discussions — you now have a plain-English, example-grounded answer for all of them.

You are, genuinely, no longer a beginner. Go build something real.

Comments

Popular posts from this blog

كونات وأجزاء عفشة السيارة كاملة

[Security]THE NEOPHYTE'S GUIDE TO HACKING

ترصيص عجل السيارات ومنع الاهتزاز