Archiv rubriky: IT blog

Architektura (nejen) C# projektu 2

Architektura souběžností

Jak jsem psal v předchozím díle, programátor by neměl používat transakční mechanismus databáze (např. SQL transakce) a za souběžnosti a zamykání dat před validací a zápisem by měl vzít zodpovědnost ve svém kódu napřímo.

Jak to ale udělat? Je vůbec možné vymyslet univerzální architekturu pro zpracování souběžností? Nebo je nutné analyzovat každou doménu zvlášť?

Čtení

V prostředí businessových API aplikací je nutné se na každý kus kódu dívat z pohledu škálovatelnosti a předpokládat, že každý řádek může běžet souběžně na 10 serverech a na každém serveru v 1000 vláknech (10×1000).

Konkrétní hodnoty nejsou důležité. 10×1000 může být v kontextu jedné aplikace zátěžový test, v kontextu jiné aplikace může jít o produkční provoz. O to nejde.

Pokud na API přistál request na čtení dat, je nám úplně jedno, kolik serverů a kolik vláken čtení obsluhuje. Díky tomu je čtení pro nás programátory ta jednodušší část kódu. Nemusíme vytvářet žádné zámky a prakticky jen delegujeme čtecí requesty na náš databázový systém, ať už jde o SQL data nebo NoSQL dokumenty (jsme v eventuální konzistenci).

Zápis

Pokud na API přistál request na zápis dat, je zamykání složitější. V jakém rozsahu se má zamykat? Na to existuje několik možností a všechny jsou validní, záleží na kontextu.

Příklad: mám 2 typy agregátů, Order (objednávka) a Product (produkt).

  1. Plošný zámek pro jakýkoliv zápis: všechny operace pro zápis používají jeden zámek.
    • Jakmile jeden uživatel provede nějakou změnu dat, všechny ostatní requesty musí čekat bez ohledu na to, zdali jde o změnu v objednávce nebo v produktu.
    • Může se zdát, že takovýto režim zamykání je absolutně nesmyslný. Berte ale na vědomí, že implementovat plošný zámek pro zápis je velmi triviální a například u POCů a malých aplikací může dávat smysl.
  2. Zámek nad typem agregátu: všechny operace pro zápis nad konkrétním typem agregátu používají jeden zámek.
    • Toto znamená, že lze souběžně změnit jakýkoliv Order a jakýkoliv Product ale už nelze souběžně změnit 2 různé Ordery.
    • Typ agregátu je z definice nezávislý na ostatních typech tzn. Order je zcela nezávislý na Product.
    • Tento typ zámku nedává dle mého názoru moc velký smysl. Implementačně je složitější a DDD již zaručuje izolaci na úrovni instance agregátu. Proto je lepší typ 3 níže.
  3. Zámek nad instancí agregátu: všechny operace pro zápis nad konkrétní instancí agregátu používají jeden zámek.
    • Toto znamená, že zle souběžně změnit 2 různé Ordery, nezávisle na sobě a na jakémkoliv jiném agregátu, např. na Productu.
    • Není však možné provádět souběžně 2 operace na jednom Orderu, na jedné objednávce.
    • Zamykání nad instancí agregátu lidem často připomíná aktory, v předchozím článku ale pojmenovávám důvody, proč je lepší se aktorům vyhnout.
  4. Detailní manipulace se zámky v instanci agregátu v závislosti na business logice
    • V rámci jedné instance agregátu je možné provádět souběžně doménovou logiku, která provádí zápis dat. Např. první metoda/handler aktualizuje informaci Order.State (stav objednávky) zatímco jiná metoda/handler aktualizuje informaci Order.CustomerNote (poznámka zákazníka). Z businessové logiky to dává smysl, není třeba zamykat stav objednávky, pokud dochází k aktualizaci poznámky zákazníka.
    • Implementace na takto podrobné úrovni by dle mého názoru měla probíhat pouze na základě reálného optimalizačního požadavku. Jinými slovy, pokud v provozu existují reálné problémy s tím, že se uživatelům nedaří něco změnit.
      • Většinu času by toto neměl být problém, protože víme, že u většiny businessů je ~80% operací čtení a pouze ~20% operací je zápis, rozložený napříč všemy instancemi agregátů.
      • POZOR: Pokud nad jednou instancí agregátu potřebuje pracovat v jednom čase velké množství lidí, pak je možné, že jsme blbě navrhli doménu. Pokud vznikne objednávka a s touto objednávkou – konkrétní instancí agregátu Order – potřebují pracovat zpracovatel objednávek, skladník, účetní, dispečer atd. pak je zjevné, že naše doména není úplně dobře vymyšlená. (Zpracovatel by měl pracovat se stavem objednávky, skladník se skladovými zásobami/výdejkami, účetní s doklady, dispečer s dopravci, helpdesk se zákazníkem atd.)

„Distribuované zámky“

V doménové architektuře je doménová logika rozsekána mezi jednotlivé agregáty. User, Product, atd. Každý agregát obsahuje metody/handlery, které obsluhují jednotlivé commandy/requesty.

Dle mého názoru je se vyplatí už od začátku implementovat zámky nad konkrétní instancí agregátu (v předchozí kapitole bod 3.). Tyto zámky musí být implementovány jako distribuované zámky. Jen pozor, že velmi často pojem „distribuovaný zámek“ (v angličtině „distributed lock“) může znamenat jednu z následujících věcí:

  • Zámek, který je používán distribuovanou aplikací (= o čem píšu)
  • Zámek, který je distribuován na více serverech/instancích (např. redis skrz redlock algoritmus)

Pochopitelně zámek používáný distribuovanou API může a nemusí být sám distribuovaný (běžet na více serverech a instancích).

Zámek v agregátu a v ságách

V DDD je každá změna vyvolaná nějakým konkrétním commandem/requestem a tato změna je zpracována konkrétní metodou agregátu nebo handlerem. V mé architektuře to je MediatRovský handler.

Request handler náležící konkrétnímu agregátu je izolovaný od všeho ostatního a pracuje pouze s daty konkrétní instance agregátu. Před provedením metody musí dojít k uzamčení celého agregátu tzn. vytvoříme zámek s identifikátorem např. Product_123 kde 123 je identifikátor produktu.

V ságách probíhá redistribuce příkazů napříč více agregáty. Už na úrovni agregátu je však velmi často nutné řešit zámky!

Příklad: mějme operaci CreateOrder která vytváří objednávku. Musí se zamknout několik agregátů najednou ještě před tím, než sága začne vytvářet příkazy na jednotlivé handlery.

  • DiscountVoucher – slevový kupón, který je v objednávce použitý
  • ProductItem – skladová zásoba zakoupeného produkt
  • Order – může být potřeba zamknout objednávku samotnou v závislosti na granulitě naší domény. Pokud objednávce stačí jeden command pak ji není třeba zamykat, košaté objekty jako Order mají ale tendenci mít spoustu dalších připojených dat, které se vkládají ne najednou ale až po vytvoření.

Nyní nám ale vzniknul problém. Pokud sága vytváří sama svoje zámky, dostává se do konfliktu s vytvářením zámků přímo v agregátech. Pokud si sága vytvoří zámek nad Order ale pak pro Order zavolá příkaz, v rámci RequestHandleru pro Order dojde k deadlocku.

V naší architektuře potřebujeme vědět, že pokud tvoříme zámek v rámci agregátu, musíme zkontorlovat, jestli ten samý zámek na ten samý klíč není vytvořen už ságou. Pokud ano, nic nezamykáme a volání na redis přeskočíme. Tuto kontrolu stačí provést v rámci requestu.

Architektura (nejen) C# projektu

Řekněme, že stojíte u nového projektu, buď jako fullstack developer nebo jako backenďák programujete nějaké API. Máte naprosto čisté pole působnosti a můžete si to udělat jak chcete.

Že si to můžete udělat jak chcete je ale i nevýhoda. Pokud nemáte dost zkušeností se zvolením špatných architektur, nemáte vůbec tušení, jestli ta, se kterou to zkusíte teď, je ta špatná nebo dobrá.

Každý zkušenější programátor vám řekne, že ať už si zvolíte jakoukoliv architekturu, vždycky se objeví use-case, který vám v této architektuře bude dělat neplechu. A to je také pravda. Ale ještě zkušenější ajťák vám řekne, že zásadní je v kódu nějakým způsobem vyčlěnit doménu a zbytek je jen technologický detail.

Nejdřív bych ale rád pojmenoval ty nejpopulárnější přístupy, podle kterých se IT projekty píšou.

3-vrstvá architektura

V paradigmatu 3-vrstvé architektury se kód odděluje na 3 vrstvy:

  • databázová vrstva
  • doménová/business vrstva
  • frontend/UI vrstva

Databázová vrstva může komunikovat pouze s doménovou a doménová vrstva může komunikovat pouze s frontendovou vrstvou. Snahou tohoto rozdělení je tendence rozdělit kód do oblastí, které spolu co nejvíc souvisí tzn. kód, který souvisí s databází, by měl být v databázové vrstvě, kód který souvisí s doménou by měl být v doménové vrstvě a kód, který souvisí s UI by měl být v UI vrstvě.

Kritika 3-vrstvé architektury: všechno všude, všichni všechno

Kdekoliv, kde jsem byl, se 3 vrstvá architektura zvrhla v píseček, kde si každý programátor staví svůj vlastní hrádeček.

Jeden programátor do týmu vstoupí, jiný vystoupí, jeden chvíli dělá PR reviewera, ten je posléze taky nahrazen jiným a celá 3-vrstvá architektura se tak časem proměňuje v balast různých optimalizací, wrapperů které wrappují wrappované, business kód v databázové vrstvě, databázový kód v UI vrstvě, UI kód v business vrstvě, SQL procedury na 2000 řádků, nějaký „cool“ mechanismus o kterém už nikdo neví jak funguje, hromady ručně psaných „podpůrných nástrojů“, které je nutné pro normální programování používat a vždy nějaký přesložitělý, komplexní generátor datové vrstvy.

