web-dev-qa-db-fra.com

Méthode générique pour récupérer DbSet <T> à partir de DbContext

J'utilise Entity Framework avec une base de données volumineuse (composée de plus de 200 tables).

Essayer de créer une méthode générique qui renvoie le DbSet<T> d’une table spécifique T (c’est-à-dire une classe pouvant être TableA).

La classe d'entité créée (automatiquement) à l'aide du modèle de données d'entité se présente comme suit:

public partial class sqlEntities : DbContext
{

    public virtual DbSet<TableA> TableA { get; set; }
    public virtual DbSet<TableB> TableB { get; set; }
    public virtual DbSet<TableC> TableC { get; set; }
    ... // other methods

}

Ma classe principale est comme ça

public class TableModifier
{
   // Should return first 10 elements from a table of that type T
   public IQueryable<T> GetFromDatabase<T>() where T : EntityObject
   {
       try
       {
           using (sqlEntities ctx = new sqlEntities())
           {
               // Get the DbSet of the type T from the entities model (i.e. DB)
               DbSet<T> dbSet = ctx.Set<T>();
               return dbSet.Take(10);
           }
       }
       catch (Exception ex)
       {
           // Invalid type was provided (i.e. table does not exist in database)
           throw new ArgumentException("Invalid Entity", ex);
       }
   }
   ... // other methods
}

