DEV Community

Godwin Oluowho
Godwin Oluowho

Posted on

Understanding Expression Trees in C# Through a Practical Example

Expression trees in C# allow you to represent code as data. Instead of executing a method directly, you can inspect its structure at runtime. This is widely used in LINQ providers, ORMs, and background job systems.

This article explains expression trees using a practical method pattern commonly seen in background processing systems.

Assume we want to schedule a method call for later execution. Instead of calling the method directly, we pass it as an Expression<Action>:

public class FireAndForgetJobs
{
    private Expression<Action> _expression;

    public FireAndForgetJobs(Expression<Action> expression)
    {
        _expression = expression; 
    }

    public async Task ExecuteAsync(CancellationToken token)
    {    
        MethodCallExpression? methodCall =
            (MethodCallExpression)_expression.Body;

        string functionName = methodCall.Method.Name;
        Type typeName = methodCall.Method.DeclaringType;

        // Logic here to invoke method dynamically
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's break down what is happening.


What Is an Expression Tree?

An expression tree is an object representation of code.

Normally, when you write:

DoSomething();
Enter fullscreen mode Exit fullscreen mode

The method executes immediately.

But when you write:

Expression<Action> expr = () => DoSomething();
Enter fullscreen mode Exit fullscreen mode

You are not executing the method.
You are creating a tree structure that represents:

  • The method being called
  • The declaring type
  • The arguments (if any)

The compiler builds a data structure instead of compiled instructions.

This structure lives in:

System.Linq.Expressions
Enter fullscreen mode Exit fullscreen mode

Why Use Expression Trees?

Expression trees allow you to:

  • Inspect the method name
  • Extract parameter values
  • Identify the declaring type
  • Serialize invocation metadata
  • Reconstruct and execute the call later

This is how libraries like Hangfire capture background job definitions.


Understanding the Code Step-by-Step

1. Capturing the Expression

Expression<Action> expression
Enter fullscreen mode Exit fullscreen mode

This means:

  • No return value
  • Represents a method call
  • Stored as a tree

If someone passes:

new FireAndForgetJobs(() => SendEmail("admin@test.com"));
Enter fullscreen mode Exit fullscreen mode

Nothing executes yet.


2. Accessing the Body

_expression.Body
Enter fullscreen mode Exit fullscreen mode

The body of this lambda is a MethodCallExpression.

So we cast:

(MethodCallExpression)_expression.Body
Enter fullscreen mode Exit fullscreen mode

Now we can access:

  • methodCall.Method.Name
  • methodCall.Method.DeclaringType
  • methodCall.Arguments

3. Extracting Metadata

string functionName = methodCall.Method.Name;
Type typeName = methodCall.Method.DeclaringType;
Enter fullscreen mode Exit fullscreen mode

This gives:

  • The exact method name
  • The class it belongs to

You now have enough information to:

  • Store this in a database
  • Serialize it
  • Invoke it using reflection

What the Expression Tree Actually Contains

For this lambda:

() => SendEmail("admin@test.com")
Enter fullscreen mode Exit fullscreen mode

The tree roughly looks like:

LambdaExpression
 └── MethodCallExpression
      ├── MethodInfo
      └── Arguments
           └── ConstantExpression ("admin@test.com")
Enter fullscreen mode Exit fullscreen mode

It is a structured object graph.


Why Not Use Delegates?

A delegate (Action) executes directly.
You cannot inspect:

  • Method name
  • Parameters
  • Target type

With Expression<Action>, you can inspect everything before execution.

This makes it ideal for:

  • ORMs (e.g., translating LINQ to SQL)
  • Job schedulers
  • Rule engines
  • Dynamic query builders

Limitations

Expression trees:

  • Cannot represent all C# syntax
  • Have performance overhead during inspection
  • Require careful casting (incorrect casts cause runtime errors)

They are powerful but should be used intentionally.


When to Use Expression Trees

Use them when you need:

  • Code analysis at runtime
  • Metadata extraction
  • Dynamic query translation
  • Delayed execution with introspection

Do not use them for simple method invocation.


Conclusion

Expression trees convert executable code into a structured data model. In the example shown:

  • The method call is captured.
  • Its metadata is extracted.
  • It can be stored and executed later.

This pattern underpins many advanced .NET systems and enables flexible runtime behavior without hardcoding execution paths.

If you are building background processing, dynamic execution engines, or LINQ providers, expression trees are a fundamental tool.

Top comments (0)