表达式编译失败: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 } ] }
问题原因分析
- 强制转换不支持跨类型直接转换:代码中用
Expression.Convert直接将字典中的object转为Decimal,但如果字典存储的是字符串(如"4"),CLR不允许直接强制转换string到decimal,导致运行时异常。 - 常量与属性类型不匹配:生成的常量
4是int类型,而转换后的属性是Decimal类型,表达式树对类型匹配要求严格,引发编译错误。 - Contains规则逻辑错误:
valueConcatenated变量在循环中累加所有规则的Value,导致多个Contains规则共用拼接后的混合值,而非当前规则的单独Value。 - 逻辑运算符使用错误:用
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
相关产品推荐
相关产品推荐

