一、引子·功能需求
我們建立了一個 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 類的檔案地址為::https://learn.microsoft.com/zh-cn/dotnet/api/system.linq.expressions.expression?view=net-6.0
// 建立引數列達式
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();
}
檔案地址
- 原始碼地址:https://github.com/zzzprojects/System.Linq.Dynamic.Core
- 檔案地址:https://dynamic-linq.net/overview
使用專案
- 規則引擎RulesEngine中解析表示式的實現:https://github.com/microsoft/RulesEngine/wiki
- 自己封裝了低程式碼中公式編輯器中公式的解析功能
四、淺析System.Linq.Dynamic.Core
System.Linq.Dynamic.Core中 DynamicExpressionParser 和 ExpressionParser 都是用於解析字串表示式並生成 Lambda 表示式樹的類,但它們之間有一些不同之處。
ExpressionParser 類支援解析任何合法的 C# 表示式,並生成對應的表示式樹。這意味著您可以在表示式中使用各種運運算元、方法呼叫、屬性訪問等特性。
DynamicExpressionParser 類則更加靈活和通用。它支援解析任何語言的表示式,包括動態語言和自定義 DSL(領域特定語言)
我們先看ExpressionParser這個類,它用於解析字串表示式並生成 Lambda 表示式樹。
我只抽取重要的和自己感興趣的屬性和方法。
- TextParser 類,實現演演算法有點類似於有限狀態自動機(FSM):https://leetcode.cn/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/solutions/372095/biao-shi-shu-zhi-de-zi-fu-chuan-by-leetcode-soluti/
- MethodFinder,使用了反射機制,透過呼叫 GetMethods() 方法獲取指定型別中定義的所有方法,並根據引數數量和型別等條件檢查引數是否符合特定的條件。如果引數滿足了條件,則將該方法新增到結果列表中。
public class ExpressionParser
{
//字串解析器的配置,比如區分大小寫、是否自動解析型別、自定義型別解析器等
private readonly ParsingConfig _parsingConfig;
//查詢指定型別中的方法資訊,透過反射獲取MethodInfo
private 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;
}
// ?: operator
private Expression ParseConditionalOperator()
{
int errorPos = _textParser.CurrentToken.Pos;
Expression expr = ParseNullCoalescingOperator();
if (_textParser.CurrentToken.Id == TokenId.Question)
{
......
}
return expr;
}
// ?? (null-coalescing) operator
private Expression ParseNullCoalescingOperator()
{
Expression expr = ParseLambdaOperator();
......
return expr;
}
// => operator - Added Support for projection operator
private Expression ParseLambdaOperator()
{
Expression expr = ParseOrOperator();
......
return expr;
}
}