LINQ to Entities só oferece suporte a cast de tipos primitivos de EDM ou enumeração com interface IEntity

96

Eu tenho o seguinte método de extensão genérico:

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;
}

Infelizmente, o Entity Framework não sabe como lidar com o, predicatejá que C # converteu o predicado para o seguinte:

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

Entity Framework lança a seguinte exceção:

Não é possível lançar o tipo 'IEntity' para 'SomeEntity'. O LINQ to Entities só oferece suporte à conversão de tipos primitivos ou de enumeração de EDM.

Como podemos fazer o Entity Framework funcionar com nossa IEntityinterface?

Steven
fonte

Respostas:

188

Consegui resolver isso adicionando a classrestrição de tipo genérico ao método de extensão. Não sei por que funciona, no entanto.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
Sam
fonte
6
Funciona para mim também! Eu adoraria que alguém pudesse explicar isso. #linqblackmagic
berko
Você pode explicar como você adicionou essa restrição
yrahman,
5
Meu palpite é que o tipo de classe é usado em vez do tipo de interface. EF não sabe sobre o tipo de interface, portanto, não pode convertê-lo para SQL. Com a restrição de classe, o tipo inferido é o tipo DbSet <T> com o qual EF sabe o que fazer.
jwize
1
Perfeito, é ótimo poder realizar consultas baseadas em interface e ainda manter a coleção como IQueryable. Um pouco chato, no entanto, que basicamente não há maneira de pensar nessa correção, sem conhecer o funcionamento interno da EF.
Anders
O que você está vendo aqui é uma restrição de tempo de compilador que permite ao compilador C # determinar que T é do tipo IEntity dentro do método, portanto, é capaz de determinar que qualquer uso de "coisas" de IEntity é válido durante o tempo de compilação pelo código MSIL gerado irá executar esta verificação automaticamente para você antes da chamada. Para esclarecer, adicionar "classe" como uma restrição de tipo aqui permite que collection.FirstOrDefault () seja executado corretamente, pois provavelmente retorna uma nova instância de T chamando um ctor padrão em um tipo baseado em classe.
Guerra
64

Algumas explicações adicionais sobre a class"correção".

Esta resposta mostra duas expressões diferentes, uma com e outra sem where T: classrestrição. Sem a classrestrição, temos:

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

e com a restrição:

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

Essas duas expressões são tratadas de maneira diferente pela estrutura da entidade. Olhando para as fontes do EF 6 , pode-se descobrir que a exceção vem daqui, vejaValidateAndAdjustCastTypes() .

O que acontece é que EF tenta lançar IEntityem algo que faça sentido no mundo do modelo de domínio, mas falha em fazer isso, portanto, a exceção é lançada.

A expressão com a classrestrição não contém o Convert()operador, o cast não é tentado e está tudo bem.

Ainda permanece a questão em aberto, por que LINQ constrói expressões diferentes? Espero que algum assistente C # seja capaz de explicar isso.

Tadej Mali
fonte
1
Obrigada pelo esclarecimento.
Jace Rhea,
9
@JonSkeet alguém tentou invocar um assistente C # aqui. Onde você está?
Nick N.
23

Entity Framework não oferece suporte para isso fora da caixa, mas um ExpressionVisitorque traduz a expressão é facilmente escrito:

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);
    }
}

A única coisa que você terá que fazer é converter o predicado passado usando a expressão visitor da seguinte maneira:

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);
    }

    ...
}

Outra abordagem menos flexível é fazer uso de 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
fonte
1

Eu tive o mesmo erro, mas um problema semelhante, mas diferente. Eu estava tentando criar uma função de extensão que retornasse IQueryable, mas os critérios de filtro eram baseados na classe base.

Acabei encontrando a solução que meu método de extensão deveria chamar .Select (e => e as T), onde T é a classe filha ee é a classe base.

os detalhes completos estão aqui: Crie a extensão IQueryable <T> usando a classe base no EF

Justin
fonte