首页 > 解决方案 > 尝试排序时无法翻译 LINQ 表达式

问题描述

我正在尝试根据附近的位置对场地进行排序,但这里出现 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;
            }

当我使用“名称”和价格进行排序时,这非常有效。但它不适用于位置。我收到 InvalidOperationException。

标签: asp.net-mvclinqasp.net-core

解决方案


问题的原因_context.VenuesIQueryable<...>

您必须了解IQueryablesIEnumerables之间的区别。

IEnumerable

实现 IEnumerable 的对象表示一个序列。它包含获取序列的第一个元素的所有内容,一旦你有了一个元素,你就可以请求下一个元素,只要有下一个元素。

在最低级别,这是通过请求枚举器,使用GetEnumerator,然后重复调用MoveNext()直到没有更多元素来完成的。每次使用 MoveNext 获得元素时,都可以使用属性 Current 访问该元素。

在更高级别上,您可以使用不返回 IEnumerable 的 foreach 或 LINQ 函数进行枚举,例如 ToList()、FirstOrDefault()、Count()、Any() 等。这些函数的深处将调用 GetEnumerator 和 MoveNext / Current

可查询的

实现 IQueryable 的对象并不代表序列本身,它代表一个查询:获得 IEnumerable的可能性。为此, IQueryable 包含 anExpression和 a Provider。表达式以某种通用格式保存查询,提供者知道谁必须执行查询(通常是数据库管理系统)以及该 DBMS 使用的语言(通常是 SQL)

连接 IQueryable 的 LINQ 语句不会执行查询,它只会更改表达式。

IQueryable 还实现了 IEnumerable。一旦您开始枚举,Expression 就会发送给 Provider,Provider 会将 Expression 转换为 SQL 并在 DBMS 上执行查询。返回的数据表示为 IEnumerable,因此您可以调用 MoveNext / Current(显式调用或使用 ToList()、Any()、FirstOrDefault() 等)。

但这与我的问题有什么关系?

问题是您的 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 创建一个扩展方法

虽然这解决了您的问题,但它的缺点是您获取的数据必须按本地进程进行排序。DBMS 在排序数据方面比本地进程更优化。

唉,你没有告诉我们 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 类

Sort 的扩展方法

既然我们已经掌握了扩展方法,为什么不创建一个扩展方法来输入一个IQueryable<VenueModel>和一个 sortOrder 字符串,并返回正确排序的IQueryable<VenueModel>?

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,只使用您的 Provider 知道如何转换为 SQL 的函数


推荐阅读