< Summary

Information
Class: Elsa.Workflows.Management.Services.WorkflowReferenceUpdater
Assembly: Elsa.Workflows.Management
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Management/Services/WorkflowReferenceUpdater.cs
Line coverage
97%
Covered lines: 109
Uncovered lines: 3
Coverable lines: 112
Total lines: 204
Line coverage: 97.3%
Branch coverage
87%
Covered branches: 49
Total branches: 56
Branch coverage: 87.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
UpdateWorkflowReferencesAsync()96.66%303098.11%
UpdateWorkflowAsync()70%101094.73%
GetOrCreateDraftAsync()50%4491.66%
FindActivities()100%1010100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Management/Services/WorkflowReferenceUpdater.cs

#LineLine coverage
 1using Elsa.Common.Models;
 2using Elsa.Extensions;
 3using Elsa.Workflows.Activities;
 4using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
 5using Elsa.Workflows.Management.Entities;
 6using Elsa.Workflows.Management.Models;
 7using Elsa.Workflows.Models;
 8
 9namespace Elsa.Workflows.Management.Services;
 10
 11internal record UpdatedWorkflowDefinition(WorkflowDefinition Definition, WorkflowGraph NewGraph);
 12
 40513public class WorkflowReferenceUpdater(
 40514    IWorkflowDefinitionPublisher publisher,
 40515    IWorkflowDefinitionService workflowDefinitionService,
 40516    IWorkflowDefinitionStore workflowDefinitionStore,
 40517    IWorkflowReferenceGraphBuilder workflowReferenceGraphBuilder,
 40518    WorkflowDefinitionActivityDescriptorFactory workflowDefinitionActivityDescriptorFactory,
 40519    IActivityRegistry activityRegistry,
 40520    IApiSerializer serializer)
 21    : IWorkflowReferenceUpdater
 22{
 23    private bool _isUpdating;
 24
 25    public async Task<UpdateWorkflowReferencesResult> UpdateWorkflowReferencesAsync(
 26        WorkflowDefinition referencedDefinition,
 27        CancellationToken cancellationToken = default)
 28    {
 529        if (_isUpdating ||
 530            referencedDefinition.Options is not { UsableAsActivity: true, AutoUpdateConsumingWorkflows: true })
 331            return new([]);
 32
 233        var referenceGraph = await workflowReferenceGraphBuilder.BuildGraphAsync(referencedDefinition.DefinitionId, canc
 34
 35        // Get all consumer (source) and referenced (target) IDs from the edges
 336        var referencingIds = referenceGraph.Edges.Select(e => e.Source).Distinct().ToList();
 337        var referencedIds = referenceGraph.Edges.Select(e => e.Target).Distinct().ToList();
 38
 239        if (referencingIds.Count == 0)
 140            return new([]);
 41
 142        var referencingWorkflowGraphs = (await workflowDefinitionService.FindWorkflowGraphsAsync(new()
 143            {
 144                DefinitionIds = referencingIds,
 145                VersionOptions = VersionOptions.Latest,
 146                IsReadonly = false
 147            }, cancellationToken))
 248            .ToDictionary(g => g.Workflow.Identity.DefinitionId);
 49
 150        var referencedWorkflowDefinitionList = (await workflowDefinitionStore.FindManyAsync(new()
 151        {
 152            DefinitionIds = referencedIds,
 153            VersionOptions = VersionOptions.Published,
 154            IsReadonly = false
 155        }, cancellationToken)).ToList();
 56
 157        var referencedWorkflowDefinitionsPublished = referencedWorkflowDefinitionList
 158            .GroupBy(x => x.DefinitionId)
 159            .Select(group =>
 160            {
 261                var publishedVersion = group.FirstOrDefault(x => x.IsPublished);
 162                return publishedVersion ?? group.First();
 163            })
 264            .ToDictionary(d => d.DefinitionId);
 65
 166        var initialPublicationState = new Dictionary<string, bool>();
 67
 468        foreach (var workflowGraph in referencingWorkflowGraphs)
 169            initialPublicationState[workflowGraph.Key] = workflowGraph.Value.Workflow.Publication.IsPublished;
 70
 71        // Add the initially referenced definition
 172        referencedWorkflowDefinitionsPublished[referencedDefinition.DefinitionId] = referencedDefinition;
 73
 74        // Use the OutboundEdges lookup from the graph (Source → what it depends on)
 175        var dependencyMap = referenceGraph.OutboundEdges;
 76
 77        // Perform topological sort to ensure dependent workflows are processed in the right order
 178        var sortedWorkflowIds = referencingIds
 279            .TSort(id => dependencyMap[id], true)
 180            // Only process workflows that exist in our referencing workflows dictionary
 281            .Where(id => referencingWorkflowGraphs.ContainsKey(id))
 182            .ToList();
 83
 184        var updatedWorkflows = new Dictionary<string, UpdatedWorkflowDefinition>();
 85
 86        // Create a cache for drafts that we've already created during this operation
 187        var draftCache = new Dictionary<string, WorkflowDefinition>();
 88
 489        foreach (var id in sortedWorkflowIds)
 90        {
 191            if (!referencingWorkflowGraphs.TryGetValue(id, out var graph) || !dependencyMap[id].Any())
 92                continue;
 93
 494            foreach (var refId in dependencyMap[id])
 95            {
 196                var target = referencedWorkflowDefinitionsPublished.GetValueOrDefault(refId);
 197                if (target == null) continue;
 98
 199                var updated = await UpdateWorkflowAsync(graph, target, draftCache, initialPublicationState, cancellation
 1100                if (updated == null) continue;
 101
 1102                graph = updated.NewGraph;
 1103                updatedWorkflows[updated.Definition.DefinitionId] = updated;
 1104                referencedWorkflowDefinitionsPublished[id] = updated.Definition;
 1105                draftCache[id] = updated.Definition;
 1106                referencingWorkflowGraphs[id] = updated.NewGraph;
 107            }
 1108        }
 109
 1110        _isUpdating = true;
 4111        foreach (var updatedWorkflow in updatedWorkflows.Values)
 112        {
 1113            var requiresPublication = initialPublicationState.GetValueOrDefault(updatedWorkflow.Definition.DefinitionId)
 1114            if (requiresPublication)
 1115                await publisher.PublishAsync(updatedWorkflow.Definition, cancellationToken);
 116            else
 0117                await publisher.SaveDraftAsync(updatedWorkflow.Definition, cancellationToken);
 118        }
 119
 1120        _isUpdating = false;
 121
 2122        return new(updatedWorkflows.Select(u => u.Value.Definition));
 5123    }
 124
 125
 126    private async Task<UpdatedWorkflowDefinition?> UpdateWorkflowAsync(
 127        WorkflowGraph graph,
 128        WorkflowDefinition target,
 129        Dictionary<string, WorkflowDefinition> draftCache,
 130        Dictionary<string, bool> initialPublicationState,
 131        CancellationToken cancellationToken)
 132    {
 1133        var willTargetBePublished = initialPublicationState.GetValueOrDefault(target.DefinitionId, target.IsPublished);
 1134        if (!willTargetBePublished)
 0135            return null;
 136
 1137        var id = graph.Workflow.Identity.DefinitionId;
 1138        var draft = await GetOrCreateDraftAsync(id, draftCache, cancellationToken);
 1139        if (draft == null) return null;
 140
 1141        var newGraph = await workflowDefinitionService.MaterializeWorkflowAsync(draft, cancellationToken);
 1142        var outdated = FindActivities(newGraph.Root, target.DefinitionId)
 1143            .Where(a => a.WorkflowDefinitionVersionId != target.Id)
 1144            .ToList();
 145
 1146        if (!outdated.Any()) return null;
 147
 4148        foreach (var act in outdated)
 149        {
 1150            act.WorkflowDefinitionVersionId = target.Id;
 1151            act.Version = target.Version;
 1152            act.LatestAvailablePublishedVersionId = target.Id;
 1153            act.LatestAvailablePublishedVersion = target.Version;
 154        }
 155
 1156        if (newGraph.Root.Activity is Workflow wf)
 1157            draft.StringData = serializer.Serialize(wf.Root);
 158
 1159        return new(draft, newGraph);
 1160    }
 161
 162    private async Task<WorkflowDefinition?> GetOrCreateDraftAsync(
 163        string definitionId,
 164        Dictionary<string, WorkflowDefinition> draftCache,
 165        CancellationToken cancellationToken)
 166    {
 167        // Check if we already have a draft for this workflow
 1168        if (draftCache.TryGetValue(definitionId, out var cachedDraft))
 0169            return cachedDraft;
 170
 171        // Create or get a draft for this workflow
 1172        var draft = await publisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken);
 1173        if (draft == null) return null;
 174
 175        // Store the draft in the cache for potential future use
 1176        draftCache[definitionId] = draft;
 177
 178        // Get the current published version of the workflow definition.
 1179        var publishedVersion = await workflowDefinitionStore.FindAsync(
 1180            WorkflowDefinitionHandle.ByDefinitionId(definitionId, VersionOptions.Published).ToFilter(),
 1181            cancellationToken);
 182
 183        // Update the activity registry to be able to materialize the workflow.
 1184        var activityDescriptor = workflowDefinitionActivityDescriptorFactory.CreateDescriptor(draft, publishedVersion);
 1185        activityRegistry.Add(typeof(WorkflowDefinitionActivityProvider), activityDescriptor);
 186
 1187        return draft;
 1188    }
 189
 190    private static IEnumerable<WorkflowDefinitionActivity> FindActivities(ActivityNode node, string definitionId)
 191    {
 192        // Do not drill into activities that are WorkflowDefinitionActivity
 5193        if (node.Activity is WorkflowDefinitionActivity)
 1194            yield break;
 195
 16196        foreach (var child in node.Children)
 197        {
 4198            if (child.Activity is WorkflowDefinitionActivity activity && activity.WorkflowDefinitionId == definitionId)
 1199                yield return activity;
 10200            foreach (var grandChildActivity in FindActivities(child, definitionId))
 1201                yield return grandChildActivity;
 4202        }
 4203    }
 204}