Laden...

.NET Core WebApi: Repositories & Dapper - Wie kann man bei vielen Objekten/DBs DRY einhalten?

Erstellt von emuuu vor 6 Jahren Letzter Beitrag vor 6 Jahren 1.836 Views
emuuu Themenstarter:in
286 Beiträge seit 2011
vor 6 Jahren
.NET Core WebApi: Repositories & Dapper - Wie kann man bei vielen Objekten/DBs DRY einhalten?

Guten Tag zusammen,

ich versuche aktuell ein Projekt von WCF zu einer .net Core WebApi zu migrieren. Zum Verständnis hab ich mich zunächst durch ein paar Tutorials und vor allem Coffeebeans Video zum Thema gehangelt.

Mein Problem ist nun, dass sämtliche Beispiele die man so findet immer mit genau einem Model arbeiten und z.B. ein Dictionary als Datengrundlage fungiert.

Im WCF-Projekt läuft der DB Zugriff bisher über SqlDataReader wobei die Connectionstrings in der Service-Klasse hinterlegt sind:


        private SqlConnection dbTimeStamp = new SqlConnection(ConfigurationManager.ConnectionStrings["connectTimeStamp"].ConnectionString);
        private SqlConnection dbAuthentication = new SqlConnection(ConfigurationManager.ConnectionStrings["connectAuthentication"].ConnectionString);
        private SqlConnection dbGlobalData = new SqlConnection(ConfigurationManager.ConnectionStrings["connectGlobalData"].ConnectionString);
        private SqlConnection dbCalendar = new SqlConnection(ConfigurationManager.ConnectionStrings["connectCalendar"].ConnectionString);

In den einzelnen Methoden werden dann SqlCommands erstellt die durch eine zwischengeschaltete Ebene ein DataTable zurückgibt.

Da ich mit der Migration zur WebApi nun das ganze zusätzlich auf Dapper umstellen möchte folgende Fragen:
Ich habe jetzt jede Klasse die ich vorher Via [DataContract] zur Verfügung gestellt habe in ein Model mit entsprechenden Dtos und Repositories umgewandelt.
Meinem Verständnis nach müsste in z.B. CustomerRepository der Datenbankzugriff via Dapper erfolgen. D.h. ich würde die IDbConnection in jeder einzelnen Repository neu instanziieren, was für mich nicht wirklich nach DRY klingt.

Wo würde ich also in dem Fall best practice die IDbConnections instanziieren? Bzw. wo würde ich diese rein formal ablegen (z.B. in einer static-Klasse)?

Und dazu die weitere Frage: Im alten Projekt liegen sowohl Service als auch die Client-Anwendungen in der gleichen Projektmappe. Wobei sämtliche Klassen in einer eigenen Bibliothek liegen, damit sowohl Clientanwendungen als auch Services die gleichen Klassen kennen.
Das würde gerne weiterhin so halten. Nur liegen diese nun als Models im WebApi-Projekt, zudem gibt es einige Klassen für die ich keine Models angelegt habe, da diese nur Client-seitig verwendet werden (wiederum in mehreren Projekten).
"Darf" ich die nur clientseitigen Klassen ebenfalls als Models in das API-Projekt packen (ohne Dtos, Repositories, usw.).

Hoffe mein Gedankengang und die Problemstellung wird klar. Mir geht es vor allem darum, dass wenn ich mir schon die Mühe mache das ganze von WCF umzustellen ich es auch in allen Bereichen sauber lösen will. D.h. mir gehts vor allem um die formal korrekte Umsetzung und nicht "hauptsache läuft".

Beste Grüße
emuuu

