Projektstrukturierung
Lino strukturiert .NET-Lösungen so, dass Architekturentscheidungen ab dem ersten Commit explizit sind. Ein generiertes Projekt startet bereits mit Aspire-Orchestrierung, gemeinsamen Bausteinen, Tests, Konfiguration fĂŒr CodequalitĂ€t und einem klaren Platz fĂŒr jeden Service, jedes Modul, jeden API-Vertrag, jede Persistenzaufgabe und jede Integrationsgrenze.
Dieser Abschnitt erklÀrt, wie lino project new, lino service new und lino module new die Lösung formen. Ziel ist nicht nur das Erstellen von Ordnern, sondern das Definieren von runtime-Grenzen, Datenbank-ownership, Modul-Isolation und einem Entwicklungspfad von einem einfachen Service zu einem modularen Monolithen oder verteilten System.
Grundlage der Lösung erstellen
Der Befehl lino project new erstellt die technische Grundlage einer neuen .NET-Lösung. FĂŒhren Sie ihn nach Installation und Authentifizierung der CLI in einem leeren Verzeichnis aus.
lino project new --name <ProjectName>
Das Argument <ProjectName> steht fĂŒr den tatsĂ€chlichen Namen der Lösung. Dieser Name wird Teil von namespaces, assemblies, Pfaden, Konfigurationen, Artefakten und Referenzen zwischen Komponenten; wĂ€hlen Sie deshalb einen kurzen, stabilen und reprĂ€sentativen Namen.
Der interaktive Assistent fragt Entscheidungen ab, die die Struktur und das runtime-Verhalten der gesamten Lösung beeinflussen:
- Projekt-Namespace: technische Root-IdentitÀt, die von den generierten Projekten verwendet wird.
- Anzeigename: lesbarer Name, der in generierten Metadaten und benutzersichtbaren Stellen verwendet wird.
- Sprache und stack: derzeit C# mit .NET 10 und Aspire.
- Code-Analyzer: aktivieren gemeinsame Pakete und Regeln, um Konsistenz und QualitÀt ab dem bootstrap zu sichern.
- CQRS: bereitet die Application-Schicht darauf vor, commands und queries zu trennen, orchestriert durch die ausgewÀhlte mediator-Bibliothek.
- Basisklassen in der Lösung: steuert, ob gemeinsame Abstraktionen lokal innerhalb der Lösung generiert werden.
- Verteilter Cache: legt fest, ob
Microsoft.Extensions.Caching.Hybridnur den lokalen Speicher der Instanz oder zusÀtzlich eine verteilte Schicht mit von Aspire konfiguriertem Redis verwendet. - Asynchrone Kommunikation: aktiviert RabbitMQ mit MassTransit und die Messaging/outbox-Bausteine, die von Integrationsereignissen verwendet werden.
- Datensprache: Sprache, die wÀhrend der Generierung zur Beschreibung von Domain-Metadaten verwendet wird.
- Von der Anwendung unterstĂŒtzte Kulturen: Lokalisierungsressourcen, die fĂŒr UI-Texte, Validierungen, Fehler und API-Antworten generiert werden.
- Standardkultur: Hauptsprache, die verwendet wird, wenn die Anwendung einen fallback benötigt.
Nach der BestĂ€tigung generiert Lino eine auf Wachstum ausgelegte Lösung. Ein minimales Projekt startet mit Aspire, gemeinsamen Schichten und Tests fĂŒr den Shared-Bereich:
<ProjectName>/
âââ <ProjectName>.slnx
âââ Directory.Build.props
âââ Directory.Packages.props
âââ src/
â âââ Aspire/
â â âââ AppHost/
â â â âââ <ProjectName>.AppHost.csproj
â â âââ ServiceDefaults/
â â âââ <ProjectName>.ServiceDefaults.csproj
â âââ Services/
â âââ Shared/
â âââ Api/
â â âââ <ProjectName>.Shared.Api.csproj
â âââ Application/
â â âââ <ProjectName>.Shared.Application.csproj
â âââ Domain/
â â âââ <ProjectName>.Shared.Domain.csproj
â âââ Infrastructure/
â â âââ <ProjectName>.Shared.Infrastructure.csproj
â âââ Infrastructure.Persistence/
â â âââ <ProjectName>.Shared.Infrastructure.Persistence.csproj
â âââ Integration.Events/ (wenn Messaging vorhanden ist)
â âââ <ProjectName>.Shared.Integration.Events.csproj
âââ tests/
âââ Services/
âââ Shared/
âââ UnitTests/
âââ Domain/
â âââ <ProjectName>.Shared.Domain.UnitTests.csproj
âââ Application/
âââ <ProjectName>.Shared.Application.UnitTests.csproj
Die Rolle der Shared-Projekte
Shared enthĂ€lt Plattformcode, der von der Lösung geteilt wird. Es ist kein fachlicher Service. Nutzen Sie diesen Bereich fĂŒr querschnittliche Abstraktionen, gemeinsame Fehler, Lokalisierungsinfrastruktur, BasisvertrĂ€ge der Anwendung, Persistenz-helper, API-Erweiterungen, Host-Integration, Observability und wiederverwendbare technische Utilities.
Vermeiden Sie GeschĂ€ftsregeln in Shared. Wenn eine Regel zu einer bestimmten FĂ€higkeit der Anwendung gehört, sollte sie im Service oder Modul liegen, das diese Verantwortung besitzt. Ziel von Shared ist es, technische Wiederholung zu reduzieren, nicht eine AbkĂŒrzung zur Kopplung unterschiedlicher Domains zu werden.
Aspire und Infrastrukturentscheidungen
AppHost komponiert die Lösung und ihre runtime-Ressourcen. Wenn Redis, RabbitMQ, SQL Server, PostgreSQL, Redis Insight, Services, WebApps oder workers hinzugefĂŒgt werden, wird Aspire zum lokalen Orchestrierungspunkt. Das vereinfacht AusfĂŒhrung, service discovery, Logs, Metriken, traces und Ressourcenvisualisierung wĂ€hrend der Entwicklung.
ServiceDefaults zentralisiert Hosting-Muster wie service discovery, health checks, Resilienz, logging, Metriken, tracing und OpenTelemetry-Integration. Statt dass jeder Service dies isoliert konfiguriert, startet die Lösung mit einem gemeinsamen Kompositionspunkt.
Code-Analyzer
Statische Code-Analyzer prĂŒfen den Code wĂ€hrend der Entwicklung und machen Probleme vor der AusfĂŒhrung sichtbar: Stilinkonsistenzen, fragile Muster, Refactoring-Möglichkeiten, mögliche Bugs und Sicherheitswarnungen.
Wenn Sie Analyzer in lino project new aktivieren, startet die Lösung bereits mit zentral konfigurierten Paketen wie StyleCop.Analyzers, SonarAnalyzer.CSharp und Roslynator.Analyzers. Dadurch muss nicht jedes Projekt selbst entscheiden, welcher QualitÀtsstandard gilt.
- QualitÀtsverbesserung: hÀlt Code lesbar, konsistent und an den Standards der Lösung ausgerichtet.
- Fehlervermeidung: weist frĂŒh auf Probleme hin, bevor sie manuelle Tests oder Produktion erreichen.
- Standardisierung: reduziert Stilunterschiede zwischen Services, Modulen und Teams.
- UnterstĂŒtztes Refactoring: hebt mögliche Vereinfachungen und Modernisierungsmöglichkeiten in C# hervor.
Verteilter Cache, Hybrid-Cache und Redis
Lino bereitet die Lösung darauf vor, Microsoft.Extensions.Caching.Hybrid zu verwenden, die Microsoft-Bibliothek, die Cache-Operationen ĂŒber HybridCache zentralisiert. Diese Abstraktion erlaubt handlers, Application Services und Infrastrukturkomponenten, Abfrageergebnisse, Berechtigungen, Einstellungen oder unterstĂŒtzende Daten zu speichern, ohne Cache-Implementierungsdetails im Code zu verteilen.
Wenn Sie bei der Projekterstellung keinen verteilten Cache aktivieren, bleibt HybridCache verfĂŒgbar, arbeitet aber nur mit dem lokalen Speicher der laufenden Instanz. Dieser Modus ist einfach und ausreichend fĂŒr lokale Szenarien, kleine Umgebungen oder Anwendungen mit einer einzelnen Replik, aber jeder Prozess hĂ€lt seinen eigenen Cache und Daten werden nicht zwischen Instanzen geteilt.
Wenn Sie verteilten Cache aktivieren, fĂŒgt Lino Redis zu den Aspire-Ressourcen hinzu und konfiguriert die Infrastruktur fĂŒr eine gemeinsame Cache-Schicht. Mit Redis können mehrere Instanzen desselben Service dieselbe Schicht abfragen, wiederholte Lesezugriffe auf Datenbanken, interne APIs oder Integrationen reduzieren und das Projekt auf horizontale Skalierung vorbereiten.
- Performance: reduziert Antwortzeiten bei wiederholten Lesezugriffen und unterstĂŒtzenden Operationen.
- Skalierbarkeit: erlaubt unterschiedlichen Instanzen, zwischengespeicherte Daten zu teilen.
- VerfĂŒgbarkeit: entkoppelt einen Teil der Leselast von der primĂ€ren Datenbank.
- Betriebskosten: reduziert wiederholte Verarbeitung in hochvolumigen Abfragen und Integrationen.
Diese Entscheidung wird in lino project new getroffen, weil sie die Grundlage der Umgebung verĂ€ndert: AppHost-Ressourcen, lokale secrets, Pakete, Infrastrukturkonfiguration und die im Aspire-Dashboard angezeigte AusfĂŒhrungstopologie.
Asynchrone Kommunikation
Asynchrone Kommunikation erlaubt Services, Modulen und Komponenten, auf Fakten des Systems zu reagieren, ohne den Hauptfluss der Operation zu blockieren. Sie ist besonders nĂŒtzlich, wenn der Produzent nicht von der unmittelbaren VerfĂŒgbarkeit des Konsumenten abhĂ€ngen soll oder wenn eine Aktion spĂ€tere Effekte auslösen muss, etwa Benachrichtigungen, Projektionen, externe Integrationen oder Synchronisierung zwischen Kontexten.
Wenn Sie asynchrone Kommunikation aktivieren, fĂŒgt Lino RabbitMQ zu Aspire hinzu und konfiguriert MassTransit zusammen mit den Messaging- und outbox-Bausteinen, die von Integrationsereignissen verwendet werden. Ereignisse, die von Services oder Modulen veröffentlicht werden, sind dadurch als explizite VertrĂ€ge in Integration.Events reprĂ€sentiert und können resilienter verarbeitet werden.
- Performance: erlaubt dem Haupt-Use-Case fortzufahren, ohne auf den Abschluss aller Konsumenten zu warten.
- Skalierbarkeit: verteilt Verarbeitung ĂŒber Konsumenten und Queues und fĂ€ngt Lastspitzen kontrollierter ab.
- Resilienz: ermöglicht Wiederverarbeitung und reduziert Nachrichtenverlust in Kombination mit outbox.
- Entkopplung: vermeidet direkte AbhÀngigkeit zwischen Produzent und Konsument, wenn keine sofortige Antwort erforderlich ist.
Verwenden Sie Integrationsereignisse, wenn ein Fehler des Konsumenten die Transaktion des Produzenten nicht rĂŒckgĂ€ngig machen soll. Wenn der Konsument sofort antworten muss, um den Use Case abzuschlieĂen, bevorzugen Sie eine explizite synchrone Integration und behandeln Sie VerfĂŒgbarkeit, timeout und fallback als Teil des Vertrags.
Lokalisierung und Kulturen
Wenn Kulturen ausgewĂ€hlt werden, generiert Lino Ressourcen, damit Nachrichten, Validierungen, labels, Fehler und UI-Texte von Beginn an lokalisiert werden können. Das vermeidet, Internationalisierung als spĂ€ten Patch zu behandeln, nachdem strings bereits ĂŒber Endpunkte, handlers und Komponenten verteilt sind.
Grundlage validieren und zu Services wechseln
Stellen Sie nach der Generierung AbhĂ€ngigkeiten wieder her, bauen Sie die Lösung und fĂŒhren Sie den Aspire-Host aus:
dotnet restore <ProjectName>.slnx dotnet build <ProjectName>.slnx dotnet run --project src/Aspire/AppHost/<ProjectName>.AppHost.csproj
In diesem Stadium ist die Lösung noch eine Grundlage. Ăffnen Sie das Projekt im Editor, prĂŒfen Sie die von Aspire erstellten Ressourcen, bestĂ€tigen Sie die Entscheidungen zu Cache, Messaging, Lokalisierung und CodequalitĂ€t und wechseln Sie dann zu den Services, die die GeschĂ€ftsfĂ€higkeiten der Anwendung reprĂ€sentieren.
Services erstellen und verwalten
Ein Service ist eine Grenze fĂŒr runtime und ownership. In Lino können Services unabhĂ€ngige APIs in einem verteilten System oder gröĂere GeschĂ€ftsbereiche innerhalb einer Lösung reprĂ€sentieren, die schrittweise wachsen kann.
Nachdem Sie die Projektgrundlage erstellt haben, fĂŒgen Sie Services hinzu mit:
lino service new
Der Service-Assistent fragt Folgendes ab:
- Service-Namespace: technischer Name, der in Ordnern, Projekten und namespaces verwendet wird.
- Anzeigename: lesbarer Name des Service.
- Service-Typ: wÀhlen Sie zwischen einfach und modular.
- Datenbankprovider: wĂ€hlen Sie SQL Server oder PostgreSQL fĂŒr die Datenbank des Service.
- Architekturstil: derzeit Clean Architecture fĂŒr einfache Services.
- Strongly Typed IDs: legt bei einfachen Services fest, ob Bezeichner als dedizierte Typen generiert werden.
Service-Typen
Einfacher Service: eine direktere Struktur fĂŒr eine GeschĂ€ftsfĂ€higkeit mit klarer Grenze. Das ist eine gute Wahl fĂŒr eine fokussierte API, einen Microservice oder einen Bereich, der klein genug ist, um als Einheit weiterentwickelt zu werden.
Modularer Service: eine Struktur fĂŒr gröĂere Systeme, modulare Monolithen oder runtimes, die mehrere unabhĂ€ngige FĂ€higkeiten hosten mĂŒssen. Module verbessern Organisation und Skalierbarkeit des Modells, erfordern aber mehr Disziplin bei AbhĂ€ngigkeiten.
UnabhĂ€ngig vom Typ besitzt jeder Service seine eigene Datenbank. Bei einfachen Services wird die Entscheidung ĂŒber Strongly Typed IDs auf Service-Ebene getroffen; bei modularen Services pro Modul.
Einfacher Service
Ein einfacher Service legt seine Schichten direkt unter src/Services/<ServiceName> ab. Verwenden Sie diese Struktur, wenn die GeschĂ€ftsfĂ€higkeit eine klare Grenze besitzt und keine mehreren isolierten Module innerhalb desselben runtime benötigt. Sie funktioniert gut fĂŒr eine fokussierte API, einen Microservice oder einen Domain-Bereich, der klein genug ist, um als Einheit weiterentwickelt zu werden.
src/Services/<ServiceName>/
âââ Domain/
â âââ <ProjectName>.<ServiceName>.Domain.csproj
âââ Application/
â âââ <ProjectName>.<ServiceName>.Application.csproj
âââ Infrastructure.Persistence/
â âââ <ProjectName>.<ServiceName>.Infrastructure.Persistence.csproj
âââ Infrastructure/
â âââ <ProjectName>.<ServiceName>.Infrastructure.csproj
âââ Api/
â âââ <ProjectName>.<ServiceName>.Api.csproj
âââ Integration.Events/ (wenn Messaging vorhanden ist)
â âââ <ProjectName>.<ServiceName>.Integration.Events.csproj
âââ Api.Contracts/ (wenn typisierter HTTP-Konsum vorhanden ist)
â âââ <ProjectName>.<ServiceName>.Api.Contracts.csproj
âââ Api.Client/ (wenn typisierter HTTP-Konsum vorhanden ist)
âââ <ProjectName>.<ServiceName>.Api.Client.csproj
tests/Services/<ServiceName>/
âââ UnitTests/
â âââ Domain/
â â âââ <ProjectName>.<ServiceName>.Domain.UnitTests.csproj
â âââ Application/
â âââ <ProjectName>.<ServiceName>.Application.UnitTests.csproj
âââ IntegrationTests/
âââ <ProjectName>.<ServiceName>.IntegrationTests.csproj
Die Schichten haben unterschiedliche Verantwortlichkeiten: Domain schĂŒtzt GeschĂ€ftsregeln und Invarianten; Application orchestriert Use Cases; Infrastructure.Persistence enthĂ€lt Entity Framework Core, Repositories, Unit of Work und migrations; Infrastructure enthĂ€lt technische Integrationen; und Api passt HTTP an Use Cases an.
Die Projekte Api.Contracts und Api.Client erscheinen, wenn typisierter HTTP-Konsum vorhanden ist, besonders in Lösungen mit einer Blazor Web App. Das erste bĂŒndelt requests, responses, DTOs, öffentliche Typen und gemeinsame Client-Interfaces; das zweite stellt die HTTP-Implementierung dieser Interfaces mit HttpClient bereit, damit Blazor die generierten APIs ohne doppelte VertrĂ€ge konsumiert.
Integration.Events erscheint, wenn die Lösung Messaging aktiviert hat. Es enthÀlt Integrationsereignisse, die von anderen Modulen, Services oder Systemen veröffentlicht und konsumiert werden, normalerweise zusammen mit Messaging- und outbox-Infrastruktur.
Modularer Service
Ein modularer Service eignet sich fĂŒr modulare Monolithen oder fĂŒr ein runtime, das mehrere bounded contexts hostet. Der Service selbst besitzt Host, gemeinsame Infrastruktur und Datenbankprovider; GeschĂ€ftsregeln leben innerhalb der Module.
src/Services/<ServiceName>/ âââ Host/ â âââ <ProjectName>.<ServiceName>.Host.csproj âââ Infrastructure/ â âââ <ProjectName>.<ServiceName>.Infrastructure.csproj âââ Modules/ tests/Services/<ServiceName>/ âââ Modules/
Host komponiert Module, Einstellungen, Endpunkte und gemeinsame Infrastruktur dieses Service. Er sollte keine GeschĂ€ftsregeln enthalten. Das Projekt Infrastructure auf Service-Ebene stellt technische KompositionsunterstĂŒtzung bereit, die von Modulen geteilt wird.
Wenn ein Modul hinzugefĂŒgt wird, erscheinen die Projekte Domain, Application, Infrastructure.Persistence, Infrastructure und Api innerhalb von Modules/<ModuleName> und erhalten die interne Grenze. Api.Contracts, Api.Client und Integration.Events folgen denselben Bedingungen: typisierter HTTP-Konsum fĂŒr Blazor/API clients und aktiviertes Messaging fĂŒr Integrationsereignisse.
Datenbank-ownership
Jeder Service besitzt seine eigene Datenbank. In einer Lösung mit mehreren Services kann ein Service PostgreSQL verwenden, wÀhrend ein anderer SQL Server verwendet. Diese Entscheidung kann je nach Domain-Bedarf, Performance, Betrieb, Reife des Teams oder Integration mit vorhandener Infrastruktur variieren.
In einem modularen Service gehört die Datenbank zum Service als runtime, aber die Module sind durch schema, Persistenzprojekte und eigene migrations isoliert. Ein Modul sollte Tabellen eines anderen Moduls nicht direkt abfragen, selbst wenn die Tabellen in derselben physischen Datenbank liegen.
Zwischen einfach und modular wÀhlen
WĂ€hlen Sie die kleinste Struktur, die die Grenze schĂŒtzt. Ein einfacher Service reicht aus, wenn es eine einzige Domain-Grenze gibt. Ein modularer Service ist sinnvoll, wenn mehrere FĂ€higkeiten runtime, deploy oder Transaktion teilen mĂŒssen, aber Modelle, Persistenz, APIs und Tests weiterhin getrennt bleiben sollen.
| Auswahl | Wann verwenden | Angenommener Aufwand |
|---|---|---|
| Einfacher Service | Eine fokussierte Domain, wenige interne Grenzen, isolierte API oder direkter Microservice. | Weniger Anfangsstruktur, aber weniger interne Isolation, wenn die Domain stark wÀchst. |
| Modularer Service | Mehrere Subdomains im selben runtime, modularer Monolith, SaaS mit unabhĂ€ngigen Bereichen oder Bedarf an schemas pro Modul. | Mehr Projekte, mehr Disziplin bei AbhĂ€ngigkeiten und gröĂere Aufmerksamkeit fĂŒr interne VertrĂ€ge. |
Architekturstil
Von Lino generierte Services folgen Clean Architecture, um GeschÀftsregeln von technischen Details zu trennen. Die Domain-Schicht muss HTTP, Entity Framework Core, Messaging, UI oder externe Provider nicht kennen; diese Anliegen bleiben an den RÀndern der Anwendung.
- Entkopplung: zentrale Regeln hÀngen nicht von Frameworks oder Auslieferungsmechanismen ab.
- Wartbarkeit: Ănderungen an Infrastruktur bleiben tendenziell von GeschĂ€ftsregeln isoliert.
- Testbarkeit: Use Cases und Domain-Code können mit weniger externen AbhÀngigkeiten getestet werden.
- Evolution: technische Details können mit geringerer Auswirkung auf den Kern des Service ersetzt werden.
Clean Architecture und Strongly Typed IDs
Generierte Services folgen Clean Architecture, damit GeschÀftscode nicht von HTTP, EF Core, Messaging, UI oder Infrastrukturdetails abhÀngt. Diese Trennung macht Tests direkter, reduziert Kopplung und erlaubt, technische Details zu ersetzen, ohne zentrale Regeln neu zu schreiben.
Strongly Typed IDs erhöhen die Sicherheit, indem sie versehentliches Vermischen von Bezeichnern verhindern. Statt irgendein Guid, long oder int zu akzeptieren, kann die Domain mit einem eigenen Typ fĂŒr jede EntitĂ€t oder jedes Aggregat arbeiten, normalerweise im Format <EntityName>Id. Das verhindert, dass der Bezeichner einer EntitĂ€t ĂŒbergeben wird, wo ein anderer erwartet wurde, und macht Signaturen in commands, queries, EntitĂ€ten, handlers und Mappings aussagekrĂ€ftiger.
- Typsicherheit: verhindert, dass Bezeichner unterschiedlicher EntitÀten versehentlich vermischt werden.
- Klarheit: macht Signaturen und VertrÀge ausdrucksstÀrker als lose primitive Typen.
- Refactoring: konzentriert Ănderungen an Format oder Serialisierung im entsprechenden ID-Typ.
- Fehlerreduzierung: macht falsche Verwendungen nach Möglichkeit bereits zur Compilezeit sichtbar.
Integration zwischen Services
Wenn ein Service auf einen anderen reagieren muss, bevorzugen Sie Integrationsereignisse, explizite HTTP-Integrationen oder bewusst replizierte Daten. Direkter Zugriff auf die Datenbank eines anderen Service erzeugt strukturelle Kopplung, erschwert migrations und macht unabhÀngige deploys riskanter.
NĂ€chste Schritte fĂŒr modulare Services
Nachdem ein modularer Service erstellt wurde, ist der nĂ€chste Schritt, Module mit lino module new hinzuzufĂŒgen. Jedes Modul sollte eine eigene GeschĂ€ftsfĂ€higkeit reprĂ€sentieren, mit Domain, Application, Persistenz, API, Tests und Integrationen, die die interne Grenze des Service erhalten.
Module erstellen und verwalten
Module existieren nur innerhalb modularer Services. Ein Modul reprÀsentiert eine fachliche Grenze innerhalb desselben runtime: Es besitzt sein eigenes Domain-Modell, eigene Use Cases, Persistenz, API-OberflÀche, Integrationsereignisse, Tests und ein eigenes Datenbankschema.
Verwenden Sie Module, wenn der Service mehr als eine GeschĂ€ftsfĂ€higkeit hosten muss, ohne EntitĂ€ten, Regeln, migrations und VertrĂ€ge zu vermischen. In einem modularen Monolithen kann alles im selben Prozess laufen, aber interne Grenzen bleiben wichtig: Echte Isolation ist Kontrolle ĂŒber AbhĂ€ngigkeiten, nicht nur Ordnertrennung.
lino module new --service <ServiceName>
WĂ€hrend der Erstellung fragt der Assistent Folgendes ab:
- Service: der modulare Service, der das Modul hostet. Lino erlaubt nicht, Module innerhalb einfacher Services zu erstellen, weil diese nicht die fĂŒr interne Isolation erforderliche Struktur besitzen.
- Modul-Namespace: der technische Name, der in Ordnern, namespaces, assemblies und Projekten verwendet wird.
- Anzeigename: der lesbare Modulname fĂŒr Stellen, die fĂŒr Menschen bestimmt sind.
- Strongly Typed IDs: legt fest, ob innerhalb des Moduls generierte Bezeichner dedizierte Typen verwenden, normalerweise im Format
<EntityName>Id.
Nach der BestĂ€tigung fĂŒgt Lino das Modul innerhalb des Service hinzu, ohne die Architekturgrenze aufzulösen:
src/Services/<ServiceName>/
âââ Host/
âââ Infrastructure/
âââ Modules/
âââ <ModuleName>/
âââ Domain/
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Domain.csproj
âââ Application/
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Application.csproj
âââ Infrastructure.Persistence/
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.Persistence.csproj
âââ Infrastructure/
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Infrastructure.csproj
âââ Api/
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Api.csproj
âââ Integration.Events/ (wenn Messaging vorhanden ist)
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Integration.Events.csproj
âââ Api.Contracts/ (wenn typisierter HTTP-Konsum vorhanden ist)
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Api.Contracts.csproj
âââ Api.Client/ (wenn typisierter HTTP-Konsum vorhanden ist)
âââ <ProjectName>.<ServiceName>.<ModuleName>.Api.Client.csproj
tests/Services/<ServiceName>/Modules/<ModuleName>/
âââ UnitTests/
â âââ Domain/
â â âââ <ProjectName>.<ServiceName>.<ModuleName>.Domain.UnitTests.csproj
â âââ Application/
â âââ <ProjectName>.<ServiceName>.<ModuleName>.Application.UnitTests.csproj
âââ IntegrationTests/
âââ <ProjectName>.<ServiceName>.<ModuleName>.IntegrationTests.csproj
Verantwortlichkeiten der generierten Artefakte
| Artefakt | Verantwortung innerhalb des Moduls |
|---|---|
| Domain | EntitÀten, Aggregate, Value Objects, enumerations, Domain Events, Repository-VertrÀge und Invarianten des Moduls. |
| Application | Use Cases, commands, queries, handlers, Validierungen, interne Ein- und AusgabevertrÀge sowie Orchestrierung der Modulregeln. |
| Infrastructure.Persistence | DbContext, Entity Framework Core-Konfigurationen, konkrete Repositories, Unit of Work und migrations des Moduls. |
| Infrastructure | Modulspezifische technische Implementierungen, Adapter, Provider und Dependency-Komposition. |
| Api | HTTP-Endpunkte, Versionierung, Filter, Autorisierung und Anpassung zwischen externen Requests und Use Cases. |
| Api.Contracts | Wird generiert, wenn typisierter HTTP-Konsum vorhanden ist, normalerweise durch Blazor Web Apps. EnthÀlt requests, responses, DTOs, öffentliche Typen und Client-Interfaces, die zwischen API und Konsument geteilt werden. |
| Api.Client | Wird zusammen mit den VertrÀgen generiert, wenn typisierter HTTP-Konsum vorhanden ist. EnthÀlt die HTTP-Implementierung der Interfaces mit HttpClient, providers, options und helpers, damit Blazor-Projekte die generierten APIs konsistent und stark typisiert konsumieren. |
| Integration.Events | Wird generiert, wenn das Projekt Messaging besitzt. EnthÀlt Integrationsereignisse, die von anderen Modulen, Services oder Systemen veröffentlicht und konsumiert werden, wobei der payload als expliziter Vertrag erhalten bleibt. |
Datenbankstruktur
Die Datenbank bleibt an den Service gebunden, nicht isoliert an das einzelne Modul. Innerhalb eines modularen Service wird jedes Modul durch sein eigenes schema in der zugehörigen Datenbank reprĂ€sentiert, mit eigenem Persistenzprojekt und eigenen migrations. Das bietet Isolation und Organisation, ohne mehrere physische Datenbanken fĂŒr jedes Modul zu verlangen.
Isolation und UnabhÀngigkeit zwischen Modulen
Ein Modul sollte nicht direkt auf den DbContext, die EntitĂ€ten, Repositories oder internen Services eines anderen Moduls zugreifen. Jedes Modul besitzt sein eigenes Modell und seine eigene Persistenz. Wenn ein anderes Modul Daten benötigt, verwenden Sie eine explizite Integration, statt die Grenze ĂŒber interne Implementierungsdetails zu ĂŒberschreiten.
Das verhindert, dass eine scheinbar lokale Ănderung einen anderen Bereich des Systems bricht. Wenn ein konsumierendes Modul Daten abfragen muss, die zu einem anderen Modul gehören, sollte es nicht von der vollstĂ€ndigen EntitĂ€t des Quellmoduls abhĂ€ngen. Es kann eine shadow entity mit den minimal erforderlichen Daten fĂŒr den eigenen Use Case halten, gespeist durch Integration oder Ereignis.
Vorteile dieser Entkopplung:
- Isolation: jedes Modul kann Regeln, Persistenz und Tests weiterentwickeln, ohne interne Details eines anderen Moduls zu ĂŒberschreiten.
- Organisation: die Anwendung respektiert bounded contexts und macht ownership explizit.
- FlexibilitĂ€t: Module können mit geringeren Auswirkungen auf den Rest des Service hinzugefĂŒgt, entfernt oder refaktoriert werden.
- Testbarkeit: jedes Modul kann unabhĂ€ngiger validiert werden, was das Vertrauen in Ănderungen erhöht.
Kommunikation zwischen Modulen
Verwenden Sie explizite VertrĂ€ge fĂŒr die Kommunikation. FĂŒr Aufrufe innerhalb desselben runtime kann eine interne Integration VertrĂ€ge in Integration.Contracts und, wenn generiert, eine in-process-Implementierung bereitstellen. FĂŒr Aufrufe zwischen runtimes verwenden Sie HTTP-Integration. FĂŒr asynchrone Weitergabe veröffentlichen Sie Integrationsereignisse in Integration.Events und verwenden die Messaging/outbox-Infrastruktur, wenn die Veröffentlichung Teil einer transaktionalen Operation ist.
Schemas, migrations und Datenbank
In einem modularen Service teilen sich die Module den Datenbankprovider des Service, aber jedes Modul wird durch sein eigenes schema und sein eigenes Projekt Infrastructure.Persistence reprÀsentiert. Migrations werden pro Modul generiert, sodass die Datenbankentwicklung an der fachlichen Grenze ausgerichtet bleibt.
Diese Trennung erlaubt jedem Modul, seine Tabellen, seeds, Indizes und foreign keys weiterzuentwickeln, ohne die Datenbank des Service in ein einziges, von allen geteiltes Modell zu verwandeln. Das schema ist eine technische Grenze, die die fachliche Grenze stÀrkt, selbst wenn Module im selben Prozess laufen und dieselbe physische Datenbank verwenden.
Gute Modulgrenzen
Erstellen Sie Module rund um GeschĂ€ftsfĂ€higkeiten, nicht rund um technische Schichten. Ein guter Modulname sollte eine fĂŒr die Domain erkennbare Verantwortung beschreiben, etwa einen Bereich, Prozess oder eine FĂ€higkeit mit eigenen Regeln. Vermeiden Sie generische Namen wie Common, Core oder Utilities fĂŒr GeschĂ€ftsregeln, weil sie ownership verbergen und dazu neigen, zu ĂŒbermĂ€Ăig geteilten AbhĂ€ngigkeiten zu werden.
- KohÀsion: das Modul sollte Sprache, Regeln und Daten enthalten, die sich gemeinsam Àndern.
- Autonomie: es sollte möglich sein, das Modul zu testen und weiterzuentwickeln, ohne auf interne EntitÀten eines anderen Moduls zuzugreifen.
- Klare VertrĂ€ge: Daten, die auĂerhalb des Moduls benötigt werden, sollten ĂŒber API, Integration, Ereignis oder shadow entity offengelegt werden, nicht durch direkten Datenbankzugriff.
- Geringe Kopplung: wenn zwei Module stĂ€ndig dieselben EntitĂ€ten Ă€ndern mĂŒssen, muss die Grenze wahrscheinlich ĂŒberprĂŒft werden.
Nachdem die Module erstellt wurden, zeigen die nÀchsten Dokumentationsthemen, wie EntitÀten, Value Objects, enumerations, commands, queries, APIs, Ereignisse, Integrationen und migrations innerhalb dieser Grenzen modelliert werden.
