IQueryable 扩展按分钟动态分组......和其他

IQueryable extension to group dynamicly by minutes ...and other

我想创建一个 IQueryable 扩展以允许其他开发人员按分钟间隔和自定义组键结果对实体进行分组。

我的想法是创建一个具有以下签名的方法:

public static IQueryable<IGrouping<TKey, TSource>> GroupByMinutelyTimePeriode<TSource, TKey>(this IQueryable<TSource> source
            , Expression<Func<DateTime, TSource, TKey>> keySelector
            , Func<TSource, DateTime> timestampSelector
            , int minutes)
{

你可以像这个例子一样使用的东西:

var query = dataContext.Datas
   .Where(d => d.Timestamp >= lowerTimestampRange && d.Timestamp < upperTimestampRange)
   
   ////// extension /////
   .GroupByMinutelyTimePeriode((t, d) => new
   {
       DeviceId = d.DeviceId,
       TimestampBoundary = t
   }
   , d => d.Date
   , 15)
   /////////////////////
   
   .Select(g => new
   {
       DeviceId = g.Key.DeviceId,
       Date = g.Key.TimestampBoundary,
       Value = g.Sum(d => d.Value)
   });

在扩展中应该会发生这样的事情(肯定不会工作,因为 linq-to-sql 无法翻译):

public static IQueryable<IGrouping<TKey, TSource>> GroupByMinutelyTimePeriode<TSource, TKey>(this IQueryable<TSource> source
            , Func<DateTime, TSource, TKey> keySelector
            , Func<TSource, DateTime> timestampSelector
            , int minutes)
    {
        return source.GroupBy(d => keySelector(new DateTime(timestampSelector(d).Year
            , timestampSelector(d).Month
            , timestampSelector(d).Day
            , timestampSelector(d).Hour
            , timestampSelector(d).Minute / minutes * minutes, 0)
            , d
            ));
    }

我必须说我完全没能将其转换为正确的 IQueryable 表达式语法。我试图了解如何使用现有 IQueryable 扩展中的表达式,例如 GitHub 中的 GroupBy

也许有人可以帮我找到一个很好的例子。

这是可能的,但是您必须传递 Expression<Func<,>> 作为参数,而不是 Func<,>。只有在这种情况下,您才能重用选择器主体。

表达式树转换的一些魔法:

public static class QueryableExtensions
{
    public static IQueryable<IGrouping<TKey, TSource>> GroupByMinutlyTimePeriode<TSource, TKey>(this IQueryable<TSource> source
        , Expression<Func<DateTime, TSource, TKey>> keySelector
        , Expression<Func<TSource, DateTime>> timestampSelector
        , int minutes)
    {
        Expression<Func<DateTime, int, DateTime>> dateTimeTemplate = (t, m) => new DateTime(t.Year
            , t.Month
            , t.Day
            , t.Hour
            , t.Minute / m * m, 0);

        var entityParam = keySelector.Parameters[1];

        var dateTimeBody =
            ExpressionReplacer.GetBody(dateTimeTemplate, ExpressionReplacer.GetBody(timestampSelector, entityParam), Expression.Constant(minutes));

        var keyBody = ExpressionReplacer.GetBody(keySelector, dateTimeBody, entityParam);
        var keyLambda = Expression.Lambda<Func<TSource, TKey>>(keyBody, entityParam);

        return source.GroupBy(keyLambda);
    }


    class ExpressionReplacer : ExpressionVisitor
    {
        readonly IDictionary<Expression, Expression> _replaceMap;

        public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
        {
            _replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
        }

        public override Expression Visit(Expression node)
        {
            if (node != null && _replaceMap.TryGetValue(node, out var replacement))
                return replacement;
            return base.Visit(node);
        }

        public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
        {
            return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
        }

        public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
        {
            return new ExpressionReplacer(replaceMap).Visit(expr);
        }

        public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
        {
            if (lambda.Parameters.Count != toReplace.Length)
                throw new InvalidOperationException();

            return new ExpressionReplacer(Enumerable.Range(0, lambda.Parameters.Count)
                .ToDictionary(i => (Expression)lambda.Parameters[i], i => toReplace[i])).Visit(lambda.Body);
        }
    }
}