Del 1 av 10

Helhetsarkitekturen — Blazor, CQRS & Event Sourcing

En översikt över hur DDD-aggregat, CQRS-commands, Event Sourcing via SqlStreamStore på MSSQL och en Blazor WebAssembly-klient hänger ihop till ett komplett system i .NET 10.

Lektion 1 — V1 mån. Vi sätter den mentala kartan: vad bygger vi, varför, och i vilken ordning kommer pusselbitarna in under kursen?

Varför just denna kombination?

Domain-Driven Design (DDD), Command Query Responsibility Segregation (CQRS) och Event Sourcing (ES) är tre olika mönster som ofta omnämns tillsammans, men de är fristående. Du kan göra DDD utan CQRS, CQRS utan ES, och ES utan DDD. När de kombineras uppstår dock en arkitektur där varje del förstärker de andra:

Systemet vi bygger

Genom kursen växer en lösning fram med fem projekt:

src/
├── Domain/          # Aggregat, värdesobjekt, domänhändelser (ren C#, inga beroenden)
├── Application/     # Commands, queries, handlers, validering (MediatR)
├── Api/             # ASP.NET Core Minimal API — exponerar HTTP-endpoints
├── Projections/     # BackgroundService som bygger read models från event-strömmen
└── Web/             # Blazor WebAssembly-klient
tests/
├── Domain.Tests/
├── Application.Tests/
└── Integration.Tests/  # Testcontainers + MSSQL

Dataflödet i en skrivning

Följ en användare som sätter in 500 kr på sitt konto:

┌──────────────┐ POST /accounts/{id}/deposit ┌─────────────────┐ │ Blazor WASM │ ───────────────────────────────────► │ Minimal API │ └──────────────┘ { amount: 500, idempKey: ... } └────────┬────────┘ ▲ │ MediatR.Send │ 202 Accepted ▼ │ ┌─────────────────┐ │ │ DepositHandler │ │ └────────┬────────┘ │ │ 1. Load(streamId) │ ▼ │ ┌─────────────────┐ │ │ Account aggregate│ │ │ .Deposit(500) │ ──► raises MoneyDeposited │ └────────┬────────┘ │ │ 2. Append events │ ▼ │ ┌─────────────────┐ │ │ SqlStreamStore │ ── MSSQL ──┐ │ │ (event store) │ │ │ └─────────────────┘ │ │ │ │ GET /accounts/{id} ┌─────────────────┐ │ └──────────────────────────────────────────────►│ ProjectionWorker│ ◄──────────┘ { balance: 1500 } │ → AccountReadModel └─────────────────┘ (samma MSSQL)

Notera tre saker:

  1. Skriv och läs går olika vägar. Skrivningen rör aggregatet och appendar händelser. Läsningen träffar aldrig aggregatet — den läser ur en read-modell.
  2. Aggregatet är inte CRUD. Klienten skickar inte "uppdatera saldo till 1500", utan en avsikt: "sätt in 500". Aggregatet validerar att det är tillåtet och bestämmer vilka händelser som uppstår.
  3. Event store och read model ligger i samma MSSQL. Det här är ett medvetet val i kursen — det löser dual-write-problemet (vi återkommer till det i lektion 7).

Varför Event Sourcing i stället för CRUD?

EgenskapCRUD (state-based)Event Sourcing
LagrarNuvarande tillståndAlla händelser som ledde dit
RevisionsloggBehövs separatInbyggd — händelserna är loggen
TidsresorSvårt eller omöjligtReplay till valfri tidpunkt
Nya rapporterMigrera databasenBygg en ny projektion från event 0
"Ångra"UPDATE tillbaka — historiken försvinnerKompenserande händelse — historiken bevaras
KomplexitetLågHög — kräver eventversionering, projektioner, eventual consistency
När du inte ska använda Event Sourcing Enkla CRUD-domäner (produktkatalog, inställningssidor, "edit a row"-UI) blir bara dyrare med ES. Använd ES när domänen är intressant — när det vad som hände är lika viktigt som det nuvarande värdet (finans, bokningar, regler-tunga arbetsflöden, audit-krav).

Teknikstack i kursen

LagerBibliotek / produktRoll
Runtime.NET 10LTS-grund
KlientBlazor WebAssemblySPA, körs i browsern, anropar API via HttpClient
APIASP.NET Core Minimal APIHTTP-endpoints för commands och queries
MediatorMediatRCommand/query-bus + pipeline behaviors
Event storeSqlStreamStore (MsSqlStreamStoreV3)Append-only event-strömmar i MSSQL
Read modelMicrosoft SQL Server + DapperProjektionstabeller, optimerade för läsning
BakgrundsarbeteBackgroundService + ChannelsProjektioner, command-kö, sagas
ValideringFluentValidationCommand-validering i pipeline
ResiliensPollyRetry vid concurrency-konflikt
TesterxUnit + TestcontainersEnhets- och integrationstester
Varför endast en MSSQL-instans? Många ES-tutorials kör event store och read model i två olika databaser (t.ex. EventStoreDB + Postgres). Det fungerar bra i stora system, men introducerar dual-write-problemet: två separata transaktioner som kan misslyckas oberoende. Genom att lägga både event-strömmen (SqlStreamStore) och read-modellerna i samma MSSQL kan vi använda en enda transaktion när vi projicerar — vilket dramatiskt förenklar driften.

Kursens röda tråd

  1. L1 — Helhet (denna sida). Vi sätter kartan.
  2. L2 — DDD-grunder. Bounded context, entity, value object, domänhändelse.
  3. L3 — AggregateRoot. Mönstret i C#, invarianter, basklass.
  4. L4 — CQRS & Commands. MediatR-pipeline, validering, idempotency.
  5. L5 — Event Sourcing. Bygg en in-memory event store för att förstå mekaniken.
  6. L6 — SqlStreamStore. Sätt upp riktig persistens i MSSQL och första projektionen.
  7. L7 — Transaktioner & concurrency. Optimistisk låsning, outbox, exactly-once.
  8. L8 — Command-kö & Undo. Sekvensering per aggregat, kompenserande commands.
  9. L9 — Snapshots & Sagas. Långa strömmar och flerstegs-flöden.
  10. L10 — Blazor-klient. Sätt ihop allt med optimistisk UI och undo-knapp.

Examinationen (sida 11) bygger ett biljettbokningssystem som använder hela stacken. Lektionsplanen (sida 12) visar tidsupplägget över fem veckor.

En aning ordbok

TermInnebörd
AggregateEn kluster av relaterade objekt som behandlas som en enhet vid datakonsistens.
Aggregate rootDen enda entiteten i ett aggregat som ytan exponeras mot. Allt går genom roten.
CommandEn avsikt att ändra något ("Sätt in 500 kr"). Kan avvisas.
EventNågot som har hänt ("500 kr sattes in"). Oföränderligt. Skrivs i dåtid.
StreamEn tidsordnad sekvens av events, typiskt en per aggregatinstans.
ProjectionEn process som läser events och bygger en read model.
Read modelEn denormaliserad tabell optimerad för en specifik query.
Compensating commandEn command som upphäver effekten av en tidigare ("Återbetala insättning").

Referenser

Elektroniska resurser

Böcker

Övningar

Lös övningarna självständigt. Det finns inget facit — lärandet sker i processen.

  1. Rita om diagrammet Rita systemets dataflöde för en läsoperation (GET /accounts/{id}) i stället för en skrivning. Vilka komponenter berörs? Vilka berörs inte?
  2. Identifiera CRUD vs ES Lista tre system du arbetat med (eller känner till). Vilka skulle vinna på Event Sourcing? Vilka skulle bli onödigt komplicerade? Motivera kort.
  3. Hitta avsikten Många UI:n är "edit a form". Ta ett sådant UI du känner till — formulera om det till en lista av tydliga commands med avsikt. Hur många commands blev det?
  4. Eventnamngivning Skriv tio events från ett system du känner till. Alla ska vara i dåtid och beskriva vad som hände, inte tekniska detaljer. Undvik ord som "Updated" och "Changed" — var specifik.

Soloprojektor

Projekt 1 — Arkitekturskiss Välj en domän du känner väl (gärna från ditt nuvarande jobb). Producera ett arkitekturdokument (max 3 sidor) som beskriver: bounded contexts, aggregat, viktiga commands och events, och vilka read models som behövs. Rita ett komponentdiagram likt det i denna sida. Inkludera en kort motivering: varför ES här, vad blir komplicerat?
Projekt 2 — Bygg ett tomt skelett (fördjupning) Sätt upp en .NET 10-lösning med de fem projekten (Domain, Application, Api, Projections, Web) plus tre testprojekt. Lägg in NuGet-referenser till MediatR, FluentValidation, Polly, SqlStreamStore, Dapper och xUnit. Lösningen ska kompilera utan kod. Sätt upp en docker-compose.yml som startar MSSQL 2022 lokalt.

← Lektionsplan Nästa: DDD-grunder →