Laden...

Wie kann ich mit EFCore mehrere DbContexts auf gleicher Datenbank nutzen?

Erstellt von Duesmannr vor 3 Jahren Letzter Beitrag vor 3 Jahren 1.135 Views
D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren
Wie kann ich mit EFCore mehrere DbContexts auf gleicher Datenbank nutzen?

Mahlzeit,

ich habe ein .NET Standard 2.0 Projekt, was ein Base Projekt ist, für alle darunterliegenden Projekte, die eine DB Verbindung brauchen.

Hier ist eine ModelBase Klasse definiert und dazu ein User Model mit deren dazugehörigen Configurations Dateien.

Hab in dem Projekt auch eine ApplicationDbContext Klasse, mit dem DbSet von User und dem Connectionstring für die Datenbank.

Jetzt habe ich noch ein zwei weitere Projekte zu zwei unterschiedlichen Themen (daher getrennte Projekte, alles .NET Standard).

Die Models in den Projekten erben alle von der Base, die die User Klasse benötigt.
Habe in beiden Projekten nochmals eine Klasse für die DbSets. Die Klassen erben von dem ApplicationDbContext.

Mein Problem ist, durch die Vererbung ist in jedem DbContext, dass DbSet<User>, was quasi richtig ist, weil ich da ja von den einzelnen Kontexten drauf zugreifen muss.
Nur bei der Erstellung der Migration, will der die Tabelle User in jedem Kontext neu erstellen, was ja nicht funktioniert, wenn diese schon existiert.

In den einzelnen Kontexten habe ich auch


modelBuilder.Ignore<User>();

versucht. Problem ist, die Models in dem Projekt brauchen die User Klasse und das Ignore lässt das nicht nur bei der Erstellung der Migration ignorieren.

Es wäre möglich, alle Models in einen Kontext zu packen, find ich aber nicht schön und ist dann auch nicht mehr modular.

Aber wie ignore ich die Tabelle User? Oder auch allgemein alle DbSets aus der Vererbung, wenn ich später in dem Database Projekt ein weiteres Model hinzufüge, möchte ich nicht, durch alle Projekte gehen um das anzupassen.

Oder gibt es einen komplett anderen Ansatz, den ich gerade nur nicht sehe?

Grüße

16.806 Beiträge seit 2008
vor 3 Jahren

Das ist im Endeffekt ein Folgefehler wie Du die Kontexte organisatorisch verwaltest.

Es wäre möglich, alle Models in einen Kontext zu packen, find ich aber nicht schön und ist dann auch nicht mehr modular.

Das ist leider jedoch der korrekte Weg im Umgang mit dem EF Context. Der Kontext ist sowieso nicht in Verantwortung für die Modularität.

Der Sinn eines Kontext ist das vollständige Abbild einer Datenbank.
Durch Deine Vererbung wird prinzipell davon ausgegangen, dass Du eben mehrere Datenbanken hast, die nur in der Struktur gleich / identisch sind.
Die Migration unterstützt aber dieses Vorgehen nicht. Davon abgesehen kannst Du auf viele Vorteile von Context-Sharing nicht profitieren, wenn Du Dir so ein Trennungs-Konstrukt bastelst.

Im Sinne von OOP kannst Du einfach entsprechende Interfaces auf dem Model nutzen, um so nur gewisse DbSet aus dem Context zur Verfügung zu stellen.

services.AddScoped<IMyContext1>(provider => provider.GetService<ApplicationDbContext>());
services.AddScoped<IMyContext2>(provider => provider.GetService<ApplicationDbContext>());
services.AddScoped<IMyContext3>(provider => provider.GetService<ApplicationDbContext>());

Das würde im Endeffekt auch dafür sorgen, dass es nur einen einzigen Context gibt, damit nur eine Migration aber Du trotzdem Deine Migration hast.

Ich bin kein sonderlich großer Fan von EF Migrations. Hab jetzt EF Core 3 und 5 mit dem neuen mycsharp.de Code wieder ne Chance gegeben, aber ich gehe doch wieder zurück zu Database First.

Willst Du das trotzdem so getrennt haben und keine Interfaces nutzen, dann bleibt Dir nur der Weg, dass Du aktiv die Migration nur auf der untersten Ebene der DbContext-Vererbung durchführst.

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

Danke für die schnelle Antwort Abt.

Ja dann alles in ein Context und dann wie du es beschrieben hast, über Interfaces lösen.

Edit:
Das mit den Interfaces funktioniert aber nicht, so wie ich es mir denke.
Hätte jetzt die Interfaces in den Projekten erstellt, in denen die genutzt werden sollen.
Dadurch ensteht eine circular dependency, weil die Projekte sich ja gegenseitig verweisen müssten.

Aber selbst, wenn ich die Interfaces in dem Database Projekt erstelle, muss ich für die Initialisierung des Interfaces eine Referenz zum Database Projekt haben, die aber wegen den Interfaces eine Referenz zum Projekt hat.

Oder habe ich einen Denkfehler?

Also wie ich es weiß, würde das Interface so aussehen:


public interface IWorldDbContext //Liegt in dem Database Project
{
    DbSet<City> Cities { get; set; } //City kommt aus dem World Project
    ...
}

Und die ApplicationDbContext Klasse würde so aussehen:


public class ApplicationDbContext : DbContext, IWorldDbContext

Grüße

16.806 Beiträge seit 2008
vor 3 Jahren

Ich habe noch nie eine Solution gesehen, die richtig organisiert ist (einfachste Regel: korrekte Anwendung von Namespaces) und eine Circular Dependency verursacht.
Daher vermute ich eher, dass es an einem Strukturfehler Deiner Solution liegt. An den Interfaces eher nicht 😉

Der einfachste Fall:

`MyCompany.MyProduct.Database.Abstractions

  • IDbContext1
  • IDbContext2
  • IDbContext3
    MyCompany.MyProduct.Database
  • DbContext - darf Abstractions kennen

MyCompany.MyApp

  • DbContext aus MyProduct
  • Interfaces aus Abstractions`
    Nirgends eine Circular Dependency.

Zu Deinem Edit:

  • Ein DbSet in der Klasse braucht immer {get;set;} sonst wird es nicht initialisiert.
  • Ein DbSet im Interface sollte kein Setter haben; wozu auch -> nur {get;}

Ich würde wann immer möglich eben nicht ein solches Interface im Database-Project halten, weil es ja nur eine Abstraction ist.
Diesen Abstraction Pattern von Namespaces siehst Du immer wieder - vor allem in quasi allen Microsoft .NET Frameworks, Assemblies und NuGets

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

Ahh touche.

Hast Recht.

Bei mir ist es derzeit so.

RD.Database
- ApplicationDbContext
- Interfaces
- IWorldDbContext
RD.World
- Models

Weiteres Projekt für die Interfaces, ok.

Danke.

16.806 Beiträge seit 2008
vor 3 Jahren

Also die Aufteilung eines Projekts in hunderte Projekte und so feindrösselig, dass die Datenbank ein eigenes Projekt ist; ja kann Vorteile haben - aber leider nur in den wenigsten Fällen.
Wichtig ist, dass Du den eigentlichen Kern der Anwendung unabhängig von der Runtime (ASP, WPF und Co machst).
Logik, Modelle und Datenbank können und dürfen in den meisten Fällen in einem Projekt bleiben; kaum eine Anwendung muss von Anfangan so modular sein, dass man zig Projekte braucht und im Zweifel die Übersicht der Abhängigkeiten verliert.
Sollte es doch irgendwann notwendig sein es zu trennen, dann ist es super einfach, wenn man C# Namespaces korrekt verwendet.

RD.Database ist an für sich schon eine Verletzung der Namespace-Empfehlungen 😉
Und wenn man sich nicht an die Empfehlungen hält, dann ist es eigentlich nur eine Frage der Zeit, bis man Organisationsprobleme bekommt 😃

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

RD.Database ist an für sich schon eine Verletzung der Namespace-Empfehlungen 😉

Konkret heißt das Projekt "RD.Common.Database".

Aber ja, da gebe ich dir Recht.

16.806 Beiträge seit 2008
vor 3 Jahren

Common ist meistens der Ablageort für alles.
Auch eher so ein Anti-Pattern im Namespace-Design - vor allem für sowas wie Datenbank-Stuff 😃

Hier mal als Beispiel ein Ausschnitt, wie das neue MyCSharp Forum in Namesspaces aufgeteilt ist.

