Laden...

Repository Pattern: Wie ein IEnumerable in einem DataModel füllen wenn die Entity eine andere ist?

Erstellt von Olii vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.486 Views
O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 5 Jahren
Repository Pattern: Wie ein IEnumerable in einem DataModel füllen wenn die Entity eine andere ist?

Hallo liebe User,

ich habe mal eine Frage an der ich mir schon zwei Tage den Kopf zerbreche.

Und zwar bin ich gerade dabei das Repository Pattern zu implementieren in meine Anwendung (WebApi).

Mein Problem ist nun das ich diese Fehlermeldung bekomme: > Fehlermeldung:

Cannot implicitly convert type 'System.Collections.Generic.IEnumerable<JobBusiness.Models.Employee.Employee>' to 'System.Collections.Generic.IEnumerable<JobBusiness.Models.Employee.EmployeeJob>'.

Das ist auch logisch, wieso zeige ich nun:

Ich habe ein DataLayer der von IRepository die Methoden erbt.
Dieser sieht so aus:

using Dapper;
using Npgsql;
using System.Collections.Generic;
using JobBusiness.Repositories;
using System;
using System.Linq;
using System.Reflection;
using System.ComponentModel.DataAnnotations;
 
namespace JobBusiness.RepositoriesPostgresql
{
    public abstract class AbstractPostgresqlRepository<TEntity,TPrimaryKey> : IRepository<TEntity, TPrimaryKey> where TEntity : BaseEntity<TPrimaryKey>
    {
        private string _connectionString;
 
        public AbstractPostgresqlRepository(string connectionString)
        {
            _connectionString = connectionString;
        }
 
        /*public IEnumerable<TEntity> Get(String strTableName)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
                string query = string.Format("SELECT * FROM {0}", strTableName);
                return connection.Query<TEntity>(query);
            }
        }*/

        public IEnumerable<TEntity> Get(String strTableName, String strColumns, int ID)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
                string query = string.Format("SELECT {0} FROM {1} WHERE Id = {2}", strColumns ,strTableName, ID);
                return connection.Query<TEntity>(query);
            }
        }

        public IEnumerable<TEntity> Get(String sql)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
                return connection.Query<TEntity>(sql);
            }
        }

        public TEntity GetSingelEntity(String sql)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
                return connection.Query<TEntity>(sql).First();
            }
        }
 
        public TEntity Get(TPrimaryKey id)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
                string query = string.Format("SELECT * FROM {0} WHERE Id = @Id LIMIT 1", TableName);
                return connection.Query<TEntity>(query, new { Id = id }).First();;
            }
        }
 
        public void Add(TEntity entity)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
 
                IEnumerable<KeyValuePair<string, string>> RowsAndValues = ResolveProperties(entity);
                IEnumerable<string> keys = RowsAndValues.Select(c => c.Key);
                IEnumerable<string> values = RowsAndValues.Select(c => c.Value);
                string query = string.Format("INSERT INTO {0} ({1}) VALUES ({2});", TableName, string.Join(",",keys), string.Join(",", values));
                connection.Execute(query);
            }
        }
 
        public void Update(TEntity entity)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
 
                IEnumerable<KeyValuePair<string, string>> RowsAndValues = ResolveProperties(entity);
                IEnumerable<string> keys = RowsAndValues.Select(c => c.Key);
                IEnumerable<string> values = RowsAndValues.Select(c => c.Value);
                string query = string.Format("UPDATE {0} SET ({1}) = ({2}) WHERE Id = @Id;", TableName, string.Join(",", keys), string.Join(",", values));
                connection.Execute(query, new { Id = entity.Id });
            }
        }
 
        public void Remove(TEntity entity)
        {
            using (NpgsqlConnection connection = new NpgsqlConnection(_connectionString))
            {
                connection.Open();
                string query = string.Format("DELETE FROM {0} WHERE Id = @Id", TableName);
                connection.Execute(query, new { Id = entity.Id });
            }
        }
 
        private IEnumerable<KeyValuePair<string, string>> ResolveProperties(TEntity entity)
        {
            List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
 
            PropertyInfo[] infos = entity.GetType().GetProperties();
            foreach (PropertyInfo info in infos)
            {
                if(info.GetCustomAttribute<KeyAttribute>() == null)
                {
                    result.Add(new KeyValuePair<string, string>(info.Name, string.Format("'{0}'", info.GetValue(entity))));
                }
            }
 
            return result;
        }
 
        //protected abstract string TableName { get; }
    }
}

