Autor Beitrag
Kasko
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 126
Erhaltene Danke: 1

Win 10
C# C++ (VS 2017/19), (Java, PHP)
BeitragVerfasst: Mo 14.02.22 18:11 
Ich möchte eine generische Repository-Struktur erstellen, die sowohl auf Datenbanken als auch auf Tabellen angewendet werden kann, sodass ich sowohl ein Repository pro Datenbank als auch ein Repository für eine bestimmte Tabelle haben kann. Daher habe ich ein Interface IRepository erstellt.

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
public interface IRepository
{
    bool SaveChangesAutomatically { get; set; }
    
    Task InsertAsync<TEntity>(TEntity entity) where TEntity : class;
    Task InsertRangeAsync<TEntity>([NotNull] params TEntity[] entities) where TEntity : class;

    Task DeleteAsync<TEntity>(TEntity entity) where TEntity : class;
    Task DeleteRangeAsync<TEntity>([NotNull] params TEntity[] entities) where TEntity : class;
    Task DeleteWhereAsync<TEntity>(Func<TEntity, bool> condition) where TEntity : class;

    Task UpdateAsync<TEntity>(TEntity entity) where TEntity : class;
    Task UpdateRangeAsync<TEntity>([NotNull] params TEntity[] entities) where TEntity : class;

    TResult Query<TEntity, TResult>(Expression<Func<IQueryable<TEntity>, TResult>> query) where TEntity : class;
    Task SaveChangesAsync();
}


Der Typparameter TEntity befindet sich nicht auf Klassenebene, da ein erbendes DatabaseRepository nicht an einen bestimmten Entitätstyp gebunden ist, da es mehrere Tabellen mit unterschiedlichen Entitätstypen enthält.

Das Interface wird in der abstrakten Klasse Repository implementiert.

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
public abstract class Repository<TDbContext> : IRepository where TDbContext : DbContext
{
    protected TDbContext DbContext { get; }
    
    public virtual bool SaveChangesAutomatically { get; set; }

    protected Repository(TDbContext dbContext)
    {
        this.DbContext = dbContext;
    }

    // implement IRepository members

    protected abstract DbSet<TEntity> UseTable<TEntity>() where TEntity : class;
}


Diese Klasse führt den DB-Kontext ein, implementiert aber die Member unabhängig davon. Es erhält die zu verwendende Tabelle nicht aus dem DB-Kontext, sondern von einer anderen Methode namens UseTable.

Diese Klasse soll dann sowohl von der DatabaseRepository- als auch von der TableRepository-Klasse geerbt werden. Das DatabaseRepository implementiert die Methode UseTable, indem es die erste Property des DB-Kontexts vom Typ DbSet<TEntity> zurückgibt. Das TableRepository soll eine bestimmte DbSet-Eigenschaft zurückgeben.

Aus diesem Grund ist ein generischer Typparameter in den Methoden eines TableRepository nicht mehr erforderlich, da immer dieselbe Tabelle verwendet wird. Aus diesem Grund möchte ich den Typparameter so einschränken, dass er nur der Entitätstyp der Tabelle ist, damit die Methode tatsächlich das DbSet zurückgeben kann, was derzeit nicht möglich ist.

Am Ende sollte es ungefähr so ​​aussehen:

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
public abstract class TableRepository<TDbContext, TEntity> : Repository<TDbContext> where TDbContext : DbContext where TEntity : class
{
    public TableRepository(TDbContext dbContext) : base(dbContext) { }
    
    protected abstract override DbSet<TEntity> UseTable<TEntity>(); // Does not use class level type parameter but introduces own.
}


Und dann die finale Klasse für eine bestimmte Tabelle:

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
public class UserTableRepository : TableRepository<ApplicationDbContext, IdentityUser>
{
    public UserTableRepository(ApplicationDbContext dbContext) : base(dbContext) { }
    
    protected override DbSet<IdentityUser> UseTable() => this.DbContext.Users;
}


Kann man dieses Verhalten irgendwie erreichen? Umgekehrt wäre es auch in Ordnung, also Typeparameter TEntity breits in IRepository und Repository eingeführt. DatabaseRepository bekommt jedoch nur TDbContext und erlaubt jede TEntity.

Ist dies irgendwie möglich?
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4764
Erhaltene Danke: 1052

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: Mo 14.02.22 18:49 
Nein, dies ist mit einem Interface nicht möglich (denn dies ist ja ein Vertrag auf Code-Ebene, der nicht gebrochen werden darf - also vom Compiler überprüft wird)!

Du solltest daher 2 verschiedene Interfaces benutzen (+ evtl. ein nicht-generisches IBaseRepository).