访问IEnumerable<T>子元素

3

以下是我们想要做的事情:

我们从数据库中获取数据,需要对其进行格式化以生成报告,包括一些计算(求和、平均值和字段之间的计算(例如:x.a / x.b))。

其中一个限制是,如果在求和中有一个数据为null、-1或-2,我们必须停止计算并显示“-”。由于我们需要生成许多报告,具有相同的逻辑和许多计算,因此我们希望集中这种逻辑。目前,我们生成的代码允许我们检查字段之间的计算(例如x.a / x.b),但无法允许我们检查组总计(例如:x.b / SUM(x.a))

测试用例

规则

  • 如果计算中使用的值为-1、-2或null,则不应该进行计算。在这种情况下,如果您发现-1或null,则返回“-”,如果您发现-2,则返回“C”
  • 如果计算中有多个“坏值”,则需要遵守以下定义的优先级:null -> -1 -> -2。此优先级独立于值所在的级别

测试

简单计算

object: new DataInfo { A = 10, B = 2, C = 4 }
calcul: x => x.A / x.B + x.C
result: 9
object: new DataInfo { A = 10, B = 2, C = -2 }
calcul: x => x.A / x.B + x.C
result: C(因为计算中有“-2”值)
object: new DataInfo { A = 10, B = -2, C = null }
calcul: x => x.A / x.B + x.C
result: -(因为计算中有“null”值,它胜过了-2值)

复杂计算

object: var list = new List();
        list.Add(new DataInfo { A = 10, B = 2, C = 4 });
        list.Add(new DataInfo { A = 6, B = 3, C = 2 });
calcul: list.Sum(x => x.A / x.B + list.Max(y => y.C))
result: 15
对象:var list = new List();
        list.Add(new DataInfo { A = 10, B = 2, C = 4 });
        list.Add(new DataInfo { A = 6, B = 3, C = -2 });
计算:list.Sum(x => x.A / x.B + list.Max(y => y.C))
结果:C(因为在计算中有一个“-2”的值)

我们迄今为止做了什么

这里是我们处理简单计算的代码,基于这个线程:
如何提取Expression<Func<T,TResult>>查询中使用的属性并测试其值?

我们创建了一个强类型类,执行计算并将结果作为字符串返回。 但是,如果表达式的任何部分等于特殊值,则计算器必须返回特殊字符。

对于简单情况,它运行良好,例如:

var data = new Rapport1Data() { UnitesDisponibles = 5, ... };
var q = new Calculator<Rapport1Data>()
    .Calcul(data, y => y.UnitesDisponibles, "N0");

但我需要能够执行更复杂的操作,比如:

IEnumerable<Rapport1Data> data = ...;
var q = new Calculator<IEnumerable<Rapport1Data>>()
    .Calcul(data, x => x.Sum(y => y.UnitesDisponibles), "N0");

当我们开始将数据封装在IEnumerable<>中时,出现了错误:

对象不符合目标类型

据我们所知,这是因为子表达式y => y.UnitesDisponibles被应用于IEnumerable而非Rapport1Data
我们该如何修复它以确保如果某天出现像下面这样复杂的表达式时,它将完全递归呢?
IEnumerable<IEnumerable<Rapport1Data>> data = ...;
var q = new Calculator<IEnumerable<IEnumerable<Rapport1Data>>>()
    .Calcul(data,x => x.Sum(y => y.Sum(z => z.UnitesDisponibles)), "N0");

我们创建的类

public class Calculator<T>
{
    public string Calcul(
        T data,
        Expression<Func<T, decimal?>> query,
        string format)
    {
        var rulesCheckerResult = RulesChecker<T>.Check(data, query);

        // l'ordre des vérifications est importante car il y a une gestion
        // des priorités des codes à retourner!
        if (rulesCheckerResult.HasManquante)
        {
            return TypeDonnee.Manquante.ReportValue;
        }

        if (rulesCheckerResult.HasDivisionParZero)
        {
            return TypeDonnee.DivisionParZero.ReportValue;
        }

        if (rulesCheckerResult.HasNonDiffusable)
        {
            return TypeDonnee.NonDiffusable.ReportValue;
        }

        if (rulesCheckerResult.HasConfidentielle)
        {
            return TypeDonnee.Confidentielle.ReportValue;
        }

        // if the query respect the rules, apply the query and return the
        // value
        var result = query.Compile().Invoke(data);

        return result != null
            ? result.Value.ToString(format)
            : TypeDonnee.Manquante.ReportValue;
    }
}

