Alder 1.0.3
dotnet add package Alder --version 1.0.3
NuGet\Install-Package Alder -Version 1.0.3
<PackageReference Include="Alder" Version="1.0.3" />
<PackageVersion Include="Alder" Version="1.0.3" />
<PackageReference Include="Alder" />
paket add Alder --version 1.0.3
#r "nuget: Alder, 1.0.3"
#:package Alder@1.0.3
#addin nuget:?package=Alder&version=1.0.3
#tool nuget:?package=Alder&version=1.0.3
Alder: C# Expression Engine for .NET
An embeddable C# expression evaluator with compiler-style binding for CLR types.
Interpreter-first execution with optional compiled delegates, Dynamic LINQ, expression-tree export, host-controlled security, and NativeAOT generated dispatch.
C# semantics · Native AOT · Async · Dynamic LINQ · Zero dependencies
Alder evaluates C# expressions and statement blocks at runtime against CLR objects supplied by the host. Before execution, the parser and binder build a semantic model. That model decides type resolution, overload resolution, conversions, and control flow. The same pipeline applies security policy and execution limits.
The interpreter is the baseline execution path. JIT-capable hosts can opt into compiled delegates. Query providers can receive Expression<TDelegate> trees. NativeAOT hosts can route registered member access through generated dispatch metadata.
Standard mode follows ECMA-334 7th edition semantics. It covers lambdas and query syntax, pattern matching, async code, iterators, and user-defined conversions and operators. The interpreter and compiled backend share parser and binder. They also share validation, security, and limits. They produce identical results; divergence is a defect.
At a glance
- C# expressions and statements at runtime. Standard mode follows ECMA-334 7th edition for runtime expressions and statement blocks. It includes lambdas and queries, pattern matching, async code, iterators, and user-defined conversions and operators. Support matrix.
- Native AOT through generated dispatch. A source generator emits reflection-free dispatch from
[AlderRegistered]declarations. The interpreter runs under AOT without trim warnings. - Async inside expressions.
EvaluateAsyncawaits inside the bound tree.IAsyncEnumerable<T>,await foreach, and iterators are first-class through the interpreter. - Shared semantics across surfaces. Expression evaluation, Dynamic LINQ (
WhereDynamic,OrderByDynamic), andExpression<TDelegate>export go through one parser and binder. They use the same security policy and execution limits.
Targets net8.0 and netstandard2.0. Zero third-party runtime dependencies.
A first look
AlderEval is the static entry point. Calls run against a default engine and need no setup:
using Alder;
AlderEval.Evaluate<int>("1 + 2"); // 3
AlderEval.Evaluate<decimal>("price * 1.2m", new { price = 100m }); // 120m
AlderEngine gives you the same evaluation surface with owned lifecycle and configuration:
using var engine = new AlderEngine();
var tier = engine.Evaluate<string>("""
var t = order switch
{
{ Total: > 1000m, IsRush: true } => "premium-express",
{ Total: > 1000m } => "premium",
{ IsRush: true } => "express",
_ => "standard"
};
return t;
""", new { order });
End-to-end integration
A configured AlderEngine carries compiler settings, security policy, and generated AOT dispatch into every call it serves:
using Alder;
using Alder.Compiled;
using var engine = new AlderEngine(options =>
{
options.UseCompiler();
options.Security = SecurityOptions.Trusted();
options.Modules.Register<PricingModule>("pricing");
options.Aot.UseGeneratedContext(RulesAotContext.Default);
});
Use TryValidate to surface parser and binder diagnostics before execution:
if (!engine.TryValidate(rule, out var diagnostics))
return diagnostics;
Synchronous evaluation dispatches through the compiled backend against host-shaped types:
var accepted = engine.Evaluate<bool>(rule, new { order, minimum = 500m });
Awaitable expression bodies cooperate with cancellation and constraints:
var quote = await engine.EvaluateAsync<decimal>(
"await pricing.QuoteAsync(order)",
new { order });
Runtime fragments export as Expression trees so EF Core can translate them to SQL:
var report = await db.Orders
.WhereDynamic(engine, """Status == "Open" && Total >= @0""", 250m)
.OrderByDynamic<Order, decimal>(engine, "Total")
.SelectDynamic<Order, OrderSummary>(engine, "new { Id, Total }")
.ToListAsync();
Install
dotnet add package Alder
The Alder package is the single public package. It includes the runtime, optional Alder.Compiled APIs for JIT-capable consumers, and the source generator for AOT generated dispatch metadata.
Language surface
Standard mode evaluates C# at the expression and statement-block level under ECMA-334 7th edition semantics. Type and member declarations, namespaces, attributes, preprocessor directives, and unsafe code are out of scope. The full support matrix lives in Standard mode language support.
Extended mode adds scripting syntax on the same parser. The additional surface includes pipelines and regex predicates, SQL-style comparisons, ranges, date arithmetic, and aggregate helpers. A valid C# expression produces the same result in either mode.
The expression engine
Alder's binder is the semantic boundary between syntax and execution. It resolves type relationships, overloads, member targets, assignment legality, and control-flow shape. It also records where runtime dispatch is still required.
Execution paths consume that bound model while preserving the same security policy and execution limits.
The interpreter evaluates the bound tree directly. It is the default synchronous path, the engine for EvaluateAsync(...), and the path used under NativeAOT and trimming-sensitive deployments.
The compiled backend lowers the same bound tree to a reusable delegate through System.Linq.Expressions. With UseCompiler() configured, synchronous Evaluate(...) uses that delegate path and recompiles when the relevant type surface changes.
Both backends share parser and binder. They also share validation rules, security policy, execution limits, and language semantics. They produce identical results. Divergence is a defect.
Architecture: Architecture, Binding system, Execution model.
Async expressions
EvaluateAsync(...) runs through the interpreter and awaits expression-level asynchronous work directly inside the bound tree.
using var engine = new AlderEngine(options =>
{
options.Modules.Register<PricingModule>("pricing");
});
var prices = await engine.EvaluateAsync<decimal[]>(
"""
var quotes = await pricing.FetchAsync(symbols);
return quotes.Select(q => q.Bid).ToArray();
""",
new { symbols });
await cooperates with CancellationToken and execution constraints. Long-running expressions surface OperationCanceledException or AlderExecutionLimitException at expression-level checkpoints.
Iterators, await foreach, and IAsyncEnumerable<T> are first-class inside the same evaluation tree.
See Async execution.
Dynamic LINQ
Dynamic LINQ adapts runtime fragments into LINQ pipelines for in-memory collections, query providers, and async streams.
using Alder;
using Alder.Compiled;
using var engine = new AlderEngine(options => options.UseCompiler());
var page = orders
.WhereDynamic(engine, """Status == "Open" && Total >= @0""", 250m)
.OrderByDynamic<Order, decimal>(engine, "Total")
.SelectDynamic<Order, OrderSummary>(
engine,
"new { Id, CustomerName = Customer.Name, Total }")
.TakeDynamic(25)
.ToList();
IEnumerable<T> executes in process through compiled delegates. IQueryable<T> exports expression trees and calls the matching Queryable operators. Provider translation belongs to the provider. IAsyncEnumerable<T> streams in process through compiled delegates during asynchronous enumeration.
Operator coverage spans filters and ordering; projection and flattening; grouping, joins, and group joins; paging and set operations; element operators, quantifiers, and aggregates.
DynamicQueryPlan captures a parsed fragment for reuse across operators, provider-backed query assembly, validation, delegate execution, and expression-tree export.
The full operator matrix is in Use Dynamic LINQ.
LINQ expression-tree export
Alder produces Expression<TDelegate> trees that LINQ providers translate.
using System.Linq.Expressions;
Expression<Func<Order, bool>> predicate =
engine.ParseAsExpression<Func<Order, bool>>(
"""order => order.Total >= 500m && order.Status == "Open" """);
EF Core can translate the verified shapes Alder emits for filters and ordering; projections and grouping; flattening, joins, and group joins; paging; null-coalescing predicates; string methods; and EF.Property<T>(...).
The export surface is narrower than runtime evaluation. Alder rejects statement-bodied lambdas, assignments, dynamic call shapes, collection expressions, and reflection-leaking members before provider translation begins.
Details in Compiled backend.
Security policy
SecurityOptions controls expression authority. Alder defaults to trusted execution for ease of adoption. Trusted() enables every gated operation for trusted expressions.
Hosts that evaluate user-authored or tenant-authored expressions should choose an explicit new SecurityOptions { ... } policy and name each allowed operation directly.
Allow and deny lists cover concrete CLR types and namespaces. Reflection metadata is blocked at evaluation boundaries so expressions can compare types and read names without escaping into reflective discovery or invocation.
options.Security = new SecurityOptions
{
AllowPropertyRead = true,
AllowStaticPropertyRead = true,
AllowStaticFieldRead = true,
AllowConstruction = true,
TrustedTypes = [typeof(StringBuilder)],
};
The default deny surface covers reflection and dynamic code generation; file and process access; networking and interop; security-sensitive runtime services; and data access. The boundary is in-process. Alder constrains expression behavior inside the host runtime; it does not provide process or operating-system isolation.
See Security model.
Execution limits
ExecutionConstraints bounds work. Limits apply across the interpreter, the compiled backend, and generated dispatch.
options.Constraints = new ExecutionConstraints
{
MaxStatements = 10_000,
MaxLoopIterations = 1_000,
MaxTimeout = TimeSpan.FromSeconds(2),
};
When a limit is exceeded, Alder throws AlderExecutionLimitException. The exception reports the limit type and configured value, the observed value, executed statement count, and elapsed time. SecurityOptions.MaxCollectionSize bounds collection-producing results separately.
NativeAOT
Alder supports NativeAOT through interpreted evaluation backed by generated dispatch metadata. The source generator reads [AlderRegistered] declarations on a partial AlderTypeContext and emits reflection-free dispatch code.
using Alder.Aot;
[AlderRegistered(typeof(Order))]
[AlderRegistered(typeof(Customer))]
public partial class RulesAotContext : AlderTypeContext;
var engine = new AlderEngine(options =>
{
options.Aot.UseGeneratedContext(RulesAotContext.Default);
});
JIT deployments adopt generated coverage incrementally because reflection fallback remains available. NativeAOT deployments use generated dispatch as the authoritative route for reflection-sensitive operations.
See Deploy with NativeAOT and AOT and generated dispatch.
Reuse and performance
Parse once. Bind once. Compile once. Reuse across the lifetime of the engine.
AlderExpression preserves parsed syntax across evaluations and engines. The engine caches bound and compiled state for calls against the same context type surface.
Compile<TDelegate>(...) produces a typed synchronous delegate for hot paths. DynamicQueryPlan reuses parsed query fragments across operators, expression-tree export, and delegate execution.
var expression = engine.Parse("price * (1 - discount)");
var first = engine.Evaluate<double>(expression, new { price = 100.0, discount = 0.10 });
var second = engine.Evaluate<double>(expression, new { price = 250.0, discount = 0.10 });
var isVisible = engine.Compile<Func<decimal, decimal, bool>>(
"total >= minimum", "total", "minimum");
Cache invalidation is conservative. Value-only changes keep prior work. Declared-type changes rebind because overload resolution, conversion legality, and the resolved-versus-dynamic boundary can shift.
See Execution and reuse.
Host integration
Hosts shape Alder's expression-facing world through AlderOptions. Variables can come from typed values, anonymous objects, dictionaries, positional @0 placeholders, or inputs that preserve runtime type.
Host APIs reach expressions through global functions and named modules, attributed registration such as [AlderModule] and [AlderFunction], registered assemblies or namespaces, and extension-method containers.
Modules resolve through IServiceProvider, so module-backed expressions obtain instance targets from the host container. Child engines inherit configuration with isolated local variable state.
var engine = new AlderEngine(options =>
{
options.Modules.Register<PricingModule>("pricing");
options.Functions.Register("hash", args => Sha256((string)args[0]!));
options.Types.AddNamespace("Acme.Domain");
options.Types.AddExtensionMethods<MoneyExtensions>();
});
See Configuration, Register types and extension methods, Expose functions and modules, and Choose variables and child engines.
Diagnostics and tracing
Parsing and binding failures report as AlderException. Validation, compilation, export, and runtime failures use the same exception type. Diagnostics carry codes (Roslyn CS#### where applicable, ALDR#### otherwise), human-readable messages, and source spans.
if (!engine.TryValidate(source, out var diagnostics))
{
foreach (var d in diagnostics)
log.Warn("{Code} at {Span}: {Message}", d.Code, d.Span, d.Message);
}
EvaluateWithTrace(...) returns a tree showing each evaluated node, its inputs, its output, and the execution path it took.
See Diagnostics and debugging.
Documentation
Full documentation lives in the GitHub repository, organized as concepts, guides, reference, and operations.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- System.Collections.Immutable (>= 8.0.0)
- System.Threading.Tasks.Extensions (>= 4.5.4)
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Documentation refresh: tightened README prose, updated code examples to use module registration, synced NUGET.md, added NuGet badge.