Mein Repository zum Implementieren von Zusatzcode sieht wie folgt aus:

using JobBusiness.Repositories;
using JobBusiness.Models.Employee;
using JobBusiness.RepositoriesPostgresql;
 
namespace JobBusiness.Repositories
{
    public class EmployeeRepository : AbstractPostgresqlRepository<Employee, int>, IEmployeeRepository
    {
        public EmployeeRepository(string connectionString) : base(connectionString)
        {

        }

        public Employee getEmployee (int ID)
        {
            Employee SingelEmployee;
            SingelEmployee = base.GetSingelEntity("SELECT id, description From Employee where id =" + ID.ToString());
            SingelEmployee.IEnumEmployeeJob = base.Get("SELECT ID from EmployeeJob WHERE IDEmployee = " + ID.ToString());
            return SingelEmployee;
        }
        
    }
}

Dazu habe ich noch zwei Datamodels:
Employee:

using System.Collections.Generic;
using JobBusiness.Repositories;
 
namespace JobBusiness.Models.Employee
{
    public class Employee : BaseEntity<int>
    {
        public string display_name { get; set; }
        public string description { get; set; }
        public IEnumerable<EmployeeJob> IEnumEmployeeJob;
    }
}

Was einen einzelnen Arbeiter darstellt. Allerdings hat jeder Arbeiter eine Liste mit Jobs für die er qualifiziert ist. Also hat mein Model für den Employee noch die Ienumerable<EmployeeJob>:

using JobBusiness.Repositories;
 
namespace JobBusiness.Models.Employee
{
    public class EmployeeJob : BaseEntity<int>
    {
        public string IDEmployeeJob { get; set; }
        public string name { get; set; }
        public string description { get; set; }
    }
}

Das Problem ist nun das in meinem Repository für den Employee möchte ich eine Methode machen um einen Arbeiter und seine Jobs zu erhalten als Rückgabewert:

public Employee getEmployee (int ID)
        {
            Employee SingelEmployee;
            SingelEmployee = base.GetSingelEntity("SELECT id, description From Employee where id =" + ID.ToString());
            SingelEmployee.IEnumEmployeeJob = base.Get("SELECT ID from EmployeeJob WHERE IDEmployee = " + ID.ToString());
            return SingelEmployee;
        }

Das funktioniert bis zu dieser Zeile:

SingelEmployee.IEnumEmployeeJob = base.Get("SELECT ID from EmployeeJob WHERE IDEmployee = " + ID.ToString());

Da das Repository vom DataAccessLayer erbt und bei der Instanziierung der Typ auf Employee gesetzt wird:

public class EmployeeRepository : AbstractPostgresqlRepository<Employee, int>, IEmployeeRepository

Somit kann ich in meiner Funktion diese Zeile nicht ausführen:

SingelEmployee.IEnumEmployeeJob = base.Get("SELECT ID from EmployeeJob WHERE IDEmployee = " + ID.ToString());

Da das IEnumerable von einem anderen Typen ist, und zwar von EmployeeJob und nicht Employee und mir die BaseFunktion nur ein Employee in dem Fall zurück geben kann.

Aber ich habe keine Lösung wie ich das nun Ändern bzw Anpassen kann.
das repository Pattern finde ich gut aber ich bin damit nicht so erfahren.

Hat jemand vielleicht einen Vorschlag wie man so einen Fall handel kann?
Ich muss ja irgendwie die Liste bzw. das IEnumerable gefüllt bekommen. Ich hatte überlegt noch extra weitere Klassen für diesen Typen zu erstellen aber dann bin ich schnell mal bei 30+Klassen irgendwann.