Z jednoduché myšlenky 3-vrstvé architektury, která sděluje, že někdy je nutné oddělovat kód podle typu použití, se stalo dogma, kterému všichni z nějakého důvodu věří ale nikdo jej ani nedodržuje. Je to totiž mylná orientace — není důležitá architektura programátorského projektu ale doména projektu (k tomu co je doména se dostanu). Každá 3-vrstvá nebo n-vrstvá architektura se tak postupně rozpadá jako domeček z karet.

Samoúčelná datová abstrakce, kterou nikdo nepotřebuje

Kdekoliv se používá 3-vrstvá architektura tak tam narazíte na nějaký přesložitělý generátor nebo kód obsahující sofistikovaný labyrint tříd a interfejsů určený pro abstrakci datové vrstvy.

  • IDataRepository<TDataModel> where TDataModel : IDataEntity
  • CrudDataSource<TDataSourceSelector, TDataModel> : IDataRepository<IDataSourceSelector, TDataModel> where TDataSourceSelector : IDataRepository<TDataModel> ….UGH!

Tak už aby někdo řekl pravdu.

Abstrakce datové vrstvy je užitečná hlavně kvůli psaní testů. Jakmile jednou zvolíte v projektu datový zdroj, nikdy se nezmění.

Prostě se to neděje. Fakt. Pokud k tomu vzácně někdy dojde u provozované, produkční aplikace, je to stejně vždy spojené se samostatným, relativně velikým projektem proporčně k velikosti projektu. Přechod na jiný datový zdroj u běžící aplikace je spojen s miliardou jiných dalších problémů a musí k tomu být opravdu dobrý důvod. Paradoxně v ten moment zjistíte, že ten váš šílený labyrint abstrakcí na tuto situaci není připravený a při přechodu na jiný datový zdroj se vám to rozpadne.

Přestaňte tedy jako programátoři přemýšlet nad tím, co se stane v momentě, kdy přejdete na jiný mechanismus persistence, protože to se v praxi téměř nikdy neděje.

Abstrakce je potřeba kvůli testům. Ale na ty vám stačí jednoduchý interface, který napíšete ručně a nepotřebujete k němu žádný šílený generátor.

Cibulová architektura, n-vrstvá architektura

Toto je všechno variace na 3-vrstvou architekturu. V cibulové architektuře jde o myšlenku, že různé oblasti kódu na sebe navazují postupně a lze si to tudíž představit jako cibuli.

Což…je naprosto neužitečné a neříká to nic o tom, jakým způsobem bychom měli psát kód. Která vrstva je uprostřed cibule? Která na vrchu? Je uprostřed databázová vrstva? Nebo business vrstva? Co když potřebuji sdílet něco napříč všemi vrstvami? („Ne to ne, na to se prej mračil jeden senior, který tu byl přede mnou“) Kam patří unit test business vrstvy? A co unit test databázové vrstvy? A kam patří definice IoC kontejneru?

Stejně jako u 3-vrstvé architektury, problém spočívá v orientaci na architekturu a nikoliv na doménu.

Relační databáze (RDBMS) musí zemřít (většinou)

3-vrstvá/n-vrstvá/cibulová architektura se používá v aplikacích, kde se jako persistence zvolilo SQL. Dlouho totiž SQLko bylo to jediné známé, jak se s daty nejlépe pracuje. V dnešní době ale už existují mnohem smysluplnější alternativy a nové projekty by tak v roce 2022 vůbec nad SQLkem neměly vznikat.

Kde dávají RDBMS smysl

Pracoval jsem pro jednu firmu, kde používali SQL Server a Oracle Server a zpracovávali geografická data. V této firmě zaměstnávali na full-time databázové specialisty, kteří veškerou aplikační logiku programovali v procedurách, triggerech, packages a dalších možnostech databázových systémů …a já jsem se svým trapným C# byl jen podřadný technik, který měl dělat různé aplikační a architektonické vyfikundace a webové servery s UI, které ti databázisti neuměli udělat. Můj trapný ASP.NET Core 2.1 MVC backend nebylo nic jiného, než wrapper okolo databázových procedur.

U takové firmy dává SQL smysl. Databáze je střed všeho, geografické údaje jsou jedním z primárních orientací relačních databází a hlavním zaměstnancem není programátor ale databázový specialista.

Dále dává SQL smysl u malých, osobních projektů, kde je výborný tooling. Např. malý blog, malý eshop a s Entity Frameworkem dokážu být produktivní velmi rychle. WordPress, na kterém běží tento blog, běží nad MySQL. Nahodím to někam na hosting a mám hotovo. MySQL není pro WordPress ideální řešení, je to ale zaběhnuté, známé řešení, existuje k němu perfektní podpora, tooling a související technologie.

Je to docela úchylné. Pokud používám MySQL/SQL Server pro osobní blog, je to jako kdybych si koupil plně vybavené BMW X7 jen abych s tím jezdil na nákup do Kauflandu a z plné výbavy používal jen plyn, brzdu, volant, blinkr a světla. Jasně, základní věci jako tabulky, primární klíč a indexy to umí. Kristova noho, tahle ďábelská mašina ale umí mnohem, mnohem víc, než si zjevně dovedu představit a nic z toho nevyužiju. V IT to samozřejmě nevadí, protože oproti BMW X7, MySQL/SQL Server (Express) mohu používat ihned a zdarma.

Kde nedávají RDBMS smysl

Všude jinde.

Vážně.

Nepoužívejte SQL pokud ho po vás nikdo vyloženě nechce a nemá k tomu dobrý důvod. Používejte pro data pouze dokumentové databáze jako mongodb. Toto píšu na základě svých zkušeností. Kdekoliv, kam jsem přišel k nějakému většímu projektu a používali nějaké RDBMS (většinou SQL Server v mém případě) tak RDBMS byl zdrojem nikdy nekončících problémů.

Důvod č.1 proč přestat používat SQL: prakticky nerealizovatelná horizontální škálovatelnost

RDBMS lze škálovat horizontálně. Prakticky to ale nikdo nedělá, protože to nikdo neumí, protože je to velmi složité. Musí se to analyzovat dopředu někým, kdo tomu rozumí a kdo už nějaké databázové servery škáloval. Programátoři to většinou nechtějí dělat, byť to jsou většinou oni, kdo díky skvělému existujícímu toolingu rozhodl, že se bude používat SQL.

Důvod č.2 proč přestat používat SQL: vysoká cena

SQL se v praxi škáluje jen vertikálně. Pokud stroj nestíhá, pořídí se silnější stroj, což znamená brutálně vysoké náklady za licence a za provoz na příšerně drahých serverech s 50 jádry a stovkami GB paměti.

Důvod č.3 proč přestat používat SQL: náročná optimalizace

Zkušení programátoři ví co jsou B+ stromy, primární/cizí/unikátní klíče, full text indexy, typy constraintů, umíme execution plány, umíme napsat proceduru i trigger byť to děláme strašně neradi…ale u velkých projektů prostě dřív nebo později narazíme na problém, který nedokážeme vyřešit. Máme všechny indexy správně ale prostě to běží z nějakého důvodu pomalu…a nejspíš to souvisí s nějakou procedurou? Ten samý problém se nám začal dít ještě někde jinde. A na podobném místě to začalo dělat deadlocky. A to jsem myslel, že když sem dám READ COMMITED tak to vyřeším…

Většinou pak my programátoři pácháme na SQL různé zločiny, dokud to nezačne fungovat, protože databáze nepovažujeme za svoji hlavní specializaci a nebaví nás se RDBMS takhle dlouho věnovat. Tyto zločiny vedou k menší stabilitě a udržitelnosti projektu a většinou se nám později vymstí někde jinde a kolotoč se opakuje. Ve velkých projektech, kde se používá RDBMS, programátoři spoustu času neřeší nic jiného, než problémy spojené s databázemi.

Důvod č.4 proč přestat používat SQL: nevyhnutelné deadlocky

Každý rostoucí projekt, který začne používat RDBMS, narazí na ochrnující problémy s deadlocky. Nevyhnutelně, zaručeně, bez absolutně žádné závislosti na profesionalitě vývojářů.

Typy transakcí v RDBMS nejsou sice tak složité na pochopení ale jak projekt roste, přestává být udržitelné sledovat, která transakce ovlivňuje kterou, jaké tabulky, řádky se uzamykají, jaké musí být čteny, kdy jsou v pořádku „dirty reads“ apod. V praxi to nikdo nesleduje, nikdo to neanalyzuje a nehlídá – programátoři volí SQL v dobré víře, že to znají, dokážou v tom být rychle produktivní a zbytek se nějak zvládne. Ale jakmile v určité velikosti projektu začne SQL vystrkovat růžky, už se tím nechtějí zabývat a projekt trpí kritickými bugy s deadlocky a špatnou optimalizací souběžností.

Důvod č.5 proč nepoužívat SQL: delegování souběžnosti

To, že programátoři zápasí s deadlocky a optimalizací není ten hlavní problém.

Většina programátorů se k SQL vztahuje tak, jako kdyby jim ACIDita a transakce všechny problémy se souběžnostmi vyřešili a oni se tak mohou soustředit na své úžasné nabobtnalé CRUD frameworky a generátory databázových vrstev. Tady mám tabulku s uživateli, tady tabulku s produkty, tady tabulku se skladovými zásobami — super, všechno narvu do transakce a SQL se za mě postará o zbytek!

Hlavní problém je v tom, že programátor vůbec delegoval souběžnosti na databázi, což je přístup, který mu projde u malého/interního projektu. Souběžnosti má mít programátor plně pod kontrolou ve svém kódu, protože stanovení souběžností je součástí domény —- jinými slovy, to, jaké procesy mohou běžet paralelně a jaké ne je zájmem zadavatele, i když to zadavatel třeba neumí přesně analyzovat a zadat.

