LINQ to Entities prend uniquement en charge le cast de types primitifs ou énumération EDM avec l'interface IEntity

96

J'ai la méthode d'extension générique suivante:

public static T GetById<T>(this IQueryable<T> collection, Guid id) 
    where T : IEntity
{
    Expression<Func<T, bool>> predicate = e => e.Id == id;

    T entity;

    // Allow reporting more descriptive error messages.
    try
    {
        entity = collection.SingleOrDefault(predicate);
    }
    catch (Exception ex)
    {
        throw new InvalidOperationException(string.Format(
            "There was an error retrieving an {0} with id {1}. {2}",
            typeof(T).Name, id, ex.Message), ex);
    }

    if (entity == null)
    {
        throw new KeyNotFoundException(string.Format(
            "{0} with id {1} was not found.",
            typeof(T).Name, id));
    }

    return entity;
}

Malheureusement, Entity Framework ne sait pas comment gérer le predicatepuisque C # a converti le prédicat en ce qui suit:

e => ((IEntity)e).Id == id

Entity Framework lève l'exception suivante:

Impossible de convertir le type «IEntity» en «SomeEntity». LINQ to Entities prend uniquement en charge le cast de types primitifs ou énumération EDM.

Comment pouvons-nous faire fonctionner Entity Framework avec notre IEntityinterface?

Steven
la source

Réponses:

188

J'ai pu résoudre ce problème en ajoutant la classcontrainte de type générique à la méthode d'extension. Je ne sais pas pourquoi cela fonctionne, cependant.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
Sam
la source
6
Fonctionne aussi pour moi! J'adorerais que quelqu'un puisse expliquer cela. #linqblackmagic
berko
Pouvez-vous s'il vous plaît expliquer comment avez-vous ajouté cette contrainte
yrahman
5
Je suppose que le type de classe est utilisé plutôt que le type d'interface. EF ne connaît pas le type d'interface et ne peut donc pas le convertir en SQL. Avec la contrainte de classe, le type déduit est le type DbSet <T> avec lequel EF sait quoi faire.
jwize
1
Parfait, c'est formidable de pouvoir effectuer des requêtes basées sur l'interface tout en conservant la collection comme IQueryable. Un peu ennuyeux cependant qu'il n'y ait fondamentalement aucun moyen d'imaginer ce correctif, sans connaître le fonctionnement interne d'EF.
Anders
Ce que vous voyez ici est une contrainte de temps du compilateur qui permet au compilateur C # de déterminer que T est de type IEntity dans la méthode afin de pouvoir déterminer que toute utilisation de IEntity "stuff" est valide car pendant la compilation, le code MSIL généré effectuera automatiquement cette vérification pour vous avant l'appel. Pour clarifier, l'ajout de "classe" comme contrainte de type ici permet à collection.FirstOrDefault () de s'exécuter correctement car elle retourne probablement une nouvelle instance de T appelant un ctor par défaut sur un type basé sur une classe.
Guerre
64

Quelques explications supplémentaires concernant le class"correctif".

Cette réponse montre deux expressions différentes, l'une avec et l'autre sans where T: classcontrainte. Sans la classcontrainte, nous avons:

e => e.Id == id // becomes: Convert(e).Id == id

et avec la contrainte:

e => e.Id == id // becomes: e.Id == id

Ces deux expressions sont traitées différemment par le framework d'entité. En regardant les sources EF 6 , on peut constater que l'exception vient d' ici, voirValidateAndAdjustCastTypes() .

Ce qui se passe, c'est que EF essaie de IEntityconvertir en quelque chose qui a du sens dans le monde du modèle de domaine, mais il échoue à le faire, par conséquent l'exception est levée.

L'expression avec la classcontrainte ne contient pas l' Convert()opérateur, le cast n'est pas essayé et tout va bien.

Il reste encore une question ouverte, pourquoi LINQ construit des expressions différentes? J'espère que certains assistants C # seront en mesure d'expliquer cela.

Tadej Mali
la source
1
Merci pour l'explication.
Jace Rhea
9
@JonSkeet quelqu'un a essayé d'invoquer un assistant C # ici. Où es-tu?
Nick N.1
23

Entity Framework ne prend pas en charge cela par défaut, mais un ExpressionVisitorqui traduit l'expression s'écrit facilement:

private sealed class EntityCastRemoverVisitor : ExpressionVisitor
{
    public static Expression<Func<T, bool>> Convert<T>(
        Expression<Func<T, bool>> predicate)
    {
        var visitor = new EntityCastRemoverVisitor();

        var visitedExpression = visitor.Visit(predicate);

        return (Expression<Func<T, bool>>)visitedExpression;
    }

    protected override Expression VisitUnary(UnaryExpression node)
    {
        if (node.NodeType == ExpressionType.Convert && node.Type == typeof(IEntity))
        {
            return node.Operand;
        }

        return base.VisitUnary(node);
    }
}

La seule chose que vous devrez faire est de convertir le prédicat passé en utilisant l'expression visiteur comme suit:

public static T GetById<T>(this IQueryable<T> collection, 
    Expression<Func<T, bool>> predicate, Guid id)
    where T : IEntity
{
    T entity;

    // Add this line!
    predicate = EntityCastRemoverVisitor.Convert(predicate);

    try
    {
        entity = collection.SingleOrDefault(predicate);
    }

    ...
}

Une autre approche - sans flexibilité - consiste à utiliser DbSet<T>.Find:

// NOTE: This is an extension method on DbSet<T> instead of IQueryable<T>
public static T GetById<T>(this DbSet<T> collection, Guid id) 
    where T : class, IEntity
{
    T entity;

    // Allow reporting more descriptive error messages.
    try
    {
        entity = collection.Find(id);
    }

    ...
}
Steven
la source
1

J'ai eu la même erreur mais un problème similaire mais différent. J'essayais de créer une fonction d'extension qui retournait IQueryable mais les critères de filtre étaient basés sur la classe de base.

J'ai finalement trouvé la solution qui était pour ma méthode d'extension d'appeler .Select (e => e comme T) où T est la classe enfant et e est la classe de base.

tous les détails sont ici: Créer l'extension IQueryable <T> à l'aide de la classe de base dans EF

Justin
la source