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.TryGetInt64(out var int64Value) => int64Value,
|
||||||
JsonValueKind.Number when element.TryGetDecimal(out var decimalValue) => decimalValue,
|
JsonValueKind.Number when element.TryGetDecimal(out var decimalValue) => decimalValue,
|
||||||
JsonValueKind.Number when element.TryGetDouble(out var doubleValue) => doubleValue,
|
JsonValueKind.Number when element.TryGetDouble(out var doubleValue) => doubleValue,
|
||||||
JsonValueKind.Object => element.EnumerateObject().ToDictionary(
|
JsonValueKind.Object => element.EnumerateObject()
|
||||||
x => x.Name,
|
.GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
x => ToRuntimeValue(x.Value),
|
.ToDictionary(
|
||||||
StringComparer.OrdinalIgnoreCase),
|
g => g.Key,
|
||||||
|
g => ToRuntimeValue(g.Last().Value),
|
||||||
|
StringComparer.OrdinalIgnoreCase),
|
||||||
JsonValueKind.Array => element.EnumerateArray().Select(ToRuntimeValue).ToArray(),
|
JsonValueKind.Array => element.EnumerateArray().Select(ToRuntimeValue).ToArray(),
|
||||||
_ => element.ToString(),
|
_ => 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)
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
|
NumberHandling = global::System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static IServiceCollection AddWorkflowRegistration<TWorkflow>(
|
public static IServiceCollection AddWorkflowRegistration<TWorkflow>(
|
||||||
@@ -190,7 +191,8 @@ public static class WorkflowRegistrationServiceCollectionExtensions
|
|||||||
return new Dictionary<string, object?>(payload, StringComparer.OrdinalIgnoreCase);
|
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)
|
return JsonSerializer.Deserialize(json, startRequestType, SerializerOptions)
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
$"Unable to bind workflow payload to '{startRequestType.FullName}'.");
|
$"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);
|
private sealed record WorkflowBusinessReferencePartProperty(PropertyInfo Property, string PartName);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user