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

Posted

技术标签:

【中文标题】IQueryable 扩展按分钟动态分组......和其他【英文标题】:IQuerable extension to group dynamicly by minutes ...and other 【发布时间】:2021-09-18 00:43:53 【问题描述】:

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

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

public static IQueryable<IGrouping<TKey, TSource>> GroupByMinutlyTimePeriode<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 /////
   .GroupByMinutlyTimePeriode((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>> GroupByMinutlyTimePeriode<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

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

【问题讨论】:

根据我使用 LINQ to SQL 的经验,我非常怀疑您是否会找到可以翻译的语句 - 但无论如何我希望它适合您????客户端执行会把你的应用炸成碎片吗? 首先让它工作,然后考虑扩展。因此,这个问题应该集中在标题和内容上的“让它发挥作用”部分。例如,根本不要提及扩展。 您的 API 对我来说似乎过于复杂 - 为什么不具有 GroupBy(d =&gt; new Key = keySelector(d), timestampSelector(d).Year, timestampSelector(d).Month, timestampSelector(d).Day, timestampSelector(d).Hour, Minute = timestampSelector(d).Minute / minutes * minutes ) 的(效果)? 【参考方案1】:

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

表达式树转换的一些神奇之处:

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);
        
    

【讨论】:

我喜欢 ExpressionReplacer - 比典型的单个 Expression 替换访问者有很好的改进。 @Svyatoslav Danyliv 失败 :( The LINQ expression 'DbSet&lt;Data&gt;() .Where(d =&gt; d.Timestamp &gt;= __lower_1 &amp;&amp; d.Timestamp &lt; __upper_2) .GroupBy( keySelector: d =&gt; new DeviceId = d.DeviceId, Boundary = new DateTime( d.Timestamp.Year, d.Timestamp.Month, d.Timestamp.Day, d.Timestamp.Hour, d.Timestamp.Minute / 15 * 15, 0 ) , elementSelector: d =&gt; d)' could not be translated. 查询对我来说看起来不错。 展示你如何使用它。 @Svyatoslav [...] .Where(d =&gt; d.Timestamp &gt;= lower &amp;&amp; d.Timestamp &lt; upper) .GroupByMinutlyTimePeriode((t, d) =&gt; new DeviceId = d.DeviceId, Boundary = t , d =&gt; d.Timestamp, 15) .Select(g =&gt; new DeviceId = g.Key.DeviceId, Date = g.Key.Boundary, Value = g.Sum(d =&gt; d.Value) ); @SteffenMangold,对此感到抱歉。固定答案。

以上是关于IQueryable 扩展按分钟动态分组......和其他的主要内容,如果未能解决你的问题,请参考以下文章

如何使用熊猫按 10 分钟对时间序列进行分组

按 15 分钟间隔对 mysql 查询进行分组

按 1 分钟间隔分组操作链 sql BigQuery

将大型 GPS 数据按 1 分钟分组

SQL 按日期时间分组,最大差异为 x 分钟

Django ORM按小时分组,不包括分钟。