Datenpersistenz verwalten

Die Datenpersistenz in Lino wird entlang der Grenzen der Clean Architecture generiert: Das Domain-Modell bleibt unabhängig von Entity Framework Core, während Datenbankbelange in Infrastructure.Persistence liegen. Dadurch bleiben Entitäten auf Geschäftsregeln fokussiert, und Mapping, provider configuration, migrations, repositories, Transaktionen und Initialisierung liegen in der Infrastrukturschicht.


Beim Erstellen eines Services mit lino service new speichert Lino wichtige Persistenzentscheidungen wie Service-Architektur und data provider. Die aktuellen provider options sind SqlServer und PostgreSql. In traditionellen Services wird die Persistenz direkt unterhalb des Services erzeugt. In modularen Services erhält jedes Modul sein eigenes Projekt Infrastructure.Persistence und seinen eigenen ApplicationDbContext, während die Datenbank vom Service gemeinsam genutzt wird.


In traditionellen oder modularen Services gibt es genau eine Datenbank pro Service. In modularen Services wird jedes Modul in ein eigenes schema innerhalb derselben Datenbank gemappt. So kann jeder bounded context einen getrennten logischen Namespace behalten, mit funktionaler Isolation, klarerer Versionierung und besser organisierten migrations.


Diese Seite erklaert, wie Lino Entity Framework Core configurations, DbContext-Registrierung, repositories, IUnitOfWork, Transaktionen, den Transactional Outbox flow und den vom CLI bereitgestellten migrations lifecycle organisiert.

Wichtig: Der generierte Persistenzcode ist production-oriented scaffolding. Prüfen Sie mappings, constraints, Indizes, delete behavior, Transaktionen und migration scripts immer, bevor Sie Änderungen auf gemeinsam genutzte Datenbanken oder Produktionsdatenbanken anwenden.

Entity-Typ-Konfigurationen

Lino folgt dem Prinzip Persistence-Ignorant: Domain-Entitäten kennen keine Infrastrukturdetails. Das gesamte ORM-Mapping liegt in Klassen, die IEntityTypeConfiguration<TEntity> implementieren und sich unter Infrastructure.Persistence/Configurations befinden. So beschreiben Entitäten das fachliche Verhalten, während die Infrastruktur definiert, wie sie gespeichert werden.

Die Konfigurationsdateien werden aus den Entscheidungen erzeugt, die im CLI getroffen wurden: primary keys, strongly typed IDs, Pflichtfelder, String-Längen, Value Objects, Beziehungen, Enumerationen, Audit-Felder, tenant fields und Kindentitäten. Das generierte Ergebnis sollte als solide Ausgangsbasis behandelt werden, nicht als etwas, das nie überprüft werden muss.

Was Konfigurationen typischerweise definieren

  • Tabelle und Schema: Tabellenname und Schema des aktuellen Services oder Moduls.
  • Primary keys: Entity-IDs, Konvertierungen von strongly typed IDs und Verhalten der Key-Generierung.
  • Properties: Pflichtfelder, maximale Länge, Spaltentypen, Enum-Persistenz, owned structures von Value Objects und Metadata-Spalten für Dateien.
  • Beziehungen: one-to-one, one-to-many, many-to-many, child collections, foreign keys und delete behavior.
  • Indizes und Constraints: Eindeutigkeit, Lookup-Performance, tenant-aware Eindeutigkeit und Konsistenz auf Datenbankebene.

Globale Konventionen

Wiederkehrende Konventionen können in ModelConfiguration oder vergleichbaren Helpers zentralisiert werden, zum Beispiel Decimal-Precision, Collation, DateTime-Konvertierung, naming convention, globale Filter, auditierbare Properties und gemeinsame Regeln für multi-tenant Felder.

Wie Konfigurationen angewendet werden

Der generierte ApplicationDbContext wendet die Konfigurationen in OnModelCreating an. Standardmäßig verwendet Lino Source Generators, um Konfigurationen performanter und ohne Reflection zu registrieren. Wenn assembly scanning verwendet wird, kann die Konfiguration so aussehen:

modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);

Wenn Messaging aktiviert ist, wendet Lino auch die Konfiguration von OutboxMessage an. Dadurch können Integration Events in derselben Datenbanktransaktion persistiert werden, bevor ein Worker sie asynchron veröffentlicht.