Zadavatelé většinou nedokážou souběžnosti identifikovat, musí vám být ale schopni říct, co je racing-condition a co ne, když se jich zeptáte. Příklad: pokud ve stejném čase vznikají dvě obědnávky nad zbožím, u kterého zbývá poslední kus, zadavatel musí říct: ano, zde platí kdo dřív příjde, ten dřív mele. Super! V takovém případě dokážete přesně říct, nad kterými entitami/hodnotami umístit zámek či semafor.

Ze začátku můžete nahrubo uzamykat celé entity a v případě potřeby můžete vždy optimalizovat uzamykání nad konkrétními hodnotami. A budete vždy rychlejší, než jakékoliv SQL. Pokud má být projekt připravený na horizontální škálovatelnost tak musíte používat distribuované zámky, ale to není těžký problém (zámky umí dobře obstarávat Redis).

Důvod č.6 pro nepoužívat SQL: delegování domény (business vrstvy)

Pokud se rozhodnete, že přestanete používat transakce, své RDBMS omezíte jen na čtení a na čistě atomické DML (insert, update, delete) a souběžnosti si pohlídáte sami tak jediné, co ještě můžete užitečně používat, je referenční integrita dat skrz primární a cizí klíče, procedury a triggery a jiné vyfikundace databázového systému.

Přestaňte používat procedury, triggery a jiné vyfikundace databázového systému. Tento článek je psaný pro programátory a ne pro databázové specialisty, kteří se psaní procedur, triggerů, viewů apod. věnují full-time. Programátor píše business vrstvu správně v kódu a ne v databázi. Projekt je tak mnohem přehlednější a jasnější.

Primární klíče, cizí klíče, unikátní klíče, constraints atd. jsou delegování business vrstvy do datové vrstvy! Referenční integrita je sice strašně cool ale to, že uživatel a objednávka je ve vztahu 1:N by mělo být patrné z kódu, nikoliv z databáze. Jakékoliv „unikátní klíče“ jsou pak jen rozmělňováním business znalostí z domény do databáze.

Jakmile všechny tyto věci vyhodíte a začnete používat SQL čistě jako mechanismus persistence tak vám dojde, že používáte BMW M7 k ježdení do Kauflandu a začnete hledat jiná NoSQL řešení.

(RDBMS je adekvátním systémem pro čtení např. u systému pro datovou analýzu, datamining, data warehousing atd. či jako uložiště pro readmodely (o readmodelech níže))

Aspektově orientované programování (AOP)

Za celou svoji profesní kariéru jsem se nikde s tímto přístupem nepotkal byť si myslím, že je to celkem cool přístup a rád bych viděl nějaký větší, primárně v AOP psaný projekt. V C# tento způsob rozhodně není moc populární – jediný známý AOP projekt je komerční PostSharp.

AOP je paradigma, které rozlišuje různé „aspekty“ které se k IT projektu vztahují napříč mnoha úrovněmi: logování, databázi, autorizaci, autentizaci, business logiku atd. apod. a každý tento „aspekt“ se programuje odděleně „navěšením se“ na existující infrastrukturu.

Dle mého se ale tento přístup v C# nepoužívá hlavně proto, protože tu samou funkcionalitu „navěšení se na něco“ splňují dekorátory (decorator pattern). Když jsem si s PostSharpem naposledy hrál, projekt se celkem dramaticky zpomalil a běžel jak želva.

Pročítal jsem si ještě https://www.postsharp.net včetně dokumentace a včetně stránky s ksichtem Scott Hanselmana, což je v .NET komunitě velké jméno z Microsoftu a nepřesvědčilo mě o výhodách tohoto komerčního projektu, který ale v nějaké edici mohu použít zdarma, absolutně nic.

Aktoři (Actor)

Actor model je velice cool model, ve kterém je váš kód tlačen do aktorů. Zjednodušeně řečeno: aktor je instance třídy, která má vlastní identifikátor a s daným identifikátorem může v rámci této třídy běžet pouze jedna metoda a ostatní volání čekají ve frontě (tzn. obyčejný semafor, ten je však implicitní).

V .NET světě existují tři projekty, které využívají aktory:

  • MS Orleans
  • akka.net
  • dapr (potažmo Service Fabric Reliable Actors, programuje se to stejně)

Výhody: skvělé pro online hry

Dva hráči hrají nějakou online hru a každý z těchto hráčů je reprezentován aktorem. Tito hráči reprezentují s objekty, které se v této hře nachází — tyto objekty jsou také reprezentovány aktory.

Aktoři jsou ideální pro use-case, pro který byly vytvořeny: pro virtuální interakce v online hrách. Každý aktor reprezentuje objekt, se kterým lze dělat právě jen jedna akce a ostatní akce čekají ve frontě, ze které mohou být vyhozeny/přerušeny či posunuty výše. Aktoři mohou vytvářet nové aktory a volat existující aktory — což přesně pasuje do modelu virtuálního světa, kde se hýbe mnoho věcí najednou a operace aktorů a interakce mezi aktory jsou definované.

Nevýhody: A pak někoho napadlo s aktorama naprogramovat API pro eshop…

Muselo k tomu nevyhnutelně dojít. Já sám jsem byl součástí projektu, kde se aktoři přes DAPR používali k programování API do mobilní aplikace pro koncové uživatele. Sám jsem byl z aktorů celkem nadšený, když jsem se o nich naučil, než jsem dospěl k názoru, že aktoři jsou skvělí jen pro online hry či virtuální online světy a pro psaní backendů se naprosto nehodí.

Nefunguje to. Nepoužívejte prosím vás aktory pro obyčejné systémy jako jsou eshopy, API (neherních, formulářových) mobilních aplikací a podobně. Aktoři jsou fakt velmi specifická architektura, která dává smysl u online her nebo něčeho podobného.

Doménu, ve které se nachází objekty jako User nebo Product nedává smysl reprezentovat skrz aktory. Jasně: působí to dost dobře a „doménově“ když definuji, že u každého uživatele či produktu lze zavolat jen jednu metodu. Dává nám to pocit, že jsme zvítězili nad problémem souběžností (concurrency) a zbavili jsme se deadlocků a v kontextu konkrétního aktora jsme si vždy jisti, že běžíme v jednom vlákně.

Jenže…potom z aktorů využíváme prakticky jen mechanismus lockování a navíc máme toto lockování striktně limitované na jedinou metodu v aktorovi. Což by neměl být až zas takový problém, musíme si ale dávat pozor na to, abychom v aktorovi nekombinovali dlouho běžící proces s často volanou metodou. To je ale zbytečné omezení, na začátku projektu nikdy nemáme informace o konkrétním použití.

Jak se má programovat proces, ve kterém potřebujeme aktivovat dva aktory různých typů najednou? Jeden nápad je skrz jiný typ „kompozitního“ aktora, jehož identita se skládá z identit všech aktorů, se kterými pracuje. Na první pohled celkem hezký nápad. Jak ale „kompozitní aktor“ interaguje s ostatními „kompozitními aktory“? Mělo by to být vůbec možné? Jak ohlídáme, že nějaký programátor nenapíše nějakého long-running metodu do kompozitního aktora, který se ale musí spouštět často? Jak by se měly třídy kompozitních aktorů pojmenovávat: UserProductCompositeActor? Co když mám operaci, kde potřebuji interagovat s 5 dalšími aktory? Kompozitní aktoři přináší víc otázek než odpovědí a víc rizik než jistot.

Místo kompozitních aktorů tak můžeme používat nějaké obyčejné služby…ale jaký je pak rozdíl mezi aktory a službami s nějakým obyčejným uzamykáním skrz semafor? K čemu je dobré používat relativně komplexní architekturu, když z této architektury používáme jen tu nejtriviálnější část? A co se vlastně v aktorech samotných píše: Volají se služby? Nebo obsahují rovnou business logiku a píšou data do repozitářů?

Měly by se přes aktory číst data? To rozhodně ne: u většiny businessů je 90% operací čtení a 10% zápis (o tom píšu ještě níže) oproti online hrám, kde aktoři napřímo ovládají svá data a poměr operací pro čtení a zápis je mnohem vyváženější (např. každý pohyb hráče v prostoru je zápis dat ve změně souřadnic). V aktorovi konkrétní instance může běžet vždy jen jedna metoda najednou což by u eshopu velmi rychle vedlo k timeoutům a nejspíš i k deadlockům.

Měly by tedy existovat aktory u objektu jako je Produkt zvlášť pro zápis a zvlášť pro čtení? Ale …to pak není nic jiného, než CQRS (o kterém píšu níže). Navíc oproti aktorům CQRS readmodel nemá problém s tím, že je čten z více vláken na jednou což je u většiny businessů žádoucí.

Doménové programování

DDD neboli Domain Driven Development je programátorské paradigma, ve kterém se soustředítě na doménu. Doménou zde není myšlena doména jako např. „domena.cz“ ale nejlépe bych to nazval jako znalosti businessu.

Co myslím znalostmi businessu? To jsou instrukce, kterou my programátoři dostáváme od svých nadřízených, od projekťáků nebo od zadavatelů. Příklad:

  • „Po kliknutí na tlačítko se musí odeslat objednávka a poslat email na dispečink.“
  • Pokud uživatel aplikuje víc jak jeden slevový kupón, vezme se ten s nejvyšší slevou“

Většina programátorů ve své profesi neurčuje znalosti businessu. My programátoři jen převádíme tyto znalosti do kódu a snažíme se, aby ten software fungoval přesně tak, jak si to zadavatelé přejí.

Programování domény vychází z myšlenky, že je velmi užitečné psát kód, který vyjadřuje znalosti businessu napřímo.

Příklad: PHP blog vs. C# API

Použiju příklad se slevovými kupóny z předchozího paragrafu – pokud uživatel aplikuje víc jak jeden slevový kupón, vezme se ten s nejvyšší slevou.

Jak to na programuje začátečník v PHP? (Jakým jsem byl já v roce 2005) Třeba následovně.

V souboru create_order.php je kus kódu, který přečte $_POST['discount_coupons'] pole IDček slevových kupónů. Přes MySQL si všechny slevové kupóny načtu a přes max(array_column($data, 'discount_percent')) (nebo bůhví jak…) si zjistim výši slevy.

Celý kód by mohl vypadat tedy asi nějak takto:

//create_order.php
if(isset($_POST['discount_coupons'])) {
   $statement = $sqlConnection->prepare("SELECT * FROM coupons WHERE id IN (?)")
   $statement->bindParam($_POST['discount_coupons']);
   $couponsSql = $statement->execute();
   $maxDiscount = max(array_column($couponsSql, 'discount_percent'));
   //...hotovo
}

A na tomto způsobu programování není absolutně nic špatného. Funguje to, napíše se to rychle, práce je hotova. Když jste začátečník, kterého zaměstnávají na dělání relativně malých projektů, webů a eshopů se sexuálními pomůckami jako mě v roce 2005 tak tento způsob psaní kódu naprosto stačí.

Podle paradigmatu doménového programování bych dnes v C# totéž zadání napsal takto:

private readonly ICouponRepository repository;
public async Task PlaceNewOrderAsync(NewOrder newOrder) 
{
    var couponIds = newOrder.Coupons.Select(c => c.ID)
    var repoCoupons = await repository.FindCouponsAsync(couponIds);
    int maxDiscount = repoCoupons.Max(c => c.DiscountPercent);
    //atd...
}

Kromě zjevného rozdílu v odlišném zápisu dvou úplně odlišných programovacích jazyků, jaký je rozdíl z pohledu programování podle doménového paradigmatu?

  • PHP: Musíte PHPko aspoň trochu znát, abyste věděli, že isset($_POST) kontroluje, že přišel HTTP POST request, že $_POST obsahuje asociativní pole, že SELECT * FROM coupons je SQL dotaz nad nějakou relační databází, že bindParam je nastavení parametru do SQL dotazu protože spojování řetězců je SQL injection….doménová znalost je ztracena uprostřed obrovského množství technologických závislostí. Ten, kdo čte ten kód, musí znát PHPko, HTTP protokol a relační databáze protože jedině s těmito znalostmi pochopí, že kód jenom vybere tu nejvyšší slevu ze všech kupónů.
  • C#: V C# kódu je vyjádřeno:
    • že zde máme nějaký kupónový repozitář, kde jsou kupóny uloženy v ICouponRepository. Schválně to je psáno takto, protože v této části kódu nás nezajímá, jak je repozitář implementován.
    • že zde máme nějakou operaci PlaceNewOrderAsync která zjevně znamená, že vytváříme novou objednávku
    • že z nové objednávky vybereme přes Select(c => c.ID) IDčka kupónů a podle nich vyhledáme kupóny v databázi přes FindCouponsAsync
    • že ze všech kupónů v databázi zjišťujeme maximální slevu

(poznámka: Zápis v C# lze napsat stejně i v PHP — „doménově“ lze napsat kód i v PHP. V C# lze napsat kód tak, že přečteme data z HTTP POSTu a z relační databáze.)

Ten rozdíl, který chci zvýraznit je v tom, že v prvním zápisu je hromada technologických závislostí — musíte znát HTTP protokol a relační databáze, abyste věděli, co ten kód dělá.

V druhém zápisu je ale míra abstrakce taková, že nám stačí jen trochu znát C# a hned ze zápisu vidíme, co se děje. Není zde HTTP protokol, není zde relační databáze ale jen klasická manipulace s informacemi, ve které se snažíme co nejpřesněji vyjádřit to, co po nás chtěl zadavatel. To je podstata doménového programování.

Když programujeme doménu, soustředíme se na:

  • jasnou sémantiku kódu — jinými slovy text, který kód vyjadřuje, odpovídá významu toho, co ten kód dělá.
  • kód, který reprezentuje myšlenky a přání vlastníka businessu což pro nás programátory je většinou náš klient, zaměstnavatel, nadřízený, manažer, projekťák, analytik, architekt, atd. (nebo my samotní)

DDD – agregáty

V DDD se pod pojmem „agregát“ (aggregate) schovává něco, čemu by klasický programátor z 3-vrstvé architektury řekl „business objekt“.

Agregát je hlavní logická jednotka s identitou, na kterou je doména dělena. Každý agregát s identitou je zcela nezávislý na zbytku domény tzn. pokud děláme něco s agregátem „Produktu“ tak nesmíme dělat vůbec nic s agregátem „Skladu“. Typy agregátů jsou od sebe izolované tak moc, že je možné si je představit jako zcela oddělené služby, které fungují na samostatných serverech a nevědí o sobě navzájem.

Sága je název pro speciální služby, do kterých vstupují příkazy, které provádí operace nad více agregáty najednou, různých i stejných typů. V rámci ságy zpravidla kontrolujeme unikátnost emailů nebo distribuujeme operace na jednotlivé agregáty v rámci operace „Vytvoření objednávky“.

CQRS – oddělení čtení a zápisu

CQRS neboli Command Query Responsibility Segragation je paradigma, které říká, že 80% operací je čtení a 20% je zápis, u některých projektů kterými jsme prošel bych řekl klidně až 95% a 5%.

S CQRS souvisí koncept read modelů což je název pro ručně tvořené pohledy, které vás zajímají, přesně optimalizované pro čtení, které potřebujete. Diskový prostor je podstatně levnější, než procesorový čas takže ve tvorbě svých readmodelů se můžete nafukovat.

Myslím si, že je velmi důležité v kódu architekturně oddělovat čtení a zápis právě kvůli CQRS. Pokud máte malý projekt nad relační databází tak v C# používáte Entity Framework na čtení a na zápis a je vám CQRS naprosto lhostejný – oddělovat čtení a zápis nedává smysl.

Pro větší projekty či projektu, u kterých plánujete růst se ale vyplatí CQRS aplikovat:

  • Spousta zátěže na serverech je ze čtení. Pokud potřebujete serveru ulevit, vytvoříte read model s vlastní indexací atd. atp. a nemusíte se bát, že to zasáhne cokoliv jiného. Nafukujte se na desítky i stovky gigabajtů nenormalizovaných dat, protože diskový prostor je levný.
  • CQRS nutí programátory přemýšlet nad doménou jako nad pravidly, které se věnují zápisu. Ostatní data se dají rekonstruovat buď naživo (silná konzistence, např. SQL View) nebo dodatečně (eventuální konzistence, např. asynchronní read model).

CQRS a eventuální konzistence

CQRS je odbočením od doménového paradigmatu. Zadavatelé totiž téměř nikdy nerozlišují mezi čtením a zápisem. Zadání „Tady se musí vypisovat X“ je pro zadavatele v úplně stejné důležitosti, jako „Po kliknutí na tlačítko se musí stát Y“. Oddělování kódu dle CQRS je ale dle mého oběť, která se vyplatí.

CQRS je obrovská výhoda pro každý rostoucí projekt, že se vyplatí jej implementovat, nebo se na něj minimálně připravit už na začátku projektu. Na začátku může být všechno čtení a zápis ze stejných databází. Z pohledu CQRS máme data v silné konzistenci tzn. to, co vidíme v UI je to, co je aktuálně (nebo téměř aktuálně) platné, protože je to napřímo přečtené z databáze.

Jak ale projekt začne růst a databáze se začne zavařovat, přejdeme na asynchronní read modely (které vůbec nemusí být ve stejné databázi, ani na stejném serveru), které na základě událostí vyhozených z domény rekonstruují pohled, který potřebujeme.

Toto znamená, že máme data v eventuální konzistenci tzn. to, co vidíme v UI, nemusí být aktuálně platné, ale víme, že po dokončení asynchronní aktualizace nad readmodelem to platné bude. Pro 99% use-case toto není problém. Samozřejmě když říkám „není problém“ tak tím nemám na mysli to, že trvá hodiny a hodiny, než se váš read model zaktualizuje. Většinou existuje vysoká tolerance pro aktuálnost zobrazované (tzn. z readmodelu, nikoliv pravdivá z domény) informace např. o aktuálním stavu zboží, ta může být u většiny businessů až v minutách ale zpravidla by neměla trvat déle, než pár vteřin.

Není to problém také proto, že náš projekt v první řadě nikdy v silné konzistenci nebyl. Většina programátorů, zvyklá z SQL, přistupuje ke čtení dat jako k něčemu, k čemu lze přistupovat paralelně a při čtení nedochází k uzamčení čtených dat ani, pro čtení ani pro zápis. Paranoidní konzistence znamená, že dokud klient neobdrží data, která chce číst, nikdo jiný k těm datům nemá přístup ani pro čtení, ani pro zápis, což je ale use-case užitečný tak možná v nějakém těžce security prostředí, než v obyčejném eshopu/CMS.

Když přijdete za svým zadavatelem a řeknete něco na způsob „Můžu vyřešit to, že náš web bude rychlejší ale znamená to, že se může nějakým uživatelům někdy zobrazit to, že zboží je skladem, i když není. Objednat zboží, které není reálně skladem, stále nepůjde, jenom to potrvá to pár vteřin, než se aktualizuje informace, že zboží skladem už není.“ tak většina zadavatelů řekne, že to není absolutně žádný problém. Pokud nemáte nadřízené nějaké negramoty tak každý projekťák rozumí trojúhelníku čas-kvalita-peníze ve kterém si vždy můžete vybrat dvě věci na úkor jedné.

Asynchronní readmodel je drobné snížení kvality – přecházíme ze silné konzistence na eventuální – rychle a za málo peněz (pokud jsme s CQRS počítali už na začátku). Snížení kvality přechodem ze silné na eventuální konzistenci je ale naprosto přemláceno zvýšením responsivity SQL serveru, který není zahlcený SQL dotazy. Díky tomu dokáže zpracovat víc objednávek (například).

Architektura á la Miroslav Bartl

Tak je na čase ukázat, jak píšu doménu já. Stejně jako tento článek, je to prostě jen můj subjektivní názor založený na mých zkušenostech a na tom, jak to vyhovuje mně a co považuji za užitečné jak při tvoření solo projektů tak i při práci v týmu.

Agregát jako logická jednotka, nikoliv jako konkrétní třída

V první řadě je nutné identifikovat agregáty. Neprogramuji své agregáty jako třídy ale spíš jako oblasti, které obsahují kód, který se k danému agregátu vztahuje. V C# projektu si takto pojmenuji celou složku jako User nebo jako Product což jsou mé agregáty. Agregát je kořen stromu tzn. mé agregáty mohou obsahovat jiné objekty, kolekce objektů atd. apod.

V rámci těchto agregátů uplatňuji stejné pravidlo, jako v DDD: jednotlivé agregáty, ať už různých nebo stejných typů, na sebe nevidí. Tzn. jakýkoliv proces, který běží v kontextu agregátu, nevidí na žádný jiný agregát. (Jak řeším kolektivní business pravidla popíšu níže).

Ságy se většinou vztahují přímo k danému agregátu – potom je umisťuji do složky „Sagas“ k danému agregátu. Proč?

V rámci agregátu chci řešit pouze izolovanou logiku agregátu a přímý kontakt s repozitářem, který patří pouze danému agregátu. V operacích, které běží přímo nad agregátem, neřeším zámky/semafory, ty řeším v dané sáze.

Někdy nelze ságu ke konkrétnímu agregátu přiřadit. Jsou to zpravidla operace, které se zabývají mnoha různými agregáty najednou, jako například „Vytvoření objednávky“. Tyto operace dávám do složky „_Sagas“ s podtržítkem na začátku pouze pro účely řazení.

Model agregátu jako anemický model

Dle DDD definice je agregát jedna třída, která obsahuje všechny vlastnosti a metody, které se k agregátu vztahují. Jak jsem popsal výše, toto spíše vede k problémům.

Dle mého názoru třída agregátu má pouze svoji reprezentaci. Tomu se říká anemický model, který je některými lidmi tak strašně moc nesnášen.

public record User
{
    public string Id { get; init; }
    public string Name { get; init; }
    //... atd
}

A teď bacha.

  1. Reprezentace agregátu je modelem dokumentové databáze. Tzn. reprezentace agregátu je to, co je uloženo v NoSQL. Není žádné rozlišení mezi „business“ a „repo“ objektem, repozitář pracuje se stejným typem, jako handler.
  2. Reprezentace agregátu může vstoupit do UI a může přicházet z UI ale není to podmínka. UI téměř vždy potřebuje nějaké své vlastní modely, za překlad mezi UI modelem a doménovým modelem je odpovědné UI.

Pokud se kroutíte odporem, tak je to dobře. Znamená to, že konfrontuji vaše zaběhlé, dogmatické představy o tom, co je udržitelný kód.

Ad. 1 + 2) Agregát je sdílený model. DB/UI modely se tvoří podle potřeby.

Koukněte se na to takto: repozitář je součástí domény. Doména zahrnuje business kód + interakce čtení/zápisu s repozitářem. Není důvod vytvářet různé modely pro business a pro repozitář, protože obojí je součástí domény.

Repozitář je abstrahovaný jako interface, to sice ano, ale hlavně kvůli testům, abych nemusel v testech složitě mockovat všechny ty typy, které náleží do MongoDB.Driver NuGetu. Nemám důvod předpokládat, že budu přecházet na jiný způsob persistence a stejně tak byste neměli ani vy!

Mapování mezi C# třídou a MongoDB BSON dokumentem je triviální a vytváření dalších modelů nepřináší vůbec žádnou další výhodu.

Jako programátoři bychom se měli snažit psát vždy co nejméně kódu a každý kód, který píšeme, by měl být dobře čitelný sám o sobě, aniž by potřeboval komentář.

V MongoDB stačí Collection<T>.InsertOneAsync(T model) a máme hotovo.

Používat stejný model v DB a v UI není žádný zločin. Stačí se na to nekoukat jako na DB/business model ale jako na doménový model. UI modely se ale mohou rozlišovat a velmi často rozlišují a proto psát zvlášť UI modely a mapovat mezi UI a doménovými modely je v pořádku — ale pouze tam, kde se UI od domény opravdu liší a ne kategoricky všude, to je úplně k ničemu.

Pokud je UI model totožný s doménovým modelem, pak děláte věci správně, programujete doménově! Vaše UI je totožné s tím, jak jsou data uložená v databázi a jak jsou reprezentována v paměti. Nemáte vůbec s ničím moc práce a to, co se po vás chtělo, je v kódu zapsáno správně nejen správně sémanticky, ale i graficky.

Pokud nastane situace, kdy v důsledku autorizace různí uživatelé musí vidět různá data nebo kdy nad existující strukturou agregátů lze vytvořit zjednodušující pohledy tak to nevadí. Prostě vytvořite UI model/endpointy s takovou strukturou, jakou potřebujete a proveďte potřebnou transformaci v obou směrech. Práce, kterou trávíte psaním takového kódu, odpovídá tomu, co je pro projekt potřeba udělat.

Pokud je UI oddělené od nějakého API tak se na API nedělají UI modely ale RequestModely – filozofie je ale stejná. Pokud UI skrz API pracuje s naprosto identickým modelem, proč bychom měli v API posílat něco jiného?

Jakmile máte zadání, vaše architektura by měla být taková, že většinu času strávíte čas psaním produktivního kódu a ne psaním architektury.

MediatR

V rámci agregátů používám populární a známý MediatR. Je to v jádru velmi jednoduchý projekt, který nedělá nic světoborného ale to co dělá, dělá správně.

V rámci agregátu má každá operace svůj vlastní request a pro každý request existuje jeho request handler, ve kterém je tato operace zpracovávána. IRequestHandler je interface, takže jedna třída může obsahovat kód pro zpracování více requestů.

Na obrázku výše mám dva handlery: LocationHandler a LocationPictureHandler které zpracovávají všechny requesty tohoto agregátu. Toto rozdělení je čistě orientační.

Místo handlerů jsem mohl psát klasické služby a fungovalo by to úplně stejně, není v tom absolutně žádný rozdíl. MediatR používám, protože:

  • sjednocení mechanismu pro vyvolávání událostí – kromě toho, že MediatR umí příjmat requesty a zpracovávat je v request handlerech (1:1) tak umí přijímat notifikace a v notification-handlerech (1:N). Notifikace využiji pro vyvolávání událostí, nad kterými mohu psát read modely.
  • služby reprezentující metody agregátu v jedné třídě jsou gigantické a partial class zápisu se snažím vyhnout, pokud nejde o generovaný kód. Request může být v RequestHandleru který zpracovává víc requestů, nebo v odděleném handleru, který zpracovává jen ten daný request.
  • requesty, které vstupují do handlerů, jsou obyčejné třídy které lze hezky mapovat na online endpointy. Pokud se online endpoint musí odlišovat, pak související mapping probíhá v daném endpoint projektu (MVC, gRPC…).

Nevýhodou tohoto zápisu je, že musíte psát zvlášť request pro ságy a v ságách musíte psát requesty pro konkrétní agregáty.

Na obrázku výše jsou dva zvýrazněné requesty:

  • CreateLocationSagaRequest
  • CreateLocationRequest

Používám konvenci, že pokud se request/request handler týká ságy, má suffix SagaRequest nebo SagaRequestHandler zatímco u agregátů je suffix pouze Request nebo RequestHandler.

CreateLocationSagaRequest vstupuje do handleru ságy. V kontextu ságy ještě nejsem v konkrétním agregátu, mám zde ale dovoleno vytvářet zámky, číst z jiných agregátů a dělat si vlastně úplně cokoliv napříč celou doménou — nesmím ale provádět přímý zápis do žádného repozitáře, to mohu dělat pouze skrz RequestHandlery konkrétních agregátů.

Na příkladu výše před vytvořením objektu Location v doméně je nutné přečíst nějakou konfiguraci a provést různé validace na základě hodnot z jiných agregátů. To je možné udělat pouze v sáze. V rámci agregátu Location dochází už pouze k lokálním validacím. RequestHandler agregátu má přístup ke svému repozitáři ale už k ničemu jinému.

Validace

Poslední, o čem chci v tomto článku mluvit, jsou validace.

Pokud píšeme API, se kterým komunikuje jeden frontend, který je odpovědný za to, že před odesláním validuje data, tak je dle mého naprosto v pořádku používat klasické vyhazování Exceptions. Každá zalogovaná a vyhozená exceptiona je totiž indikátorem chyby buď na backendu nebo na frontendu. V každém případě jde o výjmečný stav, který někdo musí fixnout.

Pokud jsme v API na které se napojuje prakticky kdokoliv a každý si ho volá jakkoliv chce a nemůžeme se od frontendu spolehnout absolutně vůbec na nic (nebo jsme ve strašidelném prostředí, ve kterém je API zodpovědné za správné validační hlášky, které pak frontend pouze zobrazuje) tak je nutné myslet na to, že vyhazování Exception je značný performance-hit a vyhazování validačních chyb je lepší napsat nějak jinak.

V MediatRu lze každou IResponse obalit do nějaké DomainResponse nebo ValidatedResponse která obsahuje výsledek dané operace. Je však dle mého důležité snažit se držet validace co nejblíž v operacích které potřebují validace vykonávat. Nějaký „validátor“ by se mělo vyplatit psát až pouze tehdy, pokud máme opravdu v kódu více než jedno místo, kde jedna a tatáž validace musí proběhnout.

