diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs index fabb7c215..7e53dbb07 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs @@ -480,10 +480,12 @@ public static class WorkflowCanonicalExpressionRuntime JsonValueKind.Number when element.TryGetInt64(out var int64Value) => int64Value, JsonValueKind.Number when element.TryGetDecimal(out var decimalValue) => decimalValue, JsonValueKind.Number when element.TryGetDouble(out var doubleValue) => doubleValue, - JsonValueKind.Object => element.EnumerateObject().ToDictionary( - x => x.Name, - x => ToRuntimeValue(x.Value), - StringComparer.OrdinalIgnoreCase), + JsonValueKind.Object => element.EnumerateObject() + .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + g => g.Key, + g => ToRuntimeValue(g.Last().Value), + StringComparer.OrdinalIgnoreCase), JsonValueKind.Array => element.EnumerateArray().Select(ToRuntimeValue).ToArray(), _ => element.ToString(), }; diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowExecutionActorContext.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowExecutionActorContext.cs new file mode 100644 index 000000000..871ce857f --- /dev/null +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowExecutionActorContext.cs @@ -0,0 +1,35 @@ +using System.Threading; + +namespace StellaOps.Workflow.Abstractions; + +/// +/// Propagates the authenticated actor ID through async workflow execution chains +/// (e.g., OnComplete -> Call -> transport) where the original request context +/// is not available. Implementations set ActorId and CallerUserId during +/// task-complete/start-workflow processing. +/// +public sealed class WorkflowExecutionActorContext +{ + private static readonly AsyncLocal CurrentActorId = new(); + private static readonly AsyncLocal CurrentCallerUserId = new(); + + /// + /// The actor ID from the task operation request (e.g., numeric user account ID). + /// Used for role resolution and task assignment. + /// + public string? ActorId + { + get => CurrentActorId.Value; + set => CurrentActorId.Value = value; + } + + /// + /// The authenticated caller's user ID extracted from the HTTP request context. + /// Used by transport implementations for backend service calls. + /// + public string? CallerUserId + { + get => CurrentCallerUserId.Value; + set => CurrentCallerUserId.Value = value; + } +} diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs index 770d41846..77486a4f1 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs @@ -78,6 +78,7 @@ public static class WorkflowRegistrationServiceCollectionExtensions private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, + NumberHandling = global::System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString, }; public static IServiceCollection AddWorkflowRegistration( @@ -190,7 +191,8 @@ public static class WorkflowRegistrationServiceCollectionExtensions return new Dictionary(payload, StringComparer.OrdinalIgnoreCase); } - var json = JsonSerializer.Serialize(payload, SerializerOptions); + var normalized = NormalizePayloadNumbers(payload); + var json = JsonSerializer.Serialize(normalized, SerializerOptions); return JsonSerializer.Deserialize(json, startRequestType, SerializerOptions) ?? throw new InvalidOperationException( $"Unable to bind workflow payload to '{startRequestType.FullName}'."); @@ -291,5 +293,27 @@ public static class WorkflowRegistrationServiceCollectionExtensions }; } + private static IDictionary NormalizePayloadNumbers(IDictionary payload) + { + var result = new Dictionary(payload.Count, StringComparer.OrdinalIgnoreCase); + foreach (var kvp in payload) + { + result[kvp.Key] = NormalizeValue(kvp.Value); + } + return result; + } + + private static object? NormalizeValue(object? value) + { + return value switch + { + decimal d when d == Math.Truncate(d) && d >= long.MinValue && d <= long.MaxValue => (long)d, + double d when d == Math.Truncate(d) && d >= long.MinValue && d <= long.MaxValue => (long)d, + IDictionary dict => NormalizePayloadNumbers(dict), + object[] arr => arr.Select(NormalizeValue).ToArray(), + _ => value, + }; + } + private sealed record WorkflowBusinessReferencePartProperty(PropertyInfo Property, string PartName); }