Entity Framework 强制类型转换错误

6
以下内容完美无误:
IQueryable<Property> PropertyQuery = PropertyDAO.SearchWithAdditionalParameters(/* elided */);
IQueryable<long> propertyIdQuery = PropertyQuery.Select(p => p.PropertyId);

var relevantFMVs = PropertyDAO.db.FMVHistories.Where(f => propertyIdQuery.Contains(f.PropertyId)).ToList();

但以下代码会导致程序崩溃:
IQueryable<Property> PropertyQuery = PropertyDAO.SearchWithAdditionalParameters(/* elided */);

var relevantFMVs = PropertyDAO.db.FMVHistories.Where(f => PropertyQuery.Select(p => p.PropertyId).Contains(f.PropertyId)).ToList();

请注意,我只是将查询本身替换到变量的位置,而不是单独创建propertyIdQuery。
异常信息为:
无法将类型“System.Linq.IQueryable1”强制转换为类型“System.Linq.IQueryable1”。LINQ to Entities仅支持将Entity Data Model原始类型强制转换。
有人能解释一下EF(4)在底层做了什么来使得第一个查询有效,即使它们表面上是等效的吗?
我知道IQueryable<T>和表达式树在底层执行了很多工作,但是为什么将中间步骤保存到本地变量会影响结果呢?
编辑: 按要求,这里是被调用的完整方法以及该方法调用的方法:
    public IQueryable<Property> BasicSearchFromConstraints(PropertyInvoiceConstraints constraints) {
        return ExecuteSearchFromConstraints((dynamic)constraints.PropertyInst, constraints.CompanyNumber, constraints.TaxSubType, constraints.PhaseID, constraints.State, constraints.County, constraints.City, constraints.Jurisdiction);
    }

    private IQueryable<T> ExecuteSearchFromConstraints<T>(T property, int CompanyNumber, byte SubType, byte PhaseID, string State, string County, string City, string Jurisdiction) where T : Property {
        IQueryable<T> result = base.db.Properties.OfType<T>();

        if (SubType > 0)
            result = result.Where(p => p.TaxSubTypeId == SubType);
        if (CompanyNumber > 0)
            result = result.Where(p => p.CompanyNum == CompanyNumber);
        if (!String.IsNullOrEmpty(State))
            result = result.Where(p => p.State == State);
        if (!String.IsNullOrEmpty(County))
            result = result.Where(p => p.County == County);
        if (!String.IsNullOrEmpty(City))
            result = result.Where(p => p.City == City);
        if (!String.IsNullOrEmpty(Jurisdiction))
            result = result.Where(p => p.Jurisdiction == Jurisdiction);

        if (PhaseID > 0)
            result = result.Where(p => p.PhaseId == PhaseID);

        return result;
    }

    public virtual IQueryable<Property> SearchWithAdditionalParameters(DataLayer.DAO.PropertyInvoiceConstraints constraints, string propertyNumber = "", string altDesc = "", string countyAcctNumber = "", string City = "", string Jurisdiction = "", string secondaryStateID = "", string LegalDesc = "", string status = "", int? TaxYear = null) {
        IQueryable<Property> result = BasicSearchFromConstraints(constraints);

        if (!String.IsNullOrEmpty(status))
            result = result.Where(p => p.Status == status);

        if (!String.IsNullOrEmpty(propertyNumber))
            result = result.Where(p => p.PropertyNum.Contains(propertyNumber));

        if (!String.IsNullOrEmpty(altDesc))
            result = result.Where(p => p.AltDescription.Contains(altDesc));

        if (!String.IsNullOrEmpty(countyAcctNumber))
            result = result.Where(p => p.CountyAccountNum.Contains(countyAcctNumber));

        if (!String.IsNullOrEmpty(City))
            result = result.Where(p => p.City.Contains(City));

        if (!String.IsNullOrEmpty(Jurisdiction))
            result = result.Where(p => p.Jurisdiction.Contains(Jurisdiction));

        if (TaxYear.HasValue)
            result = result.Where(p => p.FMVHistories.Any(f => f.TaxYear == TaxYear));

        if (constraints.FMVPhaseID > 0)
            result = result.Where(p => p.FMVHistories.Any(f => f.PhaseId == constraints.FMVPhaseID));

        if (!String.IsNullOrEmpty(secondaryStateID))
            if (constraints.PropertyInst is WellDetail)
                result = result.OfType<WellDetail>().Where(w => w.SecondaryStateId == secondaryStateID);
            else
                throw new ApplicationException("Invalid use -> Secondary State ID can only be set when searching for Well property types");

        if (!String.IsNullOrEmpty(LegalDesc))
            if (constraints.PropertyInst is RealEstateDetail)
                result = result.OfType<RealEstateDetail>().Where(r => r.LegalDescr.Contains(LegalDesc));
            else if (constraints.PropertyInst is RealEstateServicingDetail)
                result = result.OfType<RealEstateServicingDetail>().Where(r => r.LegalDescr.Contains(LegalDesc));
            else throw new ApplicationException("Invalid use -> Legal Description can only be set when searching for either real estate or real estate servicing property types");

        return result;
    }