Review-Checklist

  1. Prüfen Sie, ob Tabellennamen, Schemas und Konventionen die Modulgrenze respektieren.
  2. Prüfen Sie String-Längen, Decimal-Precision, nullable Spalten und Enum-Persistenz, bevor Sie migrations erzeugen.
  3. Prüfen Sie Beziehungen und delete behavior sorgfältig, besonders bei aggregate roots und Kindentitäten.
  4. Fügen Sie Indizes für Queries hinzu oder passen Sie sie an, die von Grids, Filtern, Integrationen und background jobs verwendet werden.
  5. Führen Sie vor dem Erstellen einer migration einen build aus, damit EF Core ein konsistentes Modell sieht.

DbContexts

Der generierte ApplicationDbContext ist der EF Core context für die Persistenzgrenze eines Services oder Moduls. Er stellt DbSet<TEntity>-Properties für die gemappten Entitäten bereit und implementiert IApplicationDbContext. Dadurch können query handlers und Application Services von einer Application-Abstraktion statt von der konkreten Infrastrukturklasse abhängen.

Traditionelle Services und modulare Services

  • Traditioneller Service: Die Persistenz wird unter src/Services/<ServiceName>/Infrastructure.Persistence erzeugt, und die API des Services wird als startup project für migrations verwendet.
  • Modularer Service: Jedes Modul besitzt sein eigenes Projekt src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistence und seinen eigenen ApplicationDbContext. Der Host des Services wird als startup project für migrations verwendet.

In modularen Services macht diese Trennung jeden bounded context im Code explizit. Module können ihr Persistenzmodell unabhängig weiterentwickeln, selbst wenn sie in derselben Service-Datenbank laufen, sofern das die gewählt Architektur ist.

Provider-Registrierung und Konfiguration

Lino erzeugt die Persistenzregistrierung über eine Extension für IHostApplicationBuilder. Der generierte Code registriert Unit of Work, repositories, domain services, eine pooled factory für ApplicationDbContext, den scoped ApplicationDbContext und die Abstraktion IApplicationDbContext.

Die provider-spezifische Konfiguration wird entsprechend der für den Service ausgewählten Datenbank erzeugt:

  • UseSqlServer(...) für SQL Server Services.
  • UseNpgsql(...).UseSnakeCaseNamingConvention() für PostgreSQL Services.
  • MigrationsHistoryTable(Constants.Database.MigrationsHistoryTable, Constants.Database.Schema), um die migration history in der erwarteten Tabelle und im erwarteten Schema zu speichern.
  • Provider-spezifische constraint validators werden registriert, um Datenbankverletzungen in konsistente Application-Fehler umzuwandeln.

Tenant-aware contexts

Wenn ein Modul tenant-aware Entitäten enthält, kann der generierte Context Tenant-State und globale query filters enthalten. In diesem Szenario erstellt die scoped factory einen für den aktuellen Scope konfigurierten Context. Dadurch müssen Handler Tenant-Filter nicht an jeder Lese- und Schreibstelle manuell wiederholen.

Repositories

Lino generiert repository interfaces in der Domain-Schicht und konkrete Implementierungen in Infrastructure.Persistence/Repositories. Das erhält die Dependency-Richtung: Die Domain definiert den Persistenzvertrag, den sie benötigt, und die Infrastruktur liefert die Implementierung mit Entity Framework Core.

Konkrete repositories können auch in <ModuleName>/Infrastructure.Persistence.Repositories liegen und interfaces aus <ModuleName>/Domain.Repositories implementieren, je nach Struktur des Services. Entscheidend ist, dass Application und Domain nicht direkt von konkreten Details des EF Core abhängen.

Repositories werden hauptsächlich von command handlers und domain-orientierten Flows verwendet, die aggregates persistieren müssen. Query Handlers können IApplicationDbContext direkt verwenden, wenn sie optimierte Leseprojektionen, Filter, Pagination und Ergebnisse im DTO-Format benötigen.

