IQueryable complex order Only fields are allowed in a $sort

IQueryable complex order Only fields are allowed in a $sort

我想对列表进行排序。当它是 IEnumerable 时它工作正常,但我将它更改为 IQueryable 以查询 MongoDB,它不起作用。给我这个错误

System.NotSupportedException: 'Only fields are allowed in a $sort.'

//Query 1
var query = from d in list
        orderby
             d.Item.Value1 + d.Item.Value2 descending
        select d;

//Query 2
var query = from d in list
       orderby
            RecommendationFormula(d) descending
       select d;

private double RecommendationFormula(IItem d)
{
    var quality = ((int)(d.ValueX) / 18.0) * 50.0;
    var recent = ((DateTime.MinValue - (d.Item.Released ?? DateTime.MinValue)).TotalDays / (DateTime.MinValue - new DateTime(1990, 1, 1)).TotalDays) * 30;
    var rating = ((d.Item.XRating + d.Item.YRating) / 20) * 15;
    var quantity = (Quantity(d) / 1000.0) * 5;
    return quality + rating + recent + quantity;
}

我也知道它不支持函数(如查询 2 所示),但是当我尝试 ToList() 列表时,查询 1 也给出了错误。 我如何为 IQueryable 列表编写这种复杂的排序?

我相信使用 LINQ lambdas 可以实现您想要的结果

var query = list.OrderByDescending(d => RecommendationFormula(d));

相信大家也可以这样写,将lambda转化为方法组,如有错误请指正:

var query = list.OrderByDescending(RecommendationFormula);

这是关于 MongoDB 驱动程序的工作方式。您能否在使用之前将 IQueryable 转换为带有 ToList() 的列表?它会增加开销,因为它需要先将查询中的所有元素加载到内存中。

如果你正在使用的数据会因为体积而让人头疼,我建议要么在数据库中解决它,要么在内存中以块的形式处理数据,例如使用 Skip() & Take().

P.S: 方法RecommendationFormula(IItem d)可以是静态方法。

如异常中所述,在 LINQ 中配置排序的类型化方式仅支持字段,不支持计算值。看起来这是基于 this 的服务器限制。所以可以通过加projection来解决:

        var query = from d in list
                    select new { Sum = d.Item.Value1 + d.Item.Value2, Item = d.Item } into projected
                    orderby projected.Sum descending
                    select projected;

触发此 MQL:

{
    "aggregate": "coll",
    "pipeline": [{
            "$project": {
                "Sum": {
                    "$add": ["$Item.Value1", "$Item.Value2"]
                },
                "Item": "$Item",
                "_id": 0
            }
        }, {
            "$sort": {
                "Sum": -1
            }
        }
    ]
}

你可以在那里添加更复杂的逻辑,但不要期望太复杂,在你的特定情况下,支持 Timespan 属性将触发不受支持的异常,我不确定服务器是否有 Timespan 的等价物.net 中的逻辑。

如果您仍然需要查看会触发类型化 LINQ 查询不受支持的异常的更复杂场景,您有 2 个选项:

  1. 查看最新的 LINQ3 implementation。与 LINQ2(默认)相比,我知道它在 LINQ 查询中支持更多不同的情况,但我没有这方面的经验,因此需要更多调查。
  2. 您可以查看您的目标是否可以使用 MQL 服务器查询本身进行存档(这肯定支持比使用 LINQ 提供程序实现的更多的情况)。如果您可以使用服务器 syntax and get the result you need in mongo shell, you can pass this raw MQL into c# driver (pay attention that all definitions inside raw MQL should match with how server represents it). See 构建您需要的查询以获取详细信息。

更新:

  1. LINQ2 好像支持$substract:

         var query = from d in list
                     select new { Sum = (DateTime.Now - d.Item.Released), Item = d.Item } into projected
                     orderby projected.Sum descending
                     select projected;
    

但它不能提取 TotalDays 和类似的属性。但是看起来服务器阶段通过 2 个运算符支持它:

  1. Example 如何通过 $substact.
  2. 从数据中提取 ms
  3. 你也可以看看 dateDiff 新引入的运算符。

想法是创建一个原始 MQL 请求,您可以在 shell 中签入类似于:

db.coll.aggregate(
[
// stage 1
{
    $addFields: {
        "DateDiff": {
            $dateDiff: {
                startDate: "$Item.Released",
                endDate: ISODate("2010-01-01"),
                unit: "day"
            }
        }
    }
},
// stage 2
{
    $addFields: {
        "ForSorting": {
            $divide : [ { $toInt : "$DateDiff"} , 9] // 9 is a random value, you can calculate it yourself 
        }
    }
},
// stage 3
{
    $sort : { "ForSorting" : -1 }
}
]
)

然后通过AppendStage方法分别通过每个阶段。

注意:您可以将类型化阶段(如果支持)和原始 MQL 阶段结合起来,类似于:

        var result = coll
            .Aggregate()
            .Match(c => c.Item != null) // just example of supported typed stage
            .AppendStage<BsonDocument>("{ $sort : { Released : 1 } }") // such simple `$sort` is supported in typed way, here is just an example how a raw query can be appended to the pipeline
            .ToList();

为上述 LINQ 查询生成的 MQL 将是:

{
    "aggregate": "coll",
    "pipeline": [{
            "$match": {
                "Item": {
                    "$ne": null
                }
            }
        }, {
            "$sort": {
                "Released": 1
            }
        }
    ]
}

更新2 如果投影输出文档中的字段与输入文档中的字段匹配,则可以避免重新投影(即额外的服务器端步骤),而只需通过 As:

替换客户端的输出序列化程序
        var list = coll
            .Aggregate<Original>() // this class has only `Item` field
            .Project(i => new { Sum = i.Item.Value1 + i.Item.Value2, Item = i.Item }) // this expression can be generated dynamically
            .SortByDescending(s=>s.Sum)
            .As<Original>()
            .ToList();

为上述案例生成的 MQL 将是:

{
    "aggregate": "coll",
    "pipeline": 
    [
        {
            "$project": {
                "Sum": {
                    "$add": ["$Item.Value1", "$Item.Value2"]
                },
                "Item": "$Item",
                "_id": 0
            }
        }, 
        {
            "$sort": {
                "Sum": -1
            }
        }
    ]
}

我能够以不同的方式对问题进行排序,但避免将对象带到内存中进行筛选和排序。但是 MongoDB.

确实需要 2 个查询
  1. Filter the List, _list is IQueryable
  1. Sort the list using projection bringing in only Ids
private IQueryable<string> Sort(IQueryable<IItem> list)
    query = from d in list
            select new
            {
                Ranking =
                    ((int)(d.ValueX) / 18.0) * 50.0 +
                    (((d.XRating + d.YRating) / 20) * 15) * 20 +
                    (d.Quantity/ 1000.0) * 5,
                ReleaseYear = d.Item.Released.HasValue ? d.Item.Released.Value.Year : 0,
                d.Id
            }
        into q
            orderby
                q.ReleaseYear descending,
                q.Ranking descending
            select q.Id;

}
  1. Use Skip and Take the sorted ids to a list.
var itemIds = this.Sort(_list).Skip(_pageSize * _page).Take(_pageSize).ToList();
  1. Find only the objects in the ids list from MongoDB
var list = _list.Where(q => itemIds.Contains(q.Id)).ToList();
  1. Order the real objects like the ids.
var list = list.OrderBy(q => itemIds.IndexOf(q.Id));