You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

表达式编译失败:String转Decimal类型转换异常排查求助

问题排查:表达式编译失败与类型转换异常

错误现象

  • 运行时抛出异常:
    System.InvalidCastException: Unable to cast object of type 'System.String' to type 'System.Decimal'
    
  • 调用query.Compile()时返回{Method = <Internal Error evaluating expression>}
  • 移除表达式中(Convert(valueBag.Item["pnt_no"], Decimal) == 4))部分后,编译恢复正常

生成的表达式内容:

{
   valueBag => (IsMatch(Convert(valueBag.Item["pnt_name"], String), "^") And 
   (IsMatch(Convert(valueBag.Item["pnt_name"], String), "FP") And 
   (Convert(valueBag.Item["pnt_no"], Decimal) == 4)))
}

核心代码片段

构建表达式的关键逻辑:

var conditions = ParseTree<T>(item, itemExpression); 

if (conditions.CanReduce)
{
    conditions = conditions.ReduceAndCheck();
}

var query = Expression.Lambda<Func<IDictionary<string, object>, bool>>(conditions, itemExpression);
return query;

完整表达式生成器代码:

using System.Reflection;
using System.Linq.Expressions;
using System.Text.Json;
using System.Text.RegularExpressions;

using System.Data;


namespace ExpressionGenerator
{
    public class JsonExpressionParser
    {
        private const string StringStr = "string";

        private readonly string BooleanStr = nameof(Boolean).ToLower();

        private readonly string Number = nameof(Number).ToLower();
        private readonly string In = nameof(In).ToLower();
        private readonly string And = nameof(And).ToLower();
        private readonly string Contains = nameof(Contains).ToLower();

        //Method construction
        private readonly MethodInfo MethodContains = typeof(Enumerable).GetMethods(
                        BindingFlags.Static | BindingFlags.Public)
                        .Single(m => m.Name == nameof(Enumerable.Contains)
                            && m.GetParameters().Length == 2);

        private readonly MethodInfo PerformMyMeth =
            typeof(Regex).GetMethods(BindingFlags.Static | BindingFlags.Public)
                    .FirstOrDefault(m => m.Name.ToString() == nameof(Regex.IsMatch)
                        && m.GetParameters().Length == 2);


        private delegate Expression Binder(Expression left, Expression right);

