fix: backport engine fixes from Serdica integration
1. Handle duplicate JSON property names in ToRuntimeValue — GroupBy before ToDictionary prevents crash on case-insensitive duplicates 2. Normalize decimal-valued integers in sub-workflow payloads — recursive NormalizePayloadNumbers converts 201000256548.0 to long 3. Add WorkflowExecutionActorContext — AsyncLocal propagation of actor identity through OnComplete execution chains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace StellaOps.Workflow.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class WorkflowExecutionActorContext
|
||||
{
|
||||
private static readonly AsyncLocal<string?> CurrentActorId = new();
|
||||
private static readonly AsyncLocal<string?> CurrentCallerUserId = new();
|
||||
|
||||
/// <summary>
|
||||
/// The actor ID from the task operation request (e.g., numeric user account ID).
|
||||
/// Used for role resolution and task assignment.
|
||||
/// </summary>
|
||||
public string? ActorId
|
||||
{
|
||||
get => CurrentActorId.Value;
|
||||
set => CurrentActorId.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The authenticated caller's user ID extracted from the HTTP request context.
|
||||
/// Used by transport implementations for backend service calls.
|
||||
/// </summary>
|
||||
public string? CallerUserId
|
||||
{
|
||||
get => CurrentCallerUserId.Value;
|
||||
set => CurrentCallerUserId.Value = value;
|
||||
}
|
||||
}
|
||||
@@ -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<TWorkflow>(
|
||||
@@ -190,7 +191,8 @@ public static class WorkflowRegistrationServiceCollectionExtensions
|
||||
return new Dictionary<string, object?>(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<string, object?> NormalizePayloadNumbers(IDictionary<string, object?> payload)
|
||||
{
|
||||
var result = new Dictionary<string, object?>(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<string, object?> dict => NormalizePayloadNumbers(dict),
|
||||
object[] arr => arr.Select(NormalizeValue).ToArray(),
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record WorkflowBusinessReferencePartProperty(PropertyInfo Property, string PartName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user