Validace je část kódu, jež reprezentuje doménu a proto bychom se zvlášť u validací měli soustředit na to, že kód je sémanticky co nejjasnější a že význam psaného kódu pokud možno přímo vyjadřuje myšlenky zadavatele/businessu.

Mé začátky s vývojem pro mobilní zařízení

Flutter vs. Xamarin.Forms

Poprvé v životě pracuji na mobilní aplikaci. Protože má appka fungovat jak pro Android tak pro iOS tak ji píšu v Xamarinu, konkrétně Xamarin.Forms.

Jsem oproti Flutteru v nevýhodě, protože Xamarin není tak moc populární, ale nakonec jdu do toho, protože znám dobře C# a znám dobře Visual Studio. Zkusil jsem nanečisto udělat projekt ve Flutteru ale je toho prostě příliš mnoho, co bych se musel naučit a já už bych raději byl produktivní, než abych trávil nejméně jeden či dva měsíce plácáním se s pro mě kompletně neznámým stackem.

Popularita Flutter vs. Xamarin vs. Electron

Microsoft udržuje Xamarin celkem intenzivně naživu a přestože je Xamarinská komunita zjevně mnohem menší než Flutterovská tak rozhodně není až zas tak malá, že by se nevyplatilo do vývoje přes Xamarin investovat.

Android vs. iOS

Flutter ani Xamarin.Forms by nemusel existovat, kdyby neexistoval Apple. Všichni by měli Androida a byl by pokoj.

Vlastně: oni by ty iPhony ani tak nevadily, kdyby se Apple nechoval jak Microsoft v historii se svým Internet Explorerem. Vývoj pro obě platformy naráz – Android a iOS – fakt připomíná dělání webů v minulosti, kdy se prostě muselo při vývoji velmi často zohledňovat, že v prohlížečích fungovalo spoustu věcí jinak a Internet Explorer byla vždycky kategorie sama o sobě.

Microsoft se poučil, Internet Explorer zabil a nahradil jej Edge (což je jen Chrome s jiným oblekem), koupil Github a opensourcoval celou svoji vývojářskou platformu .NET, vyrobil VS Code a Visual Studio nabízí zdarma (pokud nejste větší firma). Microsoft pochopil, že na vývoj v nějaké closed-box platformy mu většina lidí sere a že cloud běží většinou na Linuxech.

Google to zjistil pravděpodobně do určité míry taky, vyvíjet pro Android lze na jakémkoliv OS a Android je vlastně jen upravený Linux, který je stejně jako Linux open-source.

Jenže ne Apple. V Applu furt pracují blbci co žijí v roce 2001. Vyvíjet pro iOS na Windowsech ani na Linuxech nelze. Tedy lze — pokud máte náladu na několika denní sraní se s qemu, Docker-OSX, kvm a podobný hackování — já s tím ztratil po několika dnech trpělivost, aspoň pod Windows 11/WSL2 to neběželo moc dobře (a bez podpory GPU) a nakonec na splátky budeme kupovat Maca, jen abychom vůbec aplikaci pro iOS zbuildili. Koukal jsem ještě na cloudové služby, kdy si za nějaký peníz Maca pronajmete ale vůbec se to nevyplatí, za ty peníze je užitečnější Maca splácet vlastního.

Visual Studio má pro to naštěstí celkem funkční tooling. Stačí, aby Mac byl dostupný v síti, zadá se jeho IP adresa, administrátorský účet s heslem a VS si tam doinstaluje vše potřebné. Při zbuildění iOS projektu se pak VS na Mac samo připojí, přes XCode si mobilní appku zbuildí a obrazovku mobilní appky, která běží nad XCodem v Macu, vám zobrazí vtipně přes vzdálenou plochu.

Vývoj mobilní aplikace

Jakmile má člověk nakonfigurované správně prostředí pro vývoj, běží vše dobře. XAML a MVVM jsem už prakticky zapomněl, naposledy jsem v něm dělal pro jednoho klienta WPF aplikace, ale to bylo ještě v dobách .NET Frameworku 4.6. Pořád jsem si byl ale jistej, že proniknout zpátky do XAMLu pro mě bude pořád efektivnější, než se učit s Flutterem.

Ve WPFku jsou pro mě věci takové intuitivnější. Nikdy jsem nebyl moc frontenďák ale naučil jsem se dělat weby a desktopové aplikace pro Windows přes Winforms a WPF.

Dělat na telefon je něco jiného. Ve Windows máte koncepty oken, modálních oken, okno s focusem. V telefonu ale žádná okna nejsou a místo toho se pracuje s navigačním stackem — okno, které se vám aktuálně zobrazuje, je okno které je umístěné na vrcholu navigačního stacku, do kterého buď okna přidáváte, nebo z něj okna odebíráte.

https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/navigation/hierarchical-images/pushing.png
Přidávání do navigačního stacku (zdroj)
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/navigation/hierarchical-images/popping.png
Odebírání z navigačního stacku (zdroj)

V Xamarin.Forms je ale ještě jedna vyfikundace navíc – AppShell. Je to taková běžná struktura, kdy máte v mobilní aplikaci navigační panel vlevo a záložky vespod nebo nahoře. Díky AppShellu můžete libovolně kombinovat a zanořovat položky menu a záložky. Zajímavý je, že stránky, na který přes tyhle záložky a položky menu odkazujete, nejsou součástí navigačního stacku — AppShell má svůj vlastní navigační systém, kdy každá stránka vaší mobilní aplikace je identifikovaná nějakou route adresou, na kterou můžete odkazovat.

Navigace přes klasický navigátor

// navigace na stranku MyPage --- vlozi se do navigacniho stacku
await Navigation.PushAsync(new MyPage());

// navigace na predchozi stranku --- aktualni stranka se odebere z navigacniho stacku
await Navigation.PopAsync();

Navigace přes AppShell

// navigace na stranku MyPage
Shell.Current.GoToAsync("mypage");

// navigace na predchozis stranku
await Shell.Current.GoToAsync("..");

API klient – protokol a tooling

Mobilní aplikace většinou volá nějaké API, že jo. Ta naše aplikace není výjimkou.

REST API

V .NET Core se celkem intuitivně nabízí REST přes JSON. ASP.NET to podporuje ve svých knihovnách, stačí napsat Controllery, člověk nemusí řešit žádné ruční serializace a deserializace, můžete rovnou psát své objekty a máte vystaráno.

Pak stačí přidat Swagger nugety a ze svých controllerů máte vygenerovanou celou OpenApi specifikaci a nemuseli jste hnout ani prstem.

Tooling: RestSharp vs HttpClient

Pro C# existuje několik možností ale za zmínku stojí dle mého jen tyto dvě:

openapi-generator umí udělat klienta, který pracuje s RestSharpem zatímco NSwag pracuje přímo s HttpClientem. S HttpClientem se musí pracovat obezřetně – ve výchozí konfiguraci dispose HttpClienta nechává otevřený socket v TIME_WAIT dokud nevytimeoutuje. Používat HttpClient jako singleton je také chyba, protože HttpClient nerespektuje zjevně TTL u DNS záznamu (i když issue na githubu na tohle téma je už uzavřený, těžko říct, jak to funguje třeba v .NET 6, to bych musel otestovat). HttpClient se má používat správně přes HttpClientFactory

Z dokumentace:

Although HttpClient implements the IDisposable interface, it’s designed for reuse. Closed HttpClient instances leave sockets open in the TIME_WAIT state for a short period of time. If a code path that creates and disposes of HttpClient objects is frequently used, the app may exhaust available sockets. HttpClientFactory was introduced in ASP.NET Core 2.1 as a solution to this problem. It handles pooling HTTP connections to optimize performance and reliability.

Zdroj

NSwag a jeho HttpClient je tedy problematický …ale podle mně ne zas tolik. Nejsem si jistý, jestli u mobilní aplikace pro koncového uživatele prakticky vůbec může dojít k vyčerpání socketů, kterých teoreticky může být až 65536 (pro každý port jeden). U mobilní aplikace bych čekal, že OS (ať už Droid nebo iOS) bude na visící timeouty a spojení ve stavech jako TIME_WAIT mnohem agresivnější.

API klient – GRPC

Původně jsem chtěl dělat API a klienta přes GRPC. Věděl jsem, že pro Visual Studio je tooling hotový a skrz nugety lze zprovoznit GRPC endpointy okamžitě.

GRPC má oproti REST obrovskou výhodu: je to binární protokol. Což znamená, že přenos dat, serializace a deserializace je z podstaty věci mnohonásobně rychlejší, než JSON REST.

GRPC jsem nakonec ale nepoužil. API chci totiž provozovat jako WebAppku na AppService na Azure a bohužel, z nějakého důvodu (nejspíš lenost) Microsoft nemá dokončenou implementaci HTTP/2 na Windows Server OS a kvůli tomu GRPC na WebAppkách nefunguje. A to ani když máte Linuxovou AppServicu — což jsem nejdřív upřímně nechápal, pravděpodobně je to ale tím, že každá AppServica či WebAppka ve skutečnosti běží jako ServiceFabric, což je Windows Server.

Mobilní aplikace a unhandled exceptions

Když máte v C# konzolovku nebo nativní, desktopovou aplikaci ať už pro Windows nebo pro Linux, je někdy užitečné napsat kus kódu, který handluje nezachycené exceptiony. V .NETu je tohle triviální.

AppDomain.CurrentDomain.UnhandledException += MyHandler;
TaskScheduler.UnobservedTaskException += MyHandler; //pro zachyceni nezachycenych exception bezicich v tascich

V Xamarinu ale platí úplně jiný režim. Jakákoliv exceptiona shodí aplikaci bez ohledu na to, zdali je nebo není zachycená. Tohle je zjevně rozdíl mezi Flutterem a Xamarinem. Flutterovská aplikace beží totiž ve svém vlastním enginu a každý vygenerovaný pixel patří Flutteru. Takže i exceptiony, které si ve Flutteru vyrobíte, si můžete ve Flutteru také zpracovat.