编辑

我非常希望Akash的答案是正确的,但如果是这样,我会预计这里的中间查询会崩溃,但实际上这三个查询都可以正常工作。

我开始怀疑我在类型 Property (来自原始示例)上拥有的继承结构可能与此有关。

        DummyBookModelEntities db = new DummyBookModelEntities();

        IQueryable<int> BookIds = db.Books.Where(b => b.id < 4).Select(b => b.id);
        IQueryable<Book> BooksFromIdQuery = db.Books.Where(b => b.id < 4);

        try {
            var l1 = db.Books.Where(b => BookIds.Contains(b.id)).ToList();
            Console.WriteLine("ID Query With ID Local Var Worked: count = {0}", l1.Count);
        } catch (Exception E) {
            Console.WriteLine("ID Query Failed:");
            Console.WriteLine(E.ToString());
            Console.WriteLine();
        }

        try {
            var l1 = db.Books.Where(b => BooksFromIdQuery.Select(b_inner => b_inner.id).Contains(b.id)).ToList();
            Console.WriteLine("ID Query With Whole Book Local Var Worked: count = {0}", l1.Count);
        } catch (Exception E) {
            Console.WriteLine("ID Query With Whole Book Local Var Failed:");
            Console.WriteLine(E.ToString());
            Console.WriteLine();
        }

        try {
            var l1 = db.Books.Where(b => BooksFromIdQuery.Contains(b)).ToList();
            Console.WriteLine("Whole Book sub query without select worked: count = {0}", l1.Count);
        } catch (Exception E) {
            Console.WriteLine("Whole Book sub query without select:");
            Console.WriteLine(E.ToString());
            Console.WriteLine();
        }

编辑

我添加了一些继承,现在底部的两个查询失败了。看起来每当你在查询中使用OfType()时,EF 没有解析完整个查询内的查询,你必须将子步骤拆分为局部变量。

除非有其他东西要补充,否则我今晚将奖励给 Akash。

        DummyBookModelEntities db = new DummyBookModelEntities();

        IQueryable<int> BookIds = db.Books.OfType<SciFiBook>().Where(b => b.id < 4).Select(b => b.id);
        IQueryable<Book> BooksFromIdQuery = db.Books.OfType<SciFiBook>().Where(b => b.id < 4);

        try {
            var l1 = db.Books.Where(b => BookIds.Contains(b.id)).ToList();
            Console.WriteLine("ID Query With ID Local Var Worked: count = {0}", l1.Count);
        } catch (Exception E) {
            Console.WriteLine("ID Query Failed:");
            Console.WriteLine(E.Message);
            Console.WriteLine();
        }

        try {
            var l1 = db.Books.Where(b => BooksFromIdQuery.Select(b_inner => b_inner.id).Contains(b.id)).ToList();
            Console.WriteLine("ID Query With Whole Book Local Var Worked: count = {0}", l1.Count);
        } catch (Exception E) {
            Console.WriteLine("ID Query With Whole Book Local Var Failed:");
            Console.WriteLine(E.Message);
            Console.WriteLine();
        }

        try {
            var l1 = db.Books.Where(b => BooksFromIdQuery.Contains(b)).ToList();
            Console.WriteLine("Whole Book sub query without select worked: count = {0}", l1.Count);
        } catch (Exception E) {
            Console.WriteLine("Whole Book sub query without select:");
            Console.WriteLine(E.Message);
            Console.WriteLine();
        }

        Console.WriteLine();  

