LINQ to Entities仅支持使用IEntity接口转换EDM基本类型或枚举类型


96

我有以下通用扩展方法:

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

不幸的是,predicate由于C#将谓词转换为以下内容,因此Entity Framework不知道如何处理:

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

实体框架引发以下异常:

无法将类型“ IEntity”强制转换为类型“ SomeEntity”。LINQ to Entities仅支持强制转换EDM基本类型或枚举类型。

我们如何使实体框架与我们的IEntity界面一起工作?

Answers:


188

通过将class通用类型约束添加到扩展方法中,我能够解决此问题。不过,我不确定为什么会起作用。

public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}

6
也为我工作!我希望有人能够解释这一点。#linqblackmagic
berko

您能解释一下您是如何添加此约束的

5
我的猜测是使用类类型而不是接口类型。EF不知道接口类型,因此无法将其转换为SQL。通过类约束,可以推断出EF知道如何处理的DbSet <T>类型。
jwize 2015年

1
完美,能够执行基于接口的查询并仍将集合保持为IQueryable很棒。但是有点烦人的是,在不了解EF的内部工作原理的情况下,基本上没有办法考虑此修复程序。
安德斯

您在这里看到的是编译器时间限制,它使C#编译器可以确定方法中T的类型为IEntity,因此可以确定对IEntity“ stuff”的任何用法均有效,就像在编译期间生成的MSIL代码一样会在通话前自动为您执行此检查。为了明确起见,在此处添加“类”作为类型约束可以使collection.FirstOrDefault()正确运行,因为它很可能会返回T的新实例,该实例调用基于类的类型的默认ctor。
战争

64

有关class“修复”的一些其他说明。

该答案显示了两个不同的表达式,一个带有where T: class约束,另一个没有约束。没有class约束,我们有:

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

并受约束:

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

实体框架将这两个表达式区别对待。查看EF 6来源,可以发现异常来自此处,请参见ValidateAndAdjustCastTypes()

发生的是,EF尝试将其IEntity转换为对领域模型世界有意义的内容,但是这样做却失败了,因此引发了异常。

具有class约束的表达式不包含Convert()运算符,不尝试强制转换,并且一切都很好。

仍然有待解决的问题,为什么LINQ构建不同的表达式?我希望某些C#向导能够对此进行解释。


1
感谢您的解释。
杰斯·瑞亚

9
@JonSkeet有人试图在这里召唤C#向导。你在哪?
Nick N.

23

实体框架不开箱ExpressionVisitor即用地支持此功能,但是很容易编写可翻译表达式的:

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

您唯一需要做的就是使用表达式visitor转换传入的谓词,如下所示:

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

    ...
}

另一种不太灵活的方法是利用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);
    }

    ...
}

1

我有相同的错误,但有相似但不同的问题。我试图创建一个返回IQueryable的扩展功能,但过滤条件基于基类。

我最终找到了解决方法,该方法是我的扩展方法调用.Select(e => e as T),其中T是子类,e是基类。

完整的详细信息在这里: 使用EF中的基类创建IQueryable <T>扩展

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.