Repository-Verantwortlichkeiten

  • Aggregate roots und die für command handlers benötigten Kinddaten laden.
  • Aggregates entsprechend dem Persistenzmodell hinzufügen, aktualisieren und entfernen.
  • Persistenzabfragen kapseln, die Teil des Aggregate-Verhaltens oder der Schreiborchestrierung sind.
  • EF Core-spezifische Operationen aus dem Domain-Projekt heraushalten.
  • Komplexe Abfragen kapseln, wenn sie zum Schreibfluss gehoeren, einschliesslich LINQ, FromSql und Hilfsprojektionen.

Praktische Hinweise

  • Verwenden Sie repositories für write use cases, die Aggregate-Konsistenz bewahren müssen.
  • Verwenden Sie query projections für Reads, statt vollständige aggregates nur zum Aufbau eines DTO zu laden.
  • Halten Sie repository-Methoden explizit; vermeiden Sie generische Methoden, die beliebige Persistenzoperationen für die Application freigeben.
  • Prüfen Sie includes und tracking, wenn ein Handler aggregate, child collection oder many-to-many Beziehung ändert.
  • Stellen Sie nur Methoden bereit, die Domain und Application benötigen, und halten Sie das repository aggregate-root-centric.

Unit of Work

IUnitOfWork ist die von Lino generierte transaktionale Grenze. Sie koordiniert Persistenz mit EF Core, Transaktionssteuerung, Veröffentlichung von Domain Events und Persistenz im Outbox für Integration Events. Handler verwenden diese Abstraktion, um Änderungen explizit zu bestätigen, statt DbContext.SaveChangesAsync direkt an jeder Stelle der Application aufzurufen.

In einfachen Szenarien reicht der DbContext selbst oft als Unit of Work aus. Trotzdem generiert Lino eine dedizierte UnitOfWork-Implementierung, um konsistente Kontrolle über Transaktionen, Events und Outbox-Integration zu bieten.

Generierte Operationen

  • SaveChangesAsync(cancellationToken): speichert Änderungen und veröffentlicht Domain Events, wenn konfiguriert.
  • SaveChangesAsync(publishDomainEvents, cancellationToken): erlaubt Speichern mit oder ohne Veröffentlichung von Domain Events.
  • SaveChangesInTransactionAsync(cancellationToken): öffnet eine Transaktion, speichert Änderungen und committed.
  • BeginTransactionAsync, CommitAsync und RollbackAsync: erlauben explizite Transaktionssteuerung.
  • CommitOrRollbackAsync: committed oder rollt die Transaktion auf Basis eines Result zurück.

Domain Events und Outbox

Wenn Domain Events vorhanden sind, verlangt Lino eine offene Transaktion, bevor sie veröffentlicht werden. Das ist beabsichtigt. Wenn ein Domain Event Integration Events erzeugt, werden diese Events im Outbox registriert und in derselben Transaktion persistiert. Danach kann ein Worker die Outbox-Einträge verarbeiten und Nachrichten über die konfigurierte Messaging-Infrastruktur veröffentlichen.

Dieser Flow schützt Konsistenz: Wenn ein command Entitäten erstellt, die Events auslösen, und das Projekt Outbox verwendet, muss der Handler in einer Transaktion speichern, zum Beispiel mit SaveChangesInTransactionAsync, oder die Transaktion explizit öffnen und committen. Ohne diese Transaktion sollte die Infrastruktur fehlschlagen, statt einen unzuverlässigen Event-Flow zu erlauben.

Wann welcher save style verwendet wird

  • Verwenden Sie SaveChangesAsync für direkte Persistenz ohne Konsistenzanforderung zwischen Events und Outbox.
  • Verwenden Sie SaveChangesInTransactionAsync, wenn Domain Events und Persistenz im Outbox Teil desselben commits sein müssen.
  • Verwenden Sie BeginTransactionAsync, CommitAsync und RollbackAsync, wenn der Handler mehrere Schritte hat und das Endergebnis auf Basis eines Result entscheiden muss.

Faustregel: Wenn eine Schreiboperation Domain Events veröffentlicht, die Integration Events erzeugen können, halten Sie Datenbankänderungen und Outbox-Einträge in derselben Transaktion.

Migrations verwalten

