< Summary

Information
Class: Elsa.Workflows.Models.ActivityConstructorContext
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 227
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ActivityDescriptor()100%11100%
CreateActivity()100%11100%
CreateActivity(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs

#LineLine coverage
 1using System.Text.Json;
 2using System.Text.Json.Nodes;
 3using Elsa.Expressions.Helpers;
 4using Elsa.Extensions;
 5using Elsa.Workflows.Attributes;
 6using Elsa.Workflows.Memory;
 7using Humanizer;
 8
 9namespace Elsa.Workflows.Models;
 10
 716511public record ActivityConstructorContext(ActivityDescriptor ActivityDescriptor, Func<Type, IActivity> ActivityFactory)
 12{
 55013    public T CreateActivity<T>() where T : IActivity => (T)ActivityFactory(typeof(T));
 303014    public IActivity CreateActivity(Type type) => ActivityFactory(type);
 15}
 16
 17public static class JsonActivityConstructorContextHelper
 18{
 19    public static ActivityConstructorContext Create(ActivityDescriptor activityDescriptor, JsonElement element, JsonSeri
 20    {
 21        return new ActivityConstructorContext(activityDescriptor, type => CreateActivity(activityDescriptor, type, eleme
 22    }
 23
 24    public static T CreateActivity<T>(ActivityDescriptor activityDescriptor, JsonElement element, JsonSerializerOptions 
 25    {
 26        return (T)CreateActivity(activityDescriptor, typeof(T), element, serializerOptions);
 27    }
 28
 29    public static IActivity CreateActivity(ActivityDescriptor activityDescriptor, Type type, JsonElement element, JsonSe
 30    {
 31        // 1) Grab the raw text
 32        var raw = element.GetRawText();
 33
 34        // 2) Parse into a JsonNode so we can mutate
 35        var node = JsonNode.Parse(raw)!;
 36
 37        // 3) Strip out any "_type" metadata wrappers
 38        StripTypeMetadata(node);
 39
 40        // 4) Get a cleaned JSON string
 41        var cleanedJson = node.ToJsonString(new(JsonSerializerDefaults.Web));
 42
 43        // 5) Parse it back to a JsonDocument so we can get a clean JsonElement
 44        using var doc = JsonDocument.Parse(cleanedJson);
 45        var cleanedElement = doc.RootElement.Clone();
 46
 47        // 6) Deserialize into IActivity, or create a plain instance
 48        var activity = (IActivity)JsonSerializer.Deserialize(cleanedJson, type, serializerOptions)!;
 49
 50        // 7) Pull out your boolean flags from the cleaned element
 51        var canStartWorkflow = GetBoolean(cleanedElement, "canStartWorkflow");
 52        var runAsynchronously = GetNullableBoolean(cleanedElement, "runAsynchronously") ?? activityDescriptor.RunAsynchr
 53
 54        // 8) If composite, setup
 55        if (activity is IComposite composite)
 56            composite.Setup();
 57
 58        // 9) Existing synthetic inputs/outputs routines, using the cleanedElement
 59        ReadSyntheticInputs(activityDescriptor, activity, cleanedElement, serializerOptions);
 60        ReadSyntheticOutputs(activityDescriptor, activity, cleanedElement);
 61
 62        // 10) Finally re‑apply those flags
 63        activity.SetCanStartWorkflow(canStartWorkflow);
 64        activity.SetRunAsynchronously(runAsynchronously);
 65
 66        return activity;
 67    }
 68
 69    /// <summary>
 70    /// Recursively remove any "_type" properties and unwrap any
 71    /// { "_type": "...", "items": [ ... ] } or "values": [ ... ] wrappers
 72    /// </summary>
 73    private static void StripTypeMetadata(JsonNode node)
 74    {
 75        switch (node)
 76        {
 77            case JsonObject obj:
 78                // First recurse into each child
 79                foreach (var key in obj.Select(kvp => kvp.Key).ToList())
 80                {
 81                    var child = obj[key];
 82                    if (child is JsonObject wrapper && wrapper.ContainsKey("_type"))
 83                    {
 84                        // If it has an "items" array, replace the whole property with that array
 85                        if (wrapper["items"] is JsonArray items)
 86                        {
 87                            obj[key] = items;
 88                            StripTypeMetadata(items);
 89                            continue;
 90                        }
 91
 92                        // Or a "values" array
 93                        if (wrapper["values"] is JsonArray values)
 94                        {
 95                            obj[key] = values;
 96                            StripTypeMetadata(values);
 97                            continue;
 98                        }
 99                    }
 100
 101                    // Otherwise just recurse normally
 102                    if (child != null)
 103                        StripTypeMetadata(child);
 104                }
 105
 106                // And remove any stray _type on this object
 107                obj.Remove("_type");
 108                break;
 109
 110            case JsonArray arr:
 111                for (var i = 0; i < arr.Count; i++)
 112                {
 113                    var element = arr[i];
 114                    if (element is JsonObject w && w.ContainsKey("_type"))
 115                    {
 116                        if (w["items"] is JsonArray items)
 117                        {
 118                            arr[i] = items;
 119                            StripTypeMetadata(items);
 120                            continue;
 121                        }
 122
 123                        if (w["values"] is JsonArray values)
 124                        {
 125                            arr[i] = values;
 126                            StripTypeMetadata(values);
 127                            continue;
 128                        }
 129                    }
 130
 131                    if (element != null)
 132                        StripTypeMetadata(element);
 133                }
 134
 135                break;
 136
 137            // primitives—nothing to do
 138            default:
 139                break;
 140        }
 141    }
 142
 143    private static void ReadSyntheticInputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activ
 144    {
 145        foreach (var inputDescriptor in activityDescriptor.Inputs.Where(x => x.IsSynthetic))
 146        {
 147            var inputName = inputDescriptor.Name;
 148            var propertyName = inputName.Camelize();
 149            var nakedType = inputDescriptor.Type;
 150            var wrappedType = typeof(Input<>).MakeGenericType(nakedType);
 151
 152            if (!activityRoot.TryGetProperty(propertyName, out var propertyElement) || propertyElement.ValueKind == Json
 153                continue;
 154
 155            var isWrapped = propertyElement.ValueKind == JsonValueKind.Object && propertyElement.GetProperty("typeName")
 156
 157            if (isWrapped)
 158            {
 159                var json = propertyElement.ToString();
 160                var inputValue = JsonSerializer.Deserialize(json, wrappedType, options);
 161
 162                activity.SyntheticProperties[inputName] = inputValue!;
 163            }
 164            else
 165            {
 166                activity.SyntheticProperties[inputName] = propertyElement.ConvertTo(inputDescriptor.Type)!;
 167            }
 168        }
 169    }
 170
 171    private static void ReadSyntheticOutputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement acti
 172    {
 173        foreach (var outputDescriptor in activityDescriptor.Outputs.Where(x => x.IsSynthetic))
 174        {
 175            var outputName = outputDescriptor.Name;
 176            var propertyName = outputName.Camelize();
 177            var nakedType = outputDescriptor.Type;
 178            var wrappedType = typeof(Output<>).MakeGenericType(nakedType);
 179
 180            if (!activityRoot.TryGetProperty(propertyName, out var propertyElement) || propertyElement.ValueKind == Json
 181                continue;
 182
 183            var memoryReferenceElement = propertyElement.GetProperty("memoryReference");
 184
 185            if (!memoryReferenceElement.TryGetProperty("id", out var memoryReferenceIdElement))
 186                continue;
 187
 188            var variable = new Variable
 189            {
 190                Id = memoryReferenceIdElement.GetString()!
 191            };
 192            variable.Name = variable.Id;
 193
 194            var output = Activator.CreateInstance(wrappedType, variable)!;
 195
 196            activity.SyntheticProperties[outputName] = output!;
 197        }
 198    }
 199
 200    private static bool GetBoolean(JsonElement element, string propertyName)
 201    {
 202        return GetNullableBoolean(element, propertyName) ?? false;
 203    }
 204
 205    private static bool? GetNullableBoolean(JsonElement element, string propertyName)
 206    {
 207        var propertyNames = new[]
 208        {
 209            propertyName.Camelize(), propertyName.Pascalize()
 210        };
 211
 212        foreach (var name in propertyNames)
 213        {
 214            if (element.TryGetProperty("customProperties", out var customPropertyElement))
 215            {
 216                if (customPropertyElement.TryGetProperty(name, out var canStartWorkflowElement))
 217                    return (bool?)canStartWorkflowElement.GetValue();
 218            }
 219
 220            if (element.TryGetProperty(propertyName.Camelize(), out var property)
 221                && (bool?)property.GetValue() is { } propValue)
 222                return propValue;
 223        }
 224
 225        return null;
 226    }
 227}