Generisches Repository-Muster für Entity Framework

99185
ashutosh raina

Gedanke 1

public interface IRepository<T> : IDisposable where T : class
        {
            IQueryable<T> Fetch();        
            IEnumerable<T> GetAll();        
            IEnumerable<T> Find(Func<T, bool> predicate);        
            T Single(Func<T, bool> predicate);        
            T First(Func<T, bool> predicate);        
            void Add(T entity);        
            void Delete(T entity);        
            void SaveChanges();        
        }

Mit dem obigen Ansatz werde ich wie Repository gehen und dann ein Repository mit der Implementierung der oben beschriebenen Methoden haben.

Ich denke, das befreit meine Tests von der DbContext-Abhängigkeit.

Gedanke 2

 public class RepositoryBase<TContext> : IDisposable where TContext : DbContext, new()
    {
            private TContext _DataContext;

            protected virtual TContext DataContext
            {
                get
                {
                    if (_DataContext == null)
                    {
                        _DataContext = new TContext();
                    }
                    return _DataContext;
                }
            }

            public virtual IQueryable<T> GetAll<T>() where T : class
            {                   
                using (DataContext)
                    {
                        return DataContext.Set<T>();
                    }                   
            }

            public virtual T FindSingleBy<T>(Expression<Func<T, bool>> predicate) where T : class
            {
                if (predicate != null)
                {
                    using (DataContext)
                    {
                        return DataContext.Set<T>().Where(predicate).SingleOrDefault();
                    }
                }
                else
                {
                    throw new ArgumentNullException("Predicate value must be passed to FindSingleBy<T>.");
                }
            }

            public virtual IQueryable<T> FindAllBy<T>(Expression<Func<T, bool>> predicate) where T : class 
            {
                if (predicate != null)
                {
                    using (DataContext)
                    {
                        return DataContext.Set<T>().Where(predicate);
                    }
                }
                else
                {
                    throw new ArgumentNullException("Predicate value must be passed to FindAllBy<T>.");
                }
            }

            public virtual IQueryable<T> FindBy<T, TKey>(Expression<Func<T, bool>> predicate,Expression<Func<T, TKey>> orderBy) where T : class 
            {
                if (predicate != null)
                    {
                        if (orderBy != null)
                        {
                            using (DataContext)
                            {
                                return FindAllBy<T>(predicate).OrderBy(orderBy).AsQueryable<T>(); ;
                            }
                        }
                        else
                        {
                            throw new ArgumentNullException("OrderBy value must be passed to FindBy<T,TKey>.");
                        }
                    }
                else
                    {
                            throw new ArgumentNullException("Predicate value must be passed to FindBy<T,TKey>.");
                    }
            }

            public virtual int Save<T>(T Entity) where T : class
            {
                return DataContext.SaveChanges();
            }    
            public virtual int Update<T>(T Entity) where T : class
            {
                return DataContext.SaveChanges();
            }    
            public virtual int Delete<T>(T entity) where T : class
            {
                DataContext.Set<T>().Remove(entity);
                return DataContext.SaveChanges();
            }        
            public void Dispose()
            {
                if (DataContext != null) DataContext.Dispose();
            }
    }

Ich hätte jetzt CustomerRepository : RepositoryBase<Mycontext>,ICustomerRepository ICustomerRepositorydie Möglichkeit, Dinge wie GetCustomerWithOrdersetc zu definieren ...

Ich bin verwirrt, welchen Ansatz ich nehmen soll. Ich brauche Hilfe durch eine Überprüfung der Implementierung, dann Kommentare, welche die einfachere Herangehensweise in Bezug auf Testing und Erweiterbarkeit sind.

