Principle and application of ASP.NET Expression tree

preface

Expression tree is a binary tree data structure.
The purpose is to realize convenient superposition of various query conditions and unlimited splicing into one query condition. Improve the coding efficiency of complex query logic.

1, Lambda expression

Lambda expressions are divided into operational lambda and declarative lambda
The following two Lambdas are used to implement delegates with the same function.

(1) Expression lambda

It is also translated into declarative lambda and expression lambda.

Func<int, int> Arithmetic expression Lambda = 
(t => t + 100);

int number = Arithmetic expression Lambda(6);
//number = 106

(2) Statement lambda

Also translate idiom lambda.

Func<int, int> Sentence type Lambda = 
t =>
{
    return t + 100;
};

int number = Sentence type Lambda(6);
//number = 106

The main body of an Expression lambda is an operation expression, and the main body of a Statement lambda is a statement block (characterized by braces).

Expression lambda can be packaged into Expression tree.

2, Expression tree

The expression tree can be understood as a binary tree composed of expressions

Expression<Func<int, int>> lambdaExpression = (t => t + 100);

The corresponding binary tree is:

Through IDE rapid monitoring, focus on several main properties of the expression tree

Attribute name meaning
Body Expression of the entire tree (expanded to be the attribute of the root node)
NodeType Current node type
Parameters Input parameter set
ReturnType return type

(1) Node type of expression tree (NodeType)

Common node types of expression tree:

Node Stereotypes meaning
Parameter Variable node
Constant Constant node
Add,Subtract Four operation nodes such as addition and subtraction
And,Or Logical operation nodes such as and, or
Call Node calling the function

More node types:

https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.expressions.expressiontype?view=net-5.0

(2) Parse expression tree object

In combination with the diagram of the corresponding binary tree:

Root node (Body)


NodeType: the type of the current node is Add
Type: data type
Left: the left subtree of the current node (expanded is the root node attribute of the left subtree)
Right: the right subtree of the current node (expanded is the root node attribute of the right subtree)

Left subtree of node

You can see the attributes of the left sub node:

NodeType: the type of the current node is "Parameter".
Name: the variable name is "t".
Type: the data type is "Int32".
This node does not have Left or Right, which means it is a leaf node of a binary tree.

Right subtree of node

You can see the properties of the right child node:

NodeType: the type of the current node is "Constant"
Value: the value is 100.
The data type is "Int32".

After installing the ExpressionTreeVisualizer plug-in, you can see it more intuitively

https://github.com/zspitz/ExpressionTreeVisualizer/releases

3, Assemble your own expression tree

(1) Use leaf node assembly (four operations)

Taking the simple addition operation of t + 100 as an example, the code for creating the expression tree is as follows:

//Create an expression tree of t + 100
//Create variable node t
ParameterExpression parax = Expression.Parameter(typeof(int), "t");
//Create constant node 100
ConstantExpression consty = Expression.Constant(100, typeof(int));
//Create a lambda expression tree
LambdaExpression lambdaExp = Expression.Lambda(
  Expression.Add(
    parax,
    consty
  ),
  new List<ParameterExpression>() { parax }
);
//Compile the expression tree into a delegate for execution
var lambdaExpValue = lambdaExp.Compile().DynamicInvoke(1);
//lambdaExpValue = 101;

(2) Use leaf node assembly (logical operation)

In practical applications, there are no scenarios to use operational expressions. They are expression trees that assemble logical operations and are passed to the Where() method as parameters.
Create a student IQueryable as a simulated data source

//Student class, attributes include age and name
Stu stu1 = new Stu()
{
    Age = 10,
    Name = "Cao Cao"
};
Stu stu2 = new Stu()
{
    Age = 20,
    Name = "Liu Bei"
};
Stu stu3 = new Stu()
{
    Age = 20,
    Name = "Sun CE"
};
//Student IQueryable
IQueryable<Stu> StuQ= new List<Stu> { stu1, stu2, stu3 }.AsQueryable();

Query two result sets respectively.

List<Stu> StuListR1 = StuQ.Where(t => t.Age == 20).ToList();
List<Stu> StuListR2 = StuQ.Where(t => t.Name.Contains("Sun")).ToList();

You can see the Where() extension method. The parameter type is expression < func < stu, bool > >

Further, extract the expression tree respectively, obtain two expression < func < stu, bool > > and pass them to the Where() method as parameters.
lambda1 and lambda2 are as follows

Expression<Func<Stu, bool>> lambda1 = (t => t.Age == 20);
Expression<Func<Stu, bool>> lambda2 = (t => t.Name.Contains("Sun"));

List<Stu> StuListR1 = StuQ.Where(lambda1).ToList();
List<Stu> StuListR2 = StuQ.Where(lambda2).ToList();

If we want to get a query result with an age of 10 and a name containing sun. The expression tree lambda3 is as follows.
There are many new node types. Please sort them out according to the tree diagram

Expression<Func<Stu, bool>> lambda3 = (t => t.Age == 20 && t.Name.Contains("Sun"));


(3) Using expression tree assembly

We already have lambda1 and lambda2,
Next, try to assemble them into lambda3 that meets two conditions at the same time, and you will encounter a pit

            Expression<Func<Stu, bool>> lambda1 = (t => t.Age == 20);
            Expression<Func<Stu, bool>> lambda2 = (t => t.Name.Contains("Sun"));
            Expression<Func<Stu, bool>> lambda3 = (Expression<Func<Stu, bool>>)Expression.Lambda(
                Expression.And(
                    lambda1.Body,
                    lambda2.Body
                ),
                new List<ParameterExpression>() {
                    Expression.Parameter(typeof(Stu))
                }
            );
            //This sentence will report an error
            List<Stu> StuListR4 = StuQ.Where(lambda3).ToList();

In this way, the error "variable t undefined" will be reported.
The pit of splicing lambda is: after lambda1 and lambda2 are spliced, the variables of these two expressions will not be automatically associated even if they have the same name.
The compiler thinks that the variable t of lambda1 and the variable t of lambda2 are actually two unrelated parameters, and the resulting expression should have two parameters.
(in fact, the parameter given here is the variable of lambda3, which is not associated with the variable t of lambda1 and lambda2.)
The correct expression tree is:

            Expression<Func<Stu, Stu, bool>> lambda3 = (Expression<Func<Stu, Stu, bool>>)Expression.Lambda(
                Expression.And(
                    lambda1.Body,
                    lambda2.Body
                ),
                new List<ParameterExpression>() {
                    lambda1.Parameters[0],
                    lambda2.Parameters[1]
                }
            );

Its type is "expression < func < stu, Stu, bool > >" (two stus),
The input parameter type "expression < func < Stu, bool > >" (a Stu) required by the and Where() functions is not matched.

We want to obtain lambda3 of expression < func < stu, bool > > type before passing it to Where ().

In order to fill the pit, node replacement is required.
Let the final expression tree use the same parameter. (replace the parameter nodes in lambda1 and lambda2 with the parameter nodes we assigned to lambda3).

Reference documents:

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/expression-trees/how-to-modify-expression-trees

https://stackoverflow.com/questions/30556911/variable-of-type-referenced-from-scope-but-it-is-not-defined

4, Function encapsulating node replacement and assembly expression tree

Function: merge two expression trees into one tree and replace all parameter nodes with the same parameter.
Input: expression tree of two bool return values.
Output: spliced tree. The root node type is Add and the return value type is bool.

/*-------------------------------------------------------------------------
 *      ___
     />    フ
     |   _  _|
     /`  ミ_xノ
     /  -WuTian-|
    /  ヽ    ノ
    │  | |  |
 / ̄|   | | |
 | ( ̄ヽ__ヽ_)__)
 \II
 * Version No.: v1.0
 *  -------------------------------------------------------------------------*/

    public static class ExpressionExtension
    {
        /// <summary>
        ///Generic extension of Expression (splicing expressions and replacing parameters)
        /// </summary>
        ///< typeparam name = "tSource" > generic expression < / typeparam >
        ///< param name = "a" > source expression < / param >
        ///< param name = "B" > spliced expression < / param >
        /// <returns></returns>
        public static Expression<Func<TSource, bool>> And<TSource>(this Expression<Func<TSource, bool>> a, Expression<Func<TSource, bool>> b)
        {
            //Create a final parameter node
            ParameterExpression replacePara = Expression.Parameter(typeof(TSource), "myPara");

            var exprBody = Expression.And(a.Body, b.Body);
            exprBody = (BinaryExpression)new ParameterReplacer(replacePara).Visit(exprBody);

            return Expression.Lambda<Func<TSource, bool>>(exprBody, replacePara);
        }
    }

    /// <summary>
    ///Inheritance: ExpressionVisitor
    /// </summary>
    public class ParameterReplacer : ExpressionVisitor
    {
        private readonly ParameterExpression replacePara;

        internal ParameterReplacer(ParameterExpression _replacePara)
        {
            replacePara = _replacePara;
        }

        protected override Expression VisitParameter(ParameterExpression expression)
        {
            return base.VisitParameter(replacePara);
        }
    }

Use encapsulated functions and Lambda1 and Lambda2 to assemble Lambda3 of expression < func < stu, bool > > type

            Expression<Func<Stu, bool>> lambda1 = (t => t.Age == 20);

            Expression<Func<Stu, bool>> lambda2 = (t => t.Name.Contains("Sun"));

            Expression<Func<Stu, bool>> lambda3 = lambda1.And(lambda2);
       
            List<Stu> stuR = StuQ.Where(lambda3).ToList();

You can see that the parameter nodes in the assembled expression tree have been replaced with the same parameter (myPara):

So far, the query has been executed successfully and the results have been obtained:

More streamlined:

            Expression<Func<Stu, bool>> lambdaExpression = (t => true);
            lambdaExpression = lambdaExpression.And(t => t.Age == 20);
            lambdaExpression = lambdaExpression.And(t => t.Name.Contains("Sun"));
            List<Stu> stuR = StuQ.Where(lambdaExpression).ToList();

5, Eating mode

In the actual development, this method decouples the interface from the business layer.
The interface is responsible for only assembling query conditions into a conditional expression tree. The business layer is only responsible for executing queries, associating the Iqueryable of the involved tables, projecting (= > select) the fields of the DTO model, and performing conditional queries through the expression tree.

(1) Use encapsulated functions directly

Now there are two tables for students and schools.
Page query criteria:
Name, gender and tuition range
Page to display:
Name, age, gender, school, tuition

Create two tables of students and schools and the corresponding ORM model:

using Chloe.Annotations;
namespace EasyCore.Entity.DB_Entity
{
    /// <summary>
    ///ORM model: STU table
    /// </summary>
    [Table("STU")]
    public class Db_Stu
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
        public string Gender { get; set; }
        public string School { get; set; }
    }
}
using Chloe.Annotations;
namespace EasyCore.Entity.DB_Entity
{
    /// <summary>
    ///ORM model: SCHOOL table
    /// </summary>
    [Table("SCHOOL")]
    public class Db_School
    {
        public string School { get; set; }

        public decimal Price { get; set; }
    }
}

Create a DTO model:

namespace EasyCore.Model
{
    /// <summary>
    ///DTO model
    /// </summary>
    public class Dto_StuPrice
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Gender { get; set; }
        public decimal Price { get; set; }
    }
}

Create the parameter model of the interface:

    public class StuPriceParaModel
    {
        public string Name { get; set; }
        public string Gender { get; set; }
        public decimal? MaxPrice { get; set; }
        public decimal? MinPrice { get; set; }
    }

Start to write the interface and business layer according to the requirements:
Interface:

        public ActionResult SearchStuPrice(StuPriceParaModel paraModel)
        {
            //Create a conditional expression tree using parameters
            Expression<Func<Dto_StuPrice, bool>> lambda = (t => true);
            if (paraModel.Gender != null)
                lambda = lambda.And(a => a.Gender == paraModel.Gender);
            if (paraModel.Name != null)
                lambda = lambda.And(a => a.Name.Contains(paraModel.Name));
            if (paraModel.MaxPrice != null)
                lambda = lambda.And(a => a.Price <= paraModel.MaxPrice);
            if (paraModel.MinPrice != null)
                lambda = lambda.And(a => a.Price >= paraModel.MinPrice);

            //Call the business layer and pass in the conditional expression tree as a parameter
            List<Dto_StuPrice> dto_StuPrices = demoService.SreachStuPrice(lambda);

            //Return data
            return JsonResult(dto_StuPrices);
        }

Business layer: it is only responsible for obtaining the assembled lambda expression, executing the query and returning the query results

        public List<Dto_StuPrice> SreachStuPrice(Expression<Func<Dto_StuPrice, bool>> lambda)
        {
            //IQueryable of two tables
            IQuery<Db_Stu> dB_StuQ = DbContext.Query<Db_Stu>();
            IQuery<Db_School> dB_School = DbContext.Query<Db_School>();

            //Create IQueryable for DTO model
            IQuery<Dto_StuPrice> dto_StuPriceQ =
                dB_StuQ.LeftJoin(dB_School, (x, y) => x.School == y.School)
                .Select
                (
                (x, y) => new Dto_StuPrice
                {
                    Name = x.Name,
                    Age = x.Age,
                    Gender = x.Gender,
                    Price = y.Price
                });

            //Use the conditional expression tree to do conditional query
            dto_StuPriceQ = dto_StuPriceQ.Where(lambda);

            //Delayed query
            List<Dto_StuPrice> dto_StuList = dto_StuPriceQ.ToList();

            return dto_StuList;
        }

This decouples the interface from the business layer.
For the modification of query criteria, only the interface needs to be modified, and the business layer code does not need to be moved.

Tags: .NET

Posted on Sun, 28 Nov 2021 10:53:48 -0500 by Daniel Mott