LINQ to Entities仅支持将EDM原始类型或枚举类型强制转换为具有IEntity接口的类型。

107

我有以下通用扩展方法:

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

很遗憾,Entity Framework 不知道如何处理这个 predicate,因为 C# 将该 predicate 转换为以下代码:

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

Entity Framework抛出以下异常:

无法将类型'IEntity'强制转换为类型'SomeEntity'。LINQ to Entities仅支持将EDM原始类型或枚举类型进行转换。

我们如何使Entity Framework与我们的IEntity接口配合工作?

4个回答

212
我可以通过将 class 泛型约束添加到扩展方法来解决此问题。虽然我不确定为什么它起作用了。
public static T GetById<T>(this IQueryable<T> collection, Guid id)
    where T : class, IEntity
{
    //...
}

6
我也可以用这个!我希望有人能够解释一下。#linqblackmagic - berko
你能否解释一下你是如何添加这个约束的? - yrahman
5
我的猜测是类类型被使用,而不是接口类型。EF不知道接口类型,因此无法将其转换为SQL语句。通过类约束,推断出的类型是DbSet<T>类型,EF知道该怎么做。 - jwize
2
太棒了,能够执行基于接口的查询并仍然将集合保持为IQueryable真是太好了。但有点烦人的是,如果不了解EF的内部工作原理,基本上没有想出这个修复方法的办法。 - Anders
你在这里看到的是编译时约束,它允许C#编译器在方法内确定T是IEntity类型,因此能够确定任何IEntity“stuff”的使用都是有效的,因为在编译时生成的MSIL代码将在调用之前自动执行此检查。为了澄清,在这里添加“class”作为类型约束允许collection.FirstOrDefault()正确运行,因为它可能返回基于类的类型的新实例,并调用默认构造函数。 - War

70

关于“fix”类的一些额外解释。

这个答案展示了两个不同的表达式,一个带有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
感谢您的解释。 - Jace Rhea
13
@JonSkeet,有人在这里试图召唤一位C#巫师。你在哪里? - Nick N.

24

Entity Framework不支持此功能,但可以轻松编写一个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);
    }
}

您需要做的唯一事情就是使用表达式访问者将传入的谓词进行转换,具体方法如下:

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>扩展

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接