alder / C# expression engine for .NET

An embeddable C# expression evaluator with
compiler-style binding for your .NET types.

Alder evaluates C# expressions and statement blocks at runtime against the objects your code supplies, following ECMA-334 7th edition C# semantics. Your security policy and execution limits gate every call. Zero third-party runtime dependencies.

C# semantics·AOT-aware·Async-aware·Dynamic LINQ·Zero deps

$ dotnet add package Alder
Source on GitHub
using Alder;

// Your app supplies the data. The rule author supplies the C# fragment.
var cart = new
{
    Subtotal = 120m,
    Discount = 20m,
    Tax = 8m,
    ItemCount = 3,
    PostalCode = "94107"
};

var total =
    "cart.Subtotal - cart.Discount + cart.Tax"
    .Evaluate<decimal>(new { cart });  // 108m

var shipping = """
    var total = cart.Subtotal - cart.Discount + cart.Tax;

    if (total >= freeShippingMinimum)
        return "free-shipping";

    if (cart.ItemCount > 20)
        return "bulk-review";

    return "standard";
    """.Evaluate<string>(
        new { cart, freeShippingMinimum = 100m });  // free-shipping
using Alder;
using Alder.Compiled;

// The host owns policy, services, AOT metadata, and compiled reuse.
using var engine = new AlderEngine(options =>
{
    options.UseCompiler();
    options.Security = SecurityOptions.Trusted();
    options.Modules.Register<TaxModule>("tax");
    options.Aot.UseGeneratedContext(RulesAotContext.Default);
});

var totalRule =
    "cart.Subtotal - cart.Discount + cart.Tax";

var sampleCart = new Cart
{
    Subtotal = 120m,
    Discount = 20m,
    Tax = 8m
};

engine.SetVariable<Cart>("cart", sampleCart);

if (!engine.TryValidate(totalRule, out var diagnostics))
    return diagnostics;

var total = engine.Evaluate<decimal>(totalRule, new { cart = sampleCart });

engine.SetVariable<Cart>("cart", sampleCart);
var trace = engine.EvaluateWithTrace(totalRule);
Console.WriteLine(trace.Result);  // 108m
using Alder;
using Alder.Compiled;

// The same cart vocabulary works in Dynamic LINQ pipelines.
using var engine = new AlderEngine(options => options.UseCompiler());

var cartsForReview = await db.Carts
    .WhereDynamic(
        engine,
        "Subtotal - Discount >= @0",
        100m)
    .OrderByDynamic<Cart, int>(engine, "ItemCount")
    .SelectDynamic<Cart, CartReviewRow>(
        engine,
        "new { Id, Subtotal, Discount, ItemCount }")
    .ToListAsync();

// IQueryable<T> sources export expression trees. EF Core translates
// provider-safe shapes to SQL; IEnumerable<T> executes in process.
using Alder;

// Extended mode adds compact rule syntax one operator at a time.

using var engine = new AlderEngine(options =>
    options.LanguageMode = LanguageMode.Extended);

var cart = new Cart
{
    Subtotal = 160m,
    Discount = 20m,
    ItemCount = 3,
    PostalCode = "94107",
    CouponCode = "SHIP-2026",
    Channel = "mobile",
    Region = "bay-area"
};

var couponMatches = engine.Evaluate<bool>(
    """cart.CouponCode =~ "^SHIP-[0-9]{4}$" """,
    new { cart });

var localPostalCode = engine.Evaluate<bool>(
    """cart.PostalCode like "94%" """,
    new { cart });

var normalOrderSize = engine.Evaluate<bool>(
    "cart.ItemCount between 1 and 20",
    new { cart });

var knownChannel = engine.Evaluate<bool>(
    """cart.Channel in new[] { "web", "mobile" }""",
    new { cart });

var safeRegion = engine.Evaluate<bool>(
    """cart.Region not in new[] { "blocked", "manual-review" }""",
    new { cart });

var net = engine.Evaluate<decimal>(
    "let net = cart.Subtotal - cart.Discount in net",
    new { cart });

var oddSquares = engine.Evaluate<int[]>(
    "[x * x for x in 1..=5 if x % 2 == 1]");
01capabilities

Built like a compiler, embedded like a library.

Parser and binder build a semantic model: type resolution, overload resolution, conversions, control-flow shape, and assignment legality. Execution paths (interpreter, compiled delegate, expression-tree export) share that model, validation rules, security policy, and execution limits. Divergence is a defect.

async / await

Async inside expressions

EvaluateAsync awaits expression-level asynchronous work directly inside the expression itself. await Task<T>, iterators (yield return/yield break), cancellation, and execution limits all flow through the interpreter.

await engine.EvaluateAsync<decimal>( """ var discounted = cart.Subtotal - cart.Discount; var tax = await tax.CalculateAsync(cart.PostalCode, discounted); return discounted + tax; """, new { cart });
Read in docs
queryable

Dynamic LINQ over IQueryable

WhereDynamic, OrderByDynamic, and SelectDynamic bind through the same parser as scalar expressions. IQueryable<T> exports expression trees; providers translate.

db.Carts .WhereDynamic(engine, "Subtotal - Discount >= @0", 100m) .OrderByDynamic<Cart, int>(engine, "ItemCount")
Read in docs
expr trees

Expression-tree export

Alder emits Expression<TDelegate> trees that LINQ providers translate. EF Core consumes the verified shapes: filters, ordering, projections, joins, paging, string methods, EF.Property<T>.

Expression<Func<Cart, bool>> predicate = engine.ParseAsExpression<Func<Cart,bool>>( "cart => cart.Subtotal - cart.Discount >= 100m");
Read in docs
sandbox

Host-controlled security policy

SecurityOptions controls expression authority. Allow and deny lists cover concrete types and namespaces. Reflection metadata is blocked at evaluation boundaries; the default deny surface covers IO, networking, interop, and dynamic code generation.

options.Security = new SecurityOptions { AllowPropertyRead = true, AllowConstruction = true, TrustedTypes = [typeof(StringBuilder)], };
Read in docs
limits

Execution constraints

ExecutionConstraints bounds work across interpreter, compiled backend, and generated dispatch. Exceeded limits throw AlderExecutionLimitException with the limit type, configured limit value, observed value, executed statement count, and elapsed time.

options.Constraints = new ExecutionConstraints { MaxStatements = 10_000, MaxLoopIterations = 1_000, MaxTimeout = TimeSpan.FromSeconds(2), };
Read in docs
aot

NativeAOT generated dispatch

The source generator reads [AlderRegistered] declarations on a partial AlderTypeContext and emits reflection-free dispatch code. Register the types and methods your expressions reach; what isn’t registered falls back to reflection on JIT and surfaces an explicit ALDR0316/0317/0318 diagnostic under AOT.

[AlderRegistered(typeof(Cart))] [AlderRegistered(typeof(TaxModule))] public partial class RulesAotContext : AlderTypeContext;
Read in docs
extended mode

Extended language mode

Opt into LanguageMode.Extended when host-owned rules need compact predicates: regex matching, SQL-style in/not in, like, between, and let ... in.

options.LanguageMode = LanguageMode.Extended; engine.Evaluate<bool>( """cart.CouponCode =~ "^SHIP-[0-9]{4}$" """, new { cart });
Read in docs
isolation

Child engines

engine.CreateChild() returns a new AlderEngine that shares the parent’s configuration (parser, binder, security policy, constraints, compiled caches) with its own isolated variable scope. Use it for per-request sandboxing or concurrent evaluation against shared base config.

using var child = engine.CreateChild(); child.SetVariable<Cart>("cart", cart); var total = child.Evaluate<decimal>(totalRule);
Read in docs
tracing

Step-through evaluation traces

EvaluateWithTrace(...) returns an evaluated tree. Render it when a rule author needs to see which subexpression produced the final value.

engine.SetVariable<Cart>("cart", cart); var trace = engine.EvaluateWithTrace(totalRule); // trace.Tree rendered: // cart.Subtotal - cart.Discount + cart.Tax = 108 // cart.Subtotal - cart.Discount = 100 // cart.Subtotal = 120 // cart.Discount = 20 // cart.Tax = 8
Read in docs
02concrete

A few real snippets, end to end.

The snippets keep following the same cart rule from the first look, then show the public surfaces around it: the runtime, optional Alder.Compiled APIs, Dynamic LINQ, diagnostics, and the source generator for AOT generated dispatch.

scalar evaluationstring.Evaluate<T>
var cart = new { Subtotal = 120m, Discount = 20m, Tax = 8m }; "cart.Subtotal - cart.Discount + cart.Tax" .Evaluate<decimal>(new { cart }); // => 108m
statement blocklocals · branches · return
var shipping = """ var total = cart.Subtotal - cart.Discount + cart.Tax; if (total >= freeShippingMinimum) return "free-shipping"; if (cart.ItemCount > 20) return "bulk-review"; return "standard"; """.Evaluate<string>( new { cart, freeShippingMinimum = 100m });
Dynamic LINQ pipeline against EF Coreexpression-tree export
using Alder.Compiled; var cartsForReview = await db.Carts .WhereDynamic(engine, "Subtotal - Discount >= @0", 100m) .OrderByDynamic<Cart, int>(engine, "ItemCount") .SelectDynamic<Cart, CartReviewRow>( engine, "new { Id, Subtotal, Discount, ItemCount }") .ToListAsync(); // IQueryable<T> exports expression trees and calls the matching Queryable // operators. Provider translation belongs to the provider. EF Core renders SQL. // IEnumerable<T> executes in process through compiled delegates. // IAsyncEnumerable<T> streams in process during asynchronous enumeration.
diagnosticsCS#### · ALDR####
if (!engine.TryValidate(totalRule, out var diagnostics)) { foreach (var d in diagnostics) log.Warn("{Code} at {Span}: {Message}", d.Code, d.Span, d.Message); } // Roslyn CS#### codes where applicable, ALDR#### otherwise.
parse once, evaluate manyAlderExpression
var totalRule = engine.Parse( "cart.Subtotal - cart.Discount + cart.Tax"); var totals = new List<decimal>(); foreach (var cart in carts) { var total = engine.Evaluate<decimal>( totalRule, new { cart }); totals.Add(total); }
03install

One package. Zero third-party runtime dependencies.

Targets net8.0 and netstandard2.0. The Alder package is the single public package. It includes the runtime, the optional Alder.Compiled APIs for JIT-capable consumers, and the source generator for AOT generated dispatch metadata. No third-party runtime dependencies on net8.0; netstandard2.0 pulls Microsoft BCL polyfills (System.Collections.Immutable, Microsoft.Bcl.AsyncInterfaces, System.Threading.Tasks.Extensions).

$ dotnet add package Alder
net8.0 netstandard2.0 aot-publishable zero deps