Lino kapselt den migrations lifecycle von Entity Framework Core mit CLI-Befehlen, die Service, Modul, Architektur, provider, DbContext, startup project, Versionsdatei und Ausgabeort der Scripts kennen. Das reduziert Fehler durch manuelle Befehle und haelt die metadata der migration im Lino-Projektmodell nachverfolgbar.

Migrations erfassen die Weiterentwicklung der Datenbank aus Änderungen an Entitäten, Value Objects, Beziehungen, Indizes und Entity Framework configurations. Bevor Sie eine migration erstellen, kompilieren Sie die Solution oder mindestens den betroffenen Service, damit EF Core das aktuelle Modell laden kann.

Eine migration erstellen

lino database migrations add --service <ServiceName> --module <ModuleName>

Der Befehl fragt Service, Modul, aktuelle Service-Version in src/Services/<ServiceName>/version.txt und Beschreibung der migration ab. Lino erstellt einen Namen aus Version und Sequenz und führt dann dotnet ef migrations add für das korrekte Persistenzprojekt und startup project aus.

Lino erzeugt ausserdem ein SQL-Script der migration mit dotnet ef migrations script. Das Script wird unterhalb des betroffenen Persistenzprojekts in einem versionierten Ordner nach diesem Muster geschrieben:

Infrastructure.Persistence/Scripts/<Version>/<Sequence>_<Description>.sql
Infrastructure.Persistence/Scripts/v1.2.3/001_AddCustomerIsActive.sql

Hinweis: Neben den von Entity Framework generierten .cs-Dateien erzeugt Lino das entsprechende .sql-Script mit DDL-Anweisungen. Das erleichtert Reviews durch Infrastrukturteams, DBAs und kontrollierte Deployment-Prozesse.

Anwenden, auflisten, zurücksetzen und entfernen

lino database migrations add --service <ServiceName> --module <ModuleName>
lino database migrations list --service <ServiceName> --module <ModuleName>
lino database migrations apply --service <ServiceName> --module <ModuleName>
lino database migrations revert --service <ServiceName> --module <ModuleName>
lino database migrations remove --service <ServiceName> --module <ModuleName>
  • add: erstellt eine migration für ausstehende Modell Änderungen.
  • list: listet bekannte migrations für den ausgewählten Context auf.
  • apply: führt dotnet ef database update aus, um migrations auf die konfigurierte Datenbank anzuwenden.
  • revert: geht den letzten erfolgreichen migration path zurück, wenn der Flow das zulässt.
  • remove: entfernt den letzten erstellten migration record und die generierten EF-Dateien, sofern anwendbar.

Die kanonische Gruppe ist database migrations. Aliases wie db und migration können für Produktivität existieren, die Dokumentation sollte aber die vollständige Form bevorzugen.

Was Lino für Sie auswaehlt

  • In traditionellen Services ist das EF-Projekt src/Services/<ServiceName>/Infrastructure.Persistence und das startup project src/Services/<ServiceName>/Api.
  • In modularen Services ist das EF-Projekt src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistence und das startup project src/Services/<ServiceName>/Host.
  • Der Context ist der für den ausgewählten Service oder das ausgewählte Modul generierte ApplicationDbContext.
  • Die EF migrations history table wird mit Constants.Database.MigrationsHistoryTable und Constants.Database.Schema konfiguriert.

Empfohlener Workflow

  1. Modellieren oder ändern Sie Entitäten, Value Objects, Beziehungen, Indizes oder Persistenzkonfigurationen.
  2. Führen Sie build aus und beheben Sie Kompilierungsfehler, bevor Sie die migration erzeugen.
  3. Führen Sie lino database migrations add für den korrekten Service und das korrekte Modul aus.
  4. Prüfen Sie die generierte C# migration und das SQL script, bevor Sie sie anwenden.
  5. Wenden Sie die migration in einer lokalen oder Entwicklungsumgebung an und prüfen Sie das Datenbankschema.
  6. Committen Sie migration files und SQL scripts zusammen mit der Domain- oder Application-Änderung, die diese Anpassung erforderlich gemacht hat.

Checklist: Prüfen Sie den migration diff, wenden Sie ihn auf einer lokalen Datenbank an, führen Sie build aus und testen Sie den betroffenen Flow, bevor Sie veröffentlichen.

Ein unbehandelter Fehler ist aufgetreten. Aktualisieren 🗙