Не удалось перевести выражение LINQ при попытке сортировки - PullRequest
0 голосов
/ 12 марта 2020

Я пытаюсь отсортировать места по соседнему расположению, но здесь получаю ошибку LINQ. У меня есть имя метода GetDistanceM, который принимает 4 аргумента lat1, lat2, long1, long2 и возвращает расстояние между двумя геоординатами. Здесь я пытаюсь сортировать места, основываясь на расстоянии.

 public async Task<IActionResult> Explore(string sortOrder){

                var venues = from v in _context.Venues where v.IsApproved select new VenueModel{
                Category = v.Category,
                Price = v.Price,
                Name = v.Name,
                Id = v.Id,
                City = v.City,
                GetDistance = GetDistanceM(v.Latitude, v.Longitude,  27.692387, 85.318110)
                 };

           switch (sortOrder)
            {
                case "Price":
                    venues = venues.OrderBy(s => s.Price);
                    break;
                case "Name":
                    venues = venues.OrderBy(s => s.Name);
                    break;
                 case "Location":
                    venues = venues.OrderBy(x => x.GetDistance);
                    break;
                 default:
                    venues = venues.OrderByDescending(s => s.Name);
                    break;
            }

Это прекрасно работает, когда я сортирую по «имени» и цене. Но это не работает с Location. Я получаю InvalidOperationException.

Ответы [ 2 ]

1 голос
/ 12 марта 2020

Причина проблемы в том, что _context.Venues - это IQueryable<...>

. Вы должны знать о разнице между IQueryables и IEnumerables .

IEnumerable

Объект, который реализует IEnumerable, представляет последовательность. Он содержит все, чтобы получить первый элемент последовательности, и когда у вас есть элемент, вы можете запросить следующий элемент, если есть следующий элемент.

На самом низком уровне это делается запрашивая перечислитель, используя GetEnumerator, а затем повторно вызывая MoveNext(), пока не останется больше элементов. Каждый раз, когда у вас есть элемент, использующий MoveNext, доступ к этому элементу можно получить с помощью свойства Current.

На более высоком уровне вы перечисляете, используя функции foreach или LINQ, которые не возвращают IEnumerable, например ToList (), FirstOrDefault (), Count (), Any () и др. c. Глубоко внутри этих функций будут вызываться GetEnumerator и MoveNext / Current

IQueryable

Объект, который реализует IQueryable, не представляет саму последовательность, он представляет запрос: потенциал для получить IEnumerable. IQueryable имеет выражение и поставщика. Выражение содержит запрос в каком-то общем формате c, поставщик знает, кто должен выполнять запрос (обычно это система управления базами данных), и язык, который использует эта СУБД (обычно SQL)

Конкатенация операторов LINQ IQueryable не выполняет запрос, он только изменит выражение.

IQueryable также реализует IEnumerable. Как только вы начинаете перечисление, выражение отправляется поставщику, который преобразует выражение в SQL и выполнит запрос в СУБД. Возвращенные данные представлены в виде IEnumreable, поэтому вы можете вызвать MoveNext / Current (либо явно, либо с помощью ToList (), Any (), FirstOrDefault () и т. Д. c).

Но Какое отношение это имеет к моему вопросу?

Проблема в том, что провайдер вашего IQueryable не знает функцию GetDistance (), поэтому он не может перевести ее в SQL. На самом деле есть несколько стандартных функций LINQ, которые нельзя перевести на SQL. См. Поддерживаемые и неподдерживаемые методы LINQ (LINQ to Entities) .

Не очень хорошее решение: используйте AsEnumerable

Самый простой способ решить эту проблему, состоит в том, чтобы выбрать данные, которые вводятся для вашей формулы GetDistance, затем вызвать AsEnumerable, который выполнит ваш запрос. Ваш локальный процесс знает GetDistance, поэтому вы можете вызывать его после AsEnumerable:

var venues = context.Venues.Where(venue => venue.IsApproved)
    .Select(venue => new
    {
        Category = v.Category,
        Price = v.Price,
        Name = v.Name,
        Id = v.Id,
        City = v.City,

        // can't call GetDistance yet, select the input parameters:
        Latitude = v.Latitude,
        Longitude= v.Longitude,
    });

    // execute the query:
    .AsEnumerable()

    // now you can call GetDistance:
    .Select(v => new VenueModel
    {
        Category = v.Category,
        Price = v.Price,
        Name = v.Name,
        Id = v.Id,
        City = v.City,

        Distance = GetDistanceM(v.Latitude, v.Longitude,  27.692387, 85.318110)
    })
    .OrderBy(venueModel => venuModel.Distance);

Лучшее решение: создайте метод расширения для IQueryable

Хотя это решает вашу проблему, недостатком является то, что выбранные вами данные не сортируются системой управления базами данных. СУБД гораздо более оптимизирована для сортировки ваших данных, чем локальный процесс.

Увы, вы не сказали нам, что делает GetDistance (). Кажется, что он берет Широту и Долготу от Места и возвращает расстояние до определенной точки, используя формулу.

Что вы могли бы сделать, это перевести функцию GetDistance в метод расширения IQueryable, который принимает IQueryable в качестве входных данных и возвращает выбранные данные. См. демистифицированные методы расширения

public IQueryable<VenueModel> SelectVenueModels(this IQueryable<Venue> venues,
    double X, double Y)
{
    return venues.Select(venue => new VenueModel()
    {
        Category = v.Category,
        Price = v.Price,
        Name = v.Name,
        Id = v.Id,
        City = v.City,

        // Calculate the distance as you would do in GetDistanceM:
        Distance = Math.Sqrt(
           Math.Pow( (v.Latitude-X), 2)  +  Math.Pow( (v.Longitude-Y), 2) ),
    });

Использование:

var venues = _context.Venues.Where(venue => venue.IsApproved)
    .SelectVenueModels(27.692387, 85.318110)
    .OrderBy(venueModel => ...);

Примечание: может случиться так, что используемый вами тип структуры сущности не знает, как переводить функции как SQRT и POW, в этом случае вам придется переводить их в другие методы. Для структуры сущностей вы можете использовать класс SQLFunctions

Метод расширения для сортировки

Теперь, когда мы освоили методы расширения, почему бы не создать расширение метод, который вводит IQueryable<VenueModel>' and a sortOrder string, and returns the properly ordered IQueryable '?

public IOrderedQueryable<VenueModel> OrderBy(this IQueryable<VenueModel> venueModels,
       string sortOrder)
{
    switch (sortOrder)
    {
        case "Price":
            venues = venues.OrderBy(s => s.Price);
            break;
        case "Name":
            venues = venues.OrderBy(s => s.Name);
            break;
        case "Location":
            venues = venues.OrderBy(x => x.Distance);
            break;
         default:
            venues = venues.OrderByDescending(s => s.Name);
            break;
    }
}

Использование:

string sortOrder = ...;
var venues = _context.Venues.Where(venue => venue.IsApproved)
    .SelectVenueModels(27.692387, 85.318110)
    .OrderBy(sortOrder);

Но почему это работает?

Разница является то, что этот OrderBy не выполняет запрос, он только меняет выражение. Ввод выражения - VenueModel, GetDistance больше не вызывается, используются только те функции, которые ваш провайдер знает, как перевести в SQL

1 голос
/ 12 марта 2020

Я полагаю, что GetDistance - это метод или некоторое вычисляемое свойство, и linq может оцениваться на стороне или на стороне базы данных, и вы должны выбрать, куда оно будет переведено и выполнено. Метод, который не может быть переведен в базу данных, выдаст это исключение для вас. Вы можете решить эту проблему с помощью ToList () в venues для оценки этого выражения в приложении или на стороне linq, передав формулу, которую вы используете для вычисления расстояния.

Ссылки на документацию, которая описывает клиент против оценка сервера: - Поддерживаемые и неподдерживаемые методы LINQ (LINQ to Entities) - Оценка клиента и сервера

И некоторый блог ef основной клиент по сравнению с оценкой сервера

...