Je dois définir une contrainte where T : EntityObject sur T afin qu'elle se situe dans les limites EntityObject. Si aucune contrainte de ce type n'existait, le DbSet<T> dbSet se plaindrait (c'est-à-dire T doit être un type de référence) de ce qu'il pourrait obtenir plus que ce à quoi il s'attend en termes de types ( based this).

Le problème survient lorsque j'essaie d'appeler la méthode avec un type spécifique.

 [TestMethod]
 public void Test_GetTest()
 {
     TableModifier t_modifier = new TableModifier();

     // The get method now only accepts types of type EntityObject
     IQueryable<TableA> i_q = t_modifier.GetFromDatabase<TableA>();
 }

Cela donne une erreur:

There is no implicit reference conversion from 'TableMod.TableA' to
'System.Data.Entity.Core.Objects.DataClasses.EntityObject'.

Comment puis-je (transtyper?) Le type TableA en tant que EntityObject si je sais qu'il existe pour ce modèle d'entité?

Bien que ce soit une syntaxe (et une logique) incorrecte, voici ce que je recherche:

 t_modifier.GetFromDatabase<(EntityObject)TableA>();

Comment définir le type TableA (avec toutes les 200 autres tables) comme faisant partie de EntityObject?


Une solution potentielle

Il s’est avéré que ma contrainte était trop spécifique, tout ce que je devais changer était de where T : IEntity à

where T : class

Donc, la T correspond à ce que DbSet<T> avait initialement prévu, un type de classe

Vous évite d'avoir à ajouter des implémentations aux 200 classes de tables, TableA, TableB, ...

Ensuite, il y a bien sûr d'autres problèmes, tels que le changement du type de retour de IQueryable<T> à List<T> car la variable IQueryable serait sinon renvoyée en dehors du champ d'application de la variable DbContext (c'est-à-dire sqlEntities), la rendant inutile.

12
Serge P

Pourquoi n'essayez-vous pas de changer votre contrainte en classe au lieu d'EntityObject

public IQueryable<T> GetFromDatabase<T>() where T : class
7
Sergio Inxunxa

J'avais la même exigence et l'ai résolue en utilisant ce qui suit:

public static void GetEntitiesGeneric<TEntity>()// where TEntity : System.Data.Entity.Core.Objects.DataClasses.EntityObject  <-- NO LONGER NEEDED
{
    try
    {
        var key = typeof(TEntity).Name;
        var adapter = (IObjectContextAdapter)MyDbContext;
        var objectContext = adapter.ObjectContext;
        // 1. we need the container for the conceptual model
        var container = objectContext.MetadataWorkspace.GetEntityContainer(
            objectContext.DefaultContainerName, System.Data.Entity.Core.Metadata.Edm.DataSpace.CSpace);
        // 2. we need the name given to the element set in that conceptual model
        var name = container.BaseEntitySets.Where((s) => s.ElementType.Name.Equals(key)).FirstOrDefault().Name;
        // 3. finally, we can create a basic query for this set
        var query = objectContext.CreateQuery<TEntity>("[" + name + "]");

        // Work with your query ...
    }
    catch (Exception ex)
    {
        throw new ArgumentException("Invalid Entity Type supplied for Lookup", ex);
    }
}

Le code provient de Utilisation de Generics pour les tables de recherche dans Entity Framework et adapté pour EF 6 à l'aide de DbContext (première partie de la méthode où la objectcontext est extraite de la dbcontext

J'espère que ça aide

4
blfuentes

Problème  

Je suppose que votre classe TableA n'implémente pas EntityObject. C'est pourquoi vous obtenez cette erreur. Pour résoudre ce problème, vous pouvez avoir une classe/interface abstraite qui sera la base de toutes les entités de contexte (c'est-à-dire IContextEntity qui aura une définition d'identifiant unique):

public class TableA : IContextEntity
{
   ...
}

Ensuite, même méthode mais avec une nouvelle interface à la place de EntityObject et vous pourrez la simuler/la tester facilement

public IQueryable<T> GetFromDatabase<T>() where T : IContextEntity
{
     ...
}


Utilisation (Tests)

La deuxième chose importante est la façon dont vous souhaitez utiliser cette méthode. Dans le cas de Entity Framework context, il est réellement important de séparer intégration et tests unitaires . Dans le code que vous avez fourni, vous essayez d'accéder à la base de données, ce qui signifie que ce test sera une intégration:

using (sqlEntities ctx = new sqlEntities()) // This will open a DB connection

Se connecter à une base de données ou à des sources externes est généralement une mauvaise pratique, à moins que vous ne sachiez ce que vous faites et que c'est exactement ça. Si vous avez juste besoin de fausses données factices pour exécuter une action dessus, utilisez Stubs

0
Anatolii Gabuza

Je ne sais pas comment vous avez créé votre modèle et donc à quoi ressemblent vos entités. Cependant, s'il s'agit du code d'abord, les classes d'entité n'héritent pas d'une classe de base commune, vous ne pouvez donc pas ajouter de contrainte de type à votre nom générique.

Je ne recommande pas d'utiliser une classe de base pour pouvoir spécifier une contrainte. C'est beaucoup mieux de le faire en utilisant une interface. Une interface vide vous permettra de spécifier une contrainte sans avoir à changer vos classes.

Donc, ce que vous pouvez faire est de définir une interface comme celle-ci:

public interface IEntity {};

Et alors:

  • implémentez-le dans toutes les classes, ce qui peut être fait dans des fichiers de classes partielles, en modifiant un modèle T4 ou d'une autre manière en fonction de l'apparence de votre modèle
  • utilisez-le pour spécifier le type générique contraint avec where IEntity

C'est la manière la plus propre de le faire, sans aucune ingérence dans vos classes.

0
JotaBe

Pour tous les futurs googlers, mon collègue et moi-même avons corrigé cela dans Visual Basic (version EF 6). Cela fonctionne pour notre cas d'utilisation consistant simplement à récupérer une liste, mais fonctionnera probablement pour les autres cas d'utilisation. Pas d'essayer d'attraper ou de vérifier dans celui-ci.

Private Class getList(Of T As Class)
    Public Shared Function getList() As List(Of T)
        Using ctx As New MVPBTEntities()
            ' Get the DbSet of the type T from the entities model (i.e. DB)
            Dim dbSet = ctx.Set(Of T)()
            Return dbSet.ToList
        End Using
    End Function
End Class
0
Richard Griffiths