Antworten
33
Warum nicht beides haben und Ihre RepositoryBase die IRepository-Schnittstelle implementieren lassen? dreza vor 7 Jahren 2
@dreza, sollte das IRepository über Methoden wie Hinzufügen, Speichern, Löschen oder über Methoden zum Finden, Abrufen und Abrufen verfügen? Ich denke, beide befassen sich mit unterschiedlichen Anliegen. Ein Satz von Methoden ist Abfrage und andere ist Transaktionslogik. Die folgende Frage ist, welche Vorteile / Nachteile die Implementierung der IRepository-Schnittstelle durch RepositoryBase bewirkt. ashutosh raina vor 7 Jahren 0
Ich würde das Repository mit Methoden wie SingleOrDefault (), GetAll (), FirstOrDefault () versehen, anstatt .Fetch (). SingleOrDefault () etc dreza vor 7 Jahren 0
About edit 2: Tests of the RepositoryBase has to be "integrated" since DbContext isn't abstract or have any good interface, but a nice trick is to use Database.SetInitializer(new DropCreateDatabaseAlways()) in those tests. Which means you always have a fresh DB for your integrated tests. It's also useful to move the concrete Repository and UnitOfWork classes and tests into separate assemblies, so you don't have those dependencies in your business logic tests and classes. Otherwise you could always go buy TypeMock which can mock more or less anything. Lars-Erik vor 7 Jahren 0
@ Lars-Erik So RepositoryBase wird durch einen Integrationstest mit einer Datenbank getestet. Ich sollte jedoch in der Lage sein, meine Geschäftslogik durch Komponententests zu testen. Meine POCO-Klassen befinden sich in einer separaten Assembly und das Repository befindet sich in einer separaten Assembly. Mein Unit-Test-Projekt hat nur einen Verweis auf das Repository. Ist das die richtige Struktur? ashutosh raina vor 7 Jahren 0
Behalten Sie zwei Unit-Testprojekte. Einer, der auf die Poco-Assembly verweist (die keine Abhängigkeit von konkretem Uow / Repo hat), und einer, der beide für integrierte Tests referenziert. Lars-Erik vor 7 Jahren 0
Was kann ich in einem Projekt testen, das auf die POCO-Assembly verweist? Ich hatte erwartet, die Logik in meinem Projektarchiv in einem Projekt zu verfassen und eine Datenbank-gestützte Integration in ein anderes zu haben. ashutosh raina vor 7 Jahren 0
Das Conrete-Repo wird eng in Ihr ORM (Entity Framework) kopiert, es handelt sich also um eine DB-Backend-Integration. Die Repo-Schnittstelle ist jedoch nicht vorhanden, sodass sie verspottet und in Geschäftslogiktests verwendet werden kann. Lars-Erik vor 7 Jahren 0

2 Antworten auf die Frage

25
Lars-Erik

Sie sollten Expression<Func<T, bool>>als Vergleichselemente auf Ihrer Schnittstelle verwenden und RepositoryBasediese Schnittstelle implementieren lassen. Wenn Sie nur verwenden Func, erhalten Sie keine Übersetzung in L2E usw., sondern müssen die gesamte DB-Tabelle auflisten, bevor Sie die bewerten können Func. Die Schnittstelle kann simuliert werden, also Unit-Test ohne physische Datenbank und auch mit anderen ORMs.

Es wird im Allgemeinen bevorzugt, die SaveChangesund DbContexteine separate IUnitOfWorkImplementierung zu behalten . Sie können den UnitOfWorkan den Repository-Konstruktor übergeben, der auf eine interne Eigenschaft in der UOW zugreifen kann, die den Zugriff auf den Server bereitstellt DbContext.

Auf diese Weise können Sie dasselbe DbContextzwischen Repositorys und Stapelaktualisierungen für mehrere Entitätsstammtypen unterschiedlichen Typs freigeben. Sie haben auch weniger offene Verbindungen, was die teuerste Ressource ist, die Sie haben, wenn es um DBs geht.

Nicht zu vergessen, dass sie Transaktionen teilen können. :)

Daher sollten Sie den DbContext nicht im Repository ablegen. Das Repository muss überhaupt nicht verfügbar sein. Aber der UnitOfWork / DbContext muss durch etwas entsorgt werden.

Geben Sie auch das OrderByPrädikat auf FindBy. Da Sie ein zurückgeben IQueryableund ein Expressionfür das Prädikat verwenden, können Sie nach dem Aufruf das Queryable weiterhin erstellen. Zum Beispiel repo.FindBy(it => it.Something == something).OrderBy(it => it.OrderProperty). Es wird immer noch in "select [Felder] aus [table] where [prädikat] order by [orderprop]" übersetzt, wenn es aufgelistet wird.

Ansonsten sieht es gut aus.

Hier sind ein paar gute Beispiele:

http://blog.swink.com.au/index.php/c-sharp/generic-repository-for-entity-framework-4-3-dbcontext-with-code-first/

http://www.martinwilley.com/net/code/data/genericrepository.html

http://www.mattdurrant.com/ef-code-first-with-the-repository-and-unit-of-work-patterns/

Und so mache ich es:

Modell / Common / Geschäftssammlung

Keine Referenzen außer BCL und anderen möglichen Modellen (Schnittstellen / dtos)

public interface IUnitOfWork : IDisposable
{
    void Commit();
}

public interface IRepository<T>
{
    void Add(T item);
    void Remove(T item);
    IQueryable<T> Query();
}

// entity classes

Geschäft testet Montage

Verweist nur auf Model / Common / Business Assembly

[TestFixture]
public class BusinessTests
{
    private IRepository<Entity> repo;
    private ConcreteService service;

    [SetUp]
    public void SetUp()
    {
        repo = MockRepository.GenerateStub<IRepository<Entity>>();
        service = new ConcreteService(repo);
    }

    [Test]
    public void Service_DoSomething_DoesSomething()
    {
        var expectedName = "after";
        var entity = new Entity { Name = "before" };
        var list = new List<Entity> { entity };
        repo.Stub(r => r.Query()).Return(list.AsQueryable());
        service.DoStuff();
        Assert.AreEqual(expectedName, entity.Name);
    }

}

Implementierungsassembly für das Entity Framework

Referenzmodell und die Entity Framework / System.Data.Entity-Assemblys

public class EFUnitOfWork : IUnitOfWork
{
    private readonly DbContext context;

    public EFUnitOfWork(DbContext context)
    {
        this.context = context;
    }

    internal DbSet<T> GetDbSet<T>()
        where T : class
    {
        return context.Set<T>();
    }

    public void Commit()
    {
        context.SaveChanges();
    }

    public void Dispose()
    {
        context.Dispose();
    }
}

public class EFRepository<T> : IRepository<T>
        where T : class
{
    private readonly DbSet<T> dbSet;

    public EFRepository(IUnitOfWork unitOfWork)
    {
        var efUnitOfWork = unitOfWork as EFUnitOfWork;
        if (efUnitOfWork == null) throw new Exception("Must be EFUnitOfWork"); // TODO: Typed exception
        dbSet = efUnitOfWork.GetDbSet<T>();
    }

    public void Add(T item)
    {
        dbSet.Add(item);
    }

    public void Remove(T item)
    {
        dbSet.Remove(item);
    }

    public IQueryable<T> Query()
    {
        return dbSet;
    }
}

Integrierte Testmontage

Referenziert alles

[TestFixture]
[Category("Integrated")]
public class IntegratedTest
{
    private EFUnitOfWork uow;
    private EFRepository<Entity> repo;

    [SetUp]
    public void SetUp()
    {
        Database.SetInitializer(new DropCreateDatabaseAlways<YourContext>());
        uow = new EFUnitOfWork(new YourContext());
        repo = new EFRepository<Entity>(uow);
    }

    [TearDown]
    public void TearDown()
    {
        uow.Dispose();
    }

    [Test]
    public void Repository_Add_AddsItem()
    {
        var expected = new Entity { Name = "Test" };
        repo.Add(expected);
        uow.Commit();
        var actual = repo.Query().FirstOrDefault(e => e.Name == "Test");
        Assert.IsNotNull(actual);
    }

    [Test]
    public void Repository_Remove_RemovesItem()
    {
        var expected = new Entity { Name = "Test" };
        repo.Add(expected);
        uow.Commit();
        repo.Remove(expected);
        uow.Commit();
        Assert.AreEqual(0, repo.Query().Count());
    }
}
Ausdruck v / s Funktion ..nice catch .. Die RepositoryBase hat das richtige Prädikat. OrderBy sah für mich auch nutzlos aus. Werde das entfernen Der Link, den Sie bereitstellen, umfasst zwei verschiedene Implementierungen hinsichtlich der Weitergabe von Kontext oder des Weiterleitungskontexts und des Entitätstyps als Typparameter. Welcher ist der bessere oder sind beide gleich und nur eine Frage des Stils. Ich kann mich irgendwie nicht damit auseinandersetzen. ashutosh raina vor 7 Jahren 0
DbContext ist an sich eine UoW, was wären die Vorteile einer separaten UoW? ashutosh raina vor 7 Jahren 0
@ashutoshraina möglicherweise die UoW selbst zu abstrahieren. Die Leute denken, dass sie ihren ORM oder ihren Datenspeicher irgendwann ändern müssen. Meiner Erfahrung nach passiert das nie und je weniger Sie abstrahieren, desto besser. maxbeaudoin vor 7 Jahren 0
It's generally better to mock an interface which doesn't have dependencies on libraries other than the BCL itself. Even if you won't change your ORM, your tests can run without depending on anything but the BCL. Hence the need to wrap DbContext. Won't be much anyway. To make a point, we did this for EF 1 combined with the EF Poco Adapter project from codeplex. When migrating to EF 4, we had to swap out the old contexts and dbset implementations for the new ones, and benefited from having our own UOW and Repos. Lars-Erik vor 7 Jahren 0
@ Lars-Erik See my Edit 2. ashutosh raina vor 7 Jahren 0
6
Peter Kiss