2+2=5( (für extrem große Werte von 2)

1.029 Beiträge seit 2010
vor 6 Jahren

Hi,

Repository-Pattern ist bei mir mit dem UnitOfWork-Pattern auf Basis von Dapper umgesetzt.

Und die UnitOfWork-Instanz ist dann auch der ideale Platz um so eine IDbConnection (und besser auch die Transaction) zu halten und an Repositories weiterzugeben.

Gibt dann einen DataService, der die UnitOfWork (und ggf. auch die Repositories) erzeugt - die Repositories bekommen dann jeweils einfach die UnitOfWork.

Wenn du ein Beispiel brauchst kannst du auf folgendem Link mal reinschauen:
https://github.com/IInvocation/AppFx/tree/master/src/FluiTec.AppFx.Data.Dapper

(Ist sicher noch verbesserungswürdig - bin kein Profi)

LG

PS: Das mit dem DRY ist übrigens dann nicht dein einziges Problem - gibt ja oft sachen, wo evtl. mehr als ein Repository beteiligt ist - wenn die verschiedene Transaktionen verwenden und was nicht perfekt sauber abgefangen wird schreibst du evtl. sogar inkonsistente Datensätze, weil die Repositories keine gemeinsame Transaction hatten...

PPS: Bevor du Klassen mehrfach schreibst - definitiv in's API-Projekt schieben.

emuuu Themenstarter:in
286 Beiträge seit 2011
vor 6 Jahren

Erstmal danke für die Info, dein Link sind leider im Moment nur böhmische Dörfer für mich.
Vermute ich bin beim Verständnis von Repositories, UnitOfWork noch net weit genug (find da aber auch irgendwie keine Dokus)

Bezüglich der Connectionstrings finde ich nur beispiele wo die Connection in jeder Repository einzeln instanziiert wird.
Und wenns an Transactions geht steige ich völlig aus (auf der theoretischen Ebene verstehe ich es, allerdings nicht was eine konkrete Umsetzung wäre).

Edith:
Mein Ansatz für die Repositories wäre nun ein generisches zu erstellen in dem ich die IDbConnection einpflege und CRUD implementiere. Und in den weiteren Repositories ergänze ich dann nur die spezifischen Funktionen.

2+2=5( (für extrem große Werte von 2)

1.029 Beiträge seit 2010
vor 6 Jahren

Hi,

zum Verständnis:

Repository:
Ein Repository ist bei mir eine Klasse, die zur Verwaltung von Instanzen einer bestimmten Entität, wobei eine Entität im einfachsten Fall direkt einen Eintrag einer Tabelle beschreibt.

UnitOfWork:
Einfach ein "Arbeitsschritt", der ggf. mehrere Datenbankaktionen (ergo: mehrere Funktionsaufrufe eines oder mehrerer Repositories) miteinander verbindet.

Beispiel UnitOfWork:
Sinn macht das speziell wenn du z.B. die Entitäten "Kunde" und "Adresse" hast, wobei Kunde z.B. nur die Namen enthält (Name1, Name2, Name3 + Fremdschlüssel auf Adresse) und Adresse enthält die genauen Adressdaten (Straße, PLZ, Ort). Deine UnitOfWork soll jetzt einen Kunden anlegen - zu diesem Kunden gehört allerdings auch logischerweise eine Adresse. (Ergo: 2 Inserts, die nur gemacht werden sollen wenn auch wirklich beide klappen)
Von der Logik her - würde man nun zuerst die Adresse und dann den Kunden einfügen.

Fall a) (wenn du keine gemeinsame Transaktion nutzt)
Nun machst du zuerst den Insert für die Adresse -> klappt
Danach den Insert für den Kunden -> geht schief
Endergebnis: In deiner DB ist eine Adresse als "Leiche"

Fall b) (wenn du eine gemeinsame Transaktion nutzt)
Nun machst du zuerst den Insert für die Adresse -> klappt
Danach den Insert für den Kunden -> geht schief
Endergebnis: Durch ein Rollback der Transaction wird die Adresse gar nicht erst endgültig gespeichert und es bleibt dadurch keine Datenbankleiche übrig.

In jedem Repository eine eigene Connection zu erzeugen ist insofern doof und teilweise auch gefährlich, weil du schnell mehrere dutzend Connections machst.

Mal ohne die ganzen Schnittstellen aus meiner Umsetzung wäre somit das Minimum:

UnitOfWork:
Erzeugt bei Instanziierung:
a) eine IDbConnection unfd öffnet diese
b) erstellt auf die Connection eine Transaction
c) Implementiert IDisposable für automatisches Commit bzw. Rollback, damit z.B. folgendes geht:


using (var uow = new DapperUnitOfWork())
{
var adressRepo = new AdressRepository(uow);
adressRepo.Insert(myAdress);
var customerRepo = new CustomerRepository(uow);
customerRepo.Insert(myCustomer);
uow.Commt();
}

Bei Implementierung von IDisposable die z.B. folgendermaßen aussehen könnte:


public override void Dispose()
		{
			Dispose(disposing: true);
		}
protected virtual void Dispose(bool disposing)
		{
			if (Transaction != null)
				Rollback();
		}
public override void Commit()
		{
			if (Transaction == null)
				throw new InvalidOperationException(
					message: "UnitOfWork can't be committed since it's already finished. (Missing transaction)");
			Transaction.Commit();
			Transaction.Dispose();
			Transaction = null;
			Connection.Dispose();
			Connection = null;
		}
public override void Rollback()
		{
			if (Transaction == null)
				throw new InvalidOperationException(
					message: "UnitOfWork can't be rolled back since it's already finished. (Missing transaction)");
			Transaction.Rollback();
			Transaction.Dispose();
			Transaction = null;
			Connection.Dispose();
			Connection = null;
		}


hast du es dann so implementiert, dass wenn irgendwas innerhalb des Usings schief läuft - automatisch nichts in der Datenbank von dem fehlgeschlagenen Versuch übrig bleibt.

Grundsätzlich lassen sich die CRUD-Funktionen komplett in einem generischern Repository verankern. (Würde ich abstrakt machen) - und die eigentlichen Repositories erben dann davon und erweitern dieses Repository nach Bedarf.

Tipp für das generische Repository:
Mit Hilfe von Dapper.Contrib.Extensions musst du dafür nicht einmal SQL zusammenbasteln.

Zur Connection: das würde ich in einer sogannten Factory auslagern an deiner Stelle.

Hab dir mal ein Minimalbeispiel gemacht damit du siehst, was ich meine. (Benötigte nuget-Packages: Dapper, Dapper.Contrib)

LG

emuuu Themenstarter:in
286 Beiträge seit 2011
vor 6 Jahren

Erstmal vielen Dank! Konnte es jetzt noch nicht komplett nachvollziehen, habe aber gefühlt schon doppelt so viel verstanden wie vorher.

2+2=5( (für extrem große Werte von 2)