Почему эта команда LINQ join не работает?

У меня есть этот LINQ-запрос:

    // types...
    LinkedList<WeightedItem> itemScores = new LinkedList<WeightedItem>();

    var result = from i in _ctx.Items
                 join s in itemScores on i.Id equals s._id
                 orderby s._score descending
                 select new ItemSearchResult(i, s._score);

    // this fails:
    return result.ToList();

Что порождает эту ошибку:

Невозможно создать постоянное значение типа "System.Collections.Generic.IEnumerable`1".
В этом контексте поддерживаются только примитивные типы (такие как Int32, String и Guid).

[EDIT] Здесь код WeightedItem:

public class WeightedItem
{
    public int _id;
    public decimal? _score;

    public WeightedItem(int id, decimal? score)
    {
        _id = id;
        _score = score;
    }
}

Вы видите, что я сделал неправильно? Код отлично компилируется, и как _ctx.Items, так и itemScores содержат правильные значения.

Ответ 1

Да, это скомпилировалось бы хорошо - проблема в том, что он не может перевести его в SQL. Когда вы ссылаетесь на "локальные" значения, инфраструктура сущности должна решить, что с ними делать, когда нужно создать SQL-запрос. В принципе он не может справиться с выполнением объединения между коллекцией памяти и таблицей базы данных.

Одна вещь, которая могла бы работать, заключалась бы в использовании Contains. Я не знаю, будет ли LinkedList<T> работать для этого, но я считаю, что List<T> делает, по крайней мере, в LINQ to SQL:

List<int> requiredScoreIds = itemScores.Select(x => x._id).ToList();

var tmp = (from i in _ctx.Items
           where requiredScoreIds.Contains(i.Id)
           orderby s._score descending
           select i).AsEnumerable();

// Now do the join in memory to get the score
var result = from i in tmp
             join s in itemScores on i.Id equals s._id
             select new ItemSearchResult(i, s._score);

Теперь, когда вы выполняете соединение в запросе в памяти, что несколько не нужно. Вместо этого вы можете использовать словарь:

List<int> requiredScoreIds = itemScores.Select(x => x._id).ToList();

var tmp = (from i in _ctx.Items
           where requiredScoreIds.Contains(i.Id)
           orderby s._score descending
           select i).AsEnumerable();

// Create a map from score ID to actual score
Dictionary<int, decimal?> map = itemScores.ToDictionary(x => x._id,
                                                        x => x._score);

var result = tmp.Select(i => new ItemSearchResult(i, map[i.Id]));

Ответ 2

Вы не можете подключиться между списком в памяти и запрашиваемым объектом. Вам нужно сделать что-то вроде этого:

var criteria = itemScores.Select(x => x._id).ToList();
var result_tag = (from i in _ctx.Items
                 where criteria.Contains(i.ID)
                 select i).ToList();
var result = from i in result_tag
             join s in itemScores on i.ID equals s._id
             orderby s._score descending
             select new ItemSearchResult(i, s._score);

Ответ 3

На всякий случай таблица, представленная _ctx.Items не является большой, и вы не заботитесь о загрузке всю таблицу в памяти, а затем отфильтровать ее в памяти, вы можете просто поменять порядок элементов в оператор соединения, как в следующем фрагменте:

LinkedList<WeightedItem> itemScores = new LinkedList<WeightedItem>();

var result = from s in itemScores
             join i in _ctx.Items on s._id equals i.Id
             orderby s._score descending
             select new ItemSearchResult(i, s._score);

return result.ToList();

В исходном заявлении вызывается метод расширения Queryable:

IQueryable<TResult> Queryable.Join<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IEnumerable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> resultSelector
)

в то время как в swapped one вызывается метод Enumerable extension:

IEnumerable<TResult> Enumerable.Join<TOuter, TInner, TKey, TResult>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        Func<TOuter, TInner, TResult> resultSelector
)

поэтому в последнем выражении полная таблица _ctx.Items загружается в память и затем соединяется, через Linq to Objects, в список itemScores (я не знаю о LinkedList, я попробовал его со списком).

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

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