Clean Architecture + module
Backend-ul ASYDATA respectă o Clean Architecture strictă combinată cu un sistem de module tip Odoo: fiecare modul de business este un assembly separat, activabil per tenant și extensibil fără a modifica codul existent. Această pagină descrie direcția dependențelor, regulile de cod și mecanica modulelor.
Straturile și direcția dependențelor
Secțiune intitulată „Straturile și direcția dependențelor”Dependențele merg într-o singură direcție. Domain nu cunoaște nimic; fiecare strat exterior
cunoaște doar straturile dinspre interior.
Domain ◄── Application ◄── Platform ◄── Infrastructure ◄── WebApi ◄── Modules.* ◄───────────┘| Strat | Proiect | Responsabilitate | Cunoaște |
|---|---|---|---|
| Domain | Asy.Domain | Entități, value objects, domain events, Result<T> | nimic |
| Application | Asy.Application | Contracte de servicii (I…Service), DTO-uri, abstracțiuni (tenancy, auth) | Domain |
| Platform | Asy.Platform | Infrastructura de module, tenancy, filtre de autorizare | Application |
| Infrastructure | Asy.Infrastructure | EF Core, persistență, mesagerie Kafka, auth, integrări | Application, Platform |
| Modules.* | src/Modules/Asy.Modules.* | Module de business (servicii + endpoints) | Application, Platform |
| WebApi | Asy.WebApi | Compoziție, pipeline HTTP, endpoints, OpenAPI | toate |
Domain nu cunoaște Application. Modules.* și Infrastructure se așază pe Application +
Platform, dar nu se referențiază reciproc — de aceea filtrele partajate (ex. RequirePermission)
trăiesc în Asy.Platform, accesibil din ambele.
Reguli de cod
Secțiune intitulată „Reguli de cod”| Regulă | Detaliu |
|---|---|
| Naming | Prefix Asy. (NU Asydata.). Sub-foldere = numele bounded context-ului (Companies/, Banking/…) |
| Service pattern, NU CQRS | ICompaniesService are metode normale CreateAsync/UpdateAsync/…. Fără dispatcher, fără MediatR, fără Wolverine |
Result<T> pentru erori | Serviciile întorc Result<T> cu IsSuccess/Error; endpoint-urile mapează Error.Kind la status HTTP. Fără excepții pentru fluxuri de business |
| Build curat | TreatWarningsAsErrors=true; analyzer-ele prea zeloase se suprimă punctual prin <NoWarn>, nu se dezactivează global |
| Strongly-typed IDs | CompanyId, TenantId, UserId = readonly record struct(Guid Value), UUID v7 cu Guid.CreateVersion7() |
Sistemul de module Odoo-style
Secțiune intitulată „Sistemul de module Odoo-style”Cerința de produs: un modul = unitate izolată, activabilă per tenant, customizabilă prin
extindere fără a modifica codul modulului existent. Fiecare modul de business e un .csproj
separat sub src/Modules/ care implementează IAsyModule
(asydata-app-backend-dotnet/src/Asy.Platform/Modules/IAsyModule.cs).
Contractul IAsyModule
Secțiune intitulată „Contractul IAsyModule”Fiecare modul declară un Manifest și trei hook-uri de ciclu de viață:
| Membru | Rol |
|---|---|
Manifest | Key, Label, IsCore, IsDefaultEnabled, SortOrder, Version, Dependencies |
ConfigureServices(...) | Înregistrare DI a serviciilor modulului (la build) |
MapEndpoints(...) | Endpoint-urile HTTP ale modulului (la runtime) |
SeedAsync(...) | Date inițiale, idempotent (la runtime) |
Un modul IsCore nu poate fi dezactivat per tenant. Vezi CompaniesModule ca template:
asydata-app-backend-dotnet/src/Modules/Asy.Modules.Companies/CompaniesModule.cs.
ModuleLoader — descoperire și sortare
Secțiune intitulată „ModuleLoader — descoperire și sortare”La startup, ModuleLoader.AddAsyModules(...)
(asydata-app-backend-dotnet/src/Asy.Platform/Modules/ModuleLoader.cs):
- scanează assembly-urile date pentru tipuri ce implementează
IAsyModule(constructor fără parametri); - sortează topologic după
Manifest.Dependencies(cu detectare de cicluri și de dependențe lipsă; tie-break peSortOrder); - apelează
ConfigureServicespe fiecare modul, în ordine; - înregistrează
IModuleRegistryca singleton.
După Build(): SeedAsyModulesAsync() rulează SeedAsync pe fiecare modul, apoi MapAsyModules()
invocă MapEndpoints.
Modulele curente (mai 2026): companies (core), contacts, banking, clients, efactura,
msign, tasks — vezi lista încărcată la startup în
asydata-app-backend-dotnet/src/Asy.WebApi/Program.cs.
Gating per tenant: RequireModule
Secțiune intitulată „Gating per tenant: RequireModule”Un endpoint poate cere ca un modul să fie activ pentru tenantul curent:
routes.MapGet("/api/firms/{firmId}/clients", ListClientsAsync) .RequireModule("clients");Filtrul (asydata-app-backend-dotnet/src/Asy.Platform/Modules/RequireModuleFilter.cs) verifică
modulele activate pentru tenantul rezolvat și întoarce 403 dacă modulul nu e activ (sau 401 dacă
tenantul nu e rezolvat). Catalogul per-tenant va sta în platform_modules / company_modules;
deocamdată e servit de InMemoryTenantDirectory
(asydata-app-backend-dotnet/src/Asy.Infrastructure/Tenancy/InMemoryTenantDirectory.cs).
Pentru gating pe rol/acțiune (RBAC), există filtrul complementar RequirePermission("efactura", "view") (asydata-app-backend-dotnet/src/Asy.Platform/Authorization/RequirePermission.cs) — vezi
Autentificare și autorizare și
Actori și roluri.
Cele 3 pattern-uri de extindere
Secțiune intitulată „Cele 3 pattern-uri de extindere”Regula de aur: niciodată nu modifici codul unui modul existent ca să adaugi o customizare — adaugi un modul nou care se atașează. În ordinea cuplării:
| Pattern | Când se folosește | Cuplaj |
|---|---|---|
| Domain events (Kafka) | Cross-modul fără referințe directe (ex. OfferAccepted declanșează facturare) | Minim |
| Keyed DI services (.NET 8+) | Strategy-swap per tier (ex. pricing IT-Park vs Standard) | Mediu |
| Decorator (Scrutor) | Pre/post wrap (ex. audit log peste orice strategie) | Strict |
// Keyed DI — alegi strategia după config-ul tenantuluiservices.AddKeyedScoped<IOfferPricingStrategy, StandardPricingStrategy>("standard");services.AddKeyedScoped<IOfferPricingStrategy, ItParkPricingStrategy>("it-park");
// Decorator — wrap fără înlocuireservices.Decorate<IOfferPricingStrategy, AuditLogPricingDecorator>();Sunt plugin-uri în-binar: nu se evaluează cod customer la runtime, nu se încarcă DLL-uri dinamic de pe disc.
Custom fields (extensie de date)
Secțiune intitulată „Custom fields (extensie de date)”Echivalentul _inherit din Odoo pentru date, fără DDL per tenant:
field_definitions(company_id, entity_type, key, field_type, options, is_required, …)— registry de definiții;- coloane
custom_fields jsonbpe entitățile core (companies,clients,tasks…); - validare la write printr-un
CustomFieldsService; - frontend-ul randează formularele dinamic pe baza definițiilor.
Zero DDL per tenant, zero deploy per tenant — tot ce e custom trăiește în date.
Legături
Secțiune intitulată „Legături”- Componentele deployabile: Context de sistem.
- Izolarea datelor per tenant: Multi-tenancy și RLS.
- Decuplarea cross-modul: Flux event-driven.
- Lista bounded context-urilor: Harta domeniilor.