Skip to main content

Command Palette

Search for a command to run...

Expression Trees vs DSL Trees in .NET

Updated
13 min read
Expression Trees vs DSL Trees in .NET
P
Senior Software Engineer specialising in cloud architecture, distributed systems, and modern .NET development, with over two decades of experience designing and delivering enterprise platforms in financial, insurance, and high-scale commercial environments. My focus is on building systems that are reliable, scalable, and maintainable over the long term. I’ve led modernisation initiatives moving legacy platforms to cloud-native Azure architectures, designed high-throughput streaming solutions to eliminate performance bottlenecks, and implemented secure microservices environments using container-based deployment models and event-driven integration patterns. From an architecture perspective, I have strong practical experience applying approaches such as Vertical Slice Architecture, Domain-Driven Design, Clean Architecture, and Hexagonal Architecture. I’m particularly interested in modular system design that balances delivery speed with long-term sustainability, and I enjoy solving complex problems involving distributed workflows, performance optimisation, and system reliability. I enjoy mentoring engineers, contributing to architectural decisions, and helping teams simplify complex systems into clear, maintainable designs. I’m always open to connecting with other engineers, architects, and technology leaders working on modern cloud and distributed system challenges.

Most .NET developers eventually hit the same problem. You start with a few business rules in code. Then the rules grow. Then they need to change without a deployment. Then someone asks whether the rules can be edited from an admin screen. At that point you have a real design decision to make. Do you model the logic with C# expression trees? Or do you build your own DSL tree?

They sound similar because both are trees. They can both represent logic. They can both be inspected, transformed, and executed. But they solve different problems, and mixing them up is how a simple rules feature turns into something painful to maintain. Expression trees are a .NET feature for representing code as data. A DSL tree is your own model for representing domain logic as data. That difference is small on paper and massive in production.

What an expression tree actually is

An expression tree is a data structure that represents code. In C#, this usually means a lambda expression has been captured as an object instead of compiled directly into executable code.

Expression<Func<Customer, bool>> rule =
    customer => customer.Country == "IE" && customer.TotalSpend > 1000;

That expression can be inspected. You can walk through the tree and see that there is an AndAlso operation. On the left side there is an equality check against Country. On the right side there is a greater-than comparison against TotalSpend.

That’s why expression trees are so useful for LINQ providers. Entity Framework Core can look at an expression tree and translate it into SQL. The tree gives EF Core structure. It isn’t just a string saying what you want. It’s a strongly typed representation of the query shape.

var customers = await dbContext.Customers
    .Where(customer => customer.Country == "IE" && customer.TotalSpend > 1000)
    .ToListAsync(stopToken);

When you write that against IQueryable<T>, the provider gets the expression tree and decides what to do with it. For EF Core, that usually means translating the query into SQL and sending it to the database. This is the part expression trees are very good at. They are close to C#. They preserve type information. They compose well with LINQ. They let libraries inspect your intent before execution.

Where expression trees start to hurt

Expression trees are not a friendly business rule format. They are a developer facing representation of code. That’s fine when the rule lives in the codebase and is owned by engineers. It becomes less fine when the rule has to be stored, versioned, displayed, edited, explained, and migrated.

Take this rule:

Expression<Func<Submission, bool>> rule =
    submission =>
        submission.Country == "Ireland" &&
        submission.TotalInsurableValue >= 5_000_000 &&
        submission.CoverageType == CoverageType.Property;

That’s clear enough in C#.

Now imagine you need to save that rule in a database. Then show it in a UI. Then let an underwriter change the TIV threshold. Then explain why a submission matched. Then keep the old version of the rule around because last month’s decisions must still be reproducible. You can serialise expression trees, but that doesn’t automatically give you a clean domain model. You still have to deal with member access, method calls, constants, enum values, type names, assembly changes, and versioning. The saved format can end up coupled to your C# model in a way that’s awkward to change later. That’s usually the first sign you may need a DSL tree instead.

What a DSL tree is

A DSL tree is a tree you design around your domain. Instead of modelling arbitrary C# expressions, you define a small set of allowed nodes. For example, a rules DSL for submissions might have nodes like All, Any, FieldComparison, and LookupMatch.

public abstract record RuleNode;

public sealed record AllNode(IReadOnlyList<RuleNode> Children) : RuleNode;

public sealed record AnyNode(IReadOnlyList<RuleNode> Children) : RuleNode;

public sealed record FieldComparisonNode(
    string Field,
    string Operator,
    string Value) : RuleNode;

The stored rule might be JSON.

{
  "type": "all",
  "children": [
    {
      "type": "fieldComparison",
      "field": "country",
      "operator": "equals",
      "value": "Ireland"
    },
    {
      "type": "fieldComparison",
      "field": "totalInsurableValue",
      "operator": "greaterThanOrEqual",
      "value": "5000000"
    }
  ]
}

This is less powerful than a C# expression tree. That’s the point. A DSL tree should be deliberately limited. It should only express the operations your system is prepared to support. You decide which fields can be referenced. You decide which operators exist. You decide how values are parsed. You decide what can be displayed in the UI. That gives you control.

The key difference

Expression trees model code. DSL trees model domain intent. That’s the simplest way to think about it. When you use an expression tree, you are usually starting from C# and asking another system to understand it. That system might be EF Core. It might be your own query builder. It might be a dynamic filtering component.

When you use a DSL tree, you are starting from the domain and deciding how much logic you want to allow. The tree is not trying to represent the full shape of C#. It represents the subset of behaviour your product supports.

For example, this expression tree is natural for developers:

customer => customer.Age >= 18 && customer.Country == "IE"

A DSL version is more verbose, but easier to store and reason about outside the codebase.

{
  "type": "all",
  "children": [
    {
      "type": "fieldComparison",
      "field": "age",
      "operator": "greaterThanOrEqual",
      "value": "18"
    },
    {
      "type": "fieldComparison",
      "field": "country",
      "operator": "equals",
      "value": "IE"
    }
  ]
}

The expression tree is nicer when the author is a developer. The DSL tree is usually better when the author might be a user, admin tool, product configuration screen, workflow engine, or imported rule definition.

Use expression trees when the logic belongs in code

Expression trees are a good fit when your rules are still developer owned. They work well for dynamic filtering, reusable query predicates, specification style query composition, and cases where the output is another technical representation such as SQL.

A common example is building a dynamic search endpoint.

public static Expression<Func<Customer, bool>> BuildCustomerFilter(
    CustomerSearchRequest request)
{
    Expression<Func<Customer, bool>> filter = customer => true;

    if (!string.IsNullOrWhiteSpace(request.Country))
    {
        filter = filter.And(customer => customer.Country == request.Country);
    }

    if (request.MinimumSpend is not null)
    {
        filter = filter.And(customer => customer.TotalSpend >= request.MinimumSpend);
    }

    return filter;
}

That can work nicely because the consumers are still developers and the final target is probably LINQ. You are not asking business users to understand the structure. You are not making the expression itself your product’s configuration language. This is also where expression trees give you a lot of value. You keep compile time checking. Refactoring is safer. If TotalSpend gets renamed, the compiler can help you. A string based rule engine won’t give you that for free.

Use a DSL tree when the logic becomes product data

A DSL tree makes more sense when rules become data owned by the application. That usually means rules need to be stored. They need version history. They need approval. They need a UI. They need to be explained to someone who doesn’t care about C# syntax. At that point, the shape of the rule becomes part of your product contract.

A useful DSL tree often starts small.

public sealed record RuleEvaluationContext(
    IReadOnlyDictionary<string, object?> Fields);

public sealed class RuleEvaluator
{
    public bool Evaluate(RuleNode node, RuleEvaluationContext context)
    {
        return node switch
        {
            AllNode all => all.Children.All(child => Evaluate(child, context)),

            AnyNode any => any.Children.Any(child => Evaluate(child, context)),

            FieldComparisonNode comparison => EvaluateComparison(comparison, context),

            _ => throw new NotSupportedException(
                $"Unsupported rule node: {node.GetType().Name}")
        };
    }

    private static bool EvaluateComparison(
        FieldComparisonNode node,
        RuleEvaluationContext context)
    {
        if (!context.Fields.TryGetValue(node.Field, out var actual))
        {
            return false;
        }

        return node.Operator switch
        {
            "equals" => string.Equals(
                actual?.ToString(),
                node.Value,
                StringComparison.OrdinalIgnoreCase),

            "greaterThanOrEqual" => decimal.TryParse(node.Value, out var expected) &&
                                    Convert.ToDecimal(actual) >= expected,

            _ => throw new NotSupportedException(
                $"Unsupported operator: {node.Operator}")
        };
    }
}

That code is much less magical than an expression tree compiler. That’s a good thing. You can validate it before execution. You can return useful error messages. You can explain which node failed. You can add audit records. You can write tests against the DSL itself. You can evolve the format without exposing arbitrary C# behaviour.

The trap: using expression trees as your DSL

A tempting design is to let users configure rules and then turn those rules into expression trees. That can be a good implementation detail. It’s a poor product contract. There is nothing wrong with compiling a validated DSL tree into an expression tree for performance or database translation. The problem starts when the expression tree becomes the thing you store, expose, and treat as your rule format. You then inherit complexity from both worlds. You have the domain concerns of a rules engine, plus the technical concerns of a C# expression representation.

A cleaner design is usually this:

Your stored format is a DSL tree.

Your validation runs against the DSL tree.

Your audit trail stores the DSL tree version.

Your UI edits the DSL tree.

Then, if needed, your infrastructure can translate the DSL tree into an expression tree.

public static Expression<Func<Customer, bool>> ToExpression(RuleNode rule)
{
    var parameter = Expression.Parameter(typeof(Customer), "customer");
    var body = BuildExpressionBody(rule, parameter);

    return Expression.Lambda<Func<Customer, bool>>(body, parameter);
}

The important part is the direction of dependency. The domain owns the DSL. The expression tree is only an execution strategy. That keeps your business language stable even if your .NET implementation changes.

Translation is where the design gets serious

The moment you translate a DSL tree into something else, you need to be honest about what is supported.

Can every DSL rule become SQL?

Can every DSL rule run in memory?

Can every DSL rule be explained in plain English?

Can every DSL rule be shown in the UI?

If the answer is no, your DSL needs capability checks.

For example, a rule containing a custom calculation might be fine in memory but impossible to translate into SQL. A string comparison might behave differently in SQL depending on collation. A date rule might depend on timezone assumptions. A decimal comparison might need controlled precision.

This is where a small DSL pays off. You can define exact behaviour for each node.

public interface IRuleTranslator<T>
{
    bool CanTranslate(RuleNode node);
    T Translate(RuleNode node);
}

You might have one translator for in-memory evaluation, one for SQL or LINQ expression output, and one for human readable explanations. The DSL tree stays the same. The output changes depending on where the rule is used.

Versioning matters more with DSL trees

Expression trees tend to live close to source code. DSL trees often live in storage. That makes versioning unavoidable. Once a rule has been used to make a decision, you should think carefully before changing what that stored rule means.

A simple example is renaming a field.

{
  "field": "totalInsurableValue",
  "operator": "greaterThanOrEqual",
  "value": "5000000"
}

What happens if the application model later renames that field to tiv? With expression trees, a refactor can update code references. With stored DSL data, you need a migration strategy. That might mean keeping stable field identifiers that don’t match C# property names. It might mean versioning the DSL schema. It might mean migrating stored rules when the domain changes. Don’t tie your stored DSL directly to entity property names unless you are comfortable treating those names as a long term contract.

A better rule model often uses stable domain field IDs.

{
  "field": "submission.tiv",
  "operator": "greaterThanOrEqual",
  "value": "5000000"
}

Then your application maps submission.tiv to whatever the current .NET property happens to be. That small separation saves pain later.

Debugging is different

Expression trees are familiar to developers, but they are not especially friendly to non developers. DSL trees can be designed for explanation.

For example, the evaluator can return a result tree instead of a boolean.

public sealed record RuleResult(
    bool Passed,
    string Description,
    IReadOnlyList<RuleResult> Children);

That lets you say more than "the rule returned false"

You can show which part failed.

All conditions were required.
Country equals Ireland: passed.
TIV greater than or equal to 5000000: failed. Actual value was 3200000.

That is hard to bolt on later if you treat rules as opaque delegates. For business rules, the explanation is often as important as the result. If someone asks why a submission was blocked, "because the compiled predicate returned false" isn’t good enough.

A practical decision rule

Use expression trees when you want to compose developer owned logic that fits naturally into LINQ or another technical pipeline. Use a DSL tree when the logic is part of your product’s data model. That’s the split I’d use in most .NET systems. Search filters, query composition, reusable predicates, and provider translation are good expression tree territory. Configurable rules, approval conditions, pricing logic, workflow decisions, eligibility checks, and anything edited outside code usually deserve a DSL tree.

There is also a strong hybrid option. Store the rule as a DSL tree, validate it as domain data, and translate it into an expression tree only when that gives you something useful. That gives you a stable business format without giving up the performance and translation benefits of expression trees.

What I’d avoid

I’d avoid letting users write C# like expressions and calling that a business DSL. It feels quick at the start. You let people type something close to code, parse it, and run it. The first few rules feel manageable. Then the real work appears. You need to validate the input before it runs. You need to decide which fields are allowed. You need safe execution, useful error messages, versioning, migration, and a way to explain the result afterwards. At that point, you haven’t avoided building a language. You’ve built one by accident. I’d also avoid making the first version too flexible.

Most business DSLs should start smaller than feels comfortable. A few clear node types are easier to reason about than a flexible system that can express anything. Add new nodes when there is a real use case. Every new operator becomes part of the product. It needs validation. It needs tests. It needs documentation. It needs a migration story. It also needs a clear explanation when someone asks why a rule passed or failed. That’s the real cost of a rules feature. The tree shape is the easy part. The production behaviour around the tree is where the design proves itself.

Expression trees are powerful because they let .NET represent code as data. DSL trees are powerful because they let your application represent business intent as data. Those are different strengths. If the logic belongs in source code and needs to flow into LINQ, expression trees are often the right tool. If the logic needs to live beyond a deployment, build a DSL tree. Once rules are edited by the product, stored in a database, or used to explain past decisions, you need a domain format you control. And if you need both, keep the boundary clean. Store the DSL. Validate the DSL. Version the DSL. Translate it to expression trees only when that helps execution. Don’t make your users inherit the complexity of C# just because your backend can understand it.

Microsoft Learn - Expression Trees in C#: sion Trees in C#:

Microsoft Learn - Building Expression Trees:

Microsoft Learn - Executing Expression Trees:

Microsoft Learn - System.Linq.Expressions Namespace: sions Namespace:

Microsoft Learn - Standard Query Operators and IQueryable:

Microsoft Learn - EF Core Advanced Performance Topics:

29 views