Persistenz¶
Die App hat genau einen DbContext, abgeleitet von CoworkeeDbContext. Module tragen ihre Entities bei; Coworkee bringt die Tabellen für Identity, Einstellungen, Audit und Outbox mit.
public sealed class MyAppDbContext(DbContextOptions<MyAppDbContext> options, ICurrentUser currentUser, IEnumerable<IModelContributor> contributors)
: CoworkeeDbContext(options, currentUser, contributors);
context.Services.AddCoworkeeDbContext<MyAppDbContext>((provider, options) =>
options.UseCoworkeeDatabase(provider.GetRequiredService<IConfiguration>(), "myapp"));
UseCoworkeeDatabase wählt den EF-Core-Provider nach Coworkee:Database:Provider (siehe Datenbank-Provider) und führt die Migrationshistorie in cw.__EFMigrationsHistory.
Handler injizieren CoworkeeDbContext und arbeiten mit db.Set<T>(). Repositories gibt es nicht; EF Core ist bereits eines.
Datenbank-Provider¶
Coworkee:Database:Provider |
EF-Core-Provider | Aspire-AppHost | Passt für |
|---|---|---|---|
Postgres (Standard) |
Npgsql | AddCoworkeeApp startet einen Postgres-Container |
alles, Produktion |
SqlServer |
Microsoft.EntityFrameworkCore.SqlServer | options.UseSqlServer(), ein SQL-Server-Container |
Organisationen mit SQL Server oder Azure SQL |
Sqlite |
Microsoft.EntityFrameworkCore.Sqlite | options.UseSqlite(), die Datei .data/<name>.db |
Demos, Prototypen, ein Rechner |
Der AppHost setzt die Einstellung bei jedem Dienst, neben dem Connection-String. Anderswo (Tests, eigenes Hosting) setzen Sie sie wie jeden anderen Konfigurationswert. coworkee new --database sqlServer schreibt den Aufruf im AppHost und legt die erste Migration für diesen Provider an.
Die providerspezifischen Teile liegen hinter kleinen Nahtstellen in Coworkee.Infrastructure, in der App ist nichts zu tun:
| Teil | Postgres | SQL Server | SQLite |
|---|---|---|---|
IHasConcurrencyToken.Version |
xmin |
Spalte, bei jedem Update hochgezählt | Spalte, bei jedem Update hochgezählt |
HasColumnType("jsonb") |
jsonb |
nvarchar(max) |
TEXT |
| Outbox-Stapel | FOR UPDATE SKIP LOCKED |
UPDLOCK, READPAST |
einfache Abfrage, keine Zeilensperren |
IDistributedLock |
Advisory Lock | sp_getapplock |
Dateisperre im Temp-Ordner (ein Rechner) |
| Hintergrundjobs | Hangfire.PostgreSql | Hangfire.SqlServer | Hangfire.InMemory, jeder Prozess hat seinen eigenen Job-Server |
| Textvergleich | Groß-/Kleinschreibung zählt | Groß-/Kleinschreibung zählt (Latin1_General_100_CS_AS auf jeder Textspalte) |
Groß-/Kleinschreibung zählt (=) |
Grenzen, die man kennen sollte:
- Migrationen gehören zu einem Provider. Eine App behält die Migrationen des Providers, mit dem sie angelegt wurde. Zum Wechseln löschen Sie
Migrationsund führencoworkee migrations add Initialerneut aus. Soll eine App mehrere Provider bedienen, folgen Sie der EF-Core-Anleitung Migrations with multiple providers mit einer Migrations-Assembly pro Provider. - Datenbanksicherungen brauchen Postgres. Das Template lässt das Modul bei den anderen Providern weg.
- SQLite kennt nur einen Schreiber zur Zeit, hält Hintergrundjobs im Speicher (eingereihte und verzögerte Jobs gehen beim Neustart verloren), speichert
DateTimeOffsetals Zahl unddecimalalsdouble, prüftHasMaxLengthnicht und kennt keine Schemas (Tabellen verlieren ihrcw.). Es ist für Entwicklung und kleine Installationen auf einem Rechner gedacht. - Facetten über
bool-Spalten ([ProvideFacet]aufIsActive) scheitern auf SQL Server und SQLite, die den Wert als1/0zurückgeben; behoben im nächsten Release von Nextended. - MySQL und MariaDB gibt es nicht: Pomelo, der übliche Provider, hat noch kein Release für EF Core 10.
Basisklassen und Marker¶
| Typ | Liefert |
|---|---|
Entity |
Id, eine GUID Version 7, beim Erzeugen gesetzt |
AuditedEntity / AuditedAggregateRoot |
CreatedAt, CreatedBy, ModifiedAt, ModifiedBy, beim Speichern gefüllt |
AggregateRoot |
Raise(IDomainEvent) für die Outbox |
IMultiTenant |
Abfragefilter auf den aktuellen Mandanten, TenantId beim Anlegen gesetzt |
ISoftDelete |
Remove markiert die Zeile als gelöscht; Abfragen blenden sie aus |
IHasConcurrencyToken |
optimistische Nebenläufigkeit über eine Version-Spalte |
IVersioned |
hält pro Revision einen Schnappschuss für die Versionshistorie |
[Realtime(permission)] |
meldet Anlegen, Ändern und Löschen an SignalR-Clients mit der Berechtigung |
[NotAudited], [Sensitive] |
nimmt einen Typ oder ein Feld aus dem Audit oder protokolliert ein Feld maskiert |
Audit¶
Jede Änderung an einer auditierten Entity wird Feld für Feld mit Benutzer, Zeitpunkt sowie altem und neuem Wert gespeichert. Die Admin-Oberfläche zeigt das pro Entity (AuditTimeline) und gesamt (Seite Audit-Log).
Mandanten und aktueller Benutzer¶
ICurrentUser trägt Benutzer-ID, Mandanten-ID und Berechtigungen des Requests. Hintergrundjobs und die Outbox stellen den Benutzer wieder her, der sie gestartet hat. Wer als jemand anderes handeln muss, etwa in einem Seed, öffnet einen Scope:
using (CurrentUserScope.Begin(new ImpersonatedUser(adminId, tenantId)))
{
// Abfragen und Anlegen laufen als dieser Benutzer und Mandant
}
Migrationen und Seed¶
Migrationen liegen im Infrastruktur-Projekt und sind für dessen Datenbank-Provider erstellt:
Ein kleiner Host spielt sie ein. Der Migrations-Dienst des Templates seedet außerdem:
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddCoworkeeModules<MyAppDatabaseModule>(builder.Configuration);
builder.Services.AddCoworkeeIdentitySeed(DemoSeed.Configure);
builder.Services.AddScoped<ISetupStep, DemoData>();
using var host = builder.Build();
await using var scope = host.Services.CreateAsyncScope();
await scope.ServiceProvider.GetRequiredService<MyAppDbContext>().Database.MigrateAsync();
await host.Services.SeedCoworkeeIdentityAsync();
AddCoworkeeIdentitySeed ersetzt den Setup-Assistenten: Er legt einmalig den Standard-Mandanten, den Administrator, Rollen mit ihren Berechtigungen, weitere Benutzer und systemweite Einstellungen (seed.Settings["Theme.Mode"] = "dark") an. ISetupStep-Implementierungen laufen im selben Setup als Administrator und sind der Ort für Beispieldaten. Siehe Anmeldung.