以及自定义ExpressionVisitor

class RulesChecker<T> : ExpressionVisitor
{
    private readonly T data;
    private bool hasConfidentielle = false;
    private bool hasNonDiffusable = false;
    private bool hasDivisionParZero = false;
    private bool hasManquante = false;

    public RulesChecker(T data)
    {
        this.data = data;
    }

    public static RulesCheckerResult Check(T data, Expression expression)
    {
        var visitor = new RulesChecker<T>(data);
        visitor.Visit(expression);

        return new RulesCheckerResult(
            visitor.hasConfidentielle,
            visitor.hasNonDiffusable,
            visitor.hasDivisionParZero,
            visitor.hasManquante);
    }

    protected override Expression VisitBinary(BinaryExpression node)
    {
        if (!this.hasDivisionParZero &&
            node.NodeType == ExpressionType.Divide &&
            node.Right.NodeType == ExpressionType.MemberAccess)
        {
            var rightMemeberExpression = (MemberExpression)node.Right;
            var propertyInfo = (PropertyInfo)rightMemeberExpression.Member;
            var value = Convert.ToInt32(propertyInfo.GetValue(this.data, null));

            this.hasDivisionParZero = value == 0;
        }

        return base.VisitBinary(node);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        // Si l'un d'eux n'est pas à true, alors continuer de faire les tests
        if (!this.hasConfidentielle ||
            !this.hasNonDiffusable ||
            !this.hasManquante)
        {
            var propertyInfo = (PropertyInfo)node.Member;
            object value = propertyInfo.GetValue(this.data, null);
            int? valueNumber = MTO.Framework.Common.Convert.To<int?>(value);

            // Si la valeur est à true, il n'y a pas lieu de tester davantage
            if (!this.hasManquante)
            {
                this.hasManquante =
                    valueNumber == TypeDonnee.Manquante.BdValue;
            }

            // Si la valeur est à true, il n'y a pas lieu de tester davantage
            if (!this.hasConfidentielle)
            {
                this.hasConfidentielle =
                    valueNumber == TypeDonnee.Confidentielle.BdValue;
            }

            // Si la valeur est à true, il n'y a pas lieu de tester davantage
            if (!this.hasNonDiffusable)
            {
                this.hasNonDiffusable =
                    valueNumber == TypeDonnee.NonDiffusable.BdValue;
            }
        }

        return base.VisitMember(node);
    }
}

[更新] 更详细地说明我们的计划


2
你能解释一下你正在计算什么吗?你从来没有说过你在做什么,而且从你的代码中也不明显(这是一个问题)。请将你的解释添加到问题正文中。 - Jeff Mercado
ExpressionVisitor 的设计是用于遍历 Expression,而不是 Expression 集合。您需要编写访问者中的逻辑来处理可能具有不同类型数据的情况。现在您编写的方式假定 data 将是可以从某个属性获取值的东西。如果您想尽可能地保持通用性,那么您应该为传入的数据创建一个“对象访问者”。 - Jeff Mercado
1
看了你的代码之后,我认为你在这里尝试使用ExpressionVisitor是错误的。看起来你正在对某个对象进行一些验证。你不能只是将对象扔到ExpressionVisitor中,它并不是为此而设计的。我认为你需要重新评估你在这里做什么,并考虑使用其他工具来完成这个任务。 - Jeff Mercado
这里我们要做的是,我们有从数据库发送的数据需要进行格式化以制作报告,包括一些计算(总和、平均值等)。其中一个限制是,如果在求和中,例如有一个数据是null、-1或-2,则必须停止计算并显示“-”。由于我们需要生成许多报告,其逻辑相同且每个报告中都有很多计算,因此我们希望集中这个逻辑。目前,我们生成的代码允许我们检查字段对字段的计算(例如x.a / x.b),但不能允许我们检查组总计(SUM(x.a) for emxemple)。 - Groumy
我已经在说明中添加了一些测试用例。我认为这样每个人都会更清楚我们的需求。顺便说一下,我和Groumy正在同一个项目上工作! :) - Alexandre Jobin
2个回答

3

有几件事情需要改变才能使这个工作:

  • 创建一个新的ExpressionVisitor,预处理您的表达式以执行聚合。
  • 在Calculator.Calcul方法中使用新的ExpressionVisitor。
  • 修改RulesChecker以包括VisitConstant方法的重写。这个新方法需要包含VisitMember方法中的相同逻辑。
  • 修改RulesChecker VisitBinary 方法,检查 ConstantExpressions 的除零条件。