        private Expression ParseTree<T>(
            JsonElement condition,
            ParameterExpression parm)
        {
            Expression left = null;
            var gate = condition.GetProperty("Condition").GetString();
            
            JsonElement Rules = condition.GetProperty(nameof(Rules));

            Binder binder = gate == And ? (Binder)Expression.And : Expression.Or;
            Expression bind(Expression left, Expression right) =>
                       left == null ? right : binder(left, right);



            string valueConcatenated = string.Empty;
            foreach (var rule in Rules.EnumerateArray())
            {


                if (rule.TryGetProperty("Condition", out JsonElement check))
                {
                    var right = ParseTree<T>(rule, parm);
                    left = bind(left, right);
                    continue;
                }


                string @Operator = rule.GetProperty(nameof(@Operator)).GetString();     //eg. IN
                string @Type = rule.GetProperty(nameof(@Type)).GetString();               //eg. string
                string @Field = rule.GetProperty(nameof(@Field)).GetString();             //eg. PntName

                JsonElement @Value = rule.GetProperty(nameof(@Value));                    //værdien > Erstae
                valueConcatenated += @Value.ToString();



                
                try
                {

                    var propertyInfo = typeof(IDictionary<string, object>).GetProperty("Item");


                    var arguments = new List<Expression> {
                                        Expression.Constant(@Field)
                                    };


                    var property = Expression.Convert(
                        Expression.MakeIndex(parm, propertyInfo, arguments),
                         (@Type == StringStr || @Type == BooleanStr) ?
                            typeof(string)  : typeof(Decimal)
                        );



                    if (@Operator == In)
                    {

                        var contains = MethodContains.MakeGenericMethod(typeof(string));

                        object val = @Value.EnumerateArray().Select(e => e.GetString())
                            .ToList();

                        var right = Expression.Call(
                            contains,
                            Expression.Constant(val),
                            property);
                        left = bind(left, right);
                    }
                    else if (@Operator == Contains)
                    {
                        left = null!;

                        object val = valueConcatenated;

                        var toCompare = Expression.Constant(val);

                        var right = Expression.Call(PerformMyMeth, property, toCompare);


                        left = bind(left, right);

                    }
                    else
                    {
                        object val = (@Type == StringStr || @Type == BooleanStr) ?
                            (object)@Value.GetString() : @Value.GetDecimal();
                        var toCompare = Expression.Constant(val);
                        var right = Expression.Equal(property, toCompare);
                        left = bind(left, right);
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                    throw new Exception();

                }



            }
            return left;
        }

        public Expression<Func<IDictionary<string, object>, bool>> ParseExpressionOf<T>(JsonElement doc)
        {
            var itemExpression = Expression.Parameter(typeof(IDictionary<string, object>), "valueBag");
      
                    var conditions = ParseTree<T>(doc, itemExpression); 

                    if (conditions.CanReduce)
                    {
                        conditions = conditions.ReduceAndCheck();
                    }

                    Console.WriteLine("Conditions: " + conditions.ToString());

                    var query = Expression.Lambda<Func<IDictionary<string, object>, bool>>(conditions, itemExpression);

                    return query;

        }

        public Func<IDictionary<string, object>, bool> ParsePredicateOf<T>(JsonElement doc)
        {
            var query = ParseExpressionOf<T>(doc);
            try
            {
                return query.Compile();
            }
            catch (Exception)
            {

                throw;
            }
        }
    }
}

JSON配置文件:

{
  "condition": "and",
  "Name": null,
  "Paths": [
    {
      "ObjectClassId": null,
      "Properties": null,
      "Lvl": 0,
      "Condition": "and",
      "Id": null,
      "Name": "",
      "ParentId": null,
      "TwinInfo": null,
      "DataRules": null,
      "TwinObject": null,
      "TwinRelationships": null,
      "Children": [
        {
          "ObjectClassId": null,
          "Properties": null,
          "Lvl": 0,
          "Condition": "and",
          "Id": "Pumpestationer",
          "Name": "",
          "ParentId": "Root",
          "TwinInfo": null,
          "Rules": [
            {
              "Condition": "and",
              "Rules": [
                {
                  "Name": "Rule_Contains_Carrot",
                  "Label": "pnt_name",
                  "Field": "pnt_name",
                  "Type": "string",
                  "Value": "^",
                  "Operator": "contains"

                },
                {
                  "Condition": "and",
                  "Rules": [
                    {
                      "Name": "Rule_Contains_Carrot",
                      "Label": "pnt_name",
                      "Field": "pnt_name",
                      "Type": "string",
                      "Value": "FP",
                      "Operator": "contains"

                    },
                    {
                      "Condition": "and",
                      "Rules": [
                        {
                          "Name": "Rule_Contains_Carrot",
                          "Label": "descr",
                          "Field": "descr",
                          "Type": "string",
                          "Value": "Sidste",
                          "Operator": "contains"

                        }
                      ]

                    }
                  ]

                }
              ],
              "DataRules": null,
              "Lvl": 0,
              "ObjectClassId": null
            }
          ],
          "TwinObject": null,
          "TwinRelationships": null,
          "Children": [
            {
              "ObjectClassId": null,
              "Properties": null,
              "Lvl": 0,
              "Condition": "and",
              "Id": "PST",
              "Name": "",
              "ParentId": "Pumpestationer",
              "TwinInfo": null,
              "Rules": [
                {
                  "Condition": "and",
                  "Rules": [
                    {
                      "Name": "Rule_Contains_Carrot",
                      "Label": "pnt_name",
                      "Field": "pnt_name",
                      "Type": "string",
                      "Value": "Timetæller",
                      "Operator": "contains"

                    }
                  ]
                }
              ],
              "TwinObject": null,
              "TwinRelationships": null,
              "Children": [
                {
                  "ObjectClassId": null,
                  "Properties": null,
                  "Lvl": 0,
                  "Condition": "and",
                  "Id": "PST",
                  "Name": "",
                  "ParentId": "Pumpestationer",
                  "TwinInfo": null,
                  "DataRules": [
                    {
                      "Condition": "and",
                      "Rules": [
                        {
                          "Name": "Rule_Contains_Carrot",
                          "Label": "pnt_name",
                          "Field": "pnt_name",
                          "Type": "string",
                          "Value": "STOP",
                          "Operator": "contains"

                        }
                      ]
                    }
                  ],
                  "TwinObject": null,
                  "TwinRelationships": null,

                  "SourceParentExpression": null,
                  "SourceExpression": null,
                  "RelationshipName": null
                }
              ],
              "SourceParentExpression": null,
              "SourceExpression": null,
              "RelationshipName": null
            }
          ],
          "SourceParentExpression": null,
          "SourceExpression": null,
          "RelationshipName": null
        },
        {
          "ObjectClassId": null,
          "Properties": null,
          "Lvl": 0,
          "Condition": "and",
          "Id": "Pumpestationer",
          "Name": "",
          "ParentId": "Root",
          "TwinInfo": null,
          "Rules": [
            {
              "Condition": "and",
              "Rules": [
                {
                  "Name": "Rule_Contains_Carrot",
                  "Label": "descr",
                  "Field": "descr",
                  "Type": "string",
                  "Value": "Timetæller",
                  "Operator": "contains"

                }
              ]
            }
          ],
          "TwinObject": null,
          "TwinRelationships": null,
          "Children": [
            {
              "ObjectClassId": null,
              "Properties": null,
              "Lvl": 0,
              "Condition": "and",
              "Id": "PST",
              "Name": "",
              "ParentId": "Pumpestationer",
              "TwinInfo": null,
              "Rules": [
                {
                  "Condition": "and",
                  "Rules": [
                    {
                      "Name": "Rule_Contains_Carrot",
                      "Label": "descr",
                      "Field": "descr",
                      "Type": "string",
                      "Value": "Timetæller",
                      "Operator": "contains"

                    }
                  ]
                }
              ],
              "TwinObject": null,
              "TwinRelationships": null,
              "Children": [
              ],
              "SourceParentExpression": null,
              "SourceExpression": null,
              "RelationshipName": null
            }
          ],
          "SourceParentExpression": null,
          "SourceExpression": null,
          "RelationshipName": null
        }
      ],
      "SourceParentExpression": null,
      "SourceExpression": null,
      "RelationshipName": null
    }
  ]
}

问题原因分析