Das Rad neu erfinden

Wenn Sie eine generische Repository-Klasse benötigen, verwenden Sie IObjectSet für den Basistyp und implementieren Sie ihn bei Bedarf (im Produktionscode ist dies nicht erforderlich. In Tests können Sie eine IList <> als Backend verwenden).

Lauch über die Abstraktion

Repositorys sollten nicht über die Save-Methode verfügen, da sich ein anderer Benutzer in einem Transaktionsmoment im geteilten Kontext befindet, dann können Sie (oder der andere Typ) sich gegenseitig betrügen. Bitte erstellen Sie hierfür eine unabhängige IUnitOfWork.

interface IUnitOfWork : IDisposable {
    void Commit();
    void Rollback();
}

interface IDataStore {
    IUnitOfWork InUnitOfWork();
    IObjectSet<T> Set<T>() where T : class;
}

using (var uow = _data.InUnitOfWork()) {
    _data.Set<Order>.AddObject(order);
    _data.Set<SomethingElse>.Remove(otherObject);

   uow.Comit();
}

Der knifflige Teil ist, wie Sie mit der Transaktion umgehen. Ich habe eine ISession-Schnittstelle mit einigen Methoden (BeginTransaction (), InTransaction, Commit (), Rollback ()) erstellt und eine Standardimplementierung erstellt. In meiner IUnitOfWork-Standardimplementierung erhält der Konstruktor eine ISession-Instanz und startet damit eine Transaktion. Die ISession-Implementierung hat einen inneren Zähler, um zu zählen, wie viele andere Akteure ihre eigene Transaktion gestartet haben. Wenn ein IUnitOfWork aus dem gemeinsam genutzten Speicher Commited den Zähler um 1 dekrementiert, wenn jemand RollBack sagt, wird alles zurückgesetzt.

EDIT (03. Dezember)

Das IUnitOfWork Dispose-Verhalten wurde behoben: Der Standard ist Rollback () und nicht Commit ().

Auf diese Weise können Sie Ihren Code einfach testen.

IObjectSet ist eine MS-API-spezifische Schnittstelle, nicht wahr? Sie können es nicht wiederverwenden, wenn Sie zu NHibernate oder RavenDB wechseln, wenn Sie nicht auch das Repository abstrahieren. Lars-Erik vor 7 Jahren 1
+1 für das Rollback @ Lars-Erik sollte nicht rückgängig gemacht werden, wenn das Speichern in der UnitOfWork vorhanden ist? ashutosh raina vor 7 Jahren 0
@ Lars-Erik: Das System.IDisposable ist ein Microsoft-spezifisches Ding, das mit der ganzen Macht des .NET Slong hat. Das IObjectSethat keine besonderen Einschränkungen für TEntity, nur ein einfaches "eine Klasse sein". Ja, Sie können und sollten es wiederverwenden. Peter Kiss vor 7 Jahren 0
AFAIK, DbContext hat kein Rollback, es sei denn, Sie erstellen eine Transaktionsbereich-Instanz und setzen diese zurück. Ich verwende UOW Dispose für ein Rollback, da ich die Transaktion wahrscheinlich sowieso nicht fortsetzen möchte. @ Peter Ich schätze, ich sollte in IObjectSet nachschauen, aber ich nenne es immer noch gern IRepository, da es ein bekannter und bewährter Mustername ist. IObjectSet ist nicht .. Und es sind 20 Zeilen Code, also wen interessiert das wirklich? Lars-Erik vor 7 Jahren 0
20 Zeilen unnötigen Codes können ein großes Durcheinander in der Codebasis verursachen und warum würden Sie plus Code schreiben, wenn er bereits existiert? Eine andere Sache, wo .NET ist, gibt es ein IObjectSet, in einer beliebigen .NET-App. Peter Kiss vor 7 Jahren 0
Ich bevorzuge immer noch bekannte Musternamen, wenn es so einfach ist. Sie müssen sich immer noch an das IObjectSet anpassen, wenn die API dies nicht unterstützt. Warum also nicht etwas mit einem "besseren Namen" anpassen? Lars-Erik vor 7 Jahren 0