Für die Injection verwende ich folgende Zeile:

services.AddSingleton<IEmployeeRepository, EmployeeRepository>(parameter => new EmployeeRepository(connection));

Freu mich über jede Hilfe 😃

16.828 Beiträge seit 2008
vor 5 Jahren

IEnumerable hat in einem Repository (bzw. der Datenbankschicht und damit auch die Navigation Properties von Entities) nichts zu suchen - nie.
In Deinem Fall funktioniert das in der Kombination des Umgangs der Datenbankverbindung auch nur, weil Dapper hier die Materialisierung nicht Lazy umsetzt.
Dapper arbeitet mit einem materialisierten Result-Set; ergo kannst Du direkt auch mit IList arbeiten, was im Falle von Dapper der korrekte Weg ist.
Wenn Du das in der Form mit dem Entity Framework oder anderen ORMs, die nicht sofort materialisieren, machen würdest, würde Dir das im hohen Bogen um die Ohren fliegen 😉

Es gibt quasi zwei Rückgabevarianten: IList für materialisierte ResulSets und IQueryable für Lazy-Loading Funktionalitäten, die man dem Repository-Konsumenten (zB. für Filterungen) anbieten will.

Bei Deinem Sachverhalt macht Dir die generische Implementierung einen Strich durch die Rechnung; Du kannst kein Repository auf diese Art und Weise implementieren, die verschiedene Entitäten zurück gibt.
Die generische Implemenierung an der Stelle ist nicht flexibel genug.

Dein Basis-Repository muss so implementiert werden, dass Du weiterhin Typen übergeben kannst; sprich zusätzlich zur starren Implementierung eine weitere (Exemplarisch):

    public abstract class BaseRepository<TBaseEntity, TPrimaryKey> where TBaseEntity : BaseEntity<TPrimaryKey>
    {
        public TBaseEntity GetById(TPrimaryKey id)
        {
            throw new NotImplementedException();
        }

        public TEntity GetById<TEntity>(TPrimaryKey id) where TEntity : BaseEntity<TPrimaryKey>
        {
            throw new NotImplementedException();
        }
    }

Du siehst TBaseEntity ist nun der Typ des Repositories, TEntity eine spezifische Implementierung. So bleibst Du prinzipiell flexibler in den Abfragen.

Ist nicht schön, löst aber Dein Problem.

Weitere Punkte die mir auffallen: * Du bietest einen flexiblen Typen für den Key an via Generic; dann aber hast in den Methoden überall den fixen Typ int 😃

  • Du verwendest ASP.NET Core; bald sind asynchrone Implementierungen (wie heute schon bei mobilen Anwendungen) auch bei ASP.NET Core pflicht - und Datenbank-Operationen sollten asynchron sein. Verwende also async/await asap 😉

  • beachte, dass Du Parameter in SQL bitte überall verwendest, um sauberes SQL zu erzeugen.

  • Ebenso schaue auf korrekte Namen und die Trennung von Schichten.
    [Artikel] Drei-Schichten-Architektur
    Employee ist dem Namen zu folge ein Business Modell - gehört in den Business Layer.
    EmployeeEntity wäre Deine Entität in der Datenbank-Schicht.

  • Die Verbindung gehört via Dependency Injection in den Konstruktor umgesetzt.
    Im Falle von ASP.NET Core bekommst Du das alles geschenkt: Dependency Injection in ASP.NET Core.
    Dein Code ist so ansonsten null testbar; extrem hohe Dependency.
    Siehe Beispielimplementierung von Erste Schritte mit ASP.NET Core und Entity Framework 6

  • Deine Dependency Registrierung von AddSingleton ist ebenfalls nicht zu empfehlen. Alle Datenbank-Aktivitäten sollten via Scoped registriert, was zur Folge hat, dass es nicht wie beim Singleton eine Instanz in der gesamten Anwendung, sondern eine Instanz pro Request gibt.
    Siehe Beispielimplementierung von Erste Schritte mit ASP.NET Core und Entity Framework 6

  • Programmier defensiv. Abfrage wie First() knallen, wenn ein Datenbank-Query keine Resultate liefert. Besser. FirstOrDefault - oder wenn Du ohnehin nur einen Eintragen haben kannst (weil IDs unique sind) SingleOrDefault.

  • Dapper ist ein Micro ORM. Diesen solltest Du auch so behandeln. Sprich das ganze Property-Info Zeug will man eigentlich bewusst in Dapper nicht.
    Willst Du Automatismen wie die Auflösung von Eigenschaften für Spalten, verwende einen Provider wie EFCore.

