< Summary

Information
Class: Elsa.Workflows.Runtime.LocalWorkflowClient
Assembly: Elsa.Workflows.Runtime
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs
Line coverage
89%
Covered lines: 146
Uncovered lines: 17
Coverable lines: 163
Total lines: 294
Line coverage: 89.5%
Branch coverage
75%
Covered branches: 21
Total branches: 28
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%210%
get_WorkflowInstanceId()100%11100%
CreateInstanceAsync()100%4493.75%
RunInstanceAsync()100%11100%
CreateAndRunInstanceAsync()100%44100%
LogActivationDenial(...)100%22100%
CancelAsync()100%11100%
CancelAsync()100%22100%
ExportStateAsync()100%11100%
ImportStateAsync()100%11100%
InstanceExistsAsync(...)100%210%
DeleteAsync()100%22100%
RunInstanceAsync()50%4480.55%
CreateInstanceInternalAsync()100%210%
CreateWorkflowInstance(...)100%11100%
GetWorkflowInstanceAsync()50%22100%
TryGetWorkflowInstanceAsync(...)100%11100%
GetWorkflowGraphAsync()100%11100%
GetWorkflowGraphAsync()100%44100%
EvaluateAsync(...)0%2040%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs

#LineLine coverage
 1using Elsa.Workflows.Management;
 2using Elsa.Workflows.Management.Entities;
 3using Elsa.Workflows.Management.Exceptions;
 4using Elsa.Workflows.Management.Filters;
 5using Elsa.Workflows.Management.Mappers;
 6using Elsa.Workflows.Management.Options;
 7using Elsa.Workflows.Activities;
 8using Elsa.Workflows.ActivationValidators;
 9using Elsa.Workflows.Models;
 10using Elsa.Workflows.Options;
 11using Elsa.Workflows.Runtime.Exceptions;
 12using Elsa.Workflows.Runtime.Messages;
 13using Elsa.Workflows.State;
 14using Microsoft.Extensions.Logging;
 15
 16namespace Elsa.Workflows.Runtime;
 17
 18/// <summary>
 19/// Represents a client for executing and managing local workflows.
 20/// </summary>
 24221public class LocalWorkflowClient(
 24222    string workflowInstanceId,
 24223    IWorkflowInstanceManager workflowInstanceManager,
 24224    IWorkflowDefinitionService workflowDefinitionService,
 24225    IWorkflowRunner workflowRunner,
 24226    IWorkflowCanceler workflowCanceler,
 24227    IWorkflowActivationGate workflowActivationGate,
 24228    WorkflowStateMapper workflowStateMapper,
 24229    ILogger<LocalWorkflowClient> logger) : IWorkflowClient
 30{
 31    /// <summary>
 32    /// Retained for source compatibility. Hosts should prefer the constructor resolved by DI,
 33    /// which supplies the configured activation gate.
 34    /// </summary>
 35    public LocalWorkflowClient(
 36        string workflowInstanceId,
 37        IWorkflowInstanceManager workflowInstanceManager,
 38        IWorkflowDefinitionService workflowDefinitionService,
 39        IWorkflowRunner workflowRunner,
 40        IWorkflowCanceler workflowCanceler,
 41        WorkflowStateMapper workflowStateMapper,
 42        ILogger<LocalWorkflowClient> logger)
 043        : this(workflowInstanceId, workflowInstanceManager, workflowDefinitionService, workflowRunner, workflowCanceler,
 44    {
 045    }
 46
 47    /// <inheritdoc />
 55848    public string WorkflowInstanceId => workflowInstanceId;
 49
 50    /// <inheritdoc />
 51    public async Task<CreateWorkflowInstanceResponse> CreateInstanceAsync(CreateWorkflowInstanceRequest request, Cancell
 52    {
 4453        var workflowDefinitionHandle = request.WorkflowDefinitionHandle;
 4454        var workflowGraph = await GetWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken);
 55
 4256        await using var lease = await workflowActivationGate.EvaluateAsync(workflowGraph.Workflow, request.CorrelationId
 4257        if (!lease.CanStart)
 58        {
 159            LogActivationDenial(workflowGraph.Workflow, request.CorrelationId);
 160            return new()
 161            {
 162                CannotStart = true
 163            };
 64        }
 65
 4166        var effectiveCancellationToken = lease.GetEffectiveCancellationToken(cancellationToken);
 4167        effectiveCancellationToken.ThrowIfCancellationRequested();
 4068        var workflowInstance = CreateWorkflowInstance(workflowGraph.Workflow, request);
 4069        effectiveCancellationToken.ThrowIfCancellationRequested();
 4070        await workflowInstanceManager.SaveAsync(workflowInstance, effectiveCancellationToken);
 3871        return new();
 3972    }
 73
 74    /// <inheritdoc />
 75    public async Task<RunWorkflowInstanceResponse> RunInstanceAsync(RunWorkflowInstanceRequest request, CancellationToke
 76    {
 8777        var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken);
 8778        return await RunInstanceAsync(workflowInstance, request, cancellationToken);
 8779    }
 80
 81    /// <inheritdoc />
 82    public async Task<RunWorkflowInstanceResponse> CreateAndRunInstanceAsync(CreateAndRunWorkflowInstanceRequest request
 83    {
 14184        var workflowDefinitionHandle = request.WorkflowDefinitionHandle;
 14185        var workflowGraph = await GetWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken);
 86
 13987        await using var lease = await workflowActivationGate.EvaluateAsync(workflowGraph.Workflow, request.CorrelationId
 13688        if (!lease.CanStart)
 89        {
 890            LogActivationDenial(workflowGraph.Workflow, request.CorrelationId);
 891            return new()
 892            {
 893                CannotStart = true
 894            };
 95        }
 96
 12897        var effectiveCancellationToken = lease.GetEffectiveCancellationToken(cancellationToken);
 12898        effectiveCancellationToken.ThrowIfCancellationRequested();
 12799        var workflowInstance = CreateWorkflowInstance(workflowGraph.Workflow, new CreateWorkflowInstanceRequest
 127100        {
 127101            WorkflowDefinitionHandle = workflowDefinitionHandle,
 127102            CorrelationId = request.CorrelationId,
 127103            Name = request.Name,
 127104            ParentId = request.ParentId,
 127105            Input = request.Input,
 127106            Properties = request.Properties
 127107        });
 127108        effectiveCancellationToken.ThrowIfCancellationRequested();
 109
 110        // Do not durably publish a Running/Pending row before execution. If the run is
 111        // interrupted before WorkflowRunner commits its result, that row would occupy the
 112        // activation scope despite the failed activation.
 127113        return await RunInstanceAsync(workflowInstance, new()
 127114        {
 127115            Input = request.Input,
 127116            Variables = request.Variables,
 127117            Properties = request.Properties,
 127118            TriggerActivityId = request.TriggerActivityId,
 127119            ActivityHandle = request.ActivityHandle,
 127120            SchedulingActivityExecutionId = request.SchedulingActivityExecutionId,
 127121            SchedulingWorkflowInstanceId = request.SchedulingWorkflowInstanceId,
 127122            SchedulingCallStackDepth = request.SchedulingCallStackDepth,
 127123            IncludeWorkflowOutput = request.IncludeWorkflowOutput
 127124        }, effectiveCancellationToken);
 133125    }
 126
 127    private void LogActivationDenial(Workflow workflow, string? correlationId)
 128    {
 9129        var identity = workflow.Identity;
 9130        logger.LogWarning(
 9131            "Workflow activation strategy {ActivationStrategyType} disallowed creating an instance for definition {Workf
 9132            workflow.Options.ActivationStrategyType?.FullName,
 9133            identity.DefinitionId,
 9134            identity.Version,
 9135            identity.Id,
 9136            !string.IsNullOrWhiteSpace(correlationId));
 9137    }
 138
 139    /// <inheritdoc />
 140    public async Task CancelAsync(CancellationToken cancellationToken = default)
 141    {
 2142        var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken);
 2143        await CancelAsync(workflowInstance, cancellationToken);
 2144    }
 145
 146    private async Task CancelAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken)
 147    {
 12148        if (workflowInstance.Status != WorkflowStatus.Running) return;
 6149        var workflowGraph = await GetWorkflowGraphAsync(workflowInstance, cancellationToken);
 6150        var workflowState = await workflowCanceler.CancelWorkflowAsync(workflowGraph, workflowInstance.WorkflowState, ca
 6151        await workflowInstanceManager.SaveAsync(workflowState, cancellationToken);
 9152    }
 153
 154    /// <inheritdoc />
 155    public async Task<WorkflowState> ExportStateAsync(CancellationToken cancellationToken = default)
 156    {
 71157        var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken);
 71158        return workflowInstance.WorkflowState;
 71159    }
 160
 161    /// <inheritdoc />
 162    public async Task ImportStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default)
 163    {
 3164        var workflowInstance = workflowStateMapper.Map(workflowState)!;
 3165        await workflowInstanceManager.SaveAsync(workflowInstance, cancellationToken);
 3166    }
 167
 168    public Task<bool> InstanceExistsAsync(CancellationToken cancellationToken = default)
 169    {
 0170        return workflowInstanceManager.ExistsAsync(workflowInstanceId, cancellationToken);
 171    }
 172
 173    /// <inheritdoc />
 174    public async Task<bool> DeleteAsync(CancellationToken cancellationToken = default)
 175    {
 176        // Load the workflow instance (single DB call)
 8177        var workflowInstance = await TryGetWorkflowInstanceAsync(cancellationToken);
 8178        if (workflowInstance == null)
 1179            return false;
 180
 7181        await CancelAsync(workflowInstance, cancellationToken);
 182
 183        // Delete the workflow instance
 7184        var filter = new WorkflowInstanceFilter { Id = workflowInstanceId };
 7185        await workflowInstanceManager.DeleteAsync(filter, cancellationToken);
 7186        return true;
 8187    }
 188
 189    public async Task<RunWorkflowInstanceResponse> RunInstanceAsync(WorkflowInstance workflowInstance, RunWorkflowInstan
 190    {
 214191        cancellationToken.ThrowIfCancellationRequested();
 214192        var workflowState = workflowInstance.WorkflowState;
 193
 214194        if (workflowInstance.Status != WorkflowStatus.Running)
 195        {
 0196            logger.LogWarning("Attempt to resume workflow {WorkflowInstanceId} that is not in the Running state. The act
 0197            return new()
 0198            {
 0199                WorkflowInstanceId = WorkflowInstanceId,
 0200                Status = workflowInstance.Status,
 0201                SubStatus = workflowInstance.SubStatus
 0202            };
 203        }
 204
 214205        var runWorkflowOptions = new RunWorkflowOptions
 214206        {
 214207            Input = request.Input,
 214208            Variables = request.Variables,
 214209            Properties = request.Properties,
 214210            BookmarkId = request.BookmarkId,
 214211            TriggerActivityId = request.TriggerActivityId,
 214212            ActivityHandle = request.ActivityHandle,
 214213            SchedulingActivityExecutionId = request.SchedulingActivityExecutionId,
 214214            SchedulingWorkflowInstanceId = request.SchedulingWorkflowInstanceId,
 214215            SchedulingCallStackDepth = request.SchedulingCallStackDepth
 214216        };
 217
 214218        var workflowGraph = await GetWorkflowGraphAsync(workflowInstance, cancellationToken);
 214219        cancellationToken.ThrowIfCancellationRequested();
 214220        var workflowResult = await workflowRunner.RunAsync(workflowGraph, workflowState, runWorkflowOptions, cancellatio
 221
 212222        workflowState = workflowResult.WorkflowState;
 223
 212224        return new()
 212225        {
 212226            WorkflowInstanceId = WorkflowInstanceId,
 212227            Status = workflowState.Status,
 212228            SubStatus = workflowState.SubStatus,
 212229            Incidents = workflowState.Incidents,
 212230            Output = request.IncludeWorkflowOutput ? new Dictionary<string, object>(workflowState.Output) : null,
 212231            Bookmarks = workflowState.Bookmarks
 212232        };
 212233    }
 234
 235    public async Task<WorkflowInstance> CreateInstanceInternalAsync(CreateWorkflowInstanceRequest request, CancellationT
 236    {
 0237        var workflowDefinitionHandle = request.WorkflowDefinitionHandle;
 0238        var workflowGraph = await GetWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken);
 239
 0240        return CreateWorkflowInstance(workflowGraph.Workflow, request);
 0241    }
 242
 243    private WorkflowInstance CreateWorkflowInstance(Workflow workflow, CreateWorkflowInstanceRequest request)
 244    {
 167245        var options = new WorkflowInstanceOptions
 167246        {
 167247            WorkflowInstanceId = WorkflowInstanceId,
 167248            CorrelationId = request.CorrelationId,
 167249            Name = request.Name,
 167250            ParentWorkflowInstanceId = request.ParentId,
 167251            Input = request.Input,
 167252            Properties = request.Properties
 167253        };
 254
 167255        return workflowInstanceManager.CreateWorkflowInstance(workflow, options);
 256    }
 257
 258    private async Task<WorkflowInstance> GetWorkflowInstanceAsync(CancellationToken cancellationToken)
 259    {
 160260        var workflowInstance = await TryGetWorkflowInstanceAsync(cancellationToken);
 160261        if (workflowInstance == null) throw new WorkflowInstanceNotFoundException("Workflow instance not found.", Workfl
 160262        return workflowInstance;
 160263    }
 264
 265    private Task<WorkflowInstance?> TryGetWorkflowInstanceAsync(CancellationToken cancellationToken)
 266    {
 168267        return workflowInstanceManager.FindByIdAsync(WorkflowInstanceId, cancellationToken);
 268    }
 269
 270    private async Task<WorkflowGraph> GetWorkflowGraphAsync(WorkflowInstance workflowInstance, CancellationToken cancell
 271    {
 220272        var handle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowInstance.DefinitionVersionId);
 220273        return await GetWorkflowGraphAsync(handle, cancellationToken);
 220274    }
 275
 276    private async Task<WorkflowGraph> GetWorkflowGraphAsync(WorkflowDefinitionHandle definitionHandle, CancellationToken
 277    {
 405278        var result = await workflowDefinitionService.TryFindWorkflowGraphAsync(definitionHandle, cancellationToken);
 407279        if (!result.WorkflowDefinitionExists) throw new WorkflowDefinitionNotFoundException("Workflow definition not fou
 405280        if (!result.WorkflowGraphExists) throw new WorkflowMaterializerNotFoundException(result.WorkflowDefinition!.Mate
 401281        return result.WorkflowGraph!;
 401282    }
 283
 284    private sealed class CompatibilityActivationGate : IWorkflowActivationGate
 285    {
 286        public Task<WorkflowActivationLease> EvaluateAsync(Workflow workflow, string? correlationId, CancellationToken c
 287        {
 0288            if (workflow.Options.ActivationStrategyType != null && workflow.Options.ActivationStrategyType != typeof(All
 0289                throw new InvalidOperationException("The compatibility LocalWorkflowClient constructor cannot enforce a 
 290
 0291            return Task.FromResult(new WorkflowActivationLease(true, null, cancellationToken));
 292        }
 293    }
 294}