< Summary

Information
Class: Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Export.Export
Assembly: Elsa.Workflows.Api
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs
Line coverage
94%
Covered lines: 91
Uncovered lines: 5
Coverable lines: 96
Total lines: 212
Line coverage: 94.7%
Branch coverage
82%
Covered branches: 23
Total branches: 28
Branch coverage: 82.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Configure()100%11100%
HandleAsync()75%4483.33%
DownloadMultipleWorkflowsAsync()75%4481.81%
DownloadSingleWorkflowAsync()66.66%6688.88%
IncludeConsumersAsync()100%22100%
WriteZipResponseAsync()100%66100%
GetFileName(...)50%22100%
SerializeWorkflowDefinitionAsync()100%44100%
CreateWorkflowModelAsync()100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs

#LineLine coverage
 1using System.IO.Compression;
 2using System.Text.Json;
 3using Elsa.Abstractions;
 4using Elsa.Common.Models;
 5using Elsa.Workflows.Management;
 6using Elsa.Workflows.Management.Entities;
 7using Elsa.Workflows.Management.Filters;
 8using Elsa.Workflows.Management.Mappers;
 9using Elsa.Workflows.Management.Models;
 10using Humanizer;
 11using JetBrains.Annotations;
 12
 13namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Export;
 14
 15/// <summary>
 16/// Exports the specified workflow definition as JSON download.
 17/// </summary>
 18[UsedImplicitly]
 19internal class Export : ElsaEndpoint<Request>
 20{
 21    private readonly IApiSerializer _serializer;
 22    private readonly IWorkflowDefinitionStore _store;
 23    private readonly IWorkflowReferenceGraphBuilder _workflowReferenceGraphBuilder;
 24    private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
 25
 26    /// <inheritdoc />
 727    public Export(
 728        IWorkflowDefinitionStore store,
 729        IApiSerializer serializer,
 730        WorkflowDefinitionMapper workflowDefinitionMapper,
 731        IWorkflowReferenceGraphBuilder workflowReferenceGraphBuilder)
 32    {
 733        _store = store;
 734        _serializer = serializer;
 735        _workflowDefinitionMapper = workflowDefinitionMapper;
 736        _workflowReferenceGraphBuilder = workflowReferenceGraphBuilder;
 737    }
 38
 39    /// <inheritdoc />
 40    public override void Configure()
 41    {
 342        Routes("/bulk-actions/export/workflow-definitions", "/workflow-definitions/{definitionId}/export");
 343        Verbs(FastEndpoints.Http.GET, FastEndpoints.Http.POST);
 344        ConfigurePermissions("read:workflow-definitions");
 345    }
 46
 47    /// <inheritdoc />
 48    public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
 49    {
 450        if (request.DefinitionId != null)
 251            await DownloadSingleWorkflowAsync(request.DefinitionId, request.VersionOptions, request.IncludeConsumingWork
 252        else if (request.Ids != null)
 253            await DownloadMultipleWorkflowsAsync(request.Ids, request.IncludeConsumingWorkflows, cancellationToken);
 054        else await Send.NoContentAsync(cancellationToken);
 455    }
 56
 57    private async Task DownloadMultipleWorkflowsAsync(ICollection<string> ids, bool includeConsumingWorkflows, Cancellat
 58    {
 259        var definitions = (await _store.FindManyAsync(new()
 260        {
 261            Ids = ids
 262        }, cancellationToken)).ToList();
 63
 264        if (includeConsumingWorkflows)
 165            definitions = await IncludeConsumersAsync(definitions, cancellationToken);
 66
 267        if (!definitions.Any())
 68        {
 069            await Send.NoContentAsync(cancellationToken);
 070            return;
 71        }
 72
 273        await WriteZipResponseAsync(definitions, cancellationToken);
 274    }
 75
 76    private async Task DownloadSingleWorkflowAsync(string definitionId, string? versionOptions, bool includeConsumingWor
 77    {
 278        var parsedVersionOptions = string.IsNullOrEmpty(versionOptions) ? VersionOptions.Latest : VersionOptions.FromStr
 279        var definition = (await _store.FindManyAsync(new()
 280        {
 281            DefinitionId = definitionId,
 282            VersionOptions = parsedVersionOptions
 283        }, cancellationToken)).FirstOrDefault();
 84
 285        if (definition == null)
 86        {
 087            await Send.NotFoundAsync(cancellationToken);
 088            return;
 89        }
 90
 291        if (includeConsumingWorkflows)
 92        {
 193            var definitions = await IncludeConsumersAsync([definition], cancellationToken);
 194            await WriteZipResponseAsync(definitions, cancellationToken);
 195            return;
 96        }
 97
 198        var model = await CreateWorkflowModelAsync(definition, cancellationToken);
 199        var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken);
 1100        var fileName = GetFileName(model);
 101
 1102        await Send.BytesAsync(binaryJson, fileName, cancellation: cancellationToken);
 2103    }
 104
 105    /// <summary>
 106    /// Recursively discovers all consuming workflow definitions and includes them.
 107    /// Consumers are always resolved at <see cref="VersionOptions.Latest"/>, regardless of the version used for the ini
 108    /// </summary>
 109    private async Task<List<WorkflowDefinition>> IncludeConsumersAsync(List<WorkflowDefinition> definitions, Cancellatio
 110    {
 4111        var initialDefinitionIds = definitions.Select(d => d.DefinitionId).ToList();
 2112        var graph = await _workflowReferenceGraphBuilder.BuildGraphAsync(initialDefinitionIds, cancellationToken);
 113
 114        // Find any consumer definitions not already in our list.
 2115        var newDefinitionIds = graph.ConsumerDefinitionIds.Except(initialDefinitionIds).ToList();
 116
 2117        if (newDefinitionIds.Count > 0)
 118        {
 2119            var consumerDefinitions = await _store.FindManyAsync(new WorkflowDefinitionFilter
 2120            {
 2121                DefinitionIds = newDefinitionIds.ToArray(),
 2122                VersionOptions = VersionOptions.Latest
 2123            }, cancellationToken);
 124
 2125            definitions = definitions.Concat(consumerDefinitions).ToList();
 126        }
 127
 2128        return definitions;
 2129    }
 130
 131    private async Task WriteZipResponseAsync(List<WorkflowDefinition> definitions, CancellationToken cancellationToken)
 132    {
 3133        var zipStream = new MemoryStream();
 11134        var sortedDefinitions = definitions.OrderBy(d => d.DefinitionId).ToList();
 135
 136        // NOTE:
 137        // - ZIP timestamps cannot be earlier than 1980-01-01 (the ZIP format's minimum).
 138        // - We intentionally use a fixed timestamp (instead of DateTimeOffset.UtcNow) to keep exports deterministic.
 139        //   This avoids producing different ZIP bytes for identical exports, which helps tests, caching, and diffing.
 3140        var zipEpoch = new DateTimeOffset(1980, 1, 1, 0, 0, 0, TimeSpan.Zero);
 141
 142#if NET10_0_OR_GREATER
 3143        await using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
 144        {
 145            // Create a JSON file for each workflow definition:
 22146            foreach (var definition in sortedDefinitions)
 147            {
 8148                var model = await CreateWorkflowModelAsync(definition, cancellationToken);
 8149                var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken);
 8150                var fileName = GetFileName(model);
 8151                var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal);
 8152                entry.LastWriteTime = zipEpoch;
 8153                await using var entryStream = await entry.OpenAsync(cancellationToken);
 8154                await entryStream.WriteAsync(binaryJson, cancellationToken);
 8155            }
 156        }
 157#else
 158        using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
 159        {
 160            // Create a JSON file for each workflow definition:
 161            foreach (var definition in sortedDefinitions)
 162            {
 163                var model = await CreateWorkflowModelAsync(definition, cancellationToken);
 164                var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken);
 165                var fileName = GetFileName(model);
 166                var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal);
 167                entry.LastWriteTime = zipEpoch;
 168                await using var entryStream = entry.Open();
 169                await entryStream.WriteAsync(binaryJson, cancellationToken);
 170            }
 171        }
 172#endif
 173
 174        // Send the zip file to the client:
 3175        zipStream.Position = 0;
 3176        await Send.BytesAsync(zipStream.ToArray(), "workflow-definitions.zip", cancellation: cancellationToken);
 3177    }
 178
 179    private string GetFileName(WorkflowDefinitionModel definition)
 180    {
 9181        var hasWorkflowName = !string.IsNullOrWhiteSpace(definition.Name);
 9182        var workflowName = hasWorkflowName ? definition.Name!.Trim() : definition.DefinitionId;
 9183        var fileName = $"workflow-definition-{workflowName.Underscore().Dasherize().ToLowerInvariant()}-{definition.Defi
 9184        return fileName;
 185    }
 186
 187    private async Task<byte[]> SerializeWorkflowDefinitionAsync(WorkflowDefinitionModel model, CancellationToken cancell
 188    {
 9189        var serializerOptions = _serializer.GetOptions();
 9190        var document = JsonSerializer.SerializeToDocument(model, serializerOptions);
 9191        var rootElement = document.RootElement;
 192
 9193        using var output = new MemoryStream();
 9194        await using var writer = new Utf8JsonWriter(output);
 195
 9196        writer.WriteStartObject();
 9197        writer.WriteString("$schema", "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json");
 198
 324199        foreach (var property in rootElement.EnumerateObject())
 153200            property.WriteTo(writer);
 201
 9202        writer.WriteEndObject();
 203
 9204        await writer.FlushAsync(cancellationToken);
 9205        return output.ToArray();
 9206    }
 207
 208    private async Task<WorkflowDefinitionModel> CreateWorkflowModelAsync(WorkflowDefinition definition, CancellationToke
 209    {
 9210        return await _workflowDefinitionMapper.MapAsync(definition, cancellationToken);
 9211    }
 212}