Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs
T
Joseph Doherty eabf270d71 docs: complete XML doc coverage (returns, summaries, inheritdoc)
Resolve all 622 issues flagged by the enhanced CommentChecker: add missing
<returns> tags (incl. the standard phrasing on non-generic Task methods),
add missing <summary> tags, and replace misused/redundant <inheritdoc/> on
members that override or implement nothing with real documentation.
Documentation-only — no behavior change; solution builds clean.
2026-06-03 11:39:32 -04:00

187 lines
7.9 KiB
C#

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
/// <summary>
/// Scope-aware view onto the instance's attributes, anchored at a path prefix.
/// <c>Attributes["X"]</c> on the root scope resolves to canonical name "X";
/// on a composition with prefix "TempSensor" it resolves to "TempSensor.X".
///
/// <para>
/// Thread-model note (SiteRuntime-012): the indexer get/set block synchronously
/// on the Instance Actor Ask (and, for data-connected attributes, the DCL
/// round-trip). This is safe because script bodies execute on the dedicated
/// <see cref="ScriptExecutionScheduler"/> threads (SiteRuntime-009), not the
/// shared <see cref="System.Threading.ThreadPool"/> — so a blocked accessor
/// cannot starve unrelated Akka dispatchers or HTTP request handling. The async
/// variants (<see cref="GetAsync"/>/<see cref="SetAsync"/>) are still preferred
/// where the script can await, as they avoid holding a dedicated thread idle for
/// the duration of each round-trip.
/// </para>
/// </summary>
public class AttributeAccessor
{
private readonly ScriptRuntimeContext _ctx;
/// <summary>Canonical-name prefix, e.g. "" for root or "TempSensor" for a composition.</summary>
public string ScopePrefix { get; }
/// <summary>
/// Initializes a new instance of the AttributeAccessor with the specified context and prefix.
/// </summary>
/// <param name="ctx">The script runtime context.</param>
/// <param name="prefix">The canonical-name prefix for attribute resolution.</param>
public AttributeAccessor(ScriptRuntimeContext ctx, string prefix)
{
_ctx = ctx;
ScopePrefix = prefix;
}
/// <summary>
/// Resolves a key to its full canonical name by applying the scope prefix.
/// </summary>
/// <param name="key">The attribute key to resolve.</param>
/// <returns>The fully qualified canonical name (e.g. "TempSensor.X" or "X" for the root scope).</returns>
public string Resolve(string key) =>
ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;
/// <summary>
/// Gets or sets an attribute value synchronously by canonical name.
/// </summary>
/// <param name="key">The attribute key.</param>
public object? this[string key]
{
// Both reads and writes block on the actor Ask; the write also blocks
// on the DCL round-trip for data-connected attributes. The async
// variants (GetAsync/SetAsync) are preferred where awaiting is possible.
get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty).GetAwaiter().GetResult();
}
/// <summary>
/// Gets an attribute value asynchronously.
/// </summary>
/// <param name="key">The attribute key.</param>
/// <returns>A task that resolves to the attribute value, or null if not set.</returns>
public Task<object?> GetAsync(string key) => _ctx.GetAttribute(Resolve(key));
/// <summary>
/// Sets an attribute value asynchronously.
/// </summary>
/// <param name="key">The attribute key.</param>
/// <param name="value">The value to set.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task SetAsync(string key, object? value)
=> _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
}
/// <summary>
/// A view of one composition at a path. Exposes its attributes via
/// <see cref="AttributeAccessor"/> and an invokable <c>CallScript</c>.
/// </summary>
public class CompositionAccessor
{
private readonly ScriptRuntimeContext _ctx;
/// <summary>Canonical-name path this composition is rooted at.</summary>
public string Path { get; }
/// <summary>
/// Accessor for attributes within this composition.
/// </summary>
public AttributeAccessor Attributes { get; }
/// <summary>
/// Initializes a new instance of the CompositionAccessor with the specified context and path.
/// </summary>
/// <param name="ctx">The script runtime context.</param>
/// <param name="path">The canonical-name path for the composition.</param>
public CompositionAccessor(ScriptRuntimeContext ctx, string path)
{
_ctx = ctx;
Path = path;
Attributes = new AttributeAccessor(ctx, path);
}
/// <summary>
/// Resolves a script name to its full canonical name by applying the composition path.
/// </summary>
/// <param name="scriptName">The script name to resolve.</param>
/// <returns>The fully qualified canonical script name (e.g. "Module.MyScript" or "MyScript" at root).</returns>
public string ResolveScript(string scriptName) =>
Path.Length == 0 ? scriptName : Path + "." + scriptName;
/// <summary>
/// Calls a script within this composition.
/// </summary>
/// <param name="scriptName">The name of the script to call.</param>
/// <param name="parameters">Optional parameters to pass to the script.</param>
/// <returns>A task that resolves to the script's return value, or null if none.</returns>
public Task<object?> CallScript(string scriptName, object? parameters = null)
=> _ctx.CallScript(ResolveScript(scriptName), parameters);
}
/// <summary>
/// Dictionary-style accessor for the script's child compositions. Indexing
/// returns a <see cref="CompositionAccessor"/> rooted at the child's path.
/// </summary>
public class ChildrenAccessor
{
private readonly ScriptRuntimeContext _ctx;
private readonly string _selfPath;
/// <summary>
/// Initializes a new instance of the ChildrenAccessor with the specified context and path.
/// </summary>
/// <param name="ctx">The script runtime context.</param>
/// <param name="selfPath">The canonical-name path of the parent composition.</param>
public ChildrenAccessor(ScriptRuntimeContext ctx, string selfPath)
{
_ctx = ctx;
_selfPath = selfPath;
}
/// <summary>
/// Gets a composition accessor for a child by name.
/// </summary>
/// <param name="compositionName">The name of the child composition.</param>
public CompositionAccessor this[string compositionName]
{
get
{
var path = _selfPath.Length == 0
? compositionName
: _selfPath + "." + compositionName;
return new CompositionAccessor(_ctx, path);
}
}
}
internal static class ScopeAccessorFactory
{
/// <summary>
/// Creates an AttributeAccessor for the specified context and path.
/// </summary>
/// <param name="ctx">The script runtime context.</param>
/// <param name="selfPath">The canonical-name path.</param>
/// <returns>A new <see cref="AttributeAccessor"/> rooted at <paramref name="selfPath"/>.</returns>
public static AttributeAccessor AttributesFor(ScriptRuntimeContext ctx, string selfPath)
=> new(ctx, selfPath);
/// <summary>
/// Creates a ChildrenAccessor for the specified context and path.
/// </summary>
/// <param name="ctx">The script runtime context.</param>
/// <param name="selfPath">The canonical-name path.</param>
/// <returns>A new <see cref="ChildrenAccessor"/> rooted at <paramref name="selfPath"/>.</returns>
public static ChildrenAccessor ChildrenFor(ScriptRuntimeContext ctx, string selfPath)
=> new(ctx, selfPath);
/// <summary>
/// Creates a CompositionAccessor for the specified context and path, or null if no parent exists.
/// </summary>
/// <param name="ctx">The script runtime context.</param>
/// <param name="parentPath">The parent path, or null if no parent.</param>
/// <returns>A <see cref="CompositionAccessor"/> for the parent, or null when <paramref name="parentPath"/> is null.</returns>
public static CompositionAccessor? ParentFor(ScriptRuntimeContext ctx, string? parentPath)
=> parentPath == null ? null : new CompositionAccessor(ctx, parentPath);
}