EFPF (Entity Framework Persistence Framework) ist ein Aufsatz auf dem Entity Framework, womit die Entwurfsmuster Repository und Unit of Work umgesetzt wurden. Somit können Standardoperationen auf realtionale Datenquellen (CRUD) ohne Aufwand verwendet werden. Weiterhin bietet EFPF eine integrierte Transaktionssteuerung und einen Provider zum Verwalten der Repositories. EFPF ist nun in Version 2.0 erschienen und bietet gegenüber der Version 1.0.* mehrere neue Ansätze und Verbesserungen. Da diese aber sehr tief in das System eingreifen, sind die Versionen nicht miteinander kompatibel. Ein Umstieg lohnt sich aber trotzdem.
Im nachfolgenden Klassendiagramm ist die neue Struktur zu erkennen. IRepository<T>
und IUnitOfWork
die Basis. Aber dann folgen schon die Änderungen. Das abstrakte Basisrepository ist durch ein allgemeines Repository ersetzt wurden, welches die Basisoperationen (CRUD; Create, Read, Update, Delete) bietet. Ein solches Repository muss also nicht mehr händisch erstellt werden. Hierzu kann das Interface IRepository<T>
und die Klasse Repository<T>
direkt verwendet werden. Neu ist auch, dass nun die UnitOfWork der zentrale Zugangspunkt für alle Datenoperationen ist. Da sich hierbei aber allgemeine Informationen mit anwendungsspezifischen mischen, muss eine Separierung erfolgen. Dazu ist es notwendig, dass Teile der UnitOfWork in der Anwendung implementiert werden müssen. Alle notwendigen Basisoperationen sind schon in EFPF implementiert. In der Anwendung muss es eine Schnittstelle geben, die von IUnitOfWork
erbt. Hierin werden die Repositories als readonly Eigenschaft angegeben. Dann muss es noch eine Implementierung der UnitOfWork geben. Ich empfehle, von TransactionScopeUnitOfWork
zu erben und die ensprechenden Interfaces zu implementieren. Hier ein Bespiel:
public interface IMyUoW : CSP.EFPF.Contracts.IUnitOfWork { IPersonRepository Persons { get; } IRepository<Model.Adresse> Addresses { get; } } public class MyUnitOfWork : CSP.EFPF.TransactionScopeUnitOfWork<TestDbContext>, IMyUoW { public MyUnitOfWork(CSP.EFPF.Contracts.IRepositoryProvider repoProvider) : base(repoProvider) { } public IPersonRepository Persons { get { return GetRepo<IPersonRepository>(); } } public IRepository<Model.Adresse> Addresses { get { return GetStandardRepo<Model.Adresse>(); } } protected override void ConfigContext() { //eventuelle Konfigurationen am Context } }
Die UoW ist also der zentrale Punkt für alle Zugriffe auf die Repositories. Ein Aufruf an ein alleinstehendes Repository ist nicht mehr möglich.
Hierzu gleich noch ein sehr wichtiger Hinweis. Da nun die gesamte Kontrolle in der UoW liegt, werden auch die Speicheranweisungen, also die eigentliche Persistierung der Änderungen in der Datenbank, über die UoW gesteuert. Dazu muss immer die Methode SaveChanges()
aufgerufen werden. Ein Insert(), Update()
oder Delete()
speichert die Änderungen nicht automatisch. Die UoW verwendet somit explizites Speichern. Bei der Verwendung von Transaktionen wird vor dem CommitTransaction
das Speichern aufgerufen. Hier ist der separate Aufruf nicht notwendig.
Um auf die Repositories zuzugreifen, gibt es zwei Methoden in der UnitOfWork. GetStandardRepo
liefert immer den Typ IRepository<T>
, also den Standardtyp mit den Basisoperationen (implementiert in Repository<T>
). Mit GetRepo<T>
können die „speziellen“ Repositories abgerufen werden. Spezielle Repositories sind Erweiterungen von IRepository<T>
mit eigenen Implementierungen.
Die speziellen Repositories werden wie bisher auch erzeugt. Hierzu ein kleines Beispiel:
public interface IPersonRepository : IRepository<Person> { //methods } public class PersonRepository : CSP.EFPF.Repository<Person>, IPersonRepository { public PersonRepository(DbContext ctx) : base(ctx) { } //implementations }
Da diese Logik aber schon vorgegeben ist, wird dieser Punkt nur interessant, wenn man eigene UoW schreiben möchte.
Nun gibt es aber ein Problem mit der Erzeugung der speziellen Repositories. Was vorher per Dependency Injection realisiert wurde, findet nun in einer Factory-Klasse statt. Im Klassendiagramm ist die abstrakte Factory RepositoryFactories
zu erkennen. Von dieser muss eine abgeleitete Klasse erzeugt werden und die abstrakte Methode GetApplicationFactories
implementiert werden. Darin wird ein Dictionary erzeugt, welches als Schlüssel den Typ des Repositories und als Wert eine Funktion enthält. Diese Funktion dient dem Instanziieren des Repositories. Da einem Respository immer der Datenbankkontext übergeben werden muss, wird dieser der Funktion von außen als Parameter überreicht. Das sieht dann zum Beispiel so aus.
class TestRepoFactories : CSP.EFPF.RepositoryFactories { protected override IDictionary<Type, Func<DbContext, object>> GetApplicationFactories() { return new Dictionary<Type, Func<DbContext, object>> { {typeof(IPersonRepository), dbContext => new PersonRepository(dbContext)} }; } }
Abschließend geht es noch um die Erstellung der einzelnen Teile und deren Komposition. Das kann auf zwei Arten erfolgen. Entweder man erstellt alle Teile manuell und fügt diese im Code zusammen, oder man verwendet ein DI-Framework wie Ninject oder Autofac (oder ein beliebiges anderes). Ich empfehle auf jeden Fall die Erzeugung mit einem DI-Framework durchzuführen.
Hier mal der Vergleich der beiden Varianten im Code.
1. Manuell
var factories = new Data.TestRepoFactories(); var repoProvicder = new RepositoryProvider(factories); var uow = new Data.MyUnitOfWork(repoProvicder); _unitOfWork = uow;
2. DI-Framework (anhand von Ninject)
kernel.Bind<RepositoryFactories>().To<TestRepoFactories>() .InSingletonScope(); kernel.Bind<IRepositoryProvider>().To<RepositoryProvider>(); kernel.Bind<IMyUoW>().To<MyUnitOfWork>();
Die Verwendung ist dann ganz leicht. Hat man Zugriff auf die Instanz der UoW, sind darin alle notwendigen Methoden und alle Repositories als Eigenschaften (das ist selbst zu realisieren) vorhanden.
Hier ein beispiel der Verwendung.
[TestMethod] public void TestInsertData() { var actual = Model.Person.GetPersonInstance(); _unitOfWork.Persons.Insert(actual); _unitOfWork.SaveChanges(); var expected = _unitOfWork.Persons.GetOne(p => p.Name == actual.Name); Assert.IsNotNull(expected); Assert.AreEqual(expected.Name, actual.Name); }
Vielen Dank an John Papa für die Inspiration zu dieser Umarbeitung. Einige Teile sind aus seinem Code Camper Projekt entliehen.