Approfondimento della programmazione funzionale in C# (7) Albero delle espressioni:funzione come dati

Approfondimento della programmazione funzionale in C# (7) Albero delle espressioni:funzione come dati

[LINQ tramite serie C#]

[Serie di approfondimento programmazione funzionale C#]

Ultima versione:https://weblogs.asp.net/dixin/functional-csharp-function-as-data-and-expression-tree

L'espressione lambda C# è un potente zucchero sintattico. Oltre a rappresentare una funzione anonima, la stessa sintassi può anche rappresentare l'albero delle espressioni.

Espressione Lambda come albero delle espressioni

È possibile creare un albero delle espressioni con la stessa sintassi delle espressioni lambda per la funzione anonima:

internal static partial class ExpressionTree
{
    internal static void ExpressionLambda()
    {
        // Func<int, bool> isPositive = int32 => int32 > 0;
        Expression<Func<int, bool>> isPositiveExpression = int32 => int32 > 0;
    }
}

Questa volta, il tipo previsto per l'espressione lambda non è più un tipo di funzione Func, ma Expression>. L'espressione lambda qui non è più compilata in una funzione anonima eseguibile, ma in una struttura di dati ad albero che rappresenta la logica di quella funzione, chiamata albero delle espressioni.

Metaprogrammazione:funzione come dati

L'espressione lambda sopra viene compilata nel codice di costruzione dell'albero delle espressioni:

internal static void CompiledExpressionLambda()
{
    ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "int32"); // int32 parameter.
    ConstantExpression constantExpression = Expression.Constant(0, typeof(int)); // 0
    BinaryExpression greaterThanExpression = Expression.GreaterThan(
        left: parameterExpression, right: constantExpression); // int32 > 0

    Expression<Func<int, bool>> isPositiveExpression = Expression.Lambda<Func<int, bool>>(
        body: greaterThanExpression, // ... => int32 > 0
        parameters: parameterExpression); // int32 => ...
}

Qui l'istanza Expression> rappresenta l'intero albero, le istanze ParameterExpression, ConstantExpression, BinaryExpression sono nodi in tale albero. E sono tutti derivati ​​dal tipo System.Linq.Expressions.Expression:

namespace System.Linq.Expressions
{
    public abstract partial class Expression
    {
        public virtual ExpressionType NodeType { get; }

        public virtual Type Type { get; }

        // Other members.
    }

    public class ParameterExpression : Expression
    {
        public string Name { get; }

        // Other members.
    }

    public class ConstantExpression : Expression
    {
        public object Value { get; }

        // Other members.
    }

    public class BinaryExpression : Expression
    {
        public Expression Left { get; }

        public Expression Right { get; }

        // Other members.
    }

    public abstract class LambdaExpression : Expression
    {
        public Expression Body { get; }

        public ReadOnlyCollection<ParameterExpression> Parameters { get; }

        // Other members.
    }

    public sealed class Expression<TDelegate> : LambdaExpression
    {
        public TDelegate Compile();

        // Other members.
    }
}

La struttura dei dati dell'albero delle espressioni sopra può essere visualizzata come:

Expression<Func<int, bool>> (NodeType = Lambda, Type = Func<int, bool>)
|_Parameters
| |_ParameterExpression (NodeType = Parameter, Type = int)
|   |_Name = "int32"
|_Body
  |_BinaryExpression (NodeType = GreaterThan, Type = bool)
    |_Left
    | |_ParameterExpression (NodeType = Parameter, Type = int)
    |   |_Name = "int32"
    |_Right
      |_ConstantExpression (NodeType = Constant, Type = int)
        |_Value = 0

Quindi questo albero delle espressioni è un albero sintattico astratto, che rappresenta la struttura sintattica astratta del codice sorgente della funzione C# int32 => int32> 0. Si noti che ogni nodo ha la proprietà NodeType e la proprietà Type. NodeType restituisce il tipo di costrutto rappresentato nell'albero e Type restituisce il tipo .NET rappresentato. Ad esempio, sopra ParameterExpression c'è un nodo parametro che rappresenta un parametro int nel codice sorgente, quindi il suo NodeType è Parameter e il suo Type è int.

Per riassumere, le differenze tra

Func<int, bool> isPositive = int32 => int32 > 0; // Code.

e

Expression<Func<int, bool>> isPositiveExpression = int32 => int32 > 0; // Data.

sono:

  • La variabile isPositive è una funzione rappresentata dall'istanza del delegato e può essere chiamata. L'espressione lambda int32 => int32> 0 viene compilata in codice eseguibile. Quando isPositive viene chiamato, questo codice viene eseguito.
  • La variabile isPositiveExpression è una struttura di dati ad albero sintattica astratta. Quindi apparentemente non può essere chiamato direttamente come una funzione eseguibile. L'espressione lambda int32 => int32> 0 viene compilata per la creazione di un albero delle espressioni, in cui ogni nodo è un'istanza di Expression. L'intero albero rappresenta la struttura sintattica e la logica della funzione int32 => int32> 0. Il nodo superiore di questo albero è un'istanza Expression>, poiché si tratta di un'espressione lambda. Ha 2 nodi figlio:
    • Una raccolta ParameterExpression, che rappresenta tutti i parametri dell'espressione lambda. L'espressione lambda ha 1 parametro, quindi questa raccolta contiene un nodo:
      • Un'istanza ParameterExpression, che rappresenta il parametro int denominato "int32".
    • Un nodo Body che rappresenta il corpo dell'espressione lambda, che è un'istanza BinaryExpression, che rappresenta il corpo è un confronto ">" (maggiore di) di 2 operandi. Quindi ha 2 nodi figlio:
      • Un riferimento all'istanza ParameterExpression sopra, che rappresenta l'operando sinistro.
      • Un'istanza ConstantExpression, che rappresenta l'operando destro 0.

Perché ogni nodo nell'albero delle espressioni è fortemente tipizzato con informazioni avanzate. I nodi possono essere attraversati per ottenere la logica del codice sorgente C# della funzione rappresentata e convertirli nella logica di un altro linguaggio. Qui isPositiveExpression rappresenta la logica della funzione per stabilire se un valore int è maggiore di una costante 0 e può essere convertito nel predicato maggiore di della query SQL in una clausola SQL WHERE, ecc.

Espressioni .NET

Oltre a ParameterExpression, ConstantExpression, BinaryExpression, LambdaExpression, .NET fornisce una ricca raccolta di nodi di espressioni. Quella che segue è la loro gerarchia di eredità:

  • Espressione
    • Espressione binaria
    • Espressione di blocco
    • Espressione condizionale
    • Espressione Costante
    • DebugInfoExpression
    • Espressione predefinita
    • DynamicExpression
    • GotoExpression
    • Espressione dell'indice
    • Espressione di chiamata
    • Espressione Etichetta
    • Espressione Lambda
      • Espressione
    • Espressione ListInit
    • LoopExpression
    • Espressione membro
    • MemberInitExpression
    • EspressioneMethodCall
    • NewArrayExpression
    • Nuova espressione
    • ParameterExpression
    • RuntimeVariablesExpression
    • SwitchExpression
    • Prova Espressione
    • TipoEspressioneBinaria
    • UnaryExpression

E, come dimostrato sopra, l'espressione può essere istanziata chiamando i metodi factory di tipo Expression:

public abstract partial class Expression
{
    public static ParameterExpression Parameter(Type type, string name);

    public static ConstantExpression Constant(object value, Type type);

    public static BinaryExpression GreaterThan(Expression left, Expression right);

    public static Expression<TDelegate> Lambda<TDelegate>(Expression body, params ParameterExpression[] parameters);
}

Expression ha molti altri metodi di fabbrica per coprire tutti i casi di istanziazione di espressioni:

public abstract partial class Expression
{
    public static BinaryExpression Add(Expression left, Expression right);

    public static BinaryExpression Subtract(Expression left, Expression right);

    public static BinaryExpression Multiply(Expression left, Expression right);

    public static BinaryExpression Divide(Expression left, Expression right);

    public static BinaryExpression Equal(Expression left, Expression right);

    public static UnaryExpression ArrayLength(Expression array);

    public static UnaryExpression Not(Expression expression);

    public static ConditionalExpression Condition(Expression test, Expression ifTrue, Expression ifFalse);

    public static NewExpression New(ConstructorInfo constructor, params Expression[] arguments);

    public static MethodCallExpression Call(MethodInfo method, params Expression[] arguments);

    public static BlockExpression Block(params Expression[] expressions);

    // Other members.
}

Alcuni nodi di espressioni possono avere più valori NodeType possibili. Ad esempio:

  • UnaryExpression rappresenta qualsiasi operazione unaria con un operatore e un operando. Il suo NodeType può essere ArrayLength, Negate, Not, Convert, Decrement, Increment, Throw, UnaryPlus, ecc.
  • BinaryExpression rappresenta qualsiasi operazione binaria con un operatore, un operando sinistro e un operando destro, il relativo NodeType può essere Add, And, Assign, Divide, Equal, .GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Modulo, Multiply, NotEqual, Oppure, Potenza, Sottrai, ecc.

Finora il compilatore C# implementa solo questo zucchero sintattico "funzione come dati" per l'espressione lambda e non è ancora disponibile per l'istruzione lambda. Non è possibile compilare il seguente codice:

internal static void StatementLambda()
{
    Expression<Func<int, bool>> isPositiveExpression = int32 =>
    {
        Console.WriteLine(int32);
        return int32 > 0;
    };
}

Risulta un errore del compilatore:un'espressione lambda con un corpo di istruzione non può essere convertita in un albero delle espressioni. L'albero delle espressioni sopra deve essere compilato manualmente:

internal static void StatementLambda()
{
    ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "int32"); // int32 parameter.
    Expression<Func<int, bool>> isPositiveExpression = Expression.Lambda<Func<int, bool>>(
        body: Expression.Block( // ... => {
            // Console.WriteLine(int32);
            Expression.Call(new Action<int>(Console.WriteLine).Method, parameterExpression),
            // return int32 > 0;
            Expression.GreaterThan(parameterExpression, Expression.Constant(0, typeof(int)))), // }
        parameters: parameterExpression); // int32 => ...
}

Compila l'albero delle espressioni in CIL

L'albero delle espressioni è data - albero sintattico astratto. In C# e LINQ, l'albero delle espressioni viene solitamente utilizzato per rappresentare la struttura sintattica astratta della funzione, in modo che possa essere compilata in altri linguaggi specifici del dominio, come query SQL, query URI, ecc. Per dimostrarlo, prendi una semplice funzione matematica ad esempio, che accetta doppi parametri ed esegue i 4 calcoli aritmetici binari di base:addizione, sottrazione, moltiplicazione, divisione:

internal static void ArithmeticalExpression()
{
    Expression<Func<double, double, double, double, double, double>> expression =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
}

L'intero albero può essere visualizzato come:

Expression<Func<double, double, double, double, double, double>> (NodeType = Lambda, Type = Func<double, double, double, double, double, double>)
|_Parameters
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "a"
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "b"
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "c"
| |_ParameterExpression (NodeType = Parameter, Type = double)
| | |_Name = "d"
| |_ParameterExpression (NodeType = Parameter, Type = double)
|   |_Name = "e"
|_Body
  |_BinaryExpression (NodeType = Add, Type = double)
    |_Left
    | |_BinaryExpression (NodeType = Subtract, Type = double)
    |   |_Left
    |   | |_BinaryExpression (NodeType = Add, Type = double)
    |   |   |_Left
    |   |   | |_ParameterExpression (NodeType = Parameter, Type = double)
    |   |   |   |_Name = "a"
    |   |   |_Right
    |   |     |_ParameterExpression (NodeType = Parameter, Type = double)
    |   |       |_Name = "b"
    |   |_Right
    |     |_BinaryExpression (NodeType = Divide, Type = double)
    |       |_Left
    |       | |_BinaryExpression (NodeType = Multiply, Type = double)
    |       |   |_Left
    |       |   | |_ParameterExpression (NodeType = Parameter, Type = double)
    |       |   |   |_Name = "c"
    |       |   |_right
    |       |     |_ParameterExpression (NodeType = Parameter, Type = double)
    |       |       |_Name = "d"
    |       |_Right
    |         |_ConstantExpression (NodeType = Constant, Type = int)
    |           |_Value = 2
    |_Right
      |_BinaryExpression (NodeType = Multiply, Type = double)
        |_Left
        | |_ParameterExpression (NodeType = Parameter, Type = double)
        |   |_Name = "e"
        |_Right
          |_ConstantExpression (NodeType = Constant, Type = int)
            |_Value = 3

