Ошибка приведения Entity Framework

Следующее работает отлично:

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' to type 'System.Linq.IQueryable1". LINQ to Entities поддерживает только приведение типов примитивов модели данных сущности.

Может ли кто-нибудь пролить свет на то, что делает 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;
    }

ИЗМЕНИТЬ

Я действительно хотел, чтобы ответ Акаша был правильным, но если бы это было так, я ожидал бы, что средний запрос здесь взорвется, но на самом деле все три работают нормально.

Я начинаю подозревать, что структура наследования типа 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 просто не хочет анализировать все запросы внутри запросов; вам нужно разбить свои подэтапы на локальные переменные.

Сегодня вечером я награду Акаша наградой, если у кого-то еще не будет чего добавить.

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

person Adam Rackis    schedule 25.02.2011    source источник
comment
Не могли бы вы опубликовать более полный пример, который можно использовать для воспроизведения вашей проблемы? В моих попытках оба оператора без исключения работают в .NET 4.0. Для .NET 3.5 в качестве пункта назначения оба оператора создают исключение, но еще одно.   -  person Oleg    schedule 07.04.2011
comment
Готово, есть целые методы. Спасибо, что посмотрели на это   -  person Adam Rackis    schedule 07.04.2011
comment
И, чтобы быть на 100% ясным, я запускал это только на .NET 4   -  person Adam Rackis    schedule 07.04.2011
comment
@Adam Rackis: Прежде чем я начну воспроизводить вашу проблему, я хочу порекомендовать вам взглянуть на два моих предыдущих ответа, в которых описывается, как вы можете сделать то же самое, что и в своих примерах, но намного проще. Ответ описывает основную идею. Демонстрация из ответ создайте запрос EF динамически на основе ввода пользователя.   -  person Oleg    schedule 07.04.2011
comment
Ух, я, вероятно, должен сказать вам, что Property является абстрактным, и есть несколько других классов, которые все наследуют от свойства (отсюда OfType () в запросе. Тем не менее, независимо от модели данных, я не могу жизнь меня понимаю, почему вытаскивание подэтапа в локальную переменную имело бы значение.   -  person Adam Rackis    schedule 07.04.2011
comment
И спасибо, я планирую вскоре прочитать ваши предыдущие ответы   -  person Adam Rackis    schedule 07.04.2011


Ответы (3)


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

Выше - выражение linq, все после f => - это дерево выражений. Composite Linq может расширять запрос только на основе выражений, но не делегатов.

Оба ваших набора операторов логически верны, но с точки зрения компилятора они разные. Если вы заметили, что ваш расширенный (где где или выберите) будет работать только с тем же типом параметра шаблона. Где еще ваш IQueryable of int не будет работать, поскольку ваш linq ожидает IQueryable of T.

Во-вторых, когда вы выполняете Select на T и возвращаете IQueryable of T, среда выполнения не может узнать, что ранее T принадлежал к какому типу.

Короче говоря, сохранение промежуточного шага в качестве локальной переменной нарушает дерево выражения. Я предлагаю вам взглянуть на Reflector, чтобы увидеть исходный код того, что на самом деле сгенерировано.

Все ваше лямбда-выражение фактически строится с использованием узла выражения, а все дерево строится и возвращается в метод Where. Если еще в вашем первом примере дерево выражений отличается, оно включает выполнение чего-то еще в выражении.

person Akash Kava    schedule 12.04.2011
comment
+1 за хороший ответ, но, как покажет мое последнее изменение, вероятно, это не так. - person Adam Rackis; 12.04.2011
comment
Хорошо, судя по моему последнему редактированию, я думаю, что ваш ответ по существу правильный. Похоже, что EF не нужны подзапросы, когда (и только когда) у вас есть OfType в запросе. - person Adam Rackis; 12.04.2011

Попробуйте написать первый запрос, но вместо

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

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

Выдает ошибку? Это единственная очевидная разница в запросах ко мне.

person smartcaveman    schedule 03.03.2011
comment
Спасибо за ваш ответ. Эти две строки на 100% идентичны с точки зрения компилятора, и я почти уверен, что ни одна из них не взорвется. Из моего вопроса кажется, что механизм анализа ExpressionTree в EF хочет, чтобы подзапрос был разбит на более простой фрагмент, как то, что вы вставили выше. Я хотел знать почему - думаю, я отправлю этот вопрос команде EF и посмотрю, захотят ли они его посмотреть. +1 хотя бы на ваше время :) - person Adam Rackis; 03.03.2011
comment
Конечно, дайте нам знать, что вы узнали. - person smartcaveman; 03.03.2011

В сообщении об ошибке указано, что он поддерживает только примитивные типы.

В работающем коде вы указали, что это IQueryable<long>.

Я предполагаю, что код, который не работает, использует IQueryable<decimal>, поэтому ошибка приведения.

Вы возвращаете столбец идентичности. Столбец Identity может иметь несколько типов. Decimal - это тип данных, который может обрабатывать все возможные типы удостоверений.

Почему выбор SCOPE_IDENTITY () возвращает десятичное вместо целого?

В коде, который работает, компилятор получает подсказку использовать long.

person Shiraz Bhaiji    schedule 25.02.2011
comment
Оба вызова используют один и тот же точный код. Разница в том, что первый проходит дополнительный этап явного объявления промежуточного запроса. По какой-то причине EF требует это объявление, хотя в обоих случаях PropertyQuery.Select(p => p.PropertyId) будет рассматриваться как IQueryable ‹long›. - person Adam Rackis; 25.02.2011
comment
@ Адам, я обновил свой ответ, я думаю, что это связано с тем, как EF обрабатывает столбцы идентификаторов - person Shiraz Bhaiji; 25.02.2011