Expression trong LINQ

Chúng ta đã tìm hiểu về Biểu thức lambda có thể gán cho kiểu delegate Func hoặc delegate Action để xử lý các tập hợp trong bộ nhớ. Trình biên dịch .NET sẽ chuyển đổi biểu thức lambda được gán cho delegate Func hoặc delegate Action thành mã thực thi tại thời điểm biên dịch.

Expression trong LINQ

LINQ đã giới thiệu kiểu dữ liệu mới có tên là Expression (biểu thức) đại diện cho biểu thức lambda được định kiểu mạnh. Nó có nghĩa là biểu thức lambda cũng có thể được gán cho kiểu Expression<TDelegate>.

Trình biên dịch .NET chuyển đổi biểu thức lambda được gán cho Expression<TDelegate> thành cây biểu thức thay vì mã thực thi.

Cây biểu thức (expression tree) này được các trình cung cấp truy vấn LINQ từ xa (remote LINQ query providers) sử dụng làm cấu trúc dữ liệu để xây dựng một truy vấn lúc thực thi từ nó (như LINQ-to-SQL, EntityFramework hoặc bất kỳ trình cung cấp truy vấn LINQ nào khác triển khai interface IQueryable<T>).

Hình dưới đây minh họa sự khác biệt khi biểu thức lambda được gán cho delegate Func hoặc delegate Action và Expression trong LINQ.

Expression trong LINQ là gì?

Chúng ta sẽ tìm hiểu cây biểu thức trong phần tiếp theo, nhưng trước tiên, hãy xem cách định nghĩa và gọi Expression.

Định nghĩa một Expression

Sử dụng namespace System.Linq.Expressions và lớp Expression<TDelegate> để định nghĩa một Expression. Expression<TDelegate> yêu cầu kiểu delegate Func hoặc Action.

Ví dụ: bạn có thể gán biểu thức lambda cho biến isTeenAger của delegate Func, như được trình bày bên dưới:

public class Student 
{
    public int StudentID { get; set; }
    public string StudentName { get; set; }
    public int Age { get; set; }
}

Func<Student, bool> isTeenAger = s => s.Age > 12 && s.Age < 20;

Và bây giờ, bạn có thể chuyển đổi delegate Func ở trên thành Expression bằng cách khai báo như sau:


Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;

Cũng theo cách tương tự, bạn cũng có thể chuyển đổi delegate Action thành Expression bằng cách khai báo như sau


Expression<Action<Student>> printStudentName = s => Console.WriteLine(s.StudentName);

Như vậy là bạn đã biết cách định nghĩa một biểu thức Expression<TDelegate>. Bây giờ, hãy xem cách gọi một biểu thức Expression<TDelegate>.

Gọi một Expression

Bạn có thể gọi một biểu thức Expression giống như cách gọi một delegate, nhưng trước tiên bạn cần biên dịch nó bằng phương thức Compile().

Phương thức Compile trả về kiểu delegate Func hoặc delegate Action để bạn có thể gọi nó như một delegate. Ví dụ sau minh họa gọi một biểu thức Expression:


Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;

//compile Expression using Compile method to invoke it as Delegate
Func<Student, bool>  isTeenAger = isTeenAgerExpr.Compile();
            
//Invoke
bool result = isTeenAger(new Student(){ StudentID = 1, StudentName = "Steve", Age = 20});

Cây biểu thức trong LINQ

Cây biểu thức (expression tree) là các biểu thức được sắp xếp trong cấu trúc dữ liệu giống như cây. Mỗi nút trong cây biểu thức là một biểu thức.

Ví dụ, một cây biểu thức có thể được sử dụng để biểu diễn công thức toán học x < y trong đó x, <y sẽ được biểu diễn dưới dạng một biểu thức và được sắp xếp trong cấu trúc giống như cây.

Cây biểu thức là một mô tả trong bộ nhớ của biểu thức lambda. Nó chứa các phần tử thực tế của truy vấn, không phải là kết quả của truy vấn.

Cây biểu thức làm cho cấu trúc của biểu thức lambda trong suốt và rõ ràng. Bạn có thể tương tác với dữ liệu trong cây biểu thức giống như với bất kỳ cấu trúc dữ liệu nào khác.

Ví dụ, hãy xem xét biểu thức isTeenAgerExpr sau đây:


Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;

Trình biên dịch sẽ dịch biểu thức trên thành cây biểu thức sau:


Expression.Lambda<Func<Student, bool>>(
    Expression.AndAlso(
        Expression.GreaterThan(Expression.Property(pe, "Age"), Expression.Constant(12, typeof(int))),
        Expression.LessThan(Expression.Property(pe, "Age"), Expression.Constant(20, typeof(int)))),
    new[] { pe });

Bạn cũng có thể xây dựng một cây biểu thức bằng tay. Hãy xem cách xây dựng cây biểu thức cho biểu thức lambda đơn giản sau:

Func<Student, bool> isAdult = s => s.age >= 18;

Delegate Func này tương đương với phương thức sau:

public bool IsAdult(Student s)
{
    return s.Age >= 18;
}

Để tạo cây biểu thức, trước hết, hãy tạo một biểu thức tham số trong đó Student là kiểu tham số và 's' là tên của tham số như dưới đây:

ParameterExpression pe = Expression.Parameter(typeof(Student), "s");
Bước 1: Tạo biểu thức tham số trong LINQ

Bây giờ, sử dụng phương thức Expression.Property() để tạo biểu thức s.Age trong đó s là tham số và Age là tên thuộc tính của Student. (Expression là một lớp trừu tượng có chứa các phương thức trợ giúp tĩnh để tạo cây biểu thức theo cách thủ công.)

MemberExpression me = Expression.Property(pe, "Age");
Bước 2: Tạo biểu thức thuộc tính trong LINQ

Bây giờ, tạo một biểu thức hằng cho 18 (tuổi):


ConstantExpression constant = Expression.Constant(18, typeof(int));
Bước 3: Tạo biểu thức hằng trong LINQ

Như vậy là chúng ta đã xây dựng được cây biểu thức cho s.Age (biểu thức thuộc tính) và 18 (biểu thức hằng).

Bây giờ chúng ta cần kiểm tra xem biểu thức thuộc tính có lớn hơn biểu thức hằng hay không.

Để làm điều này, chúng ta sẽ sử dụng phương thức Expression.GreaterThanOrEqual() và truyền biểu thức thuộc tính, biểu thức hằng làm tham số như sau:


BinaryExpression body = Expression.GreaterThanOrEqual(me, constant);
Bước 4: Tạo biểu thức nhị phân trong LINQ

Vậy là chúng ta đã xây dựng xong cây biểu thức cho phần thân biểu thức lambda s.Age >= 18.

Bây giờ chúng ta cần ghép nối các tham số và thân biểu thức lại. Sử dụng phương thức Expression.Lambda(body, parameters array) để nối phần thân biểu thức và phần tham số của biểu thức lambda s => s.age >= 18 như sau:


var isAdultExprTree = Expression.Lambda<Func<Student, bool>>(body, new[] { pe });
Bước 5: Tạo biểu thức Lambda trong LINQ

Bằng cách này, bạn có thể xây dựng cây biểu thức cho các delegate Func đơn giản với biểu thức lambda.


ParameterExpression pe = Expression.Parameter(typeof(Student), "s");

MemberExpression me = Expression.Property(pe, "Age");

ConstantExpression ce = Expression.Constant(18, typeof(int));

BinaryExpression body = Expression.GreaterThanOrEqual(me, ce);

var expressionTree = Expression.Lambda<Func<Student, bool>>(body, new[] { pe });

Console.WriteLine("Expression Tree: {0}", expressionTree);
		
Console.WriteLine("Expression Tree Body: {0}", expressionTree.Body);
		
Console.WriteLine("Number of Parameters in Expression Tree: {0}", 
                                expressionTree.Parameters.Count);
		
Console.WriteLine("Parameters in Expression Tree: {0}", expressionTree.Parameters[0]);
Ví dụ: Cây biểu thức trong LINQ

Đây là kết quả khi biên dịch và thực thi chương trình:

Expression Tree: s => (s.Age >= 18)
Expression Tree Body: (s.Age >= 18)
Number of Parameters in Expression Tree: 1
Parameters in Expression Tree: s

Hình ảnh sau đây minh họa toàn bộ quá trình tạo cây biểu thức:

Tạo cây biểu thức trong LINQ

Tại sao lại dùng cây biểu thức?

Chúng ta đã thấy trong phần trước rằng biểu thức lambda gán cho delegate Func<T> được biên dịch thành mã thực thi và biểu thức lambda gán cho Expression<TDelegate> được biên dịch vào cây biểu thức (expression tree).

Mã thực thi thực hiện trong cùng một miền ứng dụng để xử lý tập hợp trong bộ nhớ. Các lớp tĩnh có thể chứa các phương thức mở rộng cho các tập hợp trong bộ nhớ triển khai interface IEnumerable<T>, ví dụ: List<T>, Dictionary<T>, v.v.

Các phương thức mở rộng trong lớp Enumerable chấp nhận một tham số kiểu delegate Func. Ví dụ, phương thức mởi rộng Where chấp nhận tham số delegate Func<TSource, bool>.

Sau đó, nó biên dịch thành IL (Intermediate Language - Ngôn ngữ trung gian) để xử lý các tập hợp trong bộ nhớ trong cùng một miền ứng dụng (AppDomain).

Hình ảnh sau đây minh họa phương thức mở rộng trong lớp Enumerable có tham số là delegate Func:

Phương thức mở rộng của lớp Enumerable