你能否提供更完整的示例以便重现你的问题?在我的尝试中,这两个语句在.NET 4.0中都可以正常工作。但是在目标为.NET 3.5时,这两个语句都会产生异常,但是异常类型不同。 - Oleg
完成了,所有的方法都在那里。感谢您查看此内容。 - Adam Rackis
只是为了100%清楚,我只在.NET 4上运行过这个。 - Adam Rackis
@Adam Rackis:在我开始重现您的问题之前,我想建议您查看两个我之前的答案,这些答案描述了如何以更简单的方式执行您在示例中执行的相同操作。答案描述了主要思路。演示文稿来自答案,根据用户输入动态构建EF查询。 - Oleg
哎呀 - 我应该告诉你Property是抽象的,还有其他几个类都继承自Property(因此在查询中使用了OfType())。但无论数据模型如何,我都无法理解为什么将子步骤提取到本地变量中会有所不同。 - Adam Rackis
谢谢,我计划很快阅读您之前的回答。 - Adam Rackis
3个回答

1

尝试编写第一个查询,但不要使用

IQueryable<long> propertyIdQuery = PropertyQuery.Select(p => p.PropertyId);

var propertyIdQuery = PropertyQuery.Select(p => p.PropertyId);

它是否抛出错误?这对我来说是唯一的明显差异。


谢谢你的回答。从编译器的角度来看,这两行代码是完全相同的,我相当确定它们都不会出问题。从我的问题来看,似乎EF中的ExpressionTree解析机制希望将子查询拆分成更简单的部分,就像你上面粘贴的那样。我想知道的是为什么要这样做 - 我打算把这个问题发送给EF团队,看看他们是否愿意研究一下。感谢你的时间 :) - Adam Rackis
当然,告诉我们你发现了什么。 - smartcaveman

1
f => PropertyQuery.Select(p => p.PropertyId).Contains(f.PropertyId)

以上是一个Linq表达式,f=>之后的所有内容都是表达式树。组合Linq只能基于表达式扩展查询,而不能基于委托。
两个语句集逻辑上都是正确的,但从编译器的角度来看,它们是不同的。如果您注意到,您扩展的(where on where或select)仅适用于相同类型的模板参数。否则,您的int IQueryable将无法工作,因为您的linq期望IQueryable of T。
其次,当您对T执行Select并返回IQueryable of T时,运行时无法知道先前的T是什么类型。
简而言之,将中间步骤保存为局部变量会破坏您的表达式树。我建议您查看Reflector以查看实际生成的源代码。
您的整个lambda表达式实际上是使用表达式节点构建的,并且整个树被构建并返回给Where方法。否则,在您的第一个示例中,表达式树是不同的,它涉及在表达式中执行其他操作。

+1 对于好的回答,但是正如我的最后一次编辑所显示的那样,这可能不是正确答案。 - Adam Rackis
好的,根据我的最后一次编辑,我认为你的答案基本是正确的。看起来 EF 不希望在查询中存在任何子查询(仅当)存在 OfType 时。 - Adam Rackis

1

错误信息显示它仅支持原始类型。

在可运行的代码中,您已经指定了它是 IQueryable<long>

我猜测不可运行的代码使用了 IQueryable<decimal>,因此出现了转换错误。

您正在返回一个标识列。标识列可以有多种类型。Decimal 是可以处理所有可能的标识类型的数据类型。

为什么 select SCOPE_IDENTITY() 返回 decimal 而不是整数?

在可运行的代码中,编译器得到了使用 long 的提示。


两个调用使用完全相同的代码。不同之处在于第一个调用经过额外的步骤明确声明了一个中间查询。由于某种原因,EF要求进行此声明,即使在两种情况下 PropertyQuery.Select(p => p.PropertyId) 都将被视为 IQueryable<long>。 - Adam Rackis
@Adam,我已经更新了我的答案,我认为这与EF如何处理标识列有关。 - Shiraz Bhaiji

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