< Summary

Information
Class: Elsa.Bpmn.Hosting.BpmnScopeVariables
Assembly: Elsa.Bpmn
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Bpmn/Hosting/BpmnScopeVariables.cs
Line coverage
100%
Covered lines: 18
Uncovered lines: 0
Coverable lines: 18
Total lines: 103
Line coverage: 100%
Branch coverage
100%
Covered branches: 24
Total branches: 24
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
TryRead(...)100%44100%
Represent(...)100%22100%
TypeHintOf(...)100%1818100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Bpmn/Hosting/BpmnScopeVariables.cs

#LineLine coverage
 1using System.Text.Json;
 2using Bpmn.Model;
 3using Bpmn.Semantics;
 4using Elsa.Extensions;
 5using Elsa.Workflows;
 6
 7namespace Elsa.Bpmn.Hosting;
 8
 9/// <summary>
 10/// Reads a BPMN scope's container-scoped variables for the interpreter — the one callback the port makes.
 11/// </summary>
 12/// <remarks>
 13/// <para>
 14/// The read is three-valued, and the three answers are not interchangeable. <c>false</c> says there is no such
 15/// variable, and faults the element that asked. <see cref="BpmnValuePresence.Null"/> says the variable exists and
 16/// holds nothing, which a collection-mode multi-instance resolves as zero instances. <see
 17/// cref="BpmnValuePresence.StoredExternally"/> says the host has a value it cannot put on the wire, and faults
 18/// rather than reading as empty. Collapsing any of them into another is exactly the quiet wrong answer the port's
 19/// three-valued read exists to prevent.
 20/// </para>
 21/// <para>
 22/// This host reaches <see cref="BpmnValuePresence.StoredExternally"/> by one route: a value JSON cannot carry. It
 23/// deliberately does <b>not</b> report a driver-backed variable that way. <c>PersistentVariablesMiddleware</c> calls
 24/// <c>IVariablePersistenceManager.LoadVariablesAsync</c> before the pipeline runs and passes no <c>excludeTags</c>,
 25/// so a driver-backed value is already materialized into its memory block by the time any scope evaluates. Were a
 26/// host to exclude a tag, the skipped variable would be undetectable from here: <c>VariablePersistenceManager</c>
 27/// marks the block <c>IsInitialized</c> <i>before</i> testing the exclusion, so a variable whose driver was never
 28/// read is indistinguishable from one whose driver returned null, and nothing else records the exclusion. Guessing
 29/// between them would either fault materialized variables or quietly report unread ones as null; answering only
 30/// what the block actually says is the honest option available without changing <c>Elsa.Workflows.Core</c>.
 31/// </para>
 32/// <para>
 33/// Synchronous, which the port requires and Elsa can honour for the same reason: nothing here needs to await a
 34/// driver, because the middleware already did.
 35/// </para>
 36/// </remarks>
 14237internal sealed class BpmnScopeVariables(ActivityExecutionContext context) : IBpmnVariableReader
 38{
 39    /// <summary>A value this host holds but cannot represent inline.</summary>
 140    private static readonly BpmnValue Unrepresentable = new(BpmnValuePresence.StoredExternally, BpmnValueTypes.Any, null
 41
 42    // Resolved once per snapshot, not per TryRead: GetOptions() rebuilds the converter list on every call, and the
 43    // interpreter calls this port once per variable it needs.
 14844    private readonly Lazy<JsonSerializerOptions> _serializerOptions = new(() => context.GetRequiredService<IPayloadSeria
 45
 46    /// <inheritdoc />
 47    public bool TryRead(string name, out BpmnValue value)
 48    {
 849        var expressionExecutionContext = context.ExpressionExecutionContext;
 50
 51        // Resolution walks outward from this scope, which is both Elsa's variable scoping and BPMN's: an inner scope
 52        // sees the enclosing scope's data. A name nothing in scope declares is genuinely absent, and saying so is what
 53        // turns a mistyped collection variable into a fault naming the element instead of a loop that never runs.
 854        if (expressionExecutionContext.GetVariable(name) is not { } variable || !expressionExecutionContext.TryGetBlock(
 55        {
 156            value = BpmnValue.Absent;
 157            return false;
 58        }
 59
 760        value = Represent(block.Value);
 761        return true;
 62    }
 63
 64    /// <summary>
 65    /// The interpreter's view of a CLR value. Values cross the port as JSON with a host-interpreted type hint, so
 66    /// anything Elsa can hold that JSON cannot carry is reported as held outside the payload rather than as absent
 67    /// or null.
 68    /// </summary>
 69    private BpmnValue Represent(object? value)
 70    {
 771        if (value is null)
 172            return BpmnValue.Null;
 73
 74        try
 75        {
 76            // Serialized against the value's own runtime type, not against the declared `object`: the declared-type
 77            // overload is what routes through PolymorphicObjectConverterFactory, which wraps a boxed array or scalar
 78            // in Elsa's own type-tagged envelope — exactly the shape the interpreter, reading plain JSON on its own
 79            // wire format, does not expect. Serializing against the runtime type keeps a plain value plain while
 80            // still picking up every other converter Elsa registers (TypeJsonConverter, VariableConverterFactory,
 81            // enum-as-string, and so on), and still reaches PolymorphicObjectConverterFactory for a value whose
 82            // runtime type genuinely is one it claims (ExpandoObject, Dictionary<string, object>).
 683            return BpmnValue.From(JsonSerializer.SerializeToElement(value, value.GetType(), _serializerOptions.Value), T
 84        }
 185        catch (Exception exception) when (exception is JsonException or NotSupportedException)
 86        {
 87            // The variable is not missing and not null: this host has it, and cannot hand it over. A cyclic object
 88            // graph and a type with no converter both land here. Reporting it as null or absent would resolve a
 89            // collection-mode loop to zero instances and complete as though there had been nothing to do.
 190            return Unrepresentable;
 91        }
 692    }
 93
 94    /// <summary>
 95    /// The type hint that travels with the value. The interpreter understands two — <see cref="BpmnValueTypes.Integer"/
 96    /// which multi-instance cardinality and loop indices use, and <see cref="BpmnValueTypes.Any"/> — and assigns no
 97    /// meaning to any other.
 98    /// </summary>
 99    private static string TypeHintOf(object value) =>
 5100        value is byte or sbyte or short or ushort or int or uint or long or ulong
 5101            ? BpmnValueTypes.Integer
 5102            : BpmnValueTypes.Any;
 103}