Questo è un albero delle espressioni molto semplice, dove:

  • ogni nodo interno è un nodo binario (istanza BinaryExpression) che rappresenta operazioni di addizione, sottrazione, moltiplicazione o divisione binaria;
  • ogni nodo foglia è un parametro (istanza di ParameterExpression) o una costante (istanza di ConstantExpression).

In totale ci sono 6 tipi di nodi in questo albero:

  • aggiungi:BinaryExpression {NodeType =ExpressionType.Add }
  • sottrai:BinaryExpression { NodeType =ExpressionType.Subtract }
  • moltiplica:BinaryExpression { NodeType =ExpressionType.Multiply }
  • divide:BinaryExpression {NodeType =ExpressionType.Divide}
  • costante:ParameterExpression { NodeType =ExpressionType.Constant }
  • parametro:ConstantExpression { NodeType =ExpressionType.Parameter }

Attraversa l'albero delle espressioni

Attraversare ricorsivamente questo albero è molto facile. Il seguente tipo di base implementa la logica di base dell'attraversamento:

internal abstract class BinaryArithmeticExpressionVisitor<TResult>
{
    internal virtual TResult VisitBody(LambdaExpression expression) => this.VisitNode(expression.Body, expression);

    protected TResult VisitNode(Expression node, LambdaExpression expression)
    {
        // Processes the 6 types of node.
        switch (node.NodeType)
        {
            case ExpressionType.Add:
                return this.VisitAdd((BinaryExpression)node, expression);

            case ExpressionType.Constant:
                return this.VisitConstant((ConstantExpression)node, expression);

            case ExpressionType.Divide:
                return this.VisitDivide((BinaryExpression)node, expression);

            case ExpressionType.Multiply:
                return this.VisitMultiply((BinaryExpression)node, expression);

            case ExpressionType.Parameter:
                return this.VisitParameter((ParameterExpression)node, expression);

            case ExpressionType.Subtract:
                return this.VisitSubtract((BinaryExpression)node, expression);

            default:
                throw new ArgumentOutOfRangeException(nameof(node));
        }
    }