Xamarin ale kompiluje všechno do nativního kódu takže handlování exception probíhá podle pravidel dané platformy.

AppDomain, TaskScheduler — to všechno sice proběhne, jakmile dojde k vyhození exceptiony, jenže aplikace stejně spadne. Android má zjevně nějaký svůj způsob, jak unhandled exceptiony handlovat. Všechno jsem to ale zkoušel a nic nezabrání tomu, že aplikace kompletně žuchne.

V iOSu to navíc údajně ani nejde, což zatím nemohu otestovat. Swift údajně nemá podporu pro zachycení nezachycených exception a někde jsem našel (teď se mi to nedaří dohledat), že Objective-C při vyhození exceptiony už není v obnovitelném stavu a jediné, co v tomto stavu ještě lze udělat, je zapsat někam lokálně log s chybou.

API klient bez exceptions

Takže v Xamarin prostředí vyhození exceptiony bezpodmínečně vede ke shození celé aplikace. Před shozením aplikace můžu exceptionu zalogovat ale to je vše – aplikace prostě žuchne.

Bomba.

V takovém případě se potřebuji exceptionám vlastně totálně vyhnout. Což je stejně správně. try/catch bloky jsou náročné na výpočetní výkon a navíc v UI aplikaci by se vůbec exceptiony neměly používat. Validace musí být součástí business logiky a jakýkoliv „nevalidní stav“ je něco, co nemělo vyhazovat exceptiony, natož shazovat celou aplikaci.

Průser je v tom, že NSwag vybleje klienta, který chrlí exceptiony jak hovado. openapi-generator používá v tomto ohledu trochu lepší strategii skrz staticky nastavený ExceptionHandler.

Pokud programujete REST api, pak pro každý endpoint vracíte různé HTTP status kódy. 200 pro vytažení dat, 204 pro změny, 400 pro validaci, 403 pro autorizaci atd. apod. Generátory mají tendenci na vše, co není 2xx, vyhodit exceptionu.

Co s tím?

Generované modely, vlastní API klient

Zkoušel jsem různé konfigurace a nastavení i s openapi-generatorem, (který jsem kvůli jiným důvodům nechtěl použít), ale nakonec jsem to vzdal, vždycky jsem narazil na nějaké dost podstatné limitace, které mi vadily a rozhodl jsem se napsat si vlastního klienta.

Modely ale naštěstí psát nemusím. Konfigurace NSwagu obsahuje parametry generateClientClasses a generateClientInterfaces. Pokud tyto parametry nastavím na false tak mi NSwag vygeneruje pouze request/response modely ale žádné klienty mi necpe.

Jednoduchého klienta jsem si napsal takto:

  • Při zavolání jakékoliv API metody dodávám zároveň funkci, která zpracovává validní odpověď (HTTP 2xx) a volitelně funkci, která zpracovává jakoukoliv jinou, než nevalidní odpověď (HTTP 4xx, 5xx).
  • Ve výchozím chování v případě nevalidní odpovědi chci, aby mi to vyplivlo nějaký popup, ve kterém je zobrazená generická zpráva o tom, co se z API vyplivlo. Toto chování ale mohu kdykoliv upravit.

Nemusím se tedy bát, že by mi NSwag vyhodil nějakou exceptionu a ani se nemusím starat o to, jak metody NSwagu wrapovat do nějakého try/catche, kterému se stejně chci v kódu vyhnout, pokud je to možné. Jasně – musím si napsat vlastního klienta ale protože kód pro mobil má sdílenou knihovnu s API tak URL endpointů mohu napsat staticky (parametrizované endpointy v tomhle použití budou muset být metody, což není vůbec hezké ale to jsem schopný zkousnout).

Ukázka volání API:

await Api.Get<GuideRequest>(url: Endpoints.User.GetLastGuideRequest, async (gr) =>
{
	if (gr != null && gr.State == GuideRequestState.Waiting)
	{
		LayoutState = LayoutState.Custom;
		CustomState = "RequestPending";
	}
	else
	{
		LayoutState = LayoutState.Success;
	}
});

Nastavení prefixu v RedLock.net nugetu

RedLock.net je knihovna pro lockování nad Redisem skrz algoritmus, který umí locky využívat distribuovaně napříč několika redis instancemi. Což v tuto chvíli nepotřebuji ale je dobré začít implementovat s něčím, co funguje i nad jednou instancí a je to potenciálně škálovatelné.

Locky, které knihovna vytváří, mají nastavený prefix redlock: což se mi úplně nehodí, společně s lockama jsem chtěl ještě ukládat informaci o počtu čekajících vláken pro monitoring. Nakonec to není tak těžké, prefix lze změnit skrz veřejnou propertu. Stačí jen při inicializaci RedLockFactory v IoC nastavit toto.

sc.AddSingleton(sp =>
{
    var redLockMx = (RedLockMultiplexer)(ConnectionMultiplexer)sp.GetService<IConnectionMultiplexer>();
    redLockMx.RedisKeyFormat = "myprefix:{0}";
    return RedLockFactory.Create(new List<RedLockMultiplexer> { redLockMx });
});

Bacha na to, že RedLockMultiplexer má implicitní konverzi z ConnectionMultiplexer

A taky bacha na to, že u prefixu nesmíte zapomenout na {0} což nahradí název locku samotného.

Přecházím z CosmosDB na MongoDB

CosmosDB je NoSQL databázový systém který provozuje Microsoft na Azure. Chtěl jsem na něm stavět novou aplikaci z následujících důvodů:

  • CosmosDB je PaaS (Platform As A Service). Nechci vůbec řešit provoz konkrétních aplikací na serverech (IaaS), chci, aby všechno bylo čistě cloudové a pokud možno se nechci vůbec zabývat monitorováním VMek.
  • V CosmosDB člověk musí pochopit jen logiku toho, jakým způsobem volba partition klíče v dokumentu ovlivňuje distribuci mezi logickými a fyzickými partitions.
  • Skrz dobře zvolenou partition strategii pak v CosmosDB nemusíte vůbec řešit škálování a platíte pouze za propustnost v jednotkách RU (Request Units) za vteřinu.
    (Rok 2021: minimum je 400 RU/s, 100 RU/hodina = 0,008 USD = ~$23 USD za měsíc)
  • V serverless režimu platíte za určitý celkový počet RU a ne za propustnost. Server se škáluje automaticky.
  • Indexování nad všemi hodnotami dokumentu by default (v MongoDB je by default indexace pouze nad primárním klíčem).

Na CosmosDB se mi ale od začátku některé věci nelíbily.

  • Chybějící podpora hromadného smazání/aktualizace všech dokumentů.
  • V platebním režimu RU/s CosmosDB vyhazuje chybu při překročení této propustnosti. Při programování aplikace člověk musí myslet na to, že každé volání CosmosDB může tuto chybu vyprodukovat.
    • Jasně, aplikace by se měly stavět resilientně a v dobře postavené aplikaci by se vždy mělo počítat s pádem jakékoliv externí komponenty.
    • Jenže tohle prostě znamená práci navíc. Toto mě nutí monitorovat metriky RU/s a monitorovat, jestli je z pohledu této metriky aplikace pro koncové uživatele vůbec použitelná.
  • Serverless mi nešlo založit v data centru Germany West Central (Frankfurt). Ind z Azure podpory mi řekl, že na portálu to sice není ale že to založím přes az cli nebo powershell. To jsem zkoušel ale dostal jsem HTTP 503 Service Unavailable. Ve West Europe mi to fungovalo ale WE (Nizozemí) je o 10ms dál než Frankfurt a u API pro mobilní aplikace se každých 10ms počítá. Dál jsem to neřešil.
  • Úchylné SDK pro C#, které se nedá prostě používát normálně napřímo a člověk si spíš k němu musí napsat nějaké vlastní extension metody. Viz. kód níže z oficiálního ukázkového repa
QueryDefinition query = new QueryDefinition("SELECT * FROM Families f WHERE f.id = @id AND f.Address.City = @city");

List<Family> results = new List<Family>();
using (FeedIterator<Family> resultSetIterator = container.GetItemQueryIterator<Family>(...))
{
    //Abych dostal všechny resulty, musím čekat na HasMoreResults=false
    //a dokud je to true, musí přečíst dostupnou kolekci dat skrz ReadNextAsync.
    while (resultSetIterator.HasMoreResults)
    {
        FeedResponse<Family> response = await resultSetIterator.ReadNextAsync();
        results.AddRange(response);
    }
}

//Nelíbí se mi, že tohle je způsob, který MS prezentuje jako to, jak by se měl CosmosDB používat.
//Proč prostě by default neexistuje v SDK metoda, která vrací IEnumerable<Model> nebo IAsyncEnumerable<Model> která skrývá tento implementační detail s HasMoreResults a ReadNextAsync?
//Proč musím psát vlastní extension metody?

CosmosDB Emulator = naprostý konec

Všechny výše zmíněné nevýhody pro mě ale stále nebyly tak významné. Automatická škálovatelnost schovaná za jednotky propustnosti RU/s a serverless varianta jsou prostě fakt skvělá lákadla. Jako programátor jsem tak nucený se soustředit pouze na to, že správně používám klíče a mám rozdistribuované partition klíče tak, aby CosmosDB mohl automatizovaně shardovat moje data bez větších omezení podle libosti. (Nutno dodat, že nemám zkušenosti s tím, jak CosmosDB doopravdy partitionuje ve vysoké zátěži, vycházím z informací zde a zde.)

Důvod, proč od CosmosDB odcházím je ten jejich posranej emulator.

Like seriously.

Microsoft místo toho, aby napsal emulátor který emuluje chování skutečného CosmosDB tak podle mého názoru vzali zdrojáky, kterým provozují skutečný CosmosDB na infrastruktuře Azure, ten zdroják jenom totálně ořezali a vznikla tak naprostá sračka, se kterou se nedá dobře pracovat.

Pokud si stáhnete emulator napřímo pod Windowsama a z C# kódu se na ten emulator připojíte, všechno funguje celkem dobře. Některý věci teda trvaj fakt relativně dlouho: funkce jako CreateDatabaseIfNotExistsAsync nebo CreateContainerIfNotExistsAsync trvají stovky milisekund až vteřiny. Funkce pro vytvoření databáze/kontejnerů jsou fakt zrovna podstatný pro psaní integračních testů (o tom ale víc níže).

Nefunkční a nepoužitelný docker container a vynucené HTTPS

Co už ale začne bejt dost na hovno je rozeběhnutí CosmosDB v docker containeru. Taková věc je totiž fakt docela potřeba, že jo, kvůli integračním testům. Já prostě nechci provozovat integrační testy proti ostré, placené verzi na cloudu. Já chci v rámci CI v nějaké build pipelajně rozchodit CosmosDB docker kontejner a proti němu spustit integrační testy.

Jenže ejhle.

Existují 2 verze docker kontejneru s CosmosDB emulátorem. Jedna je pro Windows, jehož image má 3GB a v nějakém build agentovi to prostě zabere deset minut, než se stáhne a nastartuje. Další verze je Linuxová, ta je podstatně menší.

Obě verze mají ale problém v tom, že CosmosDB emulátor generuje HTTPS certifikát při každém startu nejspíš proti IP adrese kontejneru. Takže v daném prostředí je nutné po nastartování kontejneru certifikát importovat. Takže musíte v CI pipelajně řešit a testovat sadu příkazů jako je tato:

ipaddr="`ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -n 1`"
curl -k https://$ipaddr:8081/_explorer/emulator.pem > ~/emulatorcert.crt
cp ~/emulatorcert.crt /usr/local/share/ca-certificates/
update-ca-certificates

A to prostě zabírá mrtě zbytečnýho času tohle v CI pipelajnách otestovat a zprovoznit. Velmi brzy zjistíte, kvůli problémům popsaným v kapitolách níže, že ten kontejner je prostě kurevsky nestabilní.

Proč to prostě nemůže fungovat hned? Proč nemůžu udělat něco jako docker run -p 27017:27017 mongo a všechno začne magicky fungovat, aniž bych se musel srát s nějakými certifikáty?

Řešením je vypnout ověřování certifikátu na straně aplikace ale s linuxový docker containerem se mi to stejně nepodařilo stabilně zprovoznit.

Divná a nesmyslná omezení

Na téhle stránce se dočtete podivnost, kterou je emulátor zatížen. Napovídá to tomu, že Microsoft nedělal žádný emulátor ale vzal zdrojáky z kódu, který provozuje CosmosDB na Azure infrastruktuře a nějak ho brutálně ořezal, aby to vůbec bylo použitelné. Některé věci dávají smysl z logiky věci, např. žádná podpora consistency levelů. Správný emulátor by dle mého názoru měl simulovat pouze API a neměl by vůbec obsahovat žádný sdílený kód s emulovanou věcí.

Nejzvláštnější je toto:

The emulator is not a scalable service and it doesn’t support a large number of containers. When using the Azure Cosmos DB Emulator, by default, you can create up to 25 fixed size containers at 400 RU/s (only supported using Azure Cosmos DB SDKs), or 5 unlimited containers. For more information on how to change this value, see Set the PartitionCount value article.

https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator?tabs=ssl-netstd21#differences-between-the-emulator-and-the-cloud-service

Proboha proč? Proč mě ten emulátor omezuje na 25 kontejnerů s fixní propustností nebo jen 5 kontejnerů s neomezenou propustnosti? Proč nemůžu založit neomezené množství kontejnerů? Proč si nemůžu nastavit propustnost tak jak chci stejně jako v ostré verzi? Tohle je zrovna věc, kterou emulátor vůbec emulovat vůbec nemusí nebo jen volitelně. Vždyť i všechny ty kvóty lze support requestem rozšířit.

Okay, pokud to chci zvýšit, musím při zapnutí emulátoru nastavit ručně PartitionCount argument. Tohle už je technologický detail emulátoru, který nechci řešit a absolutně mě nezajímá. V cloudové verzi vůbec žádný PartitionCount neřešíte tak proč to musím řešit v emulátoru?

Nepredikovatelné HTTP 503 errory

Při vývoji C# aplikace náhodně dochází k HTTP 503 chybám. Na tohle prostě nevím, co říct víc. Když jsem zkusil přejít na ostrou, cloudovou verzi CosmosDB tak k těm chybám nedocházelo.

Nepoužitelnost pro automatizované integrační testy

V rámci integračního testu chci postavit kompletní infrastrukturu aplikace se všemi souvisejícími komponentami jako databáze, cache apod. a proti této simulaci chci postavit čistou databázi a nad touto databází provést nějaký test (toto má smysl pouze u projektů kde dává smysl koncept „čistého prostředí“).

U integračních testů je v pořádku, že nějakou dobu trvají. Nevadí mi, pokud integrační test běží vteřinu ale už mi vadí, pokud běží desítky vteřin. U CosmosDB jsem zjistil, že prostě opakované volání vytvoření databáze, vytvoření kontejneru a následně shození kontejneru a shození databáze se prostě nasčítává až na desítky vteřin a to jen při hrstce testů.

Vzhledem k těm limitacím musíte taky hodně myslet na to, že nad emulátorem prostě nelze pouštět integrační testy paralelně. CosmosDB se z toho úplně sesype, jak nějaká traumatizovaná žába a emulátor vám začne vracet HTTP 503 i při vysoce nastaveném PartitionCount. Když jsem všech těch 10 integračních testů v xUnitu hodil do stejné [Collection] tak, aby se spouštěly v sérii za sebou, tak vše fungovalo ale běželo to skoro minutu.

Přecházím na MongoDB

S CosmosDB mi došla trpělivost, hlavně tedy kvůli tomu emulátoru. Je to nástroj, na který se nedá spolehnout a pokud databázové prostředí, nad kterým chci pracovat, nemohu dobře provozovat zdarma a lokálně z emulátoru a jsem nucený řešit záležitosti emulátoru, který mi má život zjednodušit a ne ztížit, pokud se nemohu soustředit na vývoj aplikace a musím řešit technologické záležitosti emulátoru, pak je prostě načase jít jinam.

Rozhodl jsem se pro MongoDB konkrétně MongoDB Atlas protože chci PaaS a vážně se mi nechce se starat o žádná VMka.

Jak se nastartuje lokálně MongoDB?

docker run --name mongo -p 27017:27017 mongo

A funguje to.

A jak se k němu připojím?

new MongoClient("mongodb://localhost:27017");

A funguje to. A neřeším žádný hovadiny.

Jak rychle běží integrační testy? Rychle a paralelně, 20 integračních testů je za vteřinu hotových.

MongoDB stránkování pro C#

Následující kód jsem od někud ukradl a trochu modifikoval. Jedná se o extension metodu, kterou lze zavolat nad IMongoCollection<TDocument> a vrátí to výsledek typu ListResult<TDocument> (jeho definice je níže) který obsahuje:

  • data
  • celkový počet dokumentů (estimatedDocumentCount má rychlost O(1))
  • filtrovaný počet dokumentů
  • počet stránek

Nepodařilo se mi ale zjistit, jak sloučit ten estimatedDocumentCount s tím agregovaným výstupem. V tuto chvíli tato extension metoda volá MongoDB 2x: nejdřív to vrátí agregovaný výstup skrz aggregate a pak se k tomu připojí ten estimatedDocumentCount. Líbilo by se mi, kdyby to bylo jen 1 volání ale nepřišel jsem na to, jak ten estimatedDocumentCount do toho aggregate výstupu dostat.

public static async Task<ListResult<TDocument>> ListResultAsync<TDocument>(
        this IMongoCollection<TDocument> collection,
        int pageIndex = 0,
        int pageSize = 10,
        FilterDefinition<TDocument> filterDefinition = null,
        SortDefinition<TDocument> sortDefinition = null)
    {
        var countFacet = AggregateFacet.Create("count",
            PipelineDefinition<TDocument, AggregateCountResult>.Create(new[]
            {
                PipelineStageDefinitionBuilder.Count<TDocument>()
            }));

        var stages = new List<PipelineStageDefinition<TDocument, TDocument>>();
        if (sortDefinition != null)
            stages.Add(PipelineStageDefinitionBuilder.Sort(sortDefinition));
        stages.AddRange(new[]
        {
            PipelineStageDefinitionBuilder.Skip<TDocument>(pageIndex * pageSize),
            PipelineStageDefinitionBuilder.Limit<TDocument>(pageSize)
        });
        var dataFacet = AggregateFacet.Create("data", PipelineDefinition<TDocument, TDocument>.Create(stages));

        var aggregationQuery = collection.Aggregate();
        if (filterDefinition != null)
            aggregationQuery = aggregationQuery.Match(filterDefinition);

        var aggregation = await aggregationQuery.Facet(countFacet, dataFacet).ToListAsync();

        var count = aggregation.First()
            .Facets.First(x => x.Name == "count")
            .Output<AggregateCountResult>()
            .FirstOrDefault()
            .Count;

        var data = aggregation.First()
            .Facets.First(x => x.Name == "data")
            .Output<TDocument>();

        return new ListResult<TDocument>
        {
            Data = data,
            TotalRowsUnfiltered = await collection.EstimatedDocumentCountAsync(),
            TotalPagesFiltered = (int)Math.Ceiling((double)count / pageSize),
            TotalRowsFiltered = count
        };
    }

A zde je ten ListResult<TDocument>

public class ListResult<T>
{
    public long TotalRowsUnfiltered { get; set; }
    public long TotalRowsFiltered { get; set; }
    public int TotalPagesFiltered { get; set; }
    public IEnumerable<T> Data { get; set; }
}