feat(triggers): runtime expression trigger evaluation for scripts and alarms
This commit is contained in:
@@ -87,11 +87,45 @@ public class ScriptCompilationService
|
||||
return violations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared Roslyn scripting options (references + imports) used by both full
|
||||
/// script compilation and trigger-expression compilation.
|
||||
/// </summary>
|
||||
private static ScriptOptions BuildScriptOptions() => ScriptOptions.Default
|
||||
.WithReferences(
|
||||
typeof(object).Assembly,
|
||||
typeof(Enumerable).Assembly,
|
||||
typeof(Math).Assembly,
|
||||
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly,
|
||||
typeof(Commons.Types.DynamicJsonElement).Assembly)
|
||||
.WithImports(
|
||||
"System",
|
||||
"System.Collections.Generic",
|
||||
"System.Linq",
|
||||
"System.Threading.Tasks");
|
||||
|
||||
/// <summary>
|
||||
/// Compiles a script into a reusable delegate that takes a ScriptRuntimeContext
|
||||
/// and parameters dictionary, and returns an object? result.
|
||||
/// </summary>
|
||||
public ScriptCompilationResult Compile(string scriptName, string code)
|
||||
=> CompileCore(scriptName, code, typeof(ScriptGlobals));
|
||||
|
||||
/// <summary>
|
||||
/// Compiles a bare C# boolean trigger expression against the restricted
|
||||
/// read-only <see cref="TriggerExpressionGlobals"/>. The expression is a
|
||||
/// trailing expression (no <c>return</c>); Roslyn scripting yields its
|
||||
/// value, which the caller coerces to <c>bool</c>. Reuses the same script
|
||||
/// options and forbidden-API trust validation as <see cref="Compile"/>.
|
||||
/// </summary>
|
||||
public ScriptCompilationResult CompileTriggerExpression(string name, string expression)
|
||||
=> CompileCore(name, expression, typeof(TriggerExpressionGlobals));
|
||||
|
||||
/// <summary>
|
||||
/// Shared compilation path: validates the trust model, builds the script
|
||||
/// against the given globals type, and returns the compiled result.
|
||||
/// </summary>
|
||||
private ScriptCompilationResult CompileCore(string name, string code, Type globalsType)
|
||||
{
|
||||
// Validate trust model
|
||||
var violations = ValidateTrustModel(code);
|
||||
@@ -99,29 +133,16 @@ public class ScriptCompilationService
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Script {Script} failed trust validation: {Violations}",
|
||||
scriptName, string.Join("; ", violations));
|
||||
name, string.Join("; ", violations));
|
||||
return ScriptCompilationResult.Failed(violations);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var scriptOptions = ScriptOptions.Default
|
||||
.WithReferences(
|
||||
typeof(object).Assembly,
|
||||
typeof(Enumerable).Assembly,
|
||||
typeof(Math).Assembly,
|
||||
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly,
|
||||
typeof(Commons.Types.DynamicJsonElement).Assembly)
|
||||
.WithImports(
|
||||
"System",
|
||||
"System.Collections.Generic",
|
||||
"System.Linq",
|
||||
"System.Threading.Tasks");
|
||||
|
||||
var script = CSharpScript.Create<object?>(
|
||||
code,
|
||||
scriptOptions,
|
||||
globalsType: typeof(ScriptGlobals));
|
||||
BuildScriptOptions(),
|
||||
globalsType: globalsType);
|
||||
|
||||
var diagnostics = script.Compile();
|
||||
var errors = diagnostics
|
||||
@@ -133,16 +154,16 @@ public class ScriptCompilationService
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Script {Script} compilation failed: {Errors}",
|
||||
scriptName, string.Join("; ", errors));
|
||||
name, string.Join("; ", errors));
|
||||
return ScriptCompilationResult.Failed(errors);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Script {Script} compiled successfully", scriptName);
|
||||
_logger.LogDebug("Script {Script} compiled successfully", name);
|
||||
return ScriptCompilationResult.Succeeded(script);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error compiling script {Script}", scriptName);
|
||||
_logger.LogError(ex, "Unexpected error compiling script {Script}", name);
|
||||
return ScriptCompilationResult.Failed([$"Compilation exception: {ex.Message}"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace ScadaLink.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only globals a trigger expression is compiled against. Exposes only
|
||||
/// attribute reads, backed by an in-memory snapshot — no I/O, no actor Ask,
|
||||
/// no side-effecting APIs. A missing attribute key reads as <c>null</c> and
|
||||
/// never throws.
|
||||
///
|
||||
/// Canonical attribute keys are dotted (e.g. "TempSensor.Reading"); the prefix
|
||||
/// logic here mirrors <see cref="AttributeAccessor.Resolve"/>.
|
||||
/// </summary>
|
||||
public sealed class TriggerExpressionGlobals
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, object?> _snapshot;
|
||||
|
||||
public TriggerExpressionGlobals(IReadOnlyDictionary<string, object?> snapshot)
|
||||
=> _snapshot = snapshot;
|
||||
|
||||
/// <summary>Attributes in the expression's own scope (root prefix).</summary>
|
||||
public ReadOnlyAttributes Attributes => new(_snapshot, "");
|
||||
|
||||
/// <summary>Indexed access to child compositions' attributes.</summary>
|
||||
public ReadOnlyChildren Children => new(_snapshot);
|
||||
|
||||
/// <summary>
|
||||
/// Parent composition (null at root). Set by the caller for derived/composed
|
||||
/// scopes; the runtime actors evaluate at root scope, so this stays null.
|
||||
/// </summary>
|
||||
public ReadOnlyComposition? Parent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Read-only attribute view anchored at a canonical-name prefix. Indexing
|
||||
/// resolves to the canonical key ("" → key, "TempSensor" → "TempSensor.key").
|
||||
/// </summary>
|
||||
public sealed class ReadOnlyAttributes
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, object?> _s;
|
||||
private readonly string _prefix;
|
||||
|
||||
public ReadOnlyAttributes(IReadOnlyDictionary<string, object?> s, string prefix)
|
||||
{
|
||||
_s = s;
|
||||
_prefix = prefix;
|
||||
}
|
||||
|
||||
public object? this[string key] =>
|
||||
_s.TryGetValue(_prefix.Length == 0 ? key : _prefix + "." + key, out var v) ? v : null;
|
||||
}
|
||||
|
||||
/// <summary>A read-only view of one composition at a canonical-name path.</summary>
|
||||
public sealed class ReadOnlyComposition
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, object?> _s;
|
||||
private readonly string _path;
|
||||
|
||||
public ReadOnlyComposition(IReadOnlyDictionary<string, object?> s, string path)
|
||||
{
|
||||
_s = s;
|
||||
_path = path;
|
||||
}
|
||||
|
||||
public ReadOnlyAttributes Attributes => new(_s, _path);
|
||||
}
|
||||
|
||||
/// <summary>Dictionary-style accessor for child compositions.</summary>
|
||||
public sealed class ReadOnlyChildren
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, object?> _s;
|
||||
|
||||
public ReadOnlyChildren(IReadOnlyDictionary<string, object?> s) => _s = s;
|
||||
|
||||
public ReadOnlyComposition this[string compositionName] => new(_s, compositionName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user