    protected abstract TResult VisitAdd(BinaryExpression add, LambdaExpression expression);

    protected abstract TResult VisitConstant(ConstantExpression constant, LambdaExpression expression);

    protected abstract TResult VisitDivide(BinaryExpression divide, LambdaExpression expression);

    protected abstract TResult VisitMultiply(BinaryExpression multiply, LambdaExpression expression);

    protected abstract TResult VisitParameter(ParameterExpression parameter, LambdaExpression expression);

    protected abstract TResult VisitSubtract(BinaryExpression subtract, LambdaExpression expression);
}

Il metodo VisitNode rileva il tipo di nodo e invia a 6 metodi astratti per tutti e 6 i tipi di nodi. Il tipo seguente implementa questi 6 metodi:

internal class PrefixVisitor : BinaryArithmeticExpressionVisitor<string>
{
    protected override string VisitAdd
        (BinaryExpression add, LambdaExpression expression) => this.VisitBinary(add, "add", expression);

    protected override string VisitConstant
        (ConstantExpression constant, LambdaExpression expression) => constant.Value.ToString();

    protected override string VisitDivide
        (BinaryExpression divide, LambdaExpression expression) => this.VisitBinary(divide, "div", expression);

    protected override string VisitMultiply
        (BinaryExpression multiply, LambdaExpression expression) =>
            this.VisitBinary(multiply, "mul", expression);

    protected override string VisitParameter
        (ParameterExpression parameter, LambdaExpression expression) => parameter.Name;

    protected override string VisitSubtract
        (BinaryExpression subtract, LambdaExpression expression) =>
            this.VisitBinary(subtract, "sub", expression);

    private string VisitBinary( // Recursion: operator(left, right)
        BinaryExpression binary, string @operator, LambdaExpression expression) =>
            $"{@operator}({this.VisitNode(binary.Left, expression)}, {this.VisitNode(binary.Right, expression)})";
}

Quando si visita un nodo binario, genera ricorsivamente l'operatore in stile prefisso (sinistra, destra). Ad esempio, l'espressione infissa a + b viene convertita in add(a, b), che può essere vista come una funzione di aggiunta chiamata con argomenti aeb. Il codice seguente restituisce la logica del corpo della funzione in uno stile di chiamata di funzione con prefisso:

internal static partial class ExpressionTree
{
    internal static void Prefix()
    {
        Expression<Func<double, double, double, double, double, double>> infix =
            (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
        PrefixVisitor prefixVisitor = new PrefixVisitor();
        string prefix = prefixVisitor.VisitBody(infix); // add(sub(add(a, b), div(mul(c, d), 2)), mul(e, 3))
    }
}

In realtà .NET fornisce un tipo System.Linq.Expressions.ExpressionVisitor integrato. Qui i traverser vengono implementati da zero solo a scopo dimostrativo.

Albero delle espressioni in CIL in fase di esecuzione

Se l'output è in stile suffisso (a, b, add), può essere visto come:carica a per impilare, carica b per impilare, aggiungi 2 valori in pila. Ecco come funziona il linguaggio CIL basato sullo stack. Quindi è possibile creare un visitatore diverso per emettere istruzioni CIL. Le istruzioni CIL possono essere rappresentate da strutture System.Reflection.Emit.OpCode. Quindi l'output può essere una sequenza di coppie istruzione-argomento, rappresentata da una tupla di un valore OpCode, e un valore double (operando) o null (nessun operando):

internal class PostfixVisitor : BinaryArithmeticExpressionVisitor<List<(OpCode, double?)>>
{
    protected override List<(OpCode, double?)> VisitAdd(
        BinaryExpression add, LambdaExpression expression) => this.VisitBinary(add, OpCodes.Add, expression);

    protected override List<(OpCode, double?)> VisitConstant(
        ConstantExpression constant, LambdaExpression expression) =>
            new List<(OpCode, double?)>() { (OpCodes.Ldc_R8, (double?)constant.Value) };

    protected override List<(OpCode, double?)> VisitDivide(
        BinaryExpression divide, LambdaExpression expression) =>
            this.VisitBinary(divide, OpCodes.Div, expression);

    protected override List<(OpCode, double?)> VisitMultiply(
        BinaryExpression multiply, LambdaExpression expression) =>
            this.VisitBinary(multiply, OpCodes.Mul, expression);

    protected override List<(OpCode, double?)> VisitParameter(
        ParameterExpression parameter, LambdaExpression expression)
    {
        int index = expression.Parameters.IndexOf(parameter);
        return new List<(OpCode, double?)>() { (OpCodes.Ldarg_S, (double?)index) };
    }

    protected override List<(OpCode, double?)> VisitSubtract(
        BinaryExpression subtract, LambdaExpression expression) =>
            this.VisitBinary(subtract, OpCodes.Sub, expression);

    private List<(OpCode, double?)> VisitBinary( // Recursion: left, right, operator
        BinaryExpression binary, OpCode postfix, LambdaExpression expression)
    {
        List<(OpCode, double?)> cils = this.VisitNode(binary.Left, expression);
        cils.AddRange(this.VisitNode(binary.Right, expression));
        cils.Add((postfix, (double?)null));
        return cils;
    }
}

Il codice seguente restituisce una sequenza di codice CIL:

internal static void Cil()
{
    Expression<Func<double, double, double, double, double, double>> infix =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;

    PostfixVisitor postfixVisitor = new PostfixVisitor();
    IEnumerable<(OpCode, double?)> postfix = postfixVisitor.VisitBody(infix);
    foreach ((OpCode Operator, double? Operand) code in postfix)
    {
        $"{code.Operator} {code.Operand}".WriteLine();
    }
    // ldarg.s 0
    // ldarg.s 1
    // add
    // ldarg.s 2
    // ldarg.s 3 
    // mul 
    // ldc.r8 2 
    // div 
    // sub 
    // ldarg.s 4 
    // ldc.r8 3 
    // mul 
    // add
}

Quindi la logica C# rappresentata in questo albero delle espressioni viene compilata correttamente nel linguaggio CIL.

Albero delle espressioni da utilizzare in fase di esecuzione

Il codice CIL compilato sopra è eseguibile, quindi è possibile creare una funzione in fase di esecuzione, quindi il codice CIL può essere emesso in quella funzione. Questo tipo di funzione è chiamata funzione dinamica, perché non è in un assembly statico generato in fase di compilazione, ma generato in fase di esecuzione.

internal static class BinaryArithmeticCompiler
{
    internal static TDelegate Compile<TDelegate>(Expression<TDelegate> expression)
    {
        DynamicMethod dynamicFunction = new DynamicMethod(
            name: string.Empty,
            returnType: expression.ReturnType,
            parameterTypes: expression.Parameters.Select(parameter => parameter.Type).ToArray(),
            m: typeof(BinaryArithmeticCompiler).Module);
        EmitIL(dynamicFunction.GetILGenerator(), new PostfixVisitor().VisitBody(expression));
        return (TDelegate)(object)dynamicFunction.CreateDelegate(typeof(TDelegate));
    }

    private static void EmitIL(ILGenerator ilGenerator, IEnumerable<(OpCode, double?)> il)
    {
        foreach ((OpCode Operation, double? Operand) code in il)
        {
            if (code.Operand == null)
            {
                ilGenerator.Emit(code.Operation); // add, sub, mul, div
            }
            else if (code.Operation == OpCodes.Ldarg_S)
            {
                ilGenerator.Emit(code.Operation, (int)code.Operand); // ldarg.s (int)index
            }
            else
            {
                ilGenerator.Emit(code.Operation, code.Operand.Value); // ldc.r8 (double)constant
            }
        }
        ilGenerator.Emit(OpCodes.Ret); // Returns the result.
    }
}

Il codice seguente mostra come usarlo:

internal static void Compile()
{
    Expression<Func<double, double, double, double, double, double>> expression =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
    Func<double, double, double, double, double, double> function = 
        BinaryArithmeticCompiler.Compile(expression);
    double result = function(1, 2, 3, 4, 5); // 12
}

.NET fornisce un'API integrata, il metodo Compile di System.Linq.Expressions.Expression, a questo scopo:compilare l'albero delle espressioni in una funzione eseguibile in fase di esecuzione:

internal static void BuiltInCompile()
{
    Expression<Func<double, double, double, double, double, double>> infix =
        (a, b, c, d, e) => a + b - c * d / 2 + e * 3;
    Func<double, double, double, double, double, double> function = infix.Compile();
    double result = function(1, 2, 3, 4, 5); // 12
}

Internamente, Expression.Compile chiama le API di System.Linq.Expressions.Compiler.LambdaCompile, che è un albero delle espressioni completo per l'implementazione del compilatore CIL.

Albero delle espressioni e query remota LINQ

L'albero delle espressioni è molto importante nella query remota LINQ, perché è facile creare un albero delle espressioni, in particolare con l'espressione lambda, ed è anche facile compilare/convertire/tradurre la logica di un albero delle espressioni C# in un dominio o in un linguaggio diverso. Negli esempi precedenti, l'albero delle espressioni viene convertito in CIL eseguibile. Come accennato in precedenza, esistono query LINQ locali e remote, come il database relazionale. Gli esempi seguenti sono una query LINQ to Objects locale per oggetti in memoria locali e una query LINQ to Entities remota per database relazionale:

internal static partial class ExpressionTree
{
    internal static void LinqToObjects(IEnumerable<Product> source)
    {
        IEnumerable<Product> query = source.Where(product => product.ListPrice > 0M); // Define query.
        foreach (Product result in query) // Execute query.
        {
            result.Name.WriteLine();
        }
    }

    internal static void LinqToEntities(IQueryable<Product> source)
    {
        IQueryable<Product> query = source.Where(product => product.ListPrice > 0M); // Define query.
        foreach (Product result in query) // Execute query.
        {
            result.Name.WriteLine();
        }
    }
}

L'origine dati della query LINQ to Objects sopra è una sequenza di oggetti Product nella memoria locale dell'applicazione .NET corrente. L'origine dati della query LINQ to Entities è la tabella Product nel database relazionale remoto, che non è disponibile nella memoria locale corrente. In LINQ, l'origine dati e la query locali sono rappresentate da IEnumerable e l'origine dati remota e la query sono rappresentate da IQueryable. Hanno diversi metodi di estensione delle query LINQ, tabella sopra Dove come esempio:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TSource> Where<TSource>(
            this IEnumerable<TSource> source, Func<TSource, bool> predicate);
    }

    public static class Queryable
    {
        public static IQueryable<TSource> Where<TSource>(
            this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
    }
}

Di conseguenza, la query Where e l'espressione lambda del predicato condividono la sintassi identica per le query LINQ locali e remote, ma la loro compilazione è completamente diversa. Il predicato della query locale viene compilato per funzionare e il predicato della query remota viene compilato nell'albero delle espressioni:

internal static partial class CompiledExpressionTree
{
    [CompilerGenerated]
    private static Func<Product, bool> cachedPredicate;

    [CompilerGenerated]
    private static bool Predicate(Product product) => product.ListPrice > 0M;

    public static void LinqToObjects(IEnumerable<Product> source)
    {
        Func<Product, bool> predicate = cachedPredicate ?? (cachedPredicate = Predicate);
        IEnumerable<Product> query = Enumerable.Where(source, predicate);
        foreach (Product result in query) // Execute query.
        {
            TraceExtensions.WriteLine(result.Name);
        }
    }
}

internal static partial class CompiledExpressionTree
{
    internal static void LinqToEntities(IQueryable<Product> source)
    {
        ParameterExpression productParameter = Expression.Parameter(typeof(Product), "product");
        Expression<Func<Product, bool>> predicateExpression = Expression.Lambda<Func<Product, bool>>(
            Expression.GreaterThan(
                Expression.Property(productParameter, nameof(Product.ListPrice)),
                Expression.Constant(0M, typeof(decimal))),
            productParameter);

        IQueryable<Product> query = Queryable.Where(source, predicateExpression); // Define query.
        foreach (Product result in query) // Execute query.
        {
            TraceExtensions.WriteLine(result.Name);
        }
    }
}

In fase di esecuzione, quando viene eseguita la query locale, la funzione anonima viene chiamata per ogni valore locale nella sequenza di origine e la query remota viene solitamente tradotta in un linguaggio specifico del dominio, quindi inviata all'origine dati remota ed eseguita. Qui nella query LINQ to Entities, l'albero delle espressioni del predicato viene convertito in predicato nella query SQL e inviato al database per l'esecuzione. La traduzione dall'albero delle espressioni a SQL sarà trattata nel capitolo LINQ to Entities.