Laden...

[gelöst] Entity Framework + Repository Pattern: Best Pratice gesucht

Erstellt von Mossi vor 8 Jahren Letzter Beitrag vor 8 Jahren 5.809 Views
Mossi Themenstarter:in
199 Beiträge seit 2006
vor 8 Jahren
[gelöst] Entity Framework + Repository Pattern: Best Pratice gesucht

Hallo zusammen,

ich bin gerade dabei, einige Anwendungen auf das Entity Framework umzustellen. Es handelt sich also um bestehende Datenbanken. Bisher wurde in einzelnen Repositories immer eine Tabelle gehandhabt.
Zum Beispiel eine Tabelle Customer hatte ein eigenes Repository mit den Methoden

  • GetCustomerById
  • GetCustomerByName
  • UpdateCustomer
    usw.

In diesen Methode wurden per ADO.Net die Statements zusammengebaut und entsprechend ausgeführt. In den Methoden ist ansonsten keinerlei Logik vorhanden.

Jetzt mit dem EF hab ich das prinzipiell genauso aufgebaut. Allerdings bin ich gerade am tüfteln, wie ich die Context-Klasse, am besten handle. Es gibt in meinen Augen drei Möglichkeiten:

  • Eine Context-Klasse für alle Repositories
  • Eine Context-Klasse pro Repository (bzw. jedes Repository leitet von DbContext ab)
  • Jede Methode in den Repository-Klassen instanziert einen eigenen Context

Was wäre jetzt die beste Möglichkeit?
Möglichkeit 1 fällt in meinen Augen fast aus, weil wenn man Daten aus einer Methode bekommt und diese Daten dann beim Weiterverarbeiten weitere Daten aus einer anderen Tabelle holen müssen, bekommt man eine Exception, dass der DataReader bereits einem anderen Command zugeordnet ist.

Wie handhabt ihr das?

16.806 Beiträge seit 2008
vor 8 Jahren

Der Context muss über mehrere Repositories geteilt werden, die innerhalb einer Scope arbeiten.
Bei Webanwendungen ist dabei jeder Request ein einzelner Scope.

Du injezierst diesen dabei via Interface in den Konstruktor der Repositories.
Das ist auch der allgemeine best practice.

Beispiel (wenn auch die Interfaces fehlen):

