LINQ to Entities solo admite la conversión de tipos primitivos o de enumeración de EDM con la interfaz IEntity

96

Tengo el siguiente método de extensión 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;
}

Desafortunadamente, Entity Framework no sabe cómo manejar el predicateya que C # convirtió el predicado a lo siguiente:

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

Entity Framework lanza la siguiente excepción:

No se puede convertir el tipo 'IEntity' para escribir 'SomeEntity'. LINQ to Entities solo admite la conversión de tipos primitivos o de enumeración de EDM.

¿Cómo podemos hacer que Entity Framework funcione con nuestra IEntityinterfaz?

Steven
fuente

Respuestas:

188

Pude resolver esto agregando la classrestricción de tipo genérico al método de extensión. Sin embargo, no estoy seguro de por qué funciona.

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}
Sam
fuente
6
¡Funciona para mí también! Me encantaría que alguien pudiera explicar esto. #linqblackmagic
berko
¿Puede explicar cómo agregó esta restricción?
yrahman
5
Supongo que se usa el tipo de clase en lugar del tipo de interfaz. EF no conoce el tipo de interfaz, por lo que no puede convertirlo a SQL. Con la restricción de clase, el tipo inferido es el tipo DbSet <T> con el que EF sabe qué hacer.
jwize
1
Perfecto, es genial poder realizar consultas basadas en interfaz y aún mantener la colección como IQueryable. Sin embargo, es un poco molesto que básicamente no haya forma de pensar en esta solución sin conocer el funcionamiento interno de EF.
Anders
Lo que está viendo aquí es una restricción de tiempo del compilador que permite al compilador de C # determinar que T es de tipo IEntity dentro del método, por lo que puede determinar que cualquier uso de "cosas" de IEntity es válido, ya que durante el tiempo de compilación el código MSIL generó realizará automáticamente esta verificación antes de la llamada. Para aclarar, agregar "clase" como una restricción de tipo aquí permite que collection.FirstOrDefault () se ejecute correctamente, ya que probablemente devuelve una nueva instancia de T llamando a un ctor predeterminado en un tipo basado en clase.
Guerra
64

Algunas explicaciones adicionales sobre el class"arreglo".

Esta respuesta muestra dos expresiones diferentes, una con y otra sin where T: classrestricción. Sin la classrestricción tenemos:

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

y con la restricción:

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

Estas dos expresiones son tratadas de manera diferente por el marco de la entidad. Al observar las fuentes de EF 6 , se puede encontrar que la excepción proviene de aquí, verValidateAndAdjustCastTypes() .

Lo que sucede es que EF intenta IEntityconvertir algo que tenga sentido en el mundo del modelo de dominio, sin embargo, falla al hacerlo, por lo que se lanza la excepción.

La expresión con la classrestricción no contiene el Convert()operador, no se prueba la conversión y todo está bien.

Todavía queda abierta la pregunta, ¿por qué LINQ crea diferentes expresiones? Espero que algún asistente de C # pueda explicar esto.

Tadej Mali
fuente
1
Gracias por la explicación.
Jace Rhea
9
@JonSkeet alguien intentó convocar a un asistente de C # aquí. ¿Dónde estás?
Nick N.
23

Entity Framework no admite esto de forma inmediata, pero una ExpressionVisitorque traduce la expresión se escribe fácilmente:

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

Lo único que tendrá que hacer es convertir el predicado pasado usando la expresión visitante de la siguiente manera:

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

    ...
}

Otro enfoque, menos flexible, es hacer 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
fuente
1

Tuve el mismo error pero un problema similar pero diferente. Estaba tratando de crear una función de extensión que devolviera IQueryable, pero los criterios de filtro se basaron en la clase base.

Finalmente encontré la solución que era para que mi método de extensión llamara .Select (e => e como T) donde T es la clase secundaria ye es la clase base.

los detalles completos están aquí: Cree la extensión IQueryable <T> usando la clase base en EF

Justin
fuente