Integration Guide
How to register the framework, create an EvaluationContext, and call expression evaluation from a node executor.
Contents
DI Registration
Register the framework and its directive projects in your Program.cs or Startup.cs. Each project exposes a single extension method.
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddExpressionEvaluationCore(builder.Configuration) // Core.Services: orchestrator + parsers
.AddCannedExpressionDirective() // Catalog.Services: @alias
.AddContextDirective() // Context.Services: $ctx
.AddMemoryVariableDirective() // Memory.Services: $var
.AddNodeDataDirectives() // NodeData.Services: $input, $output
.AddFlowExecutionDirectives() // FlowExecution.Services: $exec, $flow
.AddJavaScriptDirective() // Scripting.Js.Services: $js
.AddCSharpDirective() // Scripting.Cs.Services: $cs
.AddLiquidTemplateDirective() // Template.Services: $tpl
.AddExternalApiDirective() // Api.Services: $api
.AddFunctionDirectives(); // Functions.Services: $math, $items
Selective Registration
Each directive is independently registered. If you don't need
$cs or $api, simply omit those calls. Unregistered directives fall back to the default directive or return EvaluationErrorCode.DirectiveNotFound.
Building an EvaluationContext
EvaluationContext is constructed once per node execution and reused for every field resolution within that execution.
var context = new EvaluationContext
{
TenantId = executionState.TenantId.ToString(),
AppId = executionState.AppId?.ToString(),
ExecutionId = executionState.ExecutionId,
ThreadId = executionState.ThreadId,
NodeKey = nodeDefinition.Key,
ActiveSyntax = WildcardSyntax.AtBrace, // default
IsolationLevel = ExpressionIsolationLevel.Safe, // most restrictive default
EnableExpressionTrace = isDevelopment,
Cache = scopedCache, // IExpressionCache from DI
Memory = executionState.Memory, // IExecutionMemory
CurrentUser = httpContext.User.ToUserInfo(), // IUserInfo
NodeExecutionData = nodeData, // INodeExecutionData
};
Calling EvaluateAsync
Inject IExpressionOrchestrator and call its single entry-point method:
// Inject via constructor
public class MyNodeExecutor(IExpressionOrchestrator orchestrator) { ... }
// Evaluate a single field
EvaluationResponse response = await orchestrator.EvaluateAsync(
fieldValue, // string — the raw field value from node config
context, // EvaluationContext — created once per node execution
cancellationToken
);
if (response.IsSuccess)
{
var resolvedValue = response.Value; // object? — typed value
var formatted = response.FormattedValue; // string? — formatted for output
}
else
{
logger.LogWarning("Expression failed: {Code} {Msg}",
response.ErrorCode, response.ErrorMessage);
}
From a Node Executor
Node executors extend BaseNodeExecutor, which provides a protected helper so subclasses never touch the orchestrator directly:
public class HttpRequestNodeExecutor : BaseNodeExecutor<HttpRequestSettings>
{
protected override async Task<NodeExecutionResult> ExecuteInternalAsync(
NodeExecutionContext ctx,
CancellationToken ct)
{
var settings = GetSettings(ctx);
// EvaluationContext is built lazily by BaseNodeExecutor on first call
var urlResponse = await EvaluateFieldAsync(settings.Url, ct);
var bodyResponse = await EvaluateFieldAsync(settings.Body, ct);
var url = urlResponse.FormattedValue ?? settings.Url;
var body = bodyResponse.FormattedValue ?? settings.Body;
// ... make the HTTP call
}
}
EvaluationContext is lazy
BaseNodeExecutor creates the EvaluationContext on the first call to EvaluateFieldAsync and reuses it. If no field contains an expression, no context is ever allocated.
Handling the Response
| Property | Type | Description |
|---|---|---|
IsSuccess | bool | true if expression resolved without error |
Value | object? | The raw resolved value (string, int, JsonElement, etc.) |
FormattedValue | string? | String representation after option formatting. Lazy — computed on first access. |
ErrorCode | EvaluationErrorCode? | Set when IsSuccess=false |
ErrorMessage | string? | Human-readable error detail |
CacheHit | bool | true when the response came from cache |
DurationMs | long | Directive evaluation time. 0 for cache hits. |
FieldConstraints | FieldConstraints | Required / NotEmpty / DefaultValue flags populated by options |
Factory methods on EvaluationResponse:
EvaluationResponse.Success(value, activeOption) // happy path
EvaluationResponse.Failure(errorCode, errorMessage) // error path
EvaluationResponse.FromCache(cachedResponse) // cache hit wrapper
EvaluationResponse.ShortCircuit(rawFieldValue) // no wildcard — pass through
Error Codes
| Code | Meaning |
|---|---|
DirectiveNotFound | No directive registered for the given name, and no default directive |
PathNotFound | The dot-path did not resolve to a value in the data source |
IsolationViolation | Directive requires higher isolation level than the context permits |
DepthLimitExceeded | Chain depth reached 10 — likely a cycle |
CycleDetected | Expression key already in VisitedKeys |
ScriptError | JavaScript or C# script threw an exception |
ValidationError | A Required or NotEmpty constraint was violated |
ExternalApiError | HTTP call in $api failed |
DomainNotAllowed | $api target domain not in allowlist |
TemplateRenderError | Liquid template failed to render |
CannedExpressionNotFound | No canned expression with the given alias and tenant/app |
ParseError | Malformed expression syntax |
Timeout | Directive exceeded configured time limit |
Unknown | Unexpected error — see ErrorMessage for details |
Configuration (appsettings.json)
{
"ExpressionEvaluation": {
"DefaultDirectiveName": "var",
"SensitivePathPatterns": [
"*.password", "*.token", "*.secret", "*.apiKey", "*.connectionString"
]
}
}
DefaultDirectiveName sets which directive handles expressions with no $directive prefix. SensitivePathPatterns controls path-based log masking.