`
MyCSharp.Common

  • Extensions
    MyCSharp.Forum <≤= Kern der Anwendung
  • Data.Models = Enums für EF Core
  • Data.Projections = Projektionen für EF Core
  • Database - Allgemeines Datennbank-Zeug wie zB der DbContext
  • Database.Entites => Klassen der Entitäten
  • Database.Mssql => Spezifisches für MSSQL; könnte man sofort in ein eigenes Projekt auslagern. zB extra DbContext nur für MSSQL
  • Database.Mssql.Configurations => Hier liegen die EFCore ModelBuilder Dateien für MSSQL
  • Database.Mssql.Repositories => spezifische Repositorires für MSSQL
  • Database.MySql => Spezifisches für MSSQL; könnte man sofort in ein eigenes Projekt auslagern
  • Database.Repositories => allgemeine, generische Repositories
  • Database.Views => Klassen für View-Abfragen
  • Models => Logik-Modelle; hier könnten zB auch ENums drin liegen
  • Features => Hier liegt die Logik drin
  • Providers => Allgemeine Provider
  • Services => Allgemeine Services
    MyCSharp.Forum.Features.User => Spezifisches Zeug für die User Verwaltung, zB Identity
    MyCSharp.Forum.Features.EMails => zB Erzeugung der EMails; hier sind Abhängigkeiten, die nicht in das Kernprojekt sollten.
    MyCSharp.Forum.Runtimes.AspNetCore => Spezifisches für ASP.NET Core, das gemeinsam genutzt werden soll aber nicht in der App selbst liegen soll (macht zB das Testen einfacher)
    MyCSharp.Forum.WebApp => die tatsächliche Webanwendung`

Dazu sei gesagt, dass wir keinerlei wirkliche Modelle haben und Models leer sind.
Das liegt daran, dass das neue Forum komplett mit Event-Sourcing arbeiten und es daher nur Commands, Queries und Projektionen gibt.
Business Modelle sind hier gar nicht mehr notwendig; bzw. würden alles nur unnötig aufblähen.

Der Namespace MyCSharp.Forum.Features.User existiert als Projekt und innerhalb des Hauptprojekts; einfach der Erweiterung wegen.
Das Vorgehen so sieht vielleicht zunächst komisch aus weil doppelt, aber das ist tatsächlich der Sinn von Namespaces.

Wir haben auch ein "MyCSharp.Common" Projekt; da liegt aber nur Zeug drin, das für die Funktionsweise der Anwendung selbst unwichtig ist, sondern nur Extension-Klassen oder Regex-Zeugs.
Ablageort für alles, für das es sonst im Namespace-Design kein Platz gibt 😉

Aber Du siehst: der Aufbau der Namespaces ist vollständig nach C# Empfehlungen; alles lässt sich austauschen, erweitern, auslagern.

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

Danke für den Einblick.

Werde die Namespaces bei mir auch ändern.

Grüße

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

`MyCompany.MyProduct.Database.Abstractions

  • IDbContext1
  • IDbContext2
  • IDbContext3
    MyCompany.MyProduct.Database
  • DbContext - darf Abstractions kennen

MyCompany.MyApp

  • DbContext aus MyProduct
  • Interfaces aus Abstractions`

Das funktioniert nicht ganz. Die Database müsste auch das Projekt kennen, wo die Models drin sind.
Weil die Models in den einzelnen Projekten definiert sind und nicht im Database Projekt.

Dadurch kommt eine circular dependency zustande.
Anbei einmal ein Foto wie es aussieht bei mir.

Zum Foto:
Die Models müssen die DB kennen, für die ModelBase Klasse.

16.806 Beiträge seit 2008
vor 3 Jahren

Aufgrund des Namings nehme ich an, dass Models (und die City) ein Business-Modell ist.
Dass die Datenbank-Schicht jedoch Business-Modelle kennt, ist ein Designfehler.

Ansonsten musst erklären, was bei Dir Models sind.

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

Dass die Datenbank-Schicht jedoch Business-Modelle kennt, ist ein Designfehler.

Muss er doch zwangsläufig für die DbSets oder nicht?

Naja die Models in dem Foto zu sehen (User, City, etc..) sind halt normale Datenbank Modelle die eine Tabelle repräsentieren.

16.806 Beiträge seit 2008
vor 3 Jahren

Dann sind es Entitäten und keine Modelle 😉
Und Entitäten sind Teil der Datenbank und müssen / gehören daher in die Datenbank-Schicht; ergo bei Dir in Dein Datenbank-Projekt.

Willst Du eine Art Plugin-System, dann musst Du das komplett anders designen, zB. auch mit vollständig dynamischen DbSets.

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

Dann sind es Entitäten und keine Modelle 😉

Mein Fehler.

Willst Du eine Art Plugin-System, dann musst Du das komplett anders designen, zB. auch mit vollständig dynamischen DbSets.

Ja, aber weiß nicht wie ich das gescheit machen soll.

Bspw. Lizenzsystem. Customer hat Lizenz für Holiday und dadurch will ich ein Kontext haben, der mit den DbSets von Holiday arbeiten kann. Und sobald der Customer die Lizenz hat, dass die Db erst dann erweitert wird, mit den notwendigen Tabellen und dann auch gefüllt wird.
Aber wie ich das umsetzen soll, i dont know.

16.806 Beiträge seit 2008
vor 3 Jahren

Ich verstehe nicht 100% was Du vor hast, aber ein Lizenzsystem ist Applikationslogik - das gehört nicht in die Datenbank.

Man würde ja viel eher zB. eine Art IHolidayService zur Verfügung stellen, der dann bei Verwendung eine Exception wirft, wenn die Lizenz nicht da ist.
Wieso muss denn der DbContext wissen, ob dafür die Lizenz da ist? 🤔

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren

Das war nur als Beispiel.
Will ich darin auch nicht implementieren, nur so bauen, dass es Pluginmäßig funktioniert,
aber wie schon geschrieben, weiß ich nicht wie, dass man die Tabellen dynamisch hinzufügt, bzw. bei nem Neustart.

Das mit der Applikationslogik, soweit bin ich ja noch nicht annährend.

Man könnte dann einen 2. Context haben mit den Daten, die man bekommt, wenn man die Rechte hat. Und mit dem Context die DB erweitern. Nur fehlt mir dann noch die Relation zu User.

Edit. Anbei ein Foto, wie ich mir das ursprünglich vorgestellt habe.
PluginSystem. Aber wie ich das so umsetzen soll, keine Ahnung.
Fokus liegt auf den DbSet<User> in den anderen Kontexten. Bzw. geht das ja auch über Interfaces.
Aber wie die Tabellen, dann erst erstellen, wenn die benötigt werden?

16.806 Beiträge seit 2008
vor 3 Jahren

Okay, wäre toll gewesen, wenn Du schon an Anfang von einem Pluginsystem gesprochen hättest; weil das ändert nun natürlich die Aussagen zu den Namespaces etc. 😉

In der EF Sprache nennt sich Deine Anforderung Overlapping Bounded Context; also DbContext-Klassen, die teilweise gemeinsame Entitäten haben.
EF kann seit ich glaub EF 5 mit dieser Anforderung umgehen.

In EF ist dieses Feature implementiert, in dem man bei der Migrations Deiner erbenden Klassen entsprechende Attribute setzen kann, sodass Migrationen der Basis-Klasse nicht erneut durchlaufen werden.
In EF Core wird das meines Wissens noch nicht unterstützt.

Im ursprünglichen GtitHub Issue ist aber nun die Version von EF Core 5 verlinkt, sodass dieses Feature evtl. mit EF Core 5 kommt
Ability to exclude/skip/ignore parts of the model from migrations so that a table is not created (for overlapping bounded contexts)

Add an attribute to mark a table as excluded from migrations

Deine Anforderung wäre heute mit EFCore - ohne dieses Feature - nur mit zwei komplett getrennten DbContexten möglich, die auch ihre History in verschiedene Tabellen schreiben.
Was aber auch den Nachteil hat, dass diese Kontexte sich nicht kennen und damit nicht untereinander austauschbar sind.

Du kannst aber natürlich auch eine externe Migrationsstrategie verwenden (zB DACPAC oder FluentMigrations), sodass Du von den Einschränkungen von EF Core Migrations losgelöst bist.
Aber EF Core Migrations unterstützt das heute so nicht.

D
Duesmannr Themenstarter:in
161 Beiträge seit 2017
vor 3 Jahren


>

Das ist die richtige Url:
Add an attribute to mark a table as excluded from migrations

Danke für die Github Issues. Haben mir weitergeholfen.

Durch den


>

Link (Code Beispiel mit dem IDbContextFactory<Context>) bin ich zur Lösung gekommen.

Durch etwas googlen, bin ich darauf gestoßen:
Design-time DbContext Creation
Mit dem letzten Code Beispiel, hat man dann eine Factory.
Und in dem Context, der eingebunden wird, mache ich ein

modelBuilder.Ignore<Users>()

und es funktioniert jetzt wie gewünscht.

Die Users Table Creation taucht nicht mehr in den Migrations auf.

Danke Abt.