Delegate Func là một mã thực thi thô, vì vậy nếu bạn Debug, bạn sẽ thấy rằng delegate Func sẽ được biểu diễn như hình dưới đây. Bạn không thể thấy các tham số, kiểu trả về và phần thân biểu thức của nó:

Delegate Func trong LINQ ở chế độ Debug

Delegate Func dành cho các tập hợp trong bộ nhớ vì nó sẽ được xử lý trong cùng một AppDomain, nhưng còn các trình cung cấp truy vấn LINQ từ xa như LINQ-to-SQL, EntityFramework hoặc các sản phẩm của bên thứ ba khác cung cấp khả năng LINQ thì sao?

Làm thế nào họ phân tích biểu thức lambda đã được biên dịch thành mã thực thi thô để biết về các tham số, trả về loại biểu thức lambda và xây dựng truy vấn thời gian chạy để xử lý thêm?

Câu trả lời là cây biểu thức.

Expression<TDelegate> được biên dịch thành cấu trúc dữ liệu gọi là cây biểu thức. Nếu bạn Debug, delegate Expression sẽ được trình bày như dưới đây:

Cây biểu thức trong LINQ ở chế độ Debug

Bây giờ bạn có thể thấy sự khác biệt giữa một delegate bình thường và Expression. Một cây biểu thức rất rõ ràng, bạn có thể truy xuất một tham số, kiểu trả về và thông tin thân biểu thức từ biểu thức, như sau:


Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;

Console.WriteLine("Expression: {0}", isTeenAgerExpr );
        
Console.WriteLine("Expression Type: {0}", isTeenAgerExpr.NodeType);

var parameters = isTeenAgerExpr.Parameters;

foreach (var param in parameters)
{
    Console.WriteLine("Parameter Name: {0}", param.Name);
    Console.WriteLine("Parameter Type: {0}", param.Type.Name );
}
var bodyExpr = isTeenAgerExpr.Body as BinaryExpression;

Console.WriteLine("Left side of body expression: {0}", bodyExpr.Left);
Console.WriteLine("Binary Expression Type: {0}", bodyExpr.NodeType);
Console.WriteLine("Right side of body expression: {0}", bodyExpr.Right);
Console.WriteLine("Return Type: {0}", isTeenAgerExpr.ReturnType);

Đây là kết quả khi biên dịch và thực thi chương trình:

Expression: s => ((s.Age > 12) AndAlso (s.Age < 20))
Expression Type: Lambda
Parameter Name: s
Parameter Type: Student
Left side of body expression: (s.Age > 12)
Binary Expression Type: AndAlso
Right side of body expression: (s.Age < 20)
Return Type: System.Boolean

Truy vấn LINQ cho LINQ-to-SQL hoặc Entity Framework không được thực thi trong cùng một miền ứng dụng. Ví dụ: truy vấn LINQ sau đây cho Entity Framework thực sự không bao giờ được thực hiện bên trong chương trình của bạn:

var query = from s in dbContext.Students
            where s.Age >= 18
            select s;
Ví dụ: Truy vấn LINQ trong C#

Đầu tiên, nó được dịch thành một câu lệnh SQL và sau đó được thực thi trên máy chủ cơ sở dữ liệu.

Mã được tìm thấy trong một biểu thức truy vấn phải được dịch thành một truy vấn SQL có thể được gửi đến một tiến trình khác dưới dạng một chuỗi.

Đối với LINQ-to-SQL hoặc Entity Frameworks, tiến trình đó xảy ra trên một máy chủ cơ sở dữ liệu SQL.

Rõ ràng việc dịch cấu trúc dữ liệu như cây biểu thức sang SQL sẽ dễ dàng hơn nhiều so với dịch mã IL thô hoặc mã thực thi sang SQL bởi vì như bạn đã thấy, rất dễ lấy thông tin từ một biểu thức.

Cây biểu thức được tạo cho nhiệm vụ chuyển đổi mã, chẳng hạn như chuyển một biểu thức truy vấn thành một chuỗi có thể được truyền cho một số tiến trình khác và được thực hiện ở đó.

Lớp tĩnh Queryable bao gồm các phương thức mở rộng chấp nhận tham số vị ngữ kiểu Expression. Biểu thức vị ngữ này sẽ được chuyển đổi thành cây biểu thức và sau đó sẽ được chuyển đến trình cung cấp LINQ từ xa dưới dạng cấu trúc dữ liệu để nhà cung cấp có thể xây dựng một truy vấn thích hợp từ cây biểu thức và thực hiện truy vấn.

Quy trình xử lý cây biểu thức trong LINQ


Bài viết liên quan:

Bạn sẽ tìm hiểu một số truy vấn LINQ phức tạp trong hướng dẫn này.

Từ khóa let, into trong LINQ có tác dụng gì? Hướng dẫn khai báo và sử dụng từ khóa let, into trong LINQ.

Trì hoãn thực thi truy vấn LINQ là gì? Thực thi ngay lập tức truy vấn LINQ là gì? Làm sao để thực thi truy vấn LINQ.