  1. 强制转换不支持跨类型直接转换:代码中用Expression.Convert直接将字典中的object转为Decimal,但如果字典存储的是字符串(如"4"),CLR不允许直接强制转换string到decimal,导致运行时异常。
  2. 常量与属性类型不匹配:生成的常量4是int类型,而转换后的属性是Decimal类型,表达式树对类型匹配要求严格,引发编译错误。
  3. Contains规则逻辑错误:valueConcatenated变量在循环中累加所有规则的Value,导致多个Contains规则共用拼接后的混合值,而非当前规则的单独Value。
  4. 逻辑运算符使用错误:用Expression.And(按位与)替代了Expression.AndAlso(逻辑短路与),不符合业务中AND的语义。

解决方法

1. 替换强制转换为安全类型转换

用Convert.ToDecimal方法替代直接强制转换,支持字符串到decimal的安全转换:

// 替换原有的Expression.Convert逻辑
Expression property;
var indexExpr = Expression.MakeIndex(parm, propertyInfo, arguments);
if (@Type == StringStr || @Type == BooleanStr)
{
    property = Expression.Convert(indexExpr, typeof(string));
}
else
{
    var convertMethod = typeof(Convert).GetMethod("ToDecimal", new[] { typeof(object) });
    property = Expression.Call(convertMethod, indexExpr);
}

2. 确保常量与属性类型一致

显式指定常量的类型为Decimal,避免类型不匹配:

else
{
    object val;
    Type constantType;
    if (@Type == StringStr || @Type == BooleanStr)
    {
        val = @Value.GetString();
        constantType = typeof(string);
    }
    else
    {
        val = @Value.GetDecimal();
        constantType = typeof(Decimal);
    }
    var toCompare = Expression.Constant(val, constantType);
    var right = Expression.Equal(property, toCompare);
    left = bind(left, right);
}

3. 修复Contains规则的累加错误

将valueConcatenated移到循环内部,每个规则单独处理自身的Value:

foreach (var rule in Rules.EnumerateArray())
{
    string valueConcatenated = string.Empty; // 每个规则单独初始化
    
    if (rule.TryGetProperty("Condition", out JsonElement check))
    {
        var right = ParseTree<T>(rule, parm);
        left = bind(left, right);
        continue;
    }

    // ... 其他代码 ...

    JsonElement @Value = rule.GetProperty(nameof(@Value));
    valueConcatenated += @Value.ToString();

    // ... 其他代码 ...
}

4. 替换逻辑运算符为短路版本

用Expression.AndAlso和Expression.OrElse替代按位运算符:

Binder binder = gate == And ? (Binder)Expression.AndAlso : Expression.OrElse;

内容的提问来源于stack exchange,提问作者K N

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 06:02:33