PS:

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 5 Jahren

Danke erstmal für die sehr hilfreiche Antwort Abt.

Es gibt noch viel zu lernen. Async wollte ich machen sobald ich den code an sich zum funktionieren gebracht habe 😄.

Nur eine Sache wird mir nicht klar, wieso sollte man kein IEnumerable verwenden im Repository?

Ich habe mir diese Beitrag angesehen und dort wird dazu geraten den IEnumerable zu verwenden:

4 häufige Fehler beim Repository pattern

Und danke für den Lösungsansatz. Wieso wäre dieser denn nicht schön? Bzw. wie könnte man es denn schöner machen?

16.828 Beiträge seit 2008
vor 5 Jahren

Der Beispielcode in Deinem Link

public IEnumerable<OrderViewModel> GetOrders() 
{
     var orders = context.Orders.ToList();
 
     return mapper.Map<List<Order>, List<OrderViewModel>(orders);
}

finde ich alles andere als gut.
Angefangen dabei, dass er "ViewModels" im Datenbank-Code hat - ViewModels sind aber Bezeichner für Klassen in der UI-Schicht; haben also in der Datenbank-Eben nichts zu suchen.

Wenn man eine Liste hat - und hier wird durch ToList bewusst die Materialisierung forciert - dann sollte man auch die Liste (bzw. IList) zurück geben und nicht nur IEnumerable.
In meinen Augen ein Paradebeispiel für die falsche Verwendung von IEnumerable.

In einer Datenbank-Schicht hat man eigentlich nur zwei Zustände*: entweder das ResultSet ist im Speicher und kann mit IList arbeiten - oder es ist eine Lazy Implementierung und man verwendet IQueryable.

*es gibt aber Datenbank-Provider (zB Azure Cosmos DB), die Results nicht in einem großen Block zurück geben, sondern als Chunks.
Wenn ich also 100 Treffer habe, dann gibt mir die CosmosDB immer 10 Treffer, bevor ich die nächsten 10 bekomme.
Hier würde IEnumerable tatsächlich im Zusammenspiel von yield (technisch) sinn machen.
Ist hier aber nicht gegeben.

Daher stimme ich diesem Link und seiner Empfehlung zu IEnumerable Null-Komma-Null zu.
Es gibt keinen Vorteil von IEnumerable hier - sondern im Gegenteil: Nachteile.
Statische Code-Analyzers (oder auch der ReSharoper) werfen auch Warnings, wenn ein Enumerator durch das unnötige Verwenden von IEnumerable potentiell mehrfach durchlaufen werden muss.

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 5 Jahren

Oh, traue niemals dem Internet wie es aussieht 😄.
Okey ich werd das innumerable mal versuchen alles um zu bauen.

Aber wie ich meine struktur schöner gestalten kann weiß ich leider nicht 😕

16.828 Beiträge seit 2008
vor 5 Jahren

Aber wie ich meine struktur schöner gestalten kann weiß ich leider nicht 😕

Wie gesagt; der Repository Pattern bekommt Konkurrenz.
Gibt halt andere Pattern, die flexibler sind (eben CQRS).

Letzten Endes kommt es aber auf Deine Implementierung an.
Es kann sich auch lohnen pro Entität (oder Partial Entity / Projektion) ein eigenes Repository zu machen - aber das kommt halt insgesamt auf das Projekt an.

Bei kleineren Projekten kommt man beim Repository Pattern und der (strikteren) Schichtentrennung schneller in nen Overkill als in nem größeren Projekt...