< Summary

Information
Class: Elsa.Workflows.Api.Endpoints.Bookmarks.Resume.Request
Assembly: Elsa.Workflows.Api
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Api/Endpoints/Bookmarks/Resume/Endpoint.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1
Coverable lines: 1
Total lines: 187
Line coverage: 0%
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_Input()100%210%

File(s)

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

#LineLine coverage
 1using System.Text;
 2using System.Text.Json;
 3using Elsa.Abstractions;
 4using Elsa.SasTokens.Contracts;
 5using Elsa.Workflows;
 6using Elsa.Workflows.Runtime;
 7using FastEndpoints;
 8using JetBrains.Annotations;
 9using Microsoft.AspNetCore.Http;
 10
 11namespace Elsa.Workflows.Api.Endpoints.Bookmarks.Resume;
 12
 13/// <summary>
 14/// Resumes a bookmarked workflow instance with the bookmark ID specified in the provided SAS token.
 15/// </summary>
 16[PublicAPI]
 17internal class Resume(ITokenService tokenService, IWorkflowResumer workflowResumer, IBookmarkQueue bookmarkQueue, IPaylo
 18{
 19    private const long MaxBookmarkResumeBodySize = 1024 * 1024;
 20
 21    /// <inheritdoc />
 22    public override void Configure()
 23    {
 24        Routes("/bookmarks/resume");
 25        Verbs(Http.GET, Http.POST);
 26        AllowAnonymous();
 27    }
 28
 29    /// <inheritdoc />
 30    public override async Task HandleAsync(CancellationToken cancellationToken)
 31    {
 32        var token = Query<string?>("t", false);
 33
 34        if (string.IsNullOrWhiteSpace(token) || !tokenService.TryDecryptToken<BookmarkTokenPayload>(token, out var paylo
 35        {
 36            AddError("Invalid token.");
 37            await Send.ErrorsAsync(cancellation: cancellationToken);
 38            return;
 39        }
 40
 41        var asynchronous = Query<bool>("async", false);
 42        var input = await GetInputAsync(cancellationToken);
 43
 44        if (ValidationFailed)
 45        {
 46            await Send.ErrorsAsync(cancellation: cancellationToken);
 47            return;
 48        }
 49
 50        // Some clients, like Blazor, may prematurely cancel their request upon navigation away from the page.
 51        // In this case, we don't want to cancel the workflow execution.
 52        // We need to better understand the conditions that cause this.
 53        var workflowCancellationToken = CancellationToken.None;
 54        await ResumeBookmarkedWorkflowAsync(payload, input, asynchronous, workflowCancellationToken);
 55
 56        if (!HttpContext.Response.HasStarted)
 57            await Send.OkAsync(cancellation: cancellationToken);
 58    }
 59
 60    private IDictionary<string, object>? GetInputFromQueryString()
 61    {
 62        var inputJson = Query<string?>("in", false);
 63        if (string.IsNullOrWhiteSpace(inputJson))
 64            return null;
 65
 66        try
 67        {
 68            return payloadSerializer.Deserialize<IDictionary<string, object>>(inputJson);
 69        }
 70        catch (Exception e) when (e is JsonException or NotSupportedException or InvalidOperationException or FormatExce
 71        {
 72            AddError("Invalid input format. Expected a valid JSON string.");
 73            return null;
 74        }
 75    }
 76
 77    private async ValueTask<IDictionary<string, object>?> GetInputAsync(CancellationToken cancellationToken)
 78    {
 79        return HttpContext.Request.Method == HttpMethods.Post
 80            ? await GetInputFromBodyAsync(cancellationToken)
 81            : GetInputFromQueryString();
 82    }
 83
 84    private async ValueTask<IDictionary<string, object>?> GetInputFromBodyAsync(CancellationToken cancellationToken)
 85    {
 86        if (HttpContext.Request.ContentLength == 0)
 87            return null;
 88
 89        var body = await ReadRequestBodyAsync(cancellationToken);
 90        if (ValidationFailed)
 91            return null;
 92
 93        if (string.IsNullOrWhiteSpace(body))
 94            return null;
 95
 96        try
 97        {
 98            var request = apiSerializer.Deserialize<Request>(body);
 99            if (request == null)
 100            {
 101                AddError("Invalid input format. Expected a valid JSON request body.");
 102                return null;
 103            }
 104
 105            return request.Input;
 106        }
 107        catch (Exception e) when (e is JsonException or NotSupportedException or InvalidOperationException or FormatExce
 108        {
 109            AddError("Invalid input format. Expected a valid JSON request body.");
 110            return null;
 111        }
 112    }
 113
 114    private async ValueTask<string?> ReadRequestBodyAsync(CancellationToken cancellationToken)
 115    {
 116        var request = HttpContext.Request;
 117        if (request.ContentLength is > MaxBookmarkResumeBodySize)
 118        {
 119            AddError("Request body is too large.");
 120            return null;
 121        }
 122
 123        await using var body = new MemoryStream();
 124        var buffer = new byte[81920];
 125        long totalBytesRead = 0;
 126
 127        while (true)
 128        {
 129            var bytesRead = await request.Body.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
 130            if (bytesRead == 0)
 131                break;
 132
 133            totalBytesRead += bytesRead;
 134            if (totalBytesRead > MaxBookmarkResumeBodySize)
 135            {
 136                AddError("Request body is too large.");
 137                return null;
 138            }
 139
 140            await body.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
 141        }
 142
 143        return body.Length == 0 ? null : Encoding.UTF8.GetString(body.ToArray());
 144    }
 145
 146    private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary<string, object>? inp
 147    {
 148        var bookmarkId = tokenPayload.BookmarkId;
 149        var workflowInstanceId = tokenPayload.WorkflowInstanceId;
 150
 151        if (asynchronous)
 152        {
 153            var item = new NewBookmarkQueueItem
 154            {
 155                BookmarkId = bookmarkId,
 156                WorkflowInstanceId = workflowInstanceId,
 157                Options = new()
 158                {
 159                    Input = input
 160                }
 161            };
 162
 163            await bookmarkQueue.EnqueueAsync(item, cancellationToken);
 164            return;
 165        }
 166
 167        var resumeRequest = new ResumeBookmarkRequest
 168        {
 169            BookmarkId = bookmarkId,
 170            WorkflowInstanceId = workflowInstanceId,
 171            Input = input
 172        };
 173
 174        await workflowResumer.ResumeAsync(resumeRequest, cancellationToken);
 175    }
 176}
 177
 178/// <summary>
 179/// The request model for the Resume endpoint.
 180/// </summary>
 181internal class Request
 182{
 183    /// <summary>
 184    /// The input to provide to the workflow when resuming.
 185    /// </summary>
 0186    public IDictionary<string, object>? Input { get; set; }
 187}

Methods/Properties

get_Input()