C# 動態構建表達式樹(二)——構建 Select 和 GroupBy 的表達式


C# 動態構建表達式樹(二)——構建 Select 和 GroupBy 的表達式

前言

上篇中寫了表達式的基本使用,為 Where 方法動態構建了表達式。在這篇中會寫如何為 Select 和 GroupBy 動態構建(可以理解為動態表達式的其它常見形式)。

本文的操作方式似乎在實際使用中作用甚微,僅作為了解即可

准備工作

環境:.NET Framework 4.5,SQLServer 2017

建表腳本如下(由 SSMS 導出):

USE [default]
GO
/****** Object:  Table [dbo].[Person]    Script Date: 2021/6/9 12:06:43 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Person](
	[Id] [varchar](100) NOT NULL,
	[Name] [nvarchar](50) NOT NULL,
	[Age] [int] NOT NULL,
	[Gender] [nvarchar](5) NOT NULL,
	[Point] [int] NOT NULL,
	[CreateTime] [datetime] NOT NULL,
 CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

為 Select 方法動態構建表達式

假設我們要查出 Person 表中的 Name、Age、Gender 字段,返回類型為 List<PersonResult> 的對象:

class PersonResult
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Gender { get; set; }
}

常規寫法:

List<PersonResult> personList 
	= context.Person.Select(p => new PersonResult
								{
								    Name = p.Name,
								    Age = p.Age,
								    Gender = p.Gender
								}).ToList();

動態組合寫法:

ParameterExpression pe = Expression.Parameter(typeof(Person), "p");	# 創建形參 p

MemberExpression meName = Expression.MakeMemberAccess(pe, typeof(Person).GetProperty("Name"));	# 要使用 MakeMemberAccess 方法
MemberExpression meAge = Expression.MakeMemberAccess(pe, typeof(Person).GetProperty("Age"));
MemberExpression meGender = Expression.MakeMemberAccess(pe, typeof(Person).GetProperty("Gender"));

Type personResultType = typeof(PersonResult);
MemberAssignment maName = Expression.Bind(personResultType.GetProperty("Name"), meName);	# 使用 Bind 方法將目標類型的屬性與源類型的屬性值綁定
MemberAssignment maAge = Expression.Bind(personResultType.GetProperty("Age"), meAge);
MemberAssignment maGender = Expression.Bind(personResultType.GetProperty("Gender"), meGender);

NewExpression ne = Expression.New(personResultType);	# 相當於 new 關鍵字創建一個對象

MemberInitExpression mie = Expression.MemberInit(ne, maName, maAge, maGender);	# 相當於初始化時賦值操作

Expression<Func<Person, PersonResult>> personSelectExpression = Expression.Lambda<Func<Person, PersonResult>>(mie, pe);
var personList1 = context.Person.Select(personSelectExpression).ToList();

與構建 Where 方法的表達式差不多,主要是創建新對象以及賦值的寫法需要注意。

為 GroupBy 方法動態構建表達式

假設我們要統計出 Person 表中的男生女生數量,返回類型為 List<PersonGroupByResult> 對象

class PersonGroupByResult
{
    public string Gender { get; set; }
    public int Count { get; set; }
}

常規寫法:

List<PersonGroupByResult> personList =
                context.Person.GroupBy(p => p.Gender)
                                .Select(p => new PersonGroupByResult
                                {
                                    Gender = p.Key,
                                    Count = p.Count()
                                }).ToList();

動態組合寫法:

// 動態創建 GroupBy 中的 Expression
ParameterExpression pe = Expression.Parameter(typeof(Person), "p");
MemberExpression meGender = Expression.Property(pe, "Gender");
Expression<Func<Person, string>> groupByExpression = Expression.Lambda<Func<Person, string>>(meGender, pe); 

// 動態創建 Select 中的 Expression
Type groupType = typeof(IGrouping<string, Person>);	# 注意 GroupBy 函數返回的類型
ParameterExpression pge = Expression.Parameter(groupType, "pg");
MemberExpression meKeyGender = Expression.MakeMemberAccess(pge, groupType.GetProperty("Key"));	# 獲取其中的屬性,與上面動態拼接 Select 相同           

Type groupByResultType = typeof(PersonGroupByResult);
MemberAssignment maGender = Expression.Bind(groupByResultType.GetProperty("Gender"), meKeyGender);	# 使用 Bind 方法將目標類型的屬性與源類型的屬性值綁定,與上面動態拼接 Select 相同
MethodInfo countMethod = typeof(Enumerable).GetMethods().Where(a => a.Name == "Count" && a.GetParameters().Length == 1)
                                            .FirstOrDefault().MakeGenericMethod(typeof(Person));	# 獲取 Count 方法
MemberAssignment maCount = Expression.Bind(groupByResultType.GetProperty("Count"), Expression.Call(countMethod, pge));	#使用 Bind 方法將目標類型的屬性與源類型調用方法的返回值綁定
NewExpression ne = Expression.New(groupByResultType);
MemberInitExpression mie = Expression.MemberInit(ne, maGender, maCount);

Expression<Func<IGrouping<string, Person>, PersonGroupByResult>> personSelectExpression =
	Expression.Lambda<Func<IGrouping<string, Person>, PersonGroupByResult>>(mie, pge);
var personList1 = context.Person.GroupBy(groupByExpression).Select(personSelectExpression).ToList();

需要注意的是查找 Count 方法的過程。通過查看定義發現,IGrouping 類型中並沒有 Count 方法,而 IGrouping 實現了 IEnumerable,因此想到獲取 Enumerable 這個 IEnumerable 實現類中的 Count 方法

而 Enumerable中 的 Count 方法定義如下:

在查閱資料和多次嘗試后,仍然無法直接獲取到 Count(當僅傳入方法名稱時,提示有多個定義;當傳入方法名稱和參數時,一直返回為 null)。現通過參數個數來篩選,得到想要的方法。還需要注意的是,Count 方法為泛型方法,得到后還需要執行 MakeGenericMethod 以傳入泛型類型

通過查看 ChangeTracer 和 SQL 執行情況,發現即使我們使用的是 Enumerable 類型,依然是只返回了我們想要的結果,沒有全表查詢。這其中的奧秘還需要探索啊。

其它的一點思考

之前在工作中為了方便經常使用 Select 查詢出匿名類,能否使用動態創建表達式的方式創建匿名類呢(我能想到的一種使用場景是,根據某些條件返回不同的字段,但這其實可以通過冗余字段實現)。在進行了很多嘗試后,發現只能先寫好一個匿名類,再 Select 這個匿名類的相關字段。雖然看似達到了目的,但不符合我們動態組合的要求,因此是沒有意義的。

參考

c# – 使用反射創建lambda表達式,如x => new {..}

referencing desired overloaded generic method

Expression表達式目錄樹動態拼接 反射獲取泛型方法

How to create LINQ Expression Tree to select an anonymous type

How to use Expression to build an Anonymous Type?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM