20 KiB
Wholesale Framework — tervezési összefoglaló (handoff-mag)
Status: Proposed (Draft) — 2026-07-14 Jelleg: Ez NEM ADR és NEM végleges doksi. Provizórikus gondolkodási összefoglaló, ideiglenesen a FruitBankHybridApp
docs/mappájában. A tárgyalt Wholesale framework-repo még nem létezik, a neve változhat, és semmilyen döntés nem végleges. Amint a repo létrejön, ez a fájl a forrása a rendes struktúrának (ADR-ek + repo-doksik) — lásd a záró "Mi lesz ebből" szakaszt. Előzmény: két korábbi session terv-összegzése (app-oldal + nop-plugin-oldal) + ez a session konszolidációja.
1. Cél és kiváltó ok
A FruitBank (zöldség-gyümölcs nagyker, nopCommerce-plugin + Blazor Hybrid app) mellé indul a VeggieVoyage (veggievoyage.hu), egy közel azonos nagyker-rendszer apró eltérésekkel. Emellett több további zöldséges nagyker érdeklődő is van a rendszerre. Cél: egy közös nagyker-framework réteg, amelyből minden nagyker-termék származik, hogy egy új terméknél a lehető legkevesebbet kelljen újraírni.
Jelenleg (2026-07-14) csak a FruitBank-oldal létezik; sem a Wholesale-repo/könyvtár, sem a VeggieVoyage-projekt nincs még létrehozva.
2. Architektúra-döntés: base-class minta (maximalista pull-up)
Öröklés-alapú, NEM típus-átköltöztetés. A közös rétegbe szinte minden osztály felkerül — abstract vagy virtual formában (base class-ok, virtual method-ok/property-k). A konkrét (gyakran szinte üres) záró osztályok a termék-repóban élnek és származnak a base-ből.
- Amit lehet, a közös rétegben implementálunk (virtual default üzleti logika); ami termék-specifikus, az a származtatottban override-ol.
- Az eltérés a fejlesztés közben derül ki, nem előre. Default: minden felmegy közös virtual defaultként. Amikor a VeggieVoyage (vagy egy későbbi termék) fejlesztése bizonyítja, hogy valami mégsem közös, akkor az a member lemegy ("demote") a záró rétegbe. Amíg csak egy termék használ egy membert, a lehúzása senkit nem tör.
- Ez a meglévő házi minta kiterjesztése:
MgStockTakingItem<TStockTaking, TProduct> : MgEntityBase,PartnerBase → CargoPartner,MeasuringItemPalletBase → 3 pallet,AcConstabstract-constants minta.
Miért ez nyert (a mozgatásos alternatívával szemben)
| Aggály | Feloldás |
|---|---|
Tábla-nevek (fbShipping…) |
[Table(Name=…)] a származtatott osztályon, termék-konstansból — nincs migráció, nincs runtime-konfig |
AcBinary wire-compat (SignalDataType = AssemblyQualifiedName) |
Konkrét típusok helyben maradnak a termék-assembly-ben → AQN változatlan, nincs lockstep-kényszer |
AcBinaryForcedInit (AssemblyLoadContext-quirk) |
Generált szerializálók a konkrét típusokra → meglévő ForceRegister érintetlen |
LinqToDB NameCompatibility |
Termék-pluginban marad, konkrét típusokra |
Következmény: a FruitBank-oldal minden pull-up lépésnél fordítás-azonos marad; a VeggieVoyage viszont "majdnem készen" indul, mert a scaffold záró osztályai szinte üresek.
Új DB + új plugin a VeggieVoyage-nek
- A VeggieVoyage teljesen új adatbázist kap → a
[Table(Name=…)](identitás) így is a záró rétegen marad; a base hordozza az összes[Column]-t, a tábla-NÉV termék-konstans. Avv/fbprefix szabad termék-döntés, nem framework-ügy. - A VeggieVoyage új nopCommerce-plugin is → nem csak az üzleti logikát, hanem a plugin-infrastruktúrát is
base-formában ki kell szervezni: install/uninstall, locale-resource telepítés, admin-menü regisztráció,
settings-defaultok, schedule task-ok,
MgwPluginStartupBasevirtual DI-regisztrációkkal. A Veggie-pluginban ideálisan csakplugin.json(SystemName), a záró osztályok és a konstansok maradnak. - Zöldmezős schema-generálás: a friss install-nak a teljes sémát egy menetben kell felhúznia üres DB-n (a FruitBank táblái évek alatt, lépésenként jöttek létre). Ellenőrizendő, hogy a meglévő install/tábla-létrehozó út (MgDbContext-vonal) teljes és determinisztikus-e nulláról — GenericAttribute-kulcsok és seed-adatok is.
3. Rétegzés / célstruktúra
Rétegtérkép a legáltalánosabbtól (legmélyebb) a legspecifikusabbig:
L0 AyCode.Core (Ac*) + AyCode.Blazor — tech-framework, domain-mentes, bármely projekt
SignalR, AcBinary + SourceGenerator, logolás, NAV/EKÁER-protokoll, MgGrid/komponens-base-ek
L2 Mango.Nop Libraries (Mg*) — nop-generikus, nop-VERZIÓFÜGGŐ (4.70 mappa alatt)
L2.5 Mango.Nop.Wholesale.Core / .Services — nagyker-domain, nop-VERZIÓFÜGGŐ, prefix: Mgw
(Libraries alatt, 4.70) entitás-bázisok, DbTable/DbContext-bázisok, endpoint-bázisok, EKÁER-riport
L2.5 Wholesale.Common / .Common.Server — nagyker-domain, nop-verzió-FÜGGETLEN (ÚJ repo)
domain-modell, interfészek, enumok, DTO-bázisok, SignalR-tag core-tartomány, mérés-helperek,
service-bázisok (a Mango.Nop DLL-jei UTÁN build-el — pont mint ma a Common-ok)
L2.5 Wholesale.Shared (Blazor, ÚJ repo) — grid-/komponens-base-ek, SignalR-kliens base
L3 Termékek: FruitBank / VeggieVoyage / … — vékony záró osztályok + identitás + termék-delta
Kétotthonos base-réteg — a besorolás kritériuma
A "hova kerüljön" kritérium NEM az, hogy használ-e Mango.Nop-ot (a Common-ok ma is jogosan használják), hanem hogy nop-verziófüggő-e:
- nop-verziófüggő (Mg*/linq2db/nopCommerce-entitásokra épül, a nop belső API-ját tükrözi) →
Mango.Nop.Wholesale(Libraries, verziómappa alatt). - nop-verzió-független nagyker-domain →
Wholesale.Common/.Common.Server(új repo).
Fontos: a mai
FruitBank.Common.Server"nopCommerce integration"-t is tartalmaz. Pull-upnál a nop-ot érintő részek aMango.Nop.Wholesale.Services-be mennek, nem aWholesale.Common.Server-be. A kritérium szigorúan "nop-verziófüggő?", nem projektnév.
Build-lánc (interleaved — ma is így épül, csak beékelődik két réteg)
AyCode → Mango.Nop Libraries → Mango.Nop.Wholesale.Core (entitás-bázisok)
→ Wholesale.Common / .Common.Server (domain- és service-bázisok, Mgw.Core-ra épülve)
→ Mango.Nop.Wholesale.Services (DbTable/DbContext/endpoint-bázisok, Wholesale.Common-ra épülve)
→ termékek (HybridApp + plugin)
Ezért a Mango.Nop.Wholesale Core/Services szétvágása kötelező, nem ízlés: a Core a Wholesale.Common ALATT,
a Services FELETTE — különben körkörös függőség.
Per-app bin-profil
A referencia-minta már ma per-app build-profilos: az AyCode és Mango.Nop.Core DLL-jei
bin\FruitBank\$(Configuration)\net9.0-ból jönnek (HintPath). Lesz bin\VeggieVoyage\… is. TFM-ek: Common=net9.0
(WASM-kliens is fogyasztja, szerver-only függőség nélkül), Shared=net10.0. A FruitBankHybrid.Shared.Common
projekt ma üres váz — felhasználható.
4. Placement-kánon (a legfontosabb szabály)
A kód a legmélyebb rétegbe kerül, amelynek a domain-hatóköre még jogosan magába foglalja a fogalmat.
Két teszt együtt dönt:
- Kimenő-függőség teszt (szükséges, objektív): egy típus akkor mehet mélyebbre, ha a függőségei engedik.
AyCode-ba csak az kerülhet, ami nulla nagyker/nop fogalomra hivatkozik (a
using-ok és hivatkozott típusok döntik el, nem érzés). Ez adja meg, milyen mélyre mehetne a kód. - Hatókör-kohézió teszt (elégséges, logikai): hova való a fogalom. Ez megállíthatja a kódot, mielőtt a függőségei alapján túl mélyre süllyedne.
Kanonikus példa — tálca/tare-aritmetika: tiszta decimal-matematika, nulla domain-függőséggel lefordulna
AyCode.Core-ban. Mégis a Wholesale-be való, mert a fogalom hatóköre "nagyker-mérés". Core-ba nem azért nem
kerül, mert nem fordulna, hanem mert a Core hatóköre (minden projekt) tágabb, mint a fogalom valódi közönsége.
Két hibamód
- Túl mélyre (nagyker-fogalom Core-ban): a univerzális réteg beszennyeződik, a nagyker-fejlesztő nem is ott keresi. Ez a csábító hiba ("ez matek, mehet Core-ba"). Fordul, csak koszol → csendes hiba, ezt kell fogni.
- Túl sekélyen (a termékben): redundancia — a következő zöldséges projekt újraírja. Ezt a framework szünteti meg.
Iránytű: a redundancia-kerülés lefelé (általánosabb felé) tol; a hatókör-kohézió a fék. A hiba-irány aszimmetrikus: túl mélyre tett domain-kód nem fordul (a rendszer önvédő); túl magasra tett specifikus kód fordul, csak koszol. Ezért a default "a legmélyebb, ami fordul", és a feljebb-tétel az, ami indoklást igényel.
Redundancia-ellensúly (hogy a DRY ne forduljon visszájára)
"Ne írd le kétszer ugyanazt a fogalmat" — NEM "vonj össze mindent, ami hasonlít". Ha két dolog csak véletlenül hasonlít (nem ugyanaz a fogalom) és közös base-be húzod, hamis csatolást hozol létre — a legrosszabb fajta redundancia-"javítás". A rule-of-two/N a védőkorlát: legalább két valódi fogyasztó bizonyítsa, hogy a közösség valódi és nem esetleges.
Identitás soha nem megy fel (checklist a záró réteghez)
[Table(Name=…)]tábla-nevek (termék-konstansból)[AcBinarySerializable]konkrét típusok- SignalRTag termék-tartománybeli értékei
- plugin SystemName, route-URL-ek, locale-fájlok, branding
- appsettings értékek (nem a séma)
Hol él a kánon (doc-layering)
A placement-kánon kánonja a legmélyebb rétegben (AyCode.Core) él; minden alsóbb réteg copilot-instructions.md-je erre mutat + a saját befogadási kritériumát mondja ki (AyCode = "domain-mentes"; Wholesale = "nagyker-domain, nop-verzió-független"; Mango.Nop = "nop-verziófüggő"). A doksi vigyen 3-4 levezetett határeset-példát (a tare-képlet a kanonikus) — az absztrakt szabály nem elég horgony, a gondolkodásmódot a példák tanítják.
5. Zöldség-kalibráció + jövőbeli továbbbontás (elhalasztott)
Az érdeklődők egyelőre mind zöldséges nagykerek, közel azonos igénnyel. Ezért a Wholesale-réteg mai valódi hatóköre "zöldséges nagyker", nem "általános nagyker" — a "zöldséges-specifikus vs. általános-nagyker" hasítás jövőbeli, szándékosan még meg nem tett rétegvágás.
Következmények:
- A zöldséges-specifikusság ma NEM tolja fel a kódot a termékbe. Egy kimondottan zöldséges logika (érettség, osztályozás, zöldség-mérés) jogosan a framework-rétegé — bármely másik zöldséges nagyker újra használná. Óvakodni attól, hogy "ez túl zöldség-specifikus, hagyjuk a termékben" — ez kiéheztetné a framework-öt.
- NE annotáld előre a "zöldséges vs. általános" varratot — nincs második nem-zöldséges domain, ami bizonyítaná, tehát bármilyen jelölés spekuláció (ugyanaz a korai-triázs-csapda). A jövőbeli nem-zöldséges nagyker fogja felfedni a varratot, ugyanúgy, ahogy a Veggie-scaffold felfedi a termék-hardcode-ot.
- Ha egyszer jön nem-zöldséges nagyker: ugyanaz a mechanikus pull-up egy szinttel lejjebb (pl.
Trade/Distributionréteg), a Wholesale maga válik vékony leszármazottá. Mivel minden abstract/virtual, ez is fordítás-azonos lesz. - A rétegnév lehet jövő-álló ("Wholesale", prefix
Mgw), de a doksinak ki kell mondania a mai zöldség-kalibrációt, hogy egy jövőbeli session ne nézze design-hibának és ne kezdjen kéretlenül általánosítani.
6. nopCommerce-verzió probléma + karantén-stratégia
Kiváltó félelem: a nopCommerce design sok hibája miatt gyakran nop-kódot kell másolni/átírni → nagyon nehéz a FruitBank upgrade-je. A VeggieVoyage viszont már új nop-verzióval készülne. Kockázat: ha a catalog-táblák változtak, az gond lehet.
A rétegzés maga a válasz — csak explicit szabályként kell kimondani:
-
A verziófüggés karanténban van. A
Mango.Nop Libraries+Mango.Nop.Wholesaledefiníció szerint nop-verziófüggő (ezért él verziómappa alatt). Új nop-verziónál ez a két réteg forkol (4.90/Libraries,4.90/…Wholesale), és a per-app bin-profil ezt kiszolgálja: a FruitBank a 4.80-as fork ellen build-el, a VeggieVoyage a 4.90-es ellen — ugyanabból a Wholesale-forrásból. Feltétel: a Mg*/Mgw típusok publikus felülete forrás-kompatibilis maradjon a forkok között. A Mango.Nop réteg így anti-korrupciós réteg: a nop churn a fork belsejében marad, a Wholesale nem látja. -
Másolt/átírt nop-kód (a tényleges upgrade-fájdalom): jól megjelölt helyre (pl.
NopOverrides/a verziófüggő rétegben), mindegyik mellé kötelező jegyzet: miért kellett, melyik nop-fájlból, mit kell újranézni port-nál. Így a 4.90-port checklist lesz, nem régészet. -
Catalog-tábla drift: (a) minden catalog-hozzáférés a verziófüggő rétegben, a Wholesale csak interfészt/DTO-t lát; (b) a GenericAttribute-minta (IsMeasurable, Tare, AverageWeight, IncomingQuantity, NetWeight mind GA-ban, nem catalog-oszlopban) verzió-rezisztens — a GA a nop legstabilabb mechanizmusa. A kitettség arra szűkül, amit tényleg a Product/Order/Customer natív oszlopaiból olvastok.
-
A két nagy műveletet ne egyszerre. Előbb pull-up a mostani nop-verzión (FruitBank fordítás-azonos, a Veggie-scaffold ugyanazon a verzión bizonyítja a framework-öt), utána a verziófüggő réteg portja az új verzióra. Egyszerre futtatva minden hiba kétesélyes lenne ("framework-bug vagy port-bug?").
-
Reframe: a VeggieVoyage kifizeti a FruitBank upgrade-adósságát. Ha a Veggie kikényszeríti a Mango.Nop(+Wholesale) új-verziós portját, a FruitBank későbbi upgrade-je már csak "bin-profil átállítás a kész forkra + termékrétegbeli maradék".
Konkrét vizsgálat, amivel a catalog-félelem végessé tehető (még nem futott le):
- (a) Kitettség-térkép: végigmenni a plugin + Libraries kódon, mely nop-entitásokat/service-eket/natív oszlopokat használtok közvetlenül.
- (b) Séma-diff a cél nop-verzióval, a ténylegesen használt táblákra (a nop upgrade-migrációiból). → Cél nop-verzió megadása után elvégezhető.
7. Eltérés-taxonómia: settings vs. override (rule-of-N)
Több terméknél a "rule-of-two" rule-of-N lesz. Ökölszabály:
- parametrikus eltérés → settings/config (EkaerSettings-minta, DI mögé)
- strukturális eltérés → virtual override
- identitás → abstract const a záró rétegen
Ha ugyanazt a metódust minden termék máshogy override-olja, az annak a jele, hogy paraméternek kellett volna lennie. Minél több megy settings-be, annál kevesebb kódot kell egy új terméknél megírni. Nem kell előre kitalált settings-taxonómia — demote-to-settings on evidence, ugyanaz a workflow, mint a kódnál: minden fent marad közös defaultként, és az első valódi termék-eltérésnél dől el, override vagy setting lesz.
Demote-ablak szűkül N-nel: amíg egy termék használ egy membert, a lehúzása fájdalommentes; a 2. fogyasztónál már közös és fent marad. Ezért a Veggie-fázisban agresszíven demote-olni.
Termék-scaffold = template. A Veggie "új termék felhúzása" maga is termék: minden scaffold-lépést checklistként dokumentálni a Wholesale-repo doksijába — a 3. terméknél már ez A termék-létrehozási folyamat. (Nem kell most dotnet-template; a jól dokumentált Veggie-scaffold az első draft.)
Framework-evolúció N fogyasztóval: minden wholesale-változás N terméket érint. Per-app bin-profil jó izoláció
(mindenki a saját build-jét pinneli), de wire-formátumot (AcBinary) és base-séma [Column]-okat érintő változásnál
CONSUMERS.md szerinti végigfuttatás kell (P1).
SignalRTag-tartomány kiosztás (hogy két termék soha ne ütközzön közös infrastruktúrán): közös core-tartomány + termék-tartományok, pl. FruitBank 0–9999, VeggieVoyage 10000–19999, 3. termék 20000+…
8. Pilotban igazolandó mechanizmusok (a maximalista minta ezekre tesz fel mindent)
Az első pull-up ELŐTT explicit tesztelendő:
[Association]keresztreferenciák öröklésnél — interfész-kulcsú társítás (OtherKey = nameof(IShipping.Id)) VAGY a navigációk a leszármazottban maradnak (base = skalárok + FK int-ek), entitásonként döntve.- AcBinary öröklött property-kkel — a
MeasuringItemPalletBaseminta szerint; az első költözőnél explicit teszt. [SignalR]attribútum öröklött kontroller-metóduson — az AyCode dispatcher végpont-felderítése lássa a base-metódusokat.- Razor
@typeparama DevExpress template-ekkel — hol megy, hol marad vékony app-razor.
9. Migrációs fázisterv ("pull-up", lépésenként, FruitBank végig fordítás-azonos)
- Skeleton repo + per-app bin-profil bekötés + mechanikus bázis-kiemelés (interfészek, enumok, konstans-bázisok) — compile+wire-identical.
- Pilot: a legkisebb/legtisztább gráf végigvitele (jelölt: Cargo — legkisebb; vagy EKÁER-lánc — legfrissebb). Itt bizonyítjuk a 8. szakasz mechanizmusait.
- Data-réteg bázisok (DbContext/DbTable).
- Service-ek domain-enként (mérés → preorder → cargo → invoicing), FruitBank-osztályok vékony leszármazottá.
- VeggieVoyage-plugin scaffold záró-osztályokkal — itt bukik ki a maradék hardcode.
- Blazor Shared (grid-/komponens-base-ek).
- Verziófüggő réteg portja az új nop-verzióra (a pull-up UTÁN, külön lépés).
10. Meta-protokoll teendők (amikor a repo létrejön)
- Új repo:
.github/copilot-instructions.md+@repoblokk, repo-prefix (self-service aREPO_PREFIXES.mdszerint; korábbi javaslatok: nop-oldali libreMGNOPWHOLE, a nop-független Wholesale repóra pl.MGWHOLE— eldöntendő), layer-szám. - Class-prefix:
Mgw(Mango Wholesale) — collision-check lefutott, tiszta (aWs,Wh/Whs,Nkelvetve). A doksi tisztázza: a prefix jövő-álló, de a mai tartalom zöldség-kalibrált. - Doc-layering: a defining layerrel a doksik is költöznek — a FruitBank-plugin
PREORDER/doksija + aPREO/EKAER/ORDDtopic-kódok a wholesale-registry-be; a termék-pluginban pointer + delta marad. CONSUMERS.mda Wholesale-repóban (FruitBank, VeggieVoyage, …).LLMP-DECbejegyzés az új repo létéről (workspace-szignifikáns).- ADR-ek a végleges struktúrában (lásd lent).
11. Nyitott kérdések
- Wholesale-repo neve és pontos szeparációja (egy repo Common + Common.Server + Shared, vagy több?).
- Cél nop-verzió a VeggieVoyage-hez (ez indítja a 6/(b) séma-diffet).
- Repo-prefix(ek) véglegesítése.
- Generic-paraméter-robbanás mély gráfoknál (
MgwShippingDocumentBase<TShipping, TPartner, TShippingItem, …>) — a házi minta elfogadta, de ez az ára; entitásonként figyelni. Mango.NopvsWholesalehatár entitásonként: ami bármely nop-projektnek jó → Mango.Nop; ami nagyker-domain → Wholesale.
12. Mi lesz ebből (amint van repo)
Ez a fájl a forrás; a rendes struktúrába szétszedve:
- ADR-ek (
<repo>/docs/adr/NNNN-*.md, Nygard-stílus):base-class-pull-up-vs-type-move(2. szakasz)wholesale-layer-split-and-nop-version-quarantine(3. + 6. szakasz)placement-canon(4. szakasz) — vagy CONVENTIONS.md-be, a repo szokása szerint
- Repo-doksik: ARCHITECTURE (rétegtérkép + build-lánc), CONVENTIONS (placement-kánon + tare-példa + zöldség-kalibráció), CONSUMERS.md, termék-scaffold checklist.
- LLMP-DEC sor: az új repo léte + a protokoll-relevanciák.
- A FruitBank-oldali doc-layering pointer-ek frissítése.
Ez a WHOLESALE_FRAMEWORK_PLAN.md a szétszedés után archiválható vagy törölhető (a tartalma a végleges
helyekre kerül).