一、引子·功能需求
我們創建了一個 School 對象,其中包含了教師列表和學生列表。現在,我們需要計算教師平均年齡和學生平均年齡。
//創建對象
School school = new School()
{Name = "小菜學園",Teachers = new List<Teacher>(){new Teacher() {Name="波老師",Age=26},new Teacher() {Name="倉老師",Age=28},new Teacher() {Name="悠老師",Age=30},},Students= new List<Student>(){new Student() {Name="小趙",Age=22},new Student() {Name="小錢",Age=23},new Student() {Name="小孫",Age=24},},//這兩個值如何計算?TeachersAvgAge = "",StudentsAvgAge = "",
};
如果我們將計算教師平均年齡的公式交給用戶定義,那么用戶可能會定義一個字符串來表示:
Teachers.Sum(Age)/Teachers.Count
或者可以通過lambda來表示:
teachers.Average(teacher => teacher.Age)
此時我們就獲得了字符串類型的表達式,如何進行解析呢?
二、構建字符串表達式
手動構造
這種方式是使用 Expression 類手動構建表達式,雖然不符合我們的實際需求,但是它是Dynamic.Core底層實現的方式。Expression 類的文檔地址為::Expression 類 (System.Linq.Expressions) | Microsoft Learn
// 創建參數表達式
var teachersParam = Expression.Parameter(typeof(Teacher[]), "teachers");// 創建變量表達式
var teacherVar = Expression.Variable(typeof(Teacher), "teacher");// 創建 lambda 表達式
var lambdaExpr = Expression.Lambda<Func<Teacher[], double>>(Expression.Block(new[] { teacherVar }, // 定義變量Expression.Call(typeof(Enumerable),"Average",new[] { typeof(Teacher) },teachersParam,Expression.Lambda(Expression.Property(teacherVar, // 使用變量nameof(Teacher.Age)),teacherVar // 使用變量))),teachersParam
);// 編譯表達式樹為委托
var func = lambdaExpr.Compile();var avgAge = func(teachers);
使用System.Linq.Dynamic.Core
System.Linq.Dynamic.Core 是一個開源庫,它提供了在運行時構建和解析 Lambda 表達式樹的功能。它的原理是使用 C# 語言本身的語法和類型系統來表示表達式,并通過解析和編譯代碼字符串來生成表達式樹。
// 構造 lambda 表達式的字符串形式
string exprString = "teachers.Average(teacher => teacher.Age)";// 解析 lambda 表達式字符串,生成表達式樹
var parameter = Expression.Parameter(typeof(Teacher[]), "teachers");
var lambdaExpr = DynamicExpressionParser.ParseLambda(new[] { parameter }, typeof(double), exprString);// 編譯表達式樹為委托
var func = (Func<Teacher[], double>)lambdaExpr.Compile();// 計算教師平均年齡
var avgAge = func(teachers);
三、介紹System.Linq.Dynamic.Core
使用此動態 LINQ 庫,我們可以執行以下操作:
- 通過 LINQ 提供程序進行的基于字符串的動態查詢。
- 動態分析字符串以生成表達式樹,例如ParseLambda和Parse方法。
- 使用CreateType方法動態創建數據類。
功能介紹
普通的功能此處不贅述,如果感興趣,可以從下文提供文檔地址去尋找使用案例。
- 添加自定義方法類
可以通過在靜態幫助程序/實用工具類中定義一些其他邏輯來擴展動態 LINQ 的分析功能。為了能夠做到這一點,有幾個要求:
- 該類必須是公共靜態類
- 此類中的方法也需要是公共的和靜態的
- 類本身需要使用屬性進行注釋[DynamicLinqType]
[DynamicLinqType]
public static class Utils
{public static int ParseAsInt(string value){if (value == null){return 0;}return int.Parse(value);}public static int IncrementMe(this int values){return values + 1;}
}
此類有兩個簡單的方法:
當輸入字符串為 null 時返回整數值 0,否則將字符串解析為整數
使用擴展方法遞增整數值
用法:
var query = new [] { new { Value = (string) null }, new { Value = "100" } }.AsQueryable();
var result = query.Select("Utils.ParseAsInt(Value)");
除了以上添加[DynamicLinqType]屬性這樣的方法,我們還可以在配置中添加。
public class MyCustomTypeProvider : DefaultDynamicLinqCustomTypeProvider
{public override HashSet<Type> GetCustomTypes() =>new[] { typeof(Utils)}.ToHashSet();
}
文檔地址
- 源碼地址:GitHub - zzzprojects/System.Linq.Dynamic.Core: The .NET Standard / .NET Core version from the System Linq Dynamic functionality.
- 文檔地址:Overview in Dynamic LINQ
使用項目
- 規則引擎RulesEngine中解析表達式的實現:Home · microsoft/RulesEngine Wiki · GitHub
- 自己封裝了低代碼中公式編輯器中公式的解析功能
四、淺析System.Linq.Dynamic.Core
System.Linq.Dynamic.Core中 DynamicExpressionParser 和 ExpressionParser 都是用于解析字符串表達式并生成 Lambda 表達式樹的類,但它們之間有一些不同之處。
ExpressionParser 類支持解析任何合法的 C# 表達式,并生成對應的表達式樹。這意味著您可以在表達式中使用各種運算符、方法調用、屬性訪問等特性。
DynamicExpressionParser 類則更加靈活和通用。它支持解析任何語言的表達式,包括動態語言和自定義 DSL(領域特定語言)
我們先看ExpressionParser這個類,它用于解析字符串表達式并生成 Lambda 表達式樹。
我只抽取重要的和自己感興趣的屬性和方法。
- TextParser 類,實現算法有點類似于有限狀態自動機(FSM):?力扣(LeetCode)官網 - 全球極客摯愛的技術成長平臺
- MethodFinder,使用了反射機制,通過調用 GetMethods() 方法獲取指定類型中定義的所有方法,并根據參數數量和類型等條件檢查參數是否符合特定的條件。如果參數滿足了條件,則將該方法添加到結果列表中。
public class ExpressionParser
{//字符串解析器的配置,比如區分大小寫、是否自動解析類型、自定義類型解析器等private readonly ParsingConfig _parsingConfig;//查找指定類型中的方法信息,通過反射獲取MethodInfoprivate readonly MethodFinder _methodFinder;//用于幫助解析器識別關鍵字、操作符和常量值private readonly IKeywordsHelper _keywordsHelper;//解析字符串表達式中的文本,用于從字符串中讀取字符、單詞、數字等private readonly TextParser _textParser;//解析字符串表達式中的數字,用于將字符串轉換為各種數字類型private readonly NumberParser _numberParser;//用于幫助生成和操作表達式樹private readonly IExpressionHelper _expressionHelper;//用于查找指定名稱的類型信息private readonly ITypeFinder _typeFinder;//用于創建類型轉換器private readonly ITypeConverterFactory _typeConverterFactory;//用于存儲解析器內部使用的變量和選項。這些變量和選項不應該由外部代碼訪問或修改private readonly Dictionary<string, object> _internals = new();//用于存儲字符串表達式中使用的符號和值。例如,如果表達式包含 @0 占位符,則可以使用 _symbols["@0"] 訪問其值。private readonly Dictionary<string, object?> _symbols;//表示外部傳入的參數和變量。如果表達式需要引用外部的參數或變量,則應該將它們添加到 _externals 中。private IDictionary<string, object>? _externals;/// <summary>/// 使用TextParser將字符串解析為指定的結果類型./// </summary>/// <param name="resultType"></param>/// <param name="createParameterCtor">是否創建帶有相同名稱的構造函數</param>/// <returns>Expression</returns>public Expression Parse(Type? resultType, bool createParameterCtor = true){_resultType = resultType;_createParameterCtor = createParameterCtor;int exprPos = _textParser.CurrentToken.Pos;//解析條件運算符表達式Expression? expr = ParseConditionalOperator();//將返回的表達式提升為指定類型if (resultType != null){if ((expr = _parsingConfig.ExpressionPromoter.Promote(expr, resultType, true, false)) == null){throw ParseError(exprPos, Res.ExpressionTypeMismatch, TypeHelper.GetTypeName(resultType));}}//驗證最后一個標記是否為 TokenId.End,否則拋出語法錯誤異常_textParser.ValidateToken(TokenId.End, Res.SyntaxError);return expr;}// ?: operatorprivate Expression ParseConditionalOperator(){int errorPos = _textParser.CurrentToken.Pos;Expression expr = ParseNullCoalescingOperator();if (_textParser.CurrentToken.Id == TokenId.Question){......}return expr;}// ?? (null-coalescing) operatorprivate Expression ParseNullCoalescingOperator(){Expression expr = ParseLambdaOperator();......return expr;}// => operator - Added Support for projection operatorprivate Expression ParseLambdaOperator(){Expression expr = ParseOrOperator();......return expr;}}