public abstract class GenericRepository<T> : IGenericRepository<T>
      where T : BaseEntity
   {
       protected DbContext _entities;
       protected readonly IDbSet<T> _dbset;
 
       public GenericRepository(DbContext context)
       {
           _entities = context;
           _dbset = context.Set<T>();
       }

Für die Threadsicherheit bist Du zuständig.

2.207 Beiträge seit 2011
vor 8 Jahren

Hallo Mossi,

schau dir mal UnitOfWork und Repository-Pattern-Implementierungen an. Normalerweise gibt man den Db-Context in das Repo und das Repo kann das Abfragen auf dem Context ausführen.

Es gibt nur einen Db-Context normalerweise. Bei Webanwendungen sieht man ja auch zu, dass man nur einen Context pro Request erstellt (RequestScope) und den dann den Repo injected.

Gruss

Coffeebean

A
764 Beiträge seit 2007
vor 8 Jahren

Hallo Mossi,

so könnte eine entsprechende UnitOfWork aussehen.
Für den Scope existiert **ein ** Context. Andere UnitsOfWork sollten sich mit diesem nicht in die Quere kommen.
Pro Table ein Repository.


public class CategoryService // = UnitOfWork
{
	private readonly Context _context;

	public RepositoryBase<Category> Categories { get; private set; }
	public RepositoryBase<Usage> Usages { get; private set; }
	public RepositoryBase<TaskType> TaskTypes { get; private set; }

	public CategoryService(string server, string user, string password)
	{
		this._context = new ToolContext(server, user, password);
		this.Categories = new RepositoryBase<Category>(this._context);
		this.Usages = new RepositoryBase<Usage>(this._context);
		this.TaskTypes = new RepositoryBase<TaskType>(this._context);
	}
}

So ein das Repository-Interface mit den standardmässigen ACID-Methoden:


public interface IGenericRepository<TEntity> where TEntity : class
{
    IQueryable<TEntity> GetAll();
    TEntity GetSingle(int id);
    IQueryable<TEntity> FindBy(Expression<Func<TEntity, bool>> predicate);
    void Add(TEntity entity);
    void Delete(TEntity entity);
    void Edit(TEntity entity);
    void Save();
}

Erweitertes Informations-Bedürfnis könnte auch im UnitOfWork befriedigt werden:


public class CategoryService // = UnitOfWork
{
    ....

    public IQueryable<Category> GetCategoriesByUserId(int userId)
    {
        return this.Categories.FindBy(categoryItem => categoryItem.id_user == userId);
    }
}

Gruß, Alf

16.806 Beiträge seit 2008
vor 8 Jahren
public class CategoryService // = UnitOfWork
{
    private readonly Context _context;

    public RepositoryBase<Category> Categories { get; private set; }
    public RepositoryBase<Usage> Usages { get; private set; }
    public RepositoryBase<TaskType> TaskTypes { get; private set; }

    public CategoryService(string server, string user, string password)
    {
        this._context = new ToolContext(server, user, password);
        this.Categories = new RepositoryBase<Category>(this._context);
        this.Usages = new RepositoryBase<Usage>(this._context);
        this.TaskTypes = new RepositoryBase<TaskType>(this._context);
    }
}

Dein Service ist Dein Unit Of Work oder was soll das heissen? 🤔
Und sauber testbar ist das ganze auch nicht. Repositories sollte man in die Services via Dependency Injection übergeben - und der Service sollte vom Context gar nichts wissen.
Das wäre eine Schichtverletzung in meinen Augen.

public class CategoryService // = UnitOfWork
{
    ....

    public IQueryable<Category> GetCategoriesByUserId(int userId)
    {
        return this.Categories.FindBy(categoryItem => categoryItem.id_user == userId);
    }
}

Das ist _eigentlich _der Inhalt eines Repositories; nicht eines Services.

Spezifisch erweiterbar sind Deine Repositories so übrigens nicht.

Mossi Themenstarter:in
199 Beiträge seit 2006
vor 8 Jahren

Danke für die Antworten. ich hab das jetzt angefangen umzusetzen (beim aktuellen Projekt sind das zum Glück nur 5 Repositories und daher gut zum Ausprobieren) und bin dann schon über ein genanntes Problem gestoßen

Für die Threadsicherheit bist Du zuständig.

Ich denke, dass genau das mein Problem ist, weil ich folgende Fehlermeldung erhalte:> Fehlermeldung:

The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe.

Bei den aktuellen Statements handelt es sich um Stammdaten-Abfragen, die in einem Hintergrund-Thread beim Start der Anwendung erfolgen.
Wie kann ich jetzt hier für Threadsicherheit sorgen? Oder sagt die Fehlermeldung ganz was anderes aus?

1.029 Beiträge seit 2010
vor 8 Jahren

Hi,

nun - das liest sich doch recht eindeutig.

Entweder du hast im OnModelCreating was drin - oder du greifst von mehreren Threads zu.

Letzteres sollte unter korrekter Verwendung der UnitOfWork (und hier gehört in meinen Augen auch der Context rein) theoretisch nicht passieren, da deine Background-Threads auch die eigene UnitOfWork haben sollten.

LG

2.207 Beiträge seit 2011
vor 8 Jahren

Ich weiss nicht ob ich das richtig verstehe, aber solche Abfragen gehören nicht ins "OnModelCreating". Die Methode kommt vor dem Initialisieren des eigentlichen Contexts. Da wird das Model aufgebaut. Da sollte eigentlich keine "Business-Logik" rein.

Lass das Model korrekt aufbauen. Danach hast du alles was du brauchst und dann führe deine initiale Abfrage von etwaigen Daten aus.

Gruss

Coffeebean

Mossi Themenstarter:in
199 Beiträge seit 2006
vor 8 Jahren

Versteh ich das richtig, dass jeder Thread eine eigene Instanz von UnitOfWork erstellen sollte und damit dann auch jeder Thread seinen eigenen Kontext hat?

Ich hab jetzt probehalber mal meine Threads deaktiviert und dann klappt eigentlich alles wunderbar. Ist aber natürlich keine dauerhafte Lösung... die Threads haben schon einen Grund 😃

An einer Stelle hab ich das im Eingangsthread angerissene Problem:> Fehlermeldung:

Diesem Command ist bereits ein geöffneter DataReader zugeordnet, der zuerst geschlossen werden muss.

Hier sieht der Quellcode (im Service) so aus:

var updates = IoC.Get<ProductUpdatesRepository>().GetByProduct(product);

if (addGroups)
{
	IList<CustomerGroup> groups = new List<CustomerGroup>();
	foreach (var update in updates)
	{
		foreach (CustomerGroup group in IoC.Get<CustomerGroupsRepository>().GetByProductUpdate(update))
		{
...

Das Fehler tritt auf, wenn die Daten aus dem CustomerGroupsRepository abgerufen werden sollen. Man kann das Ganze relativ leicht umgehen, indem ich im ersten foreach ein "ToList()" anhänge. Aber ist das der ideale Weg?

Ich weiss nicht ob ich das richtig verstehe, aber solche Abfragen gehören nicht ins "OnModelCreating". Die Methode kommt vor dem Initialisieren des eigentlichen Contexts. Da wird das Model aufgebaut. Da sollte eigentlich keine "Business-Logik" rein.

Lass das Model korrekt aufbauen. Danach hast du alles was du brauchst und dann führe deine initiale Abfrage von etwaigen Daten aus.

Im OnModelCreating hab ich ja auch nichts drin stehen... also die Methode hab ich gar nicht überschrieben. Deswegen verwirrt mich die Meldung ein bisschen

Hinweis von Coffeebean vor 8 Jahren

Bitte benutze die richtigen Code-Tags. [Hinweis] Wie poste ich richtig? Punkt 6. Und keine Full-Quotes. Punkt 2.3

16.806 Beiträge seit 2008
vor 8 Jahren

In der Fehlermeldung steht ein "oder" - auf Englisch: or.
Die Fehlermeldung hier wird sich nur auf den Teil des fremden Threadzugriffes beziehen; nicht auf OnModelCreating.

Mossi Themenstarter:in
199 Beiträge seit 2006
vor 8 Jahren

Das denke ich auch. Ich glaub sogar, dass ich die betroffene Stelle bereits gefunden habe

Task task1 = Task.Factory.StartNew(() => Products = IoC.Get<ProductsRepository>().GetAll());
Task task2 = Task.Factory.StartNew(() => CustomerGroups = IoC.Get<CustomerGroupsRepository>().GetAll());
Task task3 = Task.Factory.StartNew(() => FTPs = IoC.Get<FTPRepository>().GetAll());

Task.WaitAll(task1, task2, task3);

Die drei Abfragen sollen an der Stelle wohl parallel ausgeführt werden (die Daten werden in einer Art Cache gehalten) und das scheint das Problem zu verursachen.

Das kann ich ja noch relativ leicht beheben. Aber grundsätzlich ist es schon so, dass mehrere Threads parallel laufen können und auf den gleichen Context zugreifen.

Jetzt müsste ich also irgendwie die IoC-Klasse im Caliburn.Micro Framework dazu bringen, dass sie mir für verschiedene Threads unterschiedliche Instanzen von UnitOfWork liefert. Oder müsste das UnitOfWork irgendwie unabhängig von MEF erstellen.

16.806 Beiträge seit 2008
vor 8 Jahren

Dafür ist mehr oder weniger Dein DI Container (zB Unity) verantwortlich.
Der muss Dir pro Thread einen eigenen Context ausspucken. Wie das jetzt genau bei WPF läuft weiß ich nicht; bei ASP.NET läuft das über die Request-Initialisierung.

Die Frage ist, ob das, was bzw. eher wie Du das machst, in WPF Anwendungen üblich ist.
Scheint mir gefühlstechnisch ziemlich komisch zu sein 😉

Mossi Themenstarter:in
199 Beiträge seit 2006
vor 8 Jahren

In diesem Fall wird MEF als DI-Container verwendet. Auf Anhieb hab ich jetzt keine Möglichkeit gefunden, dass ich da pro Thread andere Instanzen bekomme. Aber irgendwie muss das eigentlich gehen.

P
1.090 Beiträge seit 2011
vor 8 Jahren

Das mit dem ThreadSave (MEF) kannst du über die RequiredCreationPolicy (CreationPolicy.NonShared) regeln.

Zu der Frage bei deinen Schleifen oben, ob ein toList Sinn macht. Wenn du ein ToList machsts werden alle Daten auf einmal geladen, in einer For Each Schleife einzeln. Da ist das toList schneller.

Wenn deine GetAll Methoden nicht wirklich die Datenladen, sondern dir nur ein IQueryabel zurück liefern. Sehe ich jetzt auch keinen Grund sie in einem anderen Thread auszuführen.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

Mossi Themenstarter:in
199 Beiträge seit 2006
vor 8 Jahren

Super... das war's... vielen Dank für die Hilfe.
Jetzt schaut die Struktur auch wieder einigermaßen gut aus.

A
764 Beiträge seit 2007
vor 8 Jahren

Hallo Abt,

Dein Service ist dein Unit Of Work oder was soll das heissen? 👶
...
und der Service sollte vom Context gar nichts wissen.
Das wäre eine Schichtverletzung in meinen Augen.

Um dich richtig zu verstehen:
Services sind bei dir im BL angesiedelt? Dort sollte vom Context nichts zu sehen sein.


public class CategoryService
{
    private readonly CategoryUnitOfWork _unitOfWork;
        
    public CategoryService(ILoginCredentials loginCredentials)
    {
        _unitOfWork = new CategoryUnitOfWork(loginCredentials);
    }
}

Das UnitOfWork ist im DAL angesiedelt und mein CategoryService ist in Wirklichkeit ein UoW und ich nenne es deswegen auch mal so:


    public class CategoryUnitOfWork : IUnitOfWork
    {
        private readonly ToolContext _context;

        public RepositoryBase<Category> Categories { get; private set; }
        public RepositoryBase<Usage> Usages { get; private set; }
        public RepositoryBase<TaskType> TaskTypes { get; private set; }

        public CategoryUnitOfWork(ILoginCredentials loginCredentials)
        {
            _context = new ToolContext(loginCredentials);

            this.Categories = new RepositoryBase<Category>(this._context);
            this.Usages = new RepositoryBase<Usage>(this._context);
            this.TaskTypes = new RepositoryBase<TaskType>(this._context);
        }
    }

...
Und sauber testbar ist das ganze auch nicht. Repositories sollte man in die Services via Dependency Injection übergeben
...

Das verstehe ich jetzt nicht ganz: Um die Repositories zu erstellen muss ich doch den Context haben und der ist nur im UoW bekannt.

Das ist _eigentlich _der Inhalt eines Repositories; nicht eines Services.
Spezifisch erweiterbar sind Deine Repositories so übrigens nicht.

Also besser so:


public class CategoryRepository : RepositoryBase<Category>
{
    public CategoryRepository(ToolContext context) : base(context) { }

    public IQueryable<Category> GetCategoriesByUsageName(string usageName)
    {
        return this.FindBy(categoryItem => categoryItem.Usages.Any(usageItem => usageItem.Name == usageName));
    }
}

Ich glaube mein Thema hat nichts mehr mit dem Ursprünglichen zu tun. Vielleicht sollte man das in einen eigenen Thread verschieben.

A
764 Beiträge seit 2007
vor 8 Jahren

...
Und sauber testbar ist das ganze auch nicht. Repositories sollte man in die Services via Dependency Injection übergeben
...

Ich habe das nochmal angeschaut und mittels FactoryPattern gelöst:


public static class UnitOfWorkFactory
{
    public static CategoryUnitOfWork CreateCategoryUnitOfWork(ILoginCredentials loginCredentials)
    {
        var context = new ToolContext(loginCredentials);
        var categoryUnitOfWork = new CategoryUnitOfWork();
        categoryUnitOfWork.SetCategoryRepository(RepositoryFactory.CreateCategoryRepository(context));
        categoryUnitOfWork.SetUsageRepository(RepositoryFactory.CreateRepository<Usage>(context));
        categoryUnitOfWork.SetTaskTypeRepository(RepositoryFactory.CreateRepository<TaskType>(context));
        return categoryUnitOfWork;
    }
}


public class CategoryService
{
    private readonly CategoryUnitOfWork _unitOfWork;
        
    public CategoryService(ILoginCredentials loginCredentials)
    {
        _unitOfWork = UnitOfWorkFactory.CreateCategoryUnitOfWork(loginCredentials);
    }

    public void Commit()
    {
        _unitOfWork.Categories.Save();
        _unitOfWork.Usages.Save();
        _unitOfWork.TaskTypes.Save();
    }
}

16.806 Beiträge seit 2008
vor 8 Jahren

Trotzdem: warum sollte der Service was vom UoW-Container wissen? Wo ist der Sinn?
Das ist für mich eine Verletzung der Schicht. Was Du da machst sollte nicht in die BL, sondern in den DAL.
Ein Service kennt ein Repository, aber nicht den Context des Repositories.

A
764 Beiträge seit 2007
vor 8 Jahren

Ich versteh das alles nicht. ?( ?( 8o 🤔 X(

Bestimmte Repositories müssen doch den gleichen DbContext verwenden.
Diese Abhängigkeiten sollen aber nur im DAL bekannt sein.
Der Service im BL bekommt also Repositories injected, kennt die Abhängigkeiten aber nicht.

Die Abhängigkeiten müssen aber irgendwo gehalten werden, richtig?

Also habe ich eine statische DatabaseInstance erstellt, die die UnitOfWorks hält und über die die Repositories geholt werden können.


public class CategoryService
{
    public CategoryRepository Categories { get; private set; }

    public CategoryService(CategoryRepository categories)
    {
        this.Categories = categories;
    }

    public void Commit()
    {
        this.Categories.Save();
    }
}

public static class ServiceFactory
{
    public static CategoryService CreateCategoryService(ILoginCredentials loginCredentials)
    {
        return new CategoryService(DatabaseInstance.GetCategoryRepository(loginCredentials)); ;
    }
}

public static class DatabaseInstance
{
    private static CategoryUnitOfWork _categoryUnitOfWork;

    public static CategoryRepository GetCategoryRepository(ILoginCredentials loginCredentials)
    {
        if (_categoryUnitOfWork == null)
            _categoryUnitOfWork = UnitOfWorkFactory.CreateCategoryUnitOfWork(loginCredentials);

        return _categoryUnitOfWork.Categories;
    }
}

16.806 Beiträge seit 2008
vor 8 Jahren

Mir ist völlig schleierhaft, wozu Du die Factories hast.
Sowas kann jeder Dependency Injection Container übernehmen.

177 Beiträge seit 2009
vor 8 Jahren

Hi,

vielleicht hilft der Thread hier auch noch was beizutragen. 👍

A
764 Beiträge seit 2007
vor 8 Jahren

Ich habe zu dem Thema auch noch ein paar interessante Links gefunden. Ich muss mich anscheinend über gewisse Dinge noch einlesen. Wenn ich durch bin, werde ich meine entsprechende Lösung hier posten. Danke für die Anregungen und Hilfe.

Fragen Entity Framework, Repository Pattern und Schichtentrennung
Lebensdauer Entity Framework (Context)
Using Repository and Unit of Work patterns with Entity Framework 4.0
Developer's Guide to Dependency Injection Using Unity
Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application (9 of 10)

P
1.090 Beiträge seit 2011
vor 8 Jahren

Bestimmte Repositories müssen doch den gleichen DbContext verwenden.
Diese Abhängigkeiten sollen aber nur im DAL bekannt sein.
Der Service im BL bekommt also Repositories injected, kennt die Abhängigkeiten aber nicht.

Die Abhängigkeiten müssen aber irgendwo gehalten werden, richtig?

Wie Abt schon sagt, Funktioniert es, das der BL nur die Schnittstelle der Repositories kennt und der IoC die Abhänigkeit auflöst. Wobei man auch (je nach IoC) einstellen kann, das z.B. der gleiche DbContext verwendet wird oder für jede Instanz ein eigener.

Wer jetzt genau für die Entscheidung zuständig ist, hängt einmal vom verwendeten IoC (wenn man einen verwendet) ab und ist sicher auch geschmaks sache (Anforderungen, Legacy Code u.s.w.).

Die gänigen Lösungen, die man im Internet findet, haben meistens ihre eigenen Vor- und Nachteile und da muss man halt Schauen welche am besten auf die eigenen Anforderungen passen.

Und wenn man sich dann für eine Lösung entschieden hat, wird man feststellen, das man damit ca. 90 Prozent ohne Probleme Implementiern kann. Es aber immer noch Anwendungsfälle gibt in denen eine andere Lösung besser ist.

Es gibt halt keine Perfectelösung und schon eine gute Lösung zu finden ist garnicht so einfach. Was man aber mit der Zeit lehrnt ist keine schlechte zu verwenden. 😉

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern