< 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: 238
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
 519011public record ActivityConstructorContext(ActivityDescriptor ActivityDescriptor, Func<Type, IActivity> ActivityFactory)
 12{
 52013    public T CreateActivity<T>() where T : IActivity => (T)ActivityFactory(typeof(T));
 207514    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");
 53        if (runAsynchronously is null)
 54        {
 55            if (activityDescriptor.Attributes.OfType<TaskActivityAttribute>().FirstOrDefault() is { } taskActivityAttrib
 56            {
 57                runAsynchronously = taskActivityAttribute.RunAsynchronously;
 58            }
 59            else
 60            {
 61                runAsynchronously = false;
 62            }
 63        }
 64
 65        // 8) If composite, setup
 66        if (activity is IComposite composite)
 67            composite.Setup();
 68
 69        // 9) Your existing synthetic inputs/outputs routines, using the cleanedElement
 70        ReadSyntheticInputs(activityDescriptor, activity, cleanedElement, serializerOptions);
 71        ReadSyntheticOutputs(activityDescriptor, activity, cleanedElement);
 72
 73        // 10) Finally re‑apply those flags
 74        activity.SetCanStartWorkflow(canStartWorkflow);
 75        activity.SetRunAsynchronously(runAsynchronously);
 76
 77        return activity;
 78    }
 79
 80    /// <summary>
 81    /// Recursively remove any "_type" properties and unwrap any
 82    /// { "_type": "...", "items": [ ... ] } or "values": [ ... ] wrappers
 83    /// </summary>
 84    private static void StripTypeMetadata(JsonNode node)
 85    {
 86        switch (node)
 87        {
 88            case JsonObject obj:
 89                // First recurse into each child
 90                foreach (var key in obj.Select(kvp => kvp.Key).ToList())
 91                {
 92                    var child = obj[key];
 93                    if (child is JsonObject wrapper && wrapper.ContainsKey("_type"))
 94                    {
 95                        // If it has an "items" array, replace the whole property with that array
 96                        if (wrapper["items"] is JsonArray items)
 97                        {
 98                            obj[key] = items;
 99                            StripTypeMetadata(items);
 100                            continue;
 101                        }
 102
 103                        // Or a "values" array
 104                        if (wrapper["values"] is JsonArray values)
 105                        {
 106                            obj[key] = values;
 107                            StripTypeMetadata(values);
 108                            continue;
 109                        }
 110                    }
 111
 112                    // Otherwise just recurse normally
 113                    if (child != null)
 114                        StripTypeMetadata(child);
 115                }
 116
 117                // And remove any stray _type on this object
 118                obj.Remove("_type");
 119                break;
 120
 121            case JsonArray arr:
 122                for (var i = 0; i < arr.Count; i++)
 123                {
 124                    var element = arr[i];
 125                    if (element is JsonObject w && w.ContainsKey("_type"))
 126                    {
 127                        if (w["items"] is JsonArray items)
 128                        {
 129                            arr[i] = items;
 130                            StripTypeMetadata(items);
 131                            continue;
 132                        }
 133
 134                        if (w["values"] is JsonArray values)
 135                        {
 136                            arr[i] = values;
 137                            StripTypeMetadata(values);
 138                            continue;
 139                        }
 140                    }
 141
 142                    if (element != null)
 143                        StripTypeMetadata(element);
 144                }
 145
 146                break;
 147
 148            // primitives—nothing to do
 149            default:
 150                break;
 151        }
 152    }
 153
 154    private static void ReadSyntheticInputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activ
 155    {
 156        foreach (var inputDescriptor in activityDescriptor.Inputs.Where(x => x.IsSynthetic))
 157        {
 158            var inputName = inputDescriptor.Name;
 159            var propertyName = inputName.Camelize();
 160            var nakedType = inputDescriptor.Type;
 161            var wrappedType = typeof(Input<>).MakeGenericType(nakedType);
 162
 163            if (!activityRoot.TryGetProperty(propertyName, out var propertyElement) || propertyElement.ValueKind == Json
 164                continue;
 165
 166            var isWrapped = propertyElement.ValueKind == JsonValueKind.Object && propertyElement.GetProperty("typeName")
 167
 168            if (isWrapped)
 169            {
 170                var json = propertyElement.ToString();
 171                var inputValue = JsonSerializer.Deserialize(json, wrappedType, options);
 172
 173                activity.SyntheticProperties[inputName] = inputValue!;
 174            }
 175            else
 176            {
 177                activity.SyntheticProperties[inputName] = propertyElement.ConvertTo(inputDescriptor.Type)!;
 178            }
 179        }
 180    }
 181
 182    private static void ReadSyntheticOutputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement acti
 183    {
 184        foreach (var outputDescriptor in activityDescriptor.Outputs.Where(x => x.IsSynthetic))
 185        {
 186            var outputName = outputDescriptor.Name;
 187            var propertyName = outputName.Camelize();
 188            var nakedType = outputDescriptor.Type;
 189            var wrappedType = typeof(Output<>).MakeGenericType(nakedType);
 190
 191            if (!activityRoot.TryGetProperty(propertyName, out var propertyElement) || propertyElement.ValueKind == Json
 192                continue;
 193
 194            var memoryReferenceElement = propertyElement.GetProperty("memoryReference");
 195
 196            if (!memoryReferenceElement.TryGetProperty("id", out var memoryReferenceIdElement))
 197                continue;
 198
 199            var variable = new Variable
 200            {
 201                Id = memoryReferenceIdElement.GetString()!
 202            };
 203            variable.Name = variable.Id;
 204
 205            var output = Activator.CreateInstance(wrappedType, variable)!;
 206
 207            activity.SyntheticProperties[outputName] = output!;
 208        }
 209    }
 210
 211    private static bool GetBoolean(JsonElement element, string propertyName)
 212    {
 213        return GetNullableBoolean(element, propertyName) ?? false;
 214    }
 215
 216    private static bool? GetNullableBoolean(JsonElement element, string propertyName)
 217    {
 218        var propertyNames = new[]
 219        {
 220            propertyName.Camelize(), propertyName.Pascalize()
 221        };
 222
 223        foreach (var name in propertyNames)
 224        {
 225            if (element.TryGetProperty("customProperties", out var customPropertyElement))
 226            {
 227                if (customPropertyElement.TryGetProperty(name, out var canStartWorkflowElement))
 228                    return (bool?)canStartWorkflowElement.GetValue();
 229            }
 230
 231            if (element.TryGetProperty(propertyName.Camelize(), out var property)
 232                && (bool?)property.GetValue() is { } propValue)
 233                return propValue;
 234        }
 235
 236        return null;
 237    }
 238}