以下是我认为需要完成的粗略示例。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace WindowsFormsApplication1 {
    internal static class Program {
        [STAThread]
        private static void Main() {
            var calculator = new Calculator();

            //// DivideByZero - the result should be -1
            var data1 = new DataInfo { A = 10, B = 0, C = 1 };
            Expression<Func<DataInfo, decimal?>> expression1 = x => x.A / x.B + x.C;
            var result1 = calculator.Calcul(data1, expression1, "N0");

            //// Negative 1 - the result should be -
            var data2 = new DataInfo { A = 10, B = 5, C = -1 };
            Expression<Func<DataInfo, decimal?>> expression2 = x => x.A / x.B + x.C;
            var result2 = calculator.Calcul(data2, expression2, "N0");

            //// Negative 2 - the result should be C
            var data3 = new DataInfo { A = 10, B = 5, C = -2 };
            Expression<Func<DataInfo, decimal?>> expression3 = x => x.A / x.B + x.C;
            var result3 = calculator.Calcul(data3, expression3, "N0");

            //// the result should be 3
            var data4 = new DataInfo { A = 10, B = 5, C = 1 };
            Expression<Func<DataInfo, decimal?>> expression4 = x => x.A / x.B + x.C;
            var result4 = calculator.Calcul(data4, expression4, "N0");

            //// DivideByZero - the result should be -1
            var data5 = new List<DataInfo> {
                                    new DataInfo {A = 10, B = 0, C = 1},
                                    new DataInfo {A = 10, B = 0, C = 1}
                        };

            Expression<Func<IEnumerable<DataInfo>, decimal?>> expression5 = x => x.Sum(y => y.A) / x.Sum(y => y.B) + x.Sum(y => y.C);
            var result5 = calculator.Calcul(data5, expression5, "N0");

            //// the result should be 4
            var data6 = new List<DataInfo> {
                                    new DataInfo {A = 10, B = 5, C = 1},
                                    new DataInfo {A = 10, B = 5, C = 1}
                        };

            Expression<Func<IEnumerable<DataInfo>, decimal?>> expression6 = x => x.Sum(y => y.A) / x.Sum(y => y.B) + x.Sum(y => y.C);
            var result6 = calculator.Calcul(data6, expression6, "N0");

            //// the result should be -
            var data7 = new List<DataInfo> {
                                    new DataInfo {A = 10, B = 5, C = -1},
                                    new DataInfo {A = 10, B = 5, C = 1}
                        };

            Expression<Func<IEnumerable<DataInfo>, decimal?>> expression7 = x => x.Sum(y => y.A) / x.Sum(y => y.B) + x.Sum(y => y.C);
            var result7 = calculator.Calcul(data7, expression7, "N0");

            //// the result should be 14
            var c1 = 1;
            var c2 = 2;

            var data8 = new DataInfo { A = 10, B = 1, C = 1 };
            Expression<Func<DataInfo, decimal?>> expression8 = x => x.A / x.B + x.C + c1 + c2;
            var result8 = calculator.Calcul(data8, expression8, "N0");
        }
    }

    public class Calculator {
        public string Calcul<T>(T data, LambdaExpression query, string format) {
            string reportValue;

            if (HasIssue(data, query, out reportValue)) {
                return reportValue;
            }

            // executes the aggregates
            query = (LambdaExpression)ExpressionPreProcessor.PreProcessor(data, query);

            // checks the rules against the results of the aggregates
            if (HasIssue(data, query, out reportValue)) {
                return reportValue;
            }

            Delegate lambda = query.Compile();
            decimal? result = (decimal?)lambda.DynamicInvoke(data);

            return result != null
                ? result.Value.ToString(format)
                : TypeDonnee.Manquante.ReportValue;
        }

        private bool HasIssue(object data, LambdaExpression query, out string reportValue) {
            reportValue = null;

            var rulesCheckerResult = RulesChecker.Check(data, query);

            if (rulesCheckerResult.HasManquante) {
                reportValue = TypeDonnee.Manquante.ReportValue;
            }

            if (rulesCheckerResult.HasDivisionParZero) {
                reportValue = TypeDonnee.DivisionParZero.ReportValue;
            }

            if (rulesCheckerResult.HasNonDiffusable) {
                reportValue = TypeDonnee.NonDiffusable.ReportValue;
            }

            if (rulesCheckerResult.HasConfidentielle) {
                reportValue = TypeDonnee.Confidentielle.ReportValue;
            }

            return reportValue != null;
        }
    }

    internal class ExpressionPreProcessor : ExpressionVisitor {
        private readonly object _source;

        public static Expression PreProcessor(object source, Expression expression) {
            if (!IsValidSource(source)) {
                return expression;
            }

            var visitor = new ExpressionPreProcessor(source);

            return visitor.Visit(expression);
        }

        private static bool IsValidSource(object source) {
            if (source == null) {
                return false;
            }

            var type = source.GetType();

            return type.IsGenericType && type.GetInterface("IEnumerable") != null;
        }

        public ExpressionPreProcessor(object source) {
            this._source = source;
        }

        protected override Expression VisitMethodCall(MethodCallExpression node) {
            if (node.Method.DeclaringType == typeof(Enumerable) && node.Arguments.Count == 2) {

                switch (node.Method.Name) {
                    case "Count":
                    case "Min":
                    case "Max":
                    case "Sum":
                    case "Average":
                        var lambda = node.Arguments[1] as LambdaExpression;
                        var lambaDelegate = lambda.Compile();
                        var value = node.Method.Invoke(null, new object[] { this._source, lambaDelegate });

                        return Expression.Constant(value);
                }
            }

            return base.VisitMethodCall(node);
        }
    }

    internal class RulesChecker : ExpressionVisitor {
        private readonly object data;
        private bool hasConfidentielle = false;
        private bool hasNonDiffusable = false;
        private bool hasDivisionParZero = false;
        private bool hasManquante = false;

        public RulesChecker(object data) {
            this.data = data;
        }

        public static RulesCheckerResult Check(object data, Expression expression) {
            if (IsIEnumerable(data)) {
                var result = new RulesCheckerResult(false, false, false, false);

                IEnumerable dataItems = (IEnumerable)data;

                foreach (object dataItem in dataItems) {
                    result = MergeResults(result, GetResults(dataItem, expression));
                }

                return result;

            }
            else {
                return GetResults(data, expression);
            }
        }

        private static RulesCheckerResult MergeResults(RulesCheckerResult results1, RulesCheckerResult results2) {
            var hasConfidentielle = results1.HasConfidentielle || results2.HasConfidentielle;
            var hasDivisionParZero = results1.HasDivisionParZero || results2.HasDivisionParZero;
            var hasManquante = results1.HasManquante || results2.HasManquante;
            var hasNonDiffusable = results1.HasNonDiffusable || results2.HasNonDiffusable;

            return new RulesCheckerResult(hasConfidentielle, hasNonDiffusable, hasDivisionParZero, hasManquante);
        }

        private static RulesCheckerResult GetResults(object data, Expression expression) {
            var visitor = new RulesChecker(data);
            visitor.Visit(expression);

            return new RulesCheckerResult(
                visitor.hasConfidentielle,
                visitor.hasNonDiffusable,
                visitor.hasDivisionParZero,
                visitor.hasManquante);
        }

        private static bool IsIEnumerable(object source) {
            if (source == null) {
                return false;
            }

            var type = source.GetType();

            return type.IsGenericType && type.GetInterface("IEnumerable") != null;
        }

        protected override Expression VisitBinary(BinaryExpression node) {
            if (!this.hasDivisionParZero && node.NodeType == ExpressionType.Divide) {
                if (node.Right.NodeType == ExpressionType.MemberAccess) {
                    var rightMemeberExpression = (MemberExpression)node.Right;
                    var propertyInfo = (PropertyInfo)rightMemeberExpression.Member;
                    var value = Convert.ToInt32(propertyInfo.GetValue(this.data, null));

                    this.hasDivisionParZero = value == 0;
                }

                if (node.Right.NodeType == ExpressionType.Constant) {
                    var rightConstantExpression = (ConstantExpression)node.Right;
                    var value = Convert.ToInt32(rightConstantExpression.Value);

                    this.hasDivisionParZero = value == 0;
                }

            }

            return base.VisitBinary(node);
        }

        protected override Expression VisitConstant(ConstantExpression node) {
            this.CheckValue(this.ConvertToNullableInt(node.Value));

            return base.VisitConstant(node);
        }

        protected override Expression VisitMember(MemberExpression node) {
            if (!this.hasConfidentielle || !this.hasNonDiffusable || !this.hasManquante) {
                var propertyInfo = node.Member as PropertyInfo;

                if (propertyInfo != null) {
                    var value = propertyInfo.GetValue(this.data, null);

                    this.CheckValue(this.ConvertToNullableInt(value));
                }
            }

            return base.VisitMember(node);
        }

        private void CheckValue(int? value) {
            if (!this.hasManquante) {
                this.hasManquante = value == TypeDonnee.Manquante.BdValue;
            }

            if (!this.hasConfidentielle) {
                this.hasConfidentielle = value == TypeDonnee.Confidentielle.BdValue;
            }

            if (!this.hasNonDiffusable) {
                this.hasNonDiffusable = value == TypeDonnee.NonDiffusable.BdValue;
            }
        }

        private int? ConvertToNullableInt(object value) {
            if (!value.GetType().IsPrimitive) {
                return int.MinValue;
            }

            // MTO.Framework.Common.Convert.To<int?>(value);
            return (int?)value;
        }
    }

    class RulesCheckerResult {
        public bool HasConfidentielle { get; private set; }
        public bool HasNonDiffusable { get; private set; }
        public bool HasDivisionParZero { get; private set; }
        public bool HasManquante { get; private set; }

        public RulesCheckerResult(bool hasConfidentielle, bool hasNonDiffusable, bool hasDivisionParZero, bool hasManquante) {
            this.HasConfidentielle = hasConfidentielle;
            this.HasNonDiffusable = hasNonDiffusable;
            this.HasDivisionParZero = hasDivisionParZero;
            this.HasManquante = hasManquante;
        }
    }

    class TypeDonnee {
        public static readonly TypeValues Manquante = new TypeValues(null, "-");
        public static readonly TypeValues Confidentielle = new TypeValues(-1, "-");
        public static readonly TypeValues NonDiffusable = new TypeValues(-2, "C");
        public static readonly TypeValues DivisionParZero = new TypeValues(0, "-1");
    }

    class TypeValues {
        public int? BdValue { get; set; }
        public string ReportValue { get; set; }

        public TypeValues(int? bdValue, string reportValue) {
            this.BdValue = bdValue;
            this.ReportValue = reportValue;
        }
    }

    class DataInfo {
        public int A { get; set; }
        public int B { get; set; }
        public int C { get; set; }
    }
}

这是一个非常好的尝试,但你的代码并不能适用于所有情况。如果你拿data6并将其中一个C值更改为-1,结果应该是“-”。目前,代码返回2,因为你已经计算了Sum(x => x.C),实际上-1 + 1 = 0,在检查特殊字符之前。这非常重要,即在进行任何计算之前,必须检查所有值是否存在特殊字符。 - Alexandre Jobin
啊,我错过了那个条件。我修改了代码以包括那种情况。 - Tom Brothers
非常令人印象深刻!非常感谢您的帮助!这正是我们想要的解决方案!提出问题的那个人正在度假。不用担心,他回来后会尽快接受您的答案。 - Alexandre Jobin
如果在查询中使用变量,则代码中会有一个小错误。假设在以下表达式中var1 = 2: x => x.A / x.B + x.C + var1,你会得到一个 System.Reflection.RtFieldInfo 强制转换错误。我试图简化查询以仅保留计算的实际变量,但它没有起作用。如果你对此有快速想法,将非常感激...再次感谢 :) - Alexandre Jobin
我刚刚在需求会议上花费了超过一半的工作周,所以当我说这不是我的代码问题而是缺乏需求时,请原谅我 :). 无论如何,假设您可以在RuleChecker Visitor之外验证变量,我为您提供了一个简单的解决方案。我更新了代码,包括一些额外的类型检查。 - Tom Brothers
非常抱歉如果我说了“bug”这个词!这个词不应该这样用。我这周必须处理你的代码,并且有很多错误需要解决以使其正确地工作,这并不是因为你,正如你所说的,而是因为缺乏需求。我意识到使用ExpressionVisitor确实非常复杂。例如,如果您使用不同类型的值(int * decimal / double),我们想要检查的除零将无法工作,因为Convert表达式随处可见。我已经在这里打开了一个新的问题:https://dev59.com/El_Va4cB1Zd3GeqPPAIn - Alexandre Jobin

0

如果我理解正确,您正在寻找一个“递归求和”函数。我可以建议像这样的东西吗?

    var q = new Calculator<Rapport1Data>()
.Calcul(data, y => RecursiveCalc(y), "N0");

    double RecursiveCalc(object toCalc)
    {
         var asRapport = toCalc as Rapport1Data;
         if (asRapport != null)
             return asRapport.UnitesDisponibles;

         var asEnumerable = toCalc as IEnumerable;
         if (asEnumerable != null)
             return asEnumerable.Sum(y => RecursiveCalc(y));


         return 0; // handle a condition for unexpected types
    }

*注意:代码未经测试,甚至可能无法编译


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接