Merge remote-tracking branch 'origin/develop/3.5.0' into develop/3.6.0
This commit is contained in:
commit
30139dedc6
49
src/apps/Elsa.Server.Web/Activities/AuthorizeFlow.cs
Normal file
49
src/apps/Elsa.Server.Web/Activities/AuthorizeFlow.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities.Flowchart.Attributes;
|
||||
using Elsa.Workflows.Attributes;
|
||||
|
||||
namespace Elsa.Server.Web.Activities;
|
||||
|
||||
[Activity("Elsa", "Authorization", "Authorizes a flow based on the configured policies.")]
|
||||
[FlowNode("Authorized", "Unauthorized", "Error")]
|
||||
public class AuthorizeFlow : Activity<string>
|
||||
{
|
||||
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var httpContext = context.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
|
||||
if (httpContext == null)
|
||||
throw new InvalidOperationException("HttpContext is not available. Ensure that the activity is executed within an HTTP request context.");
|
||||
|
||||
var bookmark = context.CreateBookmark(new AuthorizeStimulus(), OnResumeAsync);
|
||||
var redirectUrl = context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmark.Id);
|
||||
|
||||
Result.Set(context, redirectUrl);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private async ValueTask OnResumeAsync(ActivityExecutionContext context)
|
||||
{
|
||||
if (!context.TryGetWorkflowInput<string>("Answer", out var response))
|
||||
{
|
||||
await context.CompleteActivityWithOutcomesAsync("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response)
|
||||
{
|
||||
case "Authorized":
|
||||
await context.CompleteActivityWithOutcomesAsync("Authorized");
|
||||
return;
|
||||
case "Error":
|
||||
await context.CompleteActivityWithOutcomesAsync("Error");
|
||||
return;
|
||||
default:
|
||||
await context.CompleteActivityWithOutcomesAsync("Unauthorized");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record AuthorizeStimulus;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Http;
|
||||
using Elsa.SasTokens.Contracts;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Api;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for working with <see cref="ExpressionExecutionContext"/> and generating bookmark trigger URLs.
|
||||
/// </summary>
|
||||
public static class BookmarkExecutionContextExtensions
|
||||
{
|
||||
public static string GenerateBookmarkTriggerUrl(this ActivityExecutionContext context, string bookmarkId, TimeSpan lifetime) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId, lifetime);
|
||||
public static string GenerateBookmarkTriggerUrl(this ActivityExecutionContext context, string bookmarkId, DateTimeOffset expiresAt) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId, expiresAt);
|
||||
public static string GenerateBookmarkTriggerUrl(this ActivityExecutionContext context, string bookmarkId) => context.ExpressionExecutionContext.GenerateBookmarkTriggerUrl(bookmarkId);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL that can be used to resume a bookmarked workflow.
|
||||
/// </summary>
|
||||
/// <param name="context">The expression execution context.</param>
|
||||
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
|
||||
/// <param name="lifetime">The lifetime of the bookmark trigger token.</param>
|
||||
/// <returns>A URL that can be used to resume a bookmarked workflow.</returns>
|
||||
public static string GenerateBookmarkTriggerUrl(this ExpressionExecutionContext context, string bookmarkId, TimeSpan lifetime)
|
||||
{
|
||||
var token = context.GenerateBookmarkTriggerTokenInternal(bookmarkId, lifetime);
|
||||
return context.GenerateBookmarkTriggerUrlInternal(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL that can be used to resume a bookmarked workflow.
|
||||
/// </summary>
|
||||
/// <param name="context">The expression execution context.</param>
|
||||
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
|
||||
/// <param name="expiresAt">The expiration date of the bookmark trigger token.</param>
|
||||
/// <returns>A URL that can be used to resume a bookmarked workflow.</returns>
|
||||
public static string GenerateBookmarkTriggerUrl(this ExpressionExecutionContext context, string bookmarkId, DateTimeOffset expiresAt)
|
||||
{
|
||||
var token = context.GenerateBookmarkTriggerTokenInternal(bookmarkId, expiresAt: expiresAt);
|
||||
return context.GenerateBookmarkTriggerUrlInternal(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a URL that can be used to resume a bookmarked workflow.
|
||||
/// </summary>
|
||||
/// <param name="context">The expression execution context.</param>
|
||||
/// <param name="bookmarkId">The ID of the bookmark to resume.</param>
|
||||
/// <returns>A URL that can be used to trigger an event.</returns>
|
||||
public static string GenerateBookmarkTriggerUrl(this ExpressionExecutionContext context, string bookmarkId)
|
||||
{
|
||||
var token = context.GenerateBookmarkTriggerTokenInternal(bookmarkId);
|
||||
return context.GenerateBookmarkTriggerUrlInternal(token);
|
||||
}
|
||||
|
||||
private static string GenerateBookmarkTriggerUrlInternal(this ExpressionExecutionContext context, string token)
|
||||
{
|
||||
var options = context.GetRequiredService<IOptions<ApiEndpointOptions>>().Value;
|
||||
var url = $"{options.RoutePrefix}/bookmarks/resume?t={token}";
|
||||
var absoluteUrlProvider = context.GetRequiredService<IAbsoluteUrlProvider>();
|
||||
return absoluteUrlProvider.ToAbsoluteUrl(url).ToString();
|
||||
}
|
||||
|
||||
private static string GenerateBookmarkTriggerTokenInternal(this ExpressionExecutionContext context, string bookmarkId, TimeSpan? lifetime = null, DateTimeOffset? expiresAt = null)
|
||||
{
|
||||
var workflowInstanceId = context.GetWorkflowExecutionContext().Id;
|
||||
var payload = new BookmarkTokenPayload(bookmarkId, workflowInstanceId);
|
||||
var tokenService = context.GetRequiredService<ITokenService>();
|
||||
|
||||
return lifetime != null
|
||||
? tokenService.CreateToken(payload, lifetime.Value)
|
||||
: expiresAt != null
|
||||
? tokenService.CreateToken(payload, expiresAt.Value)
|
||||
: tokenService.CreateToken(payload);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Http;
|
||||
using Elsa.Http.Options;
|
||||
using Elsa.SasTokens.Contracts;
|
||||
using Elsa.Workflows.Api;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Provides extension methods for working with <see cref="ExpressionExecutionContext"/>.
|
||||
/// </summary>
|
||||
public static class ExpressionExecutionContextExtensions
|
||||
public static class EventExpressionExecutionContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a URL that can be used to trigger an event.
|
||||
|
|
@ -52,13 +53,13 @@ public static class ExpressionExecutionContextExtensions
|
|||
|
||||
private static string GenerateEventTriggerUrlInternal(this ExpressionExecutionContext context, string token)
|
||||
{
|
||||
var options = context.GetRequiredService<IOptions<HttpActivityOptions>>().Value;
|
||||
var url = $"{options.ApiRoutePrefix}/events/trigger?t={token}";
|
||||
var options = context.GetRequiredService<IOptions<ApiEndpointOptions>>().Value;
|
||||
var url = $"{options.RoutePrefix}/events/trigger?t={token}";
|
||||
var absoluteUrlProvider = context.GetRequiredService<IAbsoluteUrlProvider>();
|
||||
return absoluteUrlProvider.ToAbsoluteUrl(url).ToString();
|
||||
}
|
||||
|
||||
private static string GenerateEventTriggerTokenInternal(this ExpressionExecutionContext context, string eventName, TimeSpan? lifetime = default, DateTimeOffset? expiresAt = default)
|
||||
private static string GenerateEventTriggerTokenInternal(this ExpressionExecutionContext context, string eventName, TimeSpan? lifetime = null, DateTimeOffset? expiresAt = null)
|
||||
{
|
||||
var workflowInstanceId = context.GetWorkflowExecutionContext().Id;
|
||||
var payload = new EventTokenPayload(eventName, workflowInstanceId);
|
||||
|
|
@ -15,7 +15,7 @@ public class HttpActivityOptions
|
|||
/// <summary>
|
||||
/// The base URL of the server. This should be set to the same value at which the Elsa Server is publicly available. It will be used when generating absolute URLs need to be generated by activities such as SendEmail.
|
||||
/// </summary>
|
||||
public Uri BaseUrl { get; set; } = default!;
|
||||
public Uri BaseUrl { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The prefix used for API routes.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
private readonly ICronParser _cronParser;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
|
||||
private bool _executing;
|
||||
private bool _cancellationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ScheduledCronTask"/>.
|
||||
|
|
@ -32,7 +35,7 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
_scopeFactory = scopeFactory;
|
||||
_systemClock = systemClock;
|
||||
_logger = logger;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
Schedule();
|
||||
}
|
||||
|
|
@ -41,6 +44,13 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
public void Cancel()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
|
||||
if (_executing)
|
||||
{
|
||||
_cancellationRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +64,7 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
var nextOccurence = _cronParser.GetNextOccurrence(_cronExpression);
|
||||
var delay = nextOccurence - now;
|
||||
|
||||
if (!adjusted && delay.Milliseconds <= 0)
|
||||
if (!adjusted && delay <= TimeSpan.Zero)
|
||||
{
|
||||
adjusted = true;
|
||||
continue;
|
||||
|
|
@ -67,7 +77,7 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
|
||||
private void TrySetupTimer(TimeSpan delay)
|
||||
{
|
||||
if (delay.Milliseconds <= 0)
|
||||
if (delay <= TimeSpan.Zero)
|
||||
return;
|
||||
|
||||
try
|
||||
|
|
@ -82,7 +92,10 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
|
||||
private void SetupTimer(TimeSpan delay)
|
||||
{
|
||||
_timer = new Timer(delay.TotalMilliseconds) { Enabled = true };
|
||||
_timer = new(delay.TotalMilliseconds)
|
||||
{
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
_timer.Elapsed += async (_, _) =>
|
||||
{
|
||||
|
|
@ -93,8 +106,36 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
if (!cancellationToken.IsCancellationRequested) await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
if (!cancellationToken.IsCancellationRequested) Schedule();
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!acquired) return;
|
||||
|
||||
_executing = true;
|
||||
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
||||
if (_cancellationRequested)
|
||||
{
|
||||
_cancellationRequested = false;
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error executing scheduled task");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
Schedule();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -102,5 +143,6 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
{
|
||||
_timer?.Dispose();
|
||||
_cancellationTokenSource.Dispose();
|
||||
_executionSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using Elsa.Common;
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Scheduling.Commands;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Elsa.Scheduling.ScheduledTasks;
|
||||
|
|
@ -14,24 +15,24 @@ public class ScheduledRecurringTask : IScheduledTask, IDisposable
|
|||
private readonly ITask _task;
|
||||
private readonly ISystemClock _systemClock;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<ScheduledRecurringTask> _logger;
|
||||
private readonly TimeSpan _interval;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
|
||||
private DateTimeOffset _startAt;
|
||||
private Timer? _timer;
|
||||
private bool _executing;
|
||||
private bool _cancellationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ScheduledRecurringTask"/>.
|
||||
/// </summary>
|
||||
/// <param name="task">The task to execute.</param>
|
||||
/// <param name="startAt">The instant at which to start executing the task.</param>
|
||||
/// <param name="interval">The interval at which to execute the task.</param>
|
||||
/// <param name="systemClock">The system clock.</param>
|
||||
/// <param name="scopeFactory">Scope factory to create the scope and get dependancies.</param>
|
||||
public ScheduledRecurringTask(ITask task, DateTimeOffset startAt, TimeSpan interval, ISystemClock systemClock, IServiceScopeFactory scopeFactory)
|
||||
public ScheduledRecurringTask(ITask task, DateTimeOffset startAt, TimeSpan interval, ISystemClock systemClock, IServiceScopeFactory scopeFactory, ILogger<ScheduledRecurringTask> logger)
|
||||
{
|
||||
_task = task;
|
||||
_systemClock = systemClock;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_startAt = startAt;
|
||||
_interval = interval;
|
||||
_cancellationTokenSource = new();
|
||||
|
|
@ -43,6 +44,13 @@ public class ScheduledRecurringTask : IScheduledTask, IDisposable
|
|||
public void Cancel()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
|
||||
if (_executing)
|
||||
{
|
||||
_cancellationRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +64,7 @@ public class ScheduledRecurringTask : IScheduledTask, IDisposable
|
|||
var now = _systemClock.UtcNow;
|
||||
var delay = startAt - now;
|
||||
|
||||
if (!adjusted && delay.Milliseconds <= 0)
|
||||
if (!adjusted && delay <= TimeSpan.Zero)
|
||||
{
|
||||
adjusted = true;
|
||||
continue;
|
||||
|
|
@ -71,7 +79,10 @@ public class ScheduledRecurringTask : IScheduledTask, IDisposable
|
|||
{
|
||||
if (delay < TimeSpan.Zero) delay = TimeSpan.FromSeconds(1);
|
||||
|
||||
_timer = new(delay.TotalMilliseconds) { Enabled = true };
|
||||
_timer = new(delay.TotalMilliseconds)
|
||||
{
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
_timer.Elapsed += async (_, _) =>
|
||||
{
|
||||
|
|
@ -81,10 +92,35 @@ public class ScheduledRecurringTask : IScheduledTask, IDisposable
|
|||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
if (!cancellationToken.IsCancellationRequested) await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
if (!cancellationToken.IsCancellationRequested) Schedule();
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!acquired) return;
|
||||
_executing = true;
|
||||
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
||||
if (_cancellationRequested)
|
||||
{
|
||||
_cancellationRequested = false;
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error executing scheduled task");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
Schedule();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -92,5 +128,6 @@ public class ScheduledRecurringTask : IScheduledTask, IDisposable
|
|||
{
|
||||
_cancellationTokenSource.Dispose();
|
||||
_timer?.Dispose();
|
||||
_executionSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,10 @@ public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable
|
|||
private readonly ILogger<ScheduledSpecificInstantTask> _logger;
|
||||
private readonly DateTimeOffset _startAt;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
|
||||
private Timer? _timer;
|
||||
private bool _executing;
|
||||
private bool _cancellationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ScheduledSpecificInstantTask"/>.
|
||||
|
|
@ -30,23 +33,37 @@ public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable
|
|||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_startAt = startAt;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
Schedule();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Cancel() => _timer?.Dispose();
|
||||
public void Cancel()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
|
||||
if (_executing)
|
||||
{
|
||||
_cancellationRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
private void Schedule()
|
||||
{
|
||||
var now = _systemClock.UtcNow;
|
||||
var delay = _startAt - now;
|
||||
|
||||
if (delay.Milliseconds <= 0)
|
||||
if (delay <= TimeSpan.Zero)
|
||||
delay = TimeSpan.FromMilliseconds(1);
|
||||
|
||||
_timer = new Timer(delay.TotalMilliseconds) { Enabled = true };
|
||||
_timer = new(delay.TotalMilliseconds)
|
||||
{
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
_timer.Elapsed += async (_, _) =>
|
||||
{
|
||||
|
|
@ -55,19 +72,31 @@ public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable
|
|||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!acquired) return;
|
||||
_executing = true;
|
||||
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
||||
if (_cancellationRequested)
|
||||
{
|
||||
_cancellationRequested = false;
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error scheduled task");
|
||||
_logger.LogError(e, "Error executing scheduled task");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -76,5 +105,6 @@ public class ScheduledSpecificInstantTask : IScheduledTask, IDisposable
|
|||
{
|
||||
_cancellationTokenSource.Dispose();
|
||||
_timer?.Dispose();
|
||||
_executionSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.SasTokens.Contracts;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using FastEndpoints;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Workflows.Api.Endpoints.Bookmarks.Resume;
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a bookmarked workflow instance with the bookmark ID specified in the provided SAS token.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
internal class Resume(ITokenService tokenService, IBookmarkQueue bookmarkQueue, IPayloadSerializer serializer) : ElsaEndpoint<Request>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Routes("/bookmarks/resume");
|
||||
Verbs(Http.GET, Http.POST);
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
|
||||
{
|
||||
var token = Query<string>("t")!;
|
||||
|
||||
if (!tokenService.TryDecryptToken<BookmarkTokenPayload>(token, out var payload))
|
||||
AddError("Invalid token.");
|
||||
|
||||
var input = HttpContext.Request.Method == HttpMethods.Post ? request.Input : GetInputFromQueryString();
|
||||
|
||||
if (ValidationFailed)
|
||||
{
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await ResumeBookmarkedWorkflowAsync(payload, input, cancellationToken);
|
||||
|
||||
if (!HttpContext.Response.HasStarted)
|
||||
await SendOkAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private IDictionary<string, object>? GetInputFromQueryString()
|
||||
{
|
||||
var inputJson = Query<string?>("in", false);
|
||||
if (string.IsNullOrWhiteSpace(inputJson))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return serializer.Deserialize<IDictionary<string, object>>(inputJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
AddError("Invalid input format. Expected a valid JSON string.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary<string, object>? input, CancellationToken cancellationToken)
|
||||
{
|
||||
var bookmarkId = tokenPayload.BookmarkId;
|
||||
var workflowInstanceId = tokenPayload.WorkflowInstanceId;
|
||||
var item = new NewBookmarkQueueItem
|
||||
{
|
||||
BookmarkId = bookmarkId,
|
||||
WorkflowInstanceId = workflowInstanceId,
|
||||
Options = new()
|
||||
{
|
||||
Input = input
|
||||
}
|
||||
};
|
||||
await bookmarkQueue.EnqueueAsync(item, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The request model for the Resume endpoint.
|
||||
/// </summary>
|
||||
internal class Request
|
||||
{
|
||||
/// <summary>
|
||||
/// The input to provide to the workflow when resuming.
|
||||
/// </summary>
|
||||
public IDictionary<string, object>? Input { get; set; }
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Http;
|
||||
using Elsa.SasTokens.Contracts;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using JetBrains.Annotations;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Elsa.Extensions;
|
|||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Http.Features;
|
||||
using Elsa.SasTokens.Features;
|
||||
using Elsa.Workflows.Api.Constants;
|
||||
using Elsa.Workflows.Api.Requirements;
|
||||
|
|
@ -21,15 +20,9 @@ namespace Elsa.Workflows.Api.Features;
|
|||
[DependsOn(typeof(WorkflowInstancesFeature))]
|
||||
[DependsOn(typeof(WorkflowManagementFeature))]
|
||||
[DependsOn(typeof(WorkflowRuntimeFeature))]
|
||||
[DependsOn(typeof(HttpFeature))]
|
||||
[DependsOn(typeof(SasTokensFeature))]
|
||||
public class WorkflowsApiFeature : FeatureBase
|
||||
public class WorkflowsApiFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public WorkflowsApiFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -43,7 +36,6 @@ public class WorkflowsApiFeature : FeatureBase
|
|||
Module.AddFastEndpointsFromModule();
|
||||
|
||||
Services.AddScoped<IWorkflowDefinitionLinker, StaticWorkflowDefinitionLinker>();
|
||||
|
||||
Services.AddScoped<IAuthorizationHandler, NotReadOnlyRequirementHandler>();
|
||||
Services.Configure<AuthorizationOptions>(options =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ namespace Elsa.Workflows.Activities.Flowchart.Models;
|
|||
/// Represents a list of outcomes that can be send when completing an activity. This information is used by <see cref="Activities.Flowchart"/>.
|
||||
/// </summary>
|
||||
/// <param name="Names">A list of outcome names.</param>
|
||||
public record Outcomes(params string[] Names)
|
||||
{
|
||||
public static readonly Outcomes Default = new([null!, "Done"]);
|
||||
public static readonly Outcomes Empty = new();
|
||||
}
|
||||
public record Outcomes(params string[] Names)
|
||||
{
|
||||
public static readonly Outcomes Default = new(null!, "Done");
|
||||
public static readonly Outcomes Empty = new();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ namespace Elsa.Workflows;
|
|||
|
||||
public class DefaultWorkflowInstanceVariableReader(IVariablePersistenceManager variablePersistenceManager) : IWorkflowInstanceVariableReader
|
||||
{
|
||||
public async Task<IEnumerable<ResolvedVariable>> GetVariables(WorkflowExecutionContext workflowExecutionContext, IEnumerable<string>? excludeTags = default, CancellationToken cancellationToken = default)
|
||||
public async Task<IEnumerable<ResolvedVariable>> GetVariables(WorkflowExecutionContext workflowExecutionContext, IEnumerable<string>? excludeTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflow = workflowExecutionContext.Workflow;
|
||||
var workflowVariables = workflow.Variables;
|
||||
var rootWorkflowActivityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.ParentActivityExecutionContext == null);
|
||||
var rootWorkflowActivityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Activity == workflow);
|
||||
|
||||
if (rootWorkflowActivityExecutionContext == null)
|
||||
return [];
|
||||
|
|
@ -17,7 +17,7 @@ public class DefaultWorkflowInstanceVariableReader(IVariablePersistenceManager v
|
|||
foreach (var workflowVariable in workflowVariables)
|
||||
{
|
||||
var value = workflowVariable.Get(rootWorkflowActivityExecutionContext.ExpressionExecutionContext);
|
||||
resolvedVariables.Add(new ResolvedVariable(workflowVariable, value));
|
||||
resolvedVariables.Add(new(workflowVariable, value));
|
||||
}
|
||||
|
||||
return resolvedVariables;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ using Elsa.Extensions;
|
|||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Services;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class WorkflowStateExtractor : IWorkflowStateExtractor
|
||||
public class WorkflowStateExtractor(ILogger<WorkflowStateExtractor> logger) : IWorkflowStateExtractor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public WorkflowState Extract(WorkflowExecutionContext workflowExecutionContext)
|
||||
|
|
@ -101,7 +102,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
|
|||
workflowExecutionContext.Properties[property.Key] = property.Value;
|
||||
}
|
||||
|
||||
private static async Task ApplyActivityExecutionContextsAsync(WorkflowState state, WorkflowExecutionContext workflowExecutionContext)
|
||||
private async Task ApplyActivityExecutionContextsAsync(WorkflowState state, WorkflowExecutionContext workflowExecutionContext)
|
||||
{
|
||||
var activityExecutionContexts = (await Task.WhenAll(state.ActivityExecutionContexts.Select(async item => await CreateActivityExecutionContextAsync(item)))).Where(x => x != null).Select(x => x!).ToList();
|
||||
|
||||
|
|
@ -110,10 +111,14 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
|
|||
// Reconstruct hierarchy.
|
||||
foreach (var contextState in state.ActivityExecutionContexts.Where(x => !string.IsNullOrWhiteSpace(x.ParentContextId)))
|
||||
{
|
||||
if (lookup.ContainsKey(contextState.ParentContextId))
|
||||
var parentContextId = contextState.ParentContextId;
|
||||
if (parentContextId == null || !lookup.TryGetValue(parentContextId, out var parentContext))
|
||||
{
|
||||
var parentContext = lookup[contextState.ParentContextId!];
|
||||
var contextId = contextState.Id;
|
||||
logger.LogWarning("Parent context with ID '{ParentContextId}' not found for context with ID '{ContextId}'.", parentContextId, contextState.Id);
|
||||
continue; // Skip if parent context is not found.
|
||||
}
|
||||
|
||||
var contextId = contextState.Id;
|
||||
|
||||
if (lookup.TryGetValue(contextId, out var context))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,17 +16,26 @@ public static class ActivityExecutionContextRecordExtensions
|
|||
context.TransientProperties[ActivityExecutionRecordKey] = record;
|
||||
}
|
||||
|
||||
public static ActivityExecutionRecord? GetCapturedActivityExecutionRecord(this ActivityExecutionContext context)
|
||||
{
|
||||
return context.TransientProperties.TryGetValue(ActivityExecutionRecordKey, out var record) ? (ActivityExecutionRecord?)record : null;
|
||||
}
|
||||
|
||||
public static async Task<ActivityExecutionRecord> GetOrMapCapturedActivityExecutionRecordAsync(this ActivityExecutionContext context)
|
||||
{
|
||||
if(context.TransientProperties.TryGetValue(ActivityExecutionRecordKey, out var record))
|
||||
return (ActivityExecutionRecord)record;
|
||||
|
||||
var mapper = context.GetRequiredService<IActivityExecutionMapper>();
|
||||
return await mapper.MapAsync(context);
|
||||
var record = await mapper.MapAsync(context);
|
||||
|
||||
if (context.TransientProperties.TryGetValue(ActivityExecutionRecordKey, out var capturedRecord))
|
||||
{
|
||||
var serializedSnapshot = ((ActivityExecutionRecord)capturedRecord).SerializedSnapshot!;
|
||||
|
||||
// Take the existing serialized snapshot.
|
||||
record.SerializedSnapshot = serializedSnapshot;
|
||||
|
||||
// Update the serialized snapshot with the current record's properties.
|
||||
// This will reflect the latest state of the activity execution context without losing the existing serialized snapshot representing e.g., variable values at the time of the record capture.
|
||||
serializedSnapshot.HasBookmarks = record.HasBookmarks;
|
||||
serializedSnapshot.Status = record.Status;
|
||||
serializedSnapshot.AggregateFaultCount = record.AggregateFaultCount;
|
||||
serializedSnapshot.CompletedAt = record.CompletedAt;
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Workflows.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the payload for a bookmark token, including the bookmark identifier and the associated workflow instance identifier.
|
||||
/// </summary>
|
||||
public record BookmarkTokenPayload(string BookmarkId, string WorkflowInstanceId);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Http;
|
||||
namespace Elsa.Workflows.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the payload of an event, serialized as a secured token.
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Mediator;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Tenants.Mediator;
|
||||
using Elsa.Workflows.Runtime.Commands;
|
||||
using Elsa.Workflows.Runtime.Requests;
|
||||
using Elsa.Workflows.Runtime.Responses;
|
||||
|
|
@ -9,13 +11,18 @@ namespace Elsa.Workflows.Runtime;
|
|||
/// <summary>
|
||||
/// A simple implementation that queues the specified request for delivering stimuli on a non-durable background worker.
|
||||
/// </summary>
|
||||
public class BackgroundStimulusDispatcher(ICommandSender commandSender) : IStimulusDispatcher
|
||||
public class BackgroundStimulusDispatcher(ICommandSender commandSender, ITenantAccessor tenantAccessor) : IStimulusDispatcher
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<DispatchStimulusResponse> SendAsync(DispatchStimulusRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var command = new DispatchStimulusCommand(request);
|
||||
await commandSender.SendAsync(command, CommandStrategy.Background, cancellationToken);
|
||||
await commandSender.SendAsync(command, CommandStrategy.Background, CreateHeaders(), cancellationToken);
|
||||
return DispatchStimulusResponse.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private IDictionary<object, object> CreateHeaders()
|
||||
{
|
||||
return TenantHeaders.CreateHeaders(tenantAccessor.Tenant?.Id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ public class LocalWorkflowClient(
|
|||
SubStatus = workflowState.SubStatus,
|
||||
Incidents = workflowState.Incidents,
|
||||
Output = request.IncludeWorkflowOutput ? new Dictionary<string, object>(workflowState.Output) : null
|
||||
Bookmarks = workflowState.Bookmarks
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue