Portal Community

Contents

  1. DI Registration
  2. Building an EvaluationContext
  3. Calling EvaluateAsync
  4. From a Node Executor
  5. Handling the Response
  6. Error Codes
  7. Configuration

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

PropertyTypeDescription
IsSuccessbooltrue if expression resolved without error
Valueobject?The raw resolved value (string, int, JsonElement, etc.)
FormattedValuestring?String representation after option formatting. Lazy — computed on first access.
ErrorCodeEvaluationErrorCode?Set when IsSuccess=false
ErrorMessagestring?Human-readable error detail
CacheHitbooltrue when the response came from cache
DurationMslongDirective evaluation time. 0 for cache hits.
FieldConstraintsFieldConstraintsRequired / 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

CodeMeaning
DirectiveNotFoundNo directive registered for the given name, and no default directive
PathNotFoundThe dot-path did not resolve to a value in the data source
IsolationViolationDirective requires higher isolation level than the context permits
DepthLimitExceededChain depth reached 10 — likely a cycle
CycleDetectedExpression key already in VisitedKeys
ScriptErrorJavaScript or C# script threw an exception
ValidationErrorA Required or NotEmpty constraint was violated
ExternalApiErrorHTTP call in $api failed
DomainNotAllowed$api target domain not in allowlist
TemplateRenderErrorLiquid template failed to render
CannedExpressionNotFoundNo canned expression with the given alias and tenant/app
ParseErrorMalformed expression syntax
TimeoutDirective exceeded configured time limit
UnknownUnexpected 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.