Integrační zprávy a kontrakty

Veřejné, verzované zprávy mezi službami – události („stalo se“) a příkazy („udělej“).

Na této stránce

V jedné větě: Integrační zpráva je smlouva mezi službami: jednoduchý záznam (record) jen s daty, s verzí v názvu (…V1), který se po zveřejnění už nemění.

Události vs. příkazy

Událost (IIntegrationEvent) Příkaz (IIntegrationCommand)
Gramatika minulý čas: StockReservedV1 rozkazovací způsob: ReserveStockV1
Kolik příjemců 0 až N – odesílatel neví kdo právě jeden – služba, které příkaz patří
Odpověď žádná očekává se událost (StockReservedV1 / …FailedV1)
Přirovnání oznámení na nástěnce objednávka u dodavatele

Pravidlo „jeden příkaz = jeden příjemce“ hlídá IntegrationMessageRouting už při startu.

Všechny kontrakty

Kontrakt Druh Oblast Pole
ProductRemovedV1 událost Inventory EventId: Guid, OccurredAtUtc: DateTime, ProductId: Int32
ProductUpdatedV1 událost Inventory EventId: Guid, OccurredAtUtc: DateTime, ProductId: Int32, Name: String, Price: Decimal
ProductUpdatedV2 událost Inventory EventId: Guid, OccurredAtUtc: DateTime, ProductId: Int32, Name: String, Description: String, Price: Decimal, CategoryId: Int32, CategoryName: String, ImageFileName: String, Availability: String, AvailabilityChangedAtUtc: DateTime
ReleaseStockV1 příkaz Inventory CommandId: Guid, OccurredAtUtc: DateTime, OrderId: Int32
ReserveStockV1 příkaz Inventory CommandId: Guid, OccurredAtUtc: DateTime, OrderId: Int32, Lines: IReadOnlyList<StockLineV1>
StockAvailabilityChangedV1 událost Inventory EventId: Guid, OccurredAtUtc: DateTime, ProductId: Int32, Availability: String
StockReleasedV1 událost Inventory EventId: Guid, OccurredAtUtc: DateTime, OrderId: Int32
StockReservationFailedV1 událost Inventory EventId: Guid, OccurredAtUtc: DateTime, OrderId: Int32, Reason: String
StockReservedV1 událost Inventory EventId: Guid, OccurredAtUtc: DateTime, OrderId: Int32
SyncCatalogV1 příkaz Inventory CommandId: Guid, OccurredAtUtc: DateTime
OrderCancelledV1 událost Orders EventId: Guid, OccurredAtUtc: DateTime, OrderId: Int32, CustomerEmail: String, Lines: IReadOnlyList<OrderLineV1>
OrderCompletedV1 událost Orders EventId: Guid, OccurredAtUtc: DateTime, OrderId: Int32, CustomerEmail: String
OrderPlacedV1 událost Orders EventId: Guid, OccurredAtUtc: DateTime, OrderId: Int32, CustomerEmail: String, TotalPrice: Decimal, Lines: IReadOnlyList<OrderLineV1>
OrderShippedV1 událost Orders EventId: Guid, OccurredAtUtc: DateTime, OrderId: Int32, CustomerEmail: String, Carrier: String, TrackingNumber: String

Tabulka je vygenerovaná z kódu (MiniShop.Contracts) při zobrazení stránky.

Kód: MiniShop.Contracts. Projekt nemá žádné závislosti, takže ho může referencovat kdokoli.

Překlad: z doménové události na integrační

Doménové události jsou interní a smí se měnit. Ven proto jde překlad – jen data, která ostatní potřebují:

flowchart LR
    d1["OrderPlaced<br/>(drží celou Order)"] --> t["PublishOrderIntegrationEventsHandler<br/>překladač"]
    t --> i1["OrderPlacedV1<br/>OrderId, e-mail, cena, řádky"]
    d2["ProductChanged"] --> ctx["InventoryDbContext.ToOutboxMessage<br/>překlad při uložení"]
    ctx --> i2["ProductUpdatedV1<br/>ProductId, název, cena"]

    click t "/Docs/architektura/moduly/ordering"
    click ctx "/Docs/architektura/moduly/inventory"

Pravidla verzování

  1. Kontrakt se po zveřejnění nepřejmenovává a nemění. Příjemce může běžet ve starší verzi.
  2. Větší změna = nový kontrakt …V2; chvíli se posílají oba, dokud všichni příjemci nepřejdou.
  3. Hlídá to IntegrationContractTests – test zná přesný tvar každého kontraktu a změnu neprojde bez vědomého rozhodnutí.

Příklad: ProductUpdatedV1 → ProductUpdatedV2 (expand → contract)

E-shop krok 2 potřeboval ve zprávě o produktu popis, kategorii a dostupnost. Postup ve dvou fázích:

Fáze Odesílatel (sklad) Příjemce (Ordering)
expand (krok 2) posílá obě verze – každá zpráva má vlastní ID zpracuje obě; V1 kvůli zprávám, které při nasazení čekaly ve frontě
contract (později) přestane posílat V1 handler V1 se odstraní

Přirovnání: Úřad vydá nový tiskopis „vzor 2“. Nějakou dobu přijímá oba, protože lidé mají doma vyplněný starý. Teprve když starý nikdo nenosí, přestane ho přijímat.

Jedna doménová událost (ProductChanged) tak vede na dvě zprávy v outboxu – ToOutboxMessages modulu jich může vrátit víc. ID druhé zprávy je odvozené z ID události (MessageIds): stejné při opakovaném uložení, ale jiné než u V1 – inbox příjemce rozlišuje zprávy podle (ID, handler) a V2 by jinak zahodil jako duplicitu.

Proč takhle

  • Služby se nasazují nezávisle – kontrakt je jediné, na čem se musí shodnout.
  • Interní model (entita Order) se může libovolně měnit, protože ven nikdy přímo nejde.

Na internetu