Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
@@ -0,0 +1,55 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Alterations.Bookmarks;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Alterations.Activities;
/// <summary>
/// Submits an alteration plan for execution.
/// </summary>
[Browsable(false)]
[Activity("Elsa", "Alterations", "Triggered when an Alteration Plan completed")]
public class AlterationPlanCompleted : Trigger
{
/// <inheritdoc />
public AlterationPlanCompleted(Variable<string> planId, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
PlanId = new Input<string>(planId);
}
/// <inheritdoc />
public AlterationPlanCompleted([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <summary>
/// The ID of the alteration plan.
/// </summary>
public Input<string> PlanId { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
if (context.IsTriggerOfWorkflow())
{
await context.CompleteActivityAsync();
return;
}
var planId = context.Get(PlanId)!;
var bookmarkPayload = new AlterationPlanCompletedPayload(planId);
context.CreateBookmark(bookmarkPayload, false);
}
/// <inheritdoc />
protected override object GetTriggerPayload(TriggerIndexingContext context)
{
var planId = context.Get(PlanId)!;
return new AlterationPlanCompletedPayload(planId);
}
}
@@ -0,0 +1,48 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Alterations.Core.Contracts;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Exceptions;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Alterations.Activities;
/// <summary>
/// Marks an alteration plan as completed.
/// </summary>
[Browsable(false)]
[Activity("Elsa", "Alterations", "Dispatches jobs for the specified Alteration Plan", Kind = ActivityKind.Task)]
public class CompleteAlterationPlan : CodeActivity
{
/// <inheritdoc />
public CompleteAlterationPlan(Variable<string> planId, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
PlanId = new Input<string>(planId);
}
/// <inheritdoc />
public CompleteAlterationPlan([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <summary>
/// The ID of the alteration plan.
/// </summary>
public Input<string> PlanId { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var cancellationToken = context.CancellationToken;
var planId = context.Get(PlanId)!;
var manager = context.GetRequiredService<IAlterationPlanManager>();
var plan = await manager.GetPlanAsync(planId, cancellationToken);
if (plan == null)
throw new FaultException(AlterationFaultCodes.PlanNotFound, AlterationFaultCategories.Alteration, DefaultFaultTypes.System, $"Alteration Plan with ID {planId} not found.");
await manager.CompletePlanAsync(plan, cancellationToken);
}
}
@@ -0,0 +1,73 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Filters;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Exceptions;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Alterations.Activities;
/// <summary>
/// Submits an alteration plan for execution.
/// </summary>
[Browsable(false)]
[Activity("Elsa", "Alterations", "Dispatches jobs for the specified Alteration Plan", Kind = ActivityKind.Task)]
public class DispatchAlterationJobs : CodeActivity
{
/// <inheritdoc />
public DispatchAlterationJobs(Variable<string> planId, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
PlanId = new Input<string>(planId);
}
/// <inheritdoc />
public DispatchAlterationJobs([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <summary>
/// The ID of the alteration plan.
/// </summary>
public Input<string> PlanId { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var cancellationToken = context.CancellationToken;
var planId = context.Get(PlanId)!;
var alterationPlanStore = context.GetRequiredService<IAlterationPlanStore>();
var planFilter = new AlterationPlanFilter
{
Id = planId
};
var plan = await alterationPlanStore.FindAsync(planFilter, cancellationToken);
if (plan == null)
throw new FaultException(AlterationFaultCodes.PlanNotFound, AlterationFaultCategories.Alteration, DefaultFaultTypes.System, $"Alteration Plan with ID {planId} not found.");
// Update status.
plan.Status = AlterationPlanStatus.Dispatching;
await alterationPlanStore.SaveAsync(plan, cancellationToken);
// Find all jobs for the plan and dispatch them.
var filter = new AlterationJobFilter
{
PlanId = plan.Id
};
var alterationJobStore = context.GetRequiredService<IAlterationJobStore>();
var alterationJobIds = await alterationJobStore.FindManyIdsAsync(filter, cancellationToken);
// Dispatch each job.
var alterationJobDispatcher = context.GetRequiredService<IAlterationJobDispatcher>();
foreach (var jobId in alterationJobIds)
await alterationJobDispatcher.DispatchAsync(jobId, cancellationToken);
// Update status.
plan.Status = AlterationPlanStatus.Running;
await alterationPlanStore.SaveAsync(plan, cancellationToken);
}
}
@@ -0,0 +1,104 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Filters;
using Elsa.Alterations.Core.Models;
using Elsa.Common;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Exceptions;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Alterations.Activities;
/// <summary>
/// Submits an alteration plan for execution.
/// </summary>
[Browsable(false)]
[Activity("Elsa", "Alterations", "Generates jobs for the specified Alteration Plan", Kind = ActivityKind.Task)]
public class GenerateAlterationJobs : CodeActivity<int>
{
/// <inheritdoc />
public GenerateAlterationJobs([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <inheritdoc />
public GenerateAlterationJobs(Variable<string> planId, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
PlanId = new Input<string>(planId);
}
/// <summary>
/// The ID of the submitted alteration plan.
/// </summary>
public Input<string> PlanId { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var plan = await GetPlanAsync(context);
await UpdatePlanStatusAsync(context, plan);
var workflowInstanceIds = (await FindMatchingWorkflowInstanceIdsAsync(context, plan.WorkflowInstanceFilter)).ToList();
if (workflowInstanceIds.Any())
await GenerateJobsAsync(context, plan, workflowInstanceIds);
context.SetResult(workflowInstanceIds.Count);
}
private async Task<AlterationPlan> GetPlanAsync(ActivityExecutionContext context)
{
var cancellationToken = context.CancellationToken;
var planId = context.Get(PlanId)!;
var alterationPlanStore = context.GetRequiredService<IAlterationPlanStore>();
var planFilter = new AlterationPlanFilter
{
Id = planId
};
var plan = await alterationPlanStore.FindAsync(planFilter, cancellationToken);
if (plan == null)
throw new FaultException(AlterationFaultCodes.PlanNotFound, AlterationFaultCategories.Alteration, DefaultFaultTypes.System, $"Alteration Plan with ID {planId} not found.");
return plan;
}
private async Task UpdatePlanStatusAsync(ActivityExecutionContext context, AlterationPlan plan)
{
var cancellationToken = context.CancellationToken;
var alterationPlanStore = context.GetRequiredService<IAlterationPlanStore>();
plan.Status = AlterationPlanStatus.Generating;
await alterationPlanStore.SaveAsync(plan, cancellationToken);
}
private async Task<IEnumerable<string>> FindMatchingWorkflowInstanceIdsAsync(ActivityExecutionContext context, AlterationWorkflowInstanceFilter filter)
{
var cancellationToken = context.CancellationToken;
var workflowInstanceFinder = context.GetRequiredService<IWorkflowInstanceFinder>();
return await workflowInstanceFinder.FindAsync(filter, cancellationToken);
}
private async Task GenerateJobsAsync(ActivityExecutionContext context, AlterationPlan plan, IEnumerable<string> workflowInstanceIds)
{
var cancellationToken = context.CancellationToken;
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
var systemClock = context.GetRequiredService<ISystemClock>();
var jobs = workflowInstanceIds.Select(workflowInstanceId => new AlterationJob
{
Id = identityGenerator.GenerateId(),
PlanId = plan.Id,
Status = AlterationJobStatus.Pending,
WorkflowInstanceId = workflowInstanceId,
CreatedAt = systemClock.UtcNow
})
.ToList();
var alterationJobStore = context.GetRequiredService<IAlterationJobStore>();
await alterationJobStore.SaveManyAsync(jobs, cancellationToken);
}
}
@@ -0,0 +1,52 @@
using System.ComponentModel;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Models;
using Elsa.Common;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
namespace Elsa.Alterations.Activities;
/// <summary>
/// Submits an alteration plan for execution.
/// </summary>
[Browsable(false)]
[Output(
DisplayName = "Plan ID",
Description = "The ID of the submitted alteration plan."
)]
[Activity("Elsa", "Alterations", "Submits an Alteration Plan", Kind = ActivityKind.Task)]
public class SubmitAlterationPlan : CodeActivity<string>
{
/// <summary>
/// The parameters for the alteration plan to be submitted.
/// </summary>
public Input<AlterationPlanParams> Params { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var systemClock = context.GetRequiredService<ISystemClock>();
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
var now = systemClock.UtcNow;
var planParams = context.Get(Params)!;
var plan = new AlterationPlan
{
Id = string.IsNullOrWhiteSpace(planParams.Id) ? identityGenerator.GenerateId() : planParams.Id,
Alterations = planParams.Alterations,
WorkflowInstanceFilter = planParams.Filter,
Status = AlterationPlanStatus.Pending,
CreatedAt = now
};
var alterationPlanStore = context.GetRequiredService<IAlterationPlanStore>();
var cancellationToken = context.CancellationToken;
await alterationPlanStore.SaveAsync(plan, cancellationToken);
Result.Set(context, plan.Id);
}
}
@@ -0,0 +1,62 @@
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Abstractions;
using Elsa.Alterations.Core.Contexts;
using Elsa.Extensions;
using Elsa.Workflows;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationHandlers;
/// <summary>
/// Cancels an activity.
/// </summary>
[UsedImplicitly]
public class CancelActivityHandler : AlterationHandlerBase<CancelActivity>
{
/// <inheritdoc />
protected override ValueTask HandleAsync(AlterationContext context, CancelActivity alteration)
{
if (alteration.ActivityInstanceId == null && alteration.ActivityId == null)
{
context.Fail("Either ActivityInstanceId or ActivityId must be specified");
return ValueTask.CompletedTask;
}
var activityExecutionContexts = GetActivityExecutionContexts(context, alteration).ToList();
if (!activityExecutionContexts.Any())
{
context.Fail(
alteration.ActivityInstanceId != null
? $"Activity execution context with ID {alteration.ActivityInstanceId} not found"
: $"Activity execution contexts for activity with ID {alteration.ActivityId} not found");
return ValueTask.CompletedTask;
}
context.Succeed(() => CancelAsync(activityExecutionContexts));
return ValueTask.CompletedTask;
}
private async Task CancelAsync(IEnumerable<ActivityExecutionContext> activityExecutionContexts)
{
foreach (var activityExecutionContext in activityExecutionContexts)
await CancelAsync(activityExecutionContext);
}
private async Task CancelAsync(ActivityExecutionContext activityExecutionContext)
{
await activityExecutionContext.CancelActivityAsync();
}
private static IEnumerable<ActivityExecutionContext> GetActivityExecutionContexts(AlterationContext context, CancelActivity alteration)
{
var workflowExecutionContext = context.WorkflowExecutionContext;
return alteration.ActivityInstanceId != null
? workflowExecutionContext.ActivityExecutionContexts.Where(x => x.Id == alteration.ActivityInstanceId)
: alteration.ActivityId != null
? workflowExecutionContext.ActivityExecutionContexts.Where(x => x.Activity.Id == alteration.ActivityId)
: Enumerable.Empty<ActivityExecutionContext>();
}
}
@@ -0,0 +1,22 @@
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Abstractions;
using Elsa.Alterations.Core.Contexts;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationHandlers;
/// <summary>
/// Upgrades the version of the workflow instance.
/// </summary>
[UsedImplicitly]
public class CancelHandler : AlterationHandlerBase<Cancel>
{
/// <inheritdoc />
protected override ValueTask HandleAsync(AlterationContext context, Cancel alteration)
{
context.WorkflowExecutionContext.Cancel();
context.Succeed();
return ValueTask.CompletedTask;
}
}
@@ -0,0 +1,37 @@
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Abstractions;
using Elsa.Alterations.Core.Contexts;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Alterations.AlterationHandlers;
/// <summary>
/// Upgrades the version of the workflow instance.
/// </summary>
[UsedImplicitly]
public class MigrateHandler : AlterationHandlerBase<Migrate>
{
/// <inheritdoc />
protected override async ValueTask HandleAsync(AlterationContext context, Migrate alteration)
{
var workflowDefinitionService = context.ServiceProvider.GetRequiredService<IWorkflowDefinitionService>();
var definitionId = context.Workflow.Identity.DefinitionId;
var targetVersion = alteration.TargetVersion;
var cancellationToken = context.CancellationToken;
var targetWorkflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(definitionId, VersionOptions.SpecificVersion(targetVersion), cancellationToken);
if (targetWorkflowDefinition == null)
{
context.Fail($"Workflow definition with ID {definitionId} and version {targetVersion} not found");
return;
}
var targetWorkflow = await workflowDefinitionService.MaterializeWorkflowAsync(targetWorkflowDefinition, cancellationToken);
await context.WorkflowExecutionContext.SetWorkflowGraphAsync(targetWorkflow);
context.Succeed();
}
}
@@ -0,0 +1,74 @@
using System.Diagnostics.CodeAnalysis;
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Abstractions;
using Elsa.Alterations.Core.Contexts;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Alterations.AlterationHandlers;
/// <summary>
/// Modifies a workflow variable.
/// </summary>
[UsedImplicitly]
public class ModifyVariableHandler : AlterationHandlerBase<ModifyVariable>
{
/// <inheritdoc />
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
protected override async ValueTask HandleAsync(AlterationContext context, ModifyVariable alteration)
{
var workflow = context.Workflow;
var cancellationToken = context.CancellationToken;
var variable = await FindVariable(context, alteration, workflow, cancellationToken);
if (variable == null)
{
context.Fail($"Variable with ID {alteration.VariableId} not found");
return;
}
var convertedValue = variable.ParseValue(alteration.Value);
UpdateVariable(context, variable, convertedValue);
context.Succeed();
}
private void UpdateVariable(AlterationContext context, Variable variable, object? value)
{
var activityExecutionContext = FindActivityExecutionContextContainingVariable(context, variable);
if (activityExecutionContext == null)
{
context.Fail($"Activity execution context containing variable with ID {variable.Id} not found");
return;
}
variable.Set(activityExecutionContext, value);
}
private ActivityExecutionContext? FindActivityExecutionContextContainingVariable(AlterationContext context, Variable variable)
{
var query =
from activityExecutionContext in context.WorkflowExecutionContext.ActivityExecutionContexts
from var in activityExecutionContext.Variables
where var.Id == variable.Id
select activityExecutionContext;
return query.FirstOrDefault();
}
private async Task<Variable?> FindVariable(AlterationContext context, ModifyVariable alteration, Workflow workflow, CancellationToken cancellationToken)
{
var activityVisitor = context.ServiceProvider.GetRequiredService<IActivityVisitor>();
var graph = await activityVisitor.VisitAsync(workflow, cancellationToken);
var flattenedList = graph.Flatten().ToList();
return flattenedList
.Where(x => x.Activity is IVariableContainer)
.SelectMany(x => ((IVariableContainer)x.Activity).Variables)
.FirstOrDefault(x => x.Id == alteration.VariableId);
}
}
@@ -0,0 +1,85 @@
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Abstractions;
using Elsa.Alterations.Core.Contexts;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Signals;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationHandlers;
/// <summary>
/// Schedules an activity for execution.
/// </summary>
[UsedImplicitly]
public class ScheduleActivityHandler : AlterationHandlerBase<ScheduleActivity>
{
/// <inheritdoc />
protected override async ValueTask HandleAsync(AlterationContext context, ScheduleActivity alteration)
{
if (alteration.ActivityInstanceId == null && alteration.ActivityId == null)
{
context.Fail("Either ActivityInstanceId or ActivityId must be specified");
return;
}
var workflowExecutionContext = context.WorkflowExecutionContext;
var existingActivityExecutionContext = GetActivityExecutionContext(context, alteration);
if (existingActivityExecutionContext != null)
{
// If the activity is in a faulted state, reset it to Running.
if (existingActivityExecutionContext.Status == ActivityStatus.Faulted)
existingActivityExecutionContext.TransitionTo(ActivityStatus.Running);
// Schedule the activity execution context.
var parentContext = existingActivityExecutionContext.ParentActivityExecutionContext;
await parentContext!.SendSignalAsync(new ScheduleChildActivity(existingActivityExecutionContext));
context.Succeed();
return;
}
// Schedule a new activity instance.
var activityId = alteration.ActivityId;
if (activityId == null)
{
context.Fail("No existing activity execution context was found and no activity ID was specified");
return;
}
var activityNode = workflowExecutionContext.FindNodeByActivityId(activityId);
if (activityNode == null)
{
context.Fail($"Activity with ID {activityId} not found");
return;
}
// Find the parent activity execution context within which to schedule the activity.
var parentActivityContexts = workflowExecutionContext.ActivityExecutionContexts.Reverse().ToList();
var parentExecutionContext =
(from ancestorNode in activityNode.Ancestors()
from parentActivityContext in parentActivityContexts
where parentActivityContext.Activity.Id == ancestorNode.Activity.Id
select parentActivityContext).FirstOrDefault();
if (parentExecutionContext == null)
{
context.Fail($"Could not find parent activity execution context for activity with ID {activityId}");
return;
}
await parentExecutionContext.SendSignalAsync(new ScheduleChildActivity(activityNode.Activity));
context.Succeed();
}
private static ActivityExecutionContext? GetActivityExecutionContext(AlterationContext context, ScheduleActivity alteration)
{
if (alteration.ActivityInstanceId != null)
return context.WorkflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == alteration.ActivityInstanceId);
return null;
}
}
@@ -0,0 +1,10 @@
using Elsa.Alterations.Core.Abstractions;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationTypes;
/// <summary>
/// Cancels the workflow instances in an alteration plan.
/// </summary>
[UsedImplicitly]
public class Cancel : AlterationBase;
@@ -0,0 +1,21 @@
using Elsa.Alterations.Core.Abstractions;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationTypes;
/// <summary>
/// Cancels an activity.
/// </summary>
[UsedImplicitly]
public class CancelActivity : AlterationBase
{
/// <summary>
/// The ID of the activity to be cancelled. If not specified, the activity instance ID will be used.
/// </summary>
public string? ActivityId { get; set; }
/// <summary>
/// The ID of the activity instance to be cancelled. If specified, overrides <see cref="ActivityId"/>.
/// </summary>
public string? ActivityInstanceId { get; set; }
}
@@ -0,0 +1,16 @@
using Elsa.Alterations.Core.Abstractions;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationTypes;
/// <summary>
/// Migrates a workflow instance to a newer version.
/// </summary>
[UsedImplicitly]
public class Migrate : AlterationBase
{
/// <summary>
/// The target version to upgrade to.
/// </summary>
public int TargetVersion { get; set; }
}
@@ -0,0 +1,22 @@
using Elsa.Alterations.Core.Abstractions;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationTypes;
/// <summary>
/// Modifies a variable.
/// </summary>
[UsedImplicitly]
public class ModifyVariable : AlterationBase
{
/// <summary>
/// The ID of the variable to modify.
/// </summary>
public string VariableId { get; set; } = default!;
/// <summary>
/// The new value of the variable.
/// </summary>
public object? Value { get; set; }
}
@@ -0,0 +1,21 @@
using Elsa.Alterations.Core.Abstractions;
using JetBrains.Annotations;
namespace Elsa.Alterations.AlterationTypes;
/// <summary>
/// Schedules an activity for execution.
/// </summary>
[UsedImplicitly]
public class ScheduleActivity : AlterationBase
{
/// <summary>
/// The ID of the next activity to be scheduled. If not specified, the ActivityInstanceId will be used.
/// </summary>
public string? ActivityId { get; set; }
/// <summary>
/// The ID of the activity instance to be scheduled. If not specified, the ActivityId will be used.
/// </summary>
public string? ActivityInstanceId { get; set; }
}
@@ -0,0 +1,9 @@
using Elsa.Alterations.Activities;
namespace Elsa.Alterations.Bookmarks;
/// <summary>
/// Represents bookmark payload for the <see cref="AlterationPlanCompleted"/> trigger.
/// </summary>
/// <param name="PlanId">The ID of the alteration plan.</param>
public record AlterationPlanCompletedPayload(string PlanId);
@@ -0,0 +1,6 @@
namespace Elsa.Alterations;
public static class AlterationFaultCategories
{
public const string Alteration = "Alteration";
}
@@ -0,0 +1,6 @@
namespace Elsa.Alterations;
public static class AlterationFaultCodes
{
public const string PlanNotFound = "PlanNotFound";
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides alterations for workflow instances.
</Description>
<PackageTags>elsa module alterations</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj"/>
<ProjectReference Include="..\Elsa.Alterations.Core\Elsa.Alterations.Core.csproj"/>
</ItemGroup>
</Project>
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=constants/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -0,0 +1,28 @@
using Elsa.Abstractions;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Alterations.Endpoints.Alterations.DryRun;
/// <summary>
/// Determines which workflow instances a "Submit" request would target without actually running an alteration.
/// </summary>
[PublicAPI]
public class DryRun(IWorkflowInstanceFinder workflowInstanceFinder) : ElsaEndpoint<AlterationWorkflowInstanceFilter, Response>
{
/// <inheritdoc />
public override void Configure()
{
Post("/alterations/dry-run");
ConfigurePermissions("run:alterations");
}
/// <inheritdoc />
public override async Task HandleAsync(AlterationWorkflowInstanceFilter filter, CancellationToken cancellationToken)
{
var workflowInstanceIds = await workflowInstanceFinder.FindAsync(filter, cancellationToken);
var response = new Response(workflowInstanceIds.ToList());
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Alterations.Endpoints.Alterations.DryRun;
/// <summary>
/// The response from the <see cref="DryRun"/> endpoint.
/// </summary>
public record Response(ICollection<string> WorkflowInstanceIds);
@@ -0,0 +1,52 @@
using Elsa.Abstractions;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Filters;
using JetBrains.Annotations;
namespace Elsa.Alterations.Endpoints.Alterations.Get;
/// <summary>
/// Gets an alteration plan and its associated jobs.
/// </summary>
[PublicAPI]
public class Get : ElsaEndpointWithoutRequest<Response>
{
private readonly IAlterationPlanStore _alterationPlanStore;
private readonly IAlterationJobStore _alterationJobStore;
/// <inheritdoc />
public Get(IAlterationPlanStore alterationPlanStore, IAlterationJobStore alterationJobStore)
{
_alterationPlanStore = alterationPlanStore;
_alterationJobStore = alterationJobStore;
}
/// <inheritdoc />
public override void Configure()
{
Get("/alterations/{id}");
ConfigurePermissions("read:alterations");
}
/// <inheritdoc />
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var planId = Route<string>("id");
// Load the plan.
var plan = await _alterationPlanStore.FindAsync(new AlterationPlanFilter { Id = planId }, cancellationToken);
if (plan == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
// Load the jobs.
var jobs = (await _alterationJobStore.FindManyAsync(new AlterationJobFilter { PlanId = planId }, cancellationToken)).ToList();
// Write response.
var response = new Response(plan, jobs);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,10 @@
using Elsa.Alterations.Core.Entities;
namespace Elsa.Alterations.Endpoints.Alterations.Get;
/// <summary>
/// Represents a response to a request to get an alteration plan.
/// </summary>
/// <param name="Plan">The alteration plan.</param>
/// <param name="Jobs">The jobs created for the plan.</param>
public record Response(AlterationPlan Plan, ICollection<AlterationJob> Jobs);
@@ -0,0 +1,43 @@
using Elsa.Abstractions;
using Elsa.Alterations.Core.Contracts;
using JetBrains.Annotations;
namespace Elsa.Alterations.Endpoints.Alterations.Run;
/// <summary>
/// Executes an alteration plan by targeting workflow instances by ID.
/// </summary>
[PublicAPI]
public class Run : ElsaEndpoint<Request, Response>
{
private readonly IAlterationRunner _alterationRunner;
private readonly IAlteredWorkflowDispatcher _workflowDispatcher;
/// <inheritdoc />
public Run(IAlterationRunner alterationRunner, IAlteredWorkflowDispatcher workflowDispatcher)
{
_alterationRunner = alterationRunner;
_workflowDispatcher = workflowDispatcher;
}
/// <inheritdoc />
public override void Configure()
{
Post("/alterations/run");
ConfigurePermissions("run:alterations");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
// Run the alterations.
var results = await _alterationRunner.RunAsync(request.WorkflowInstanceIds, request.Alterations, cancellationToken);
// Schedule each successfully updated workflow containing scheduled work.
await _workflowDispatcher.DispatchAsync(results, cancellationToken);
// Write response.
var response = new Response(results);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,20 @@
using Elsa.Alterations.Core.Contracts;
// ReSharper disable CollectionNeverUpdated.Global
namespace Elsa.Alterations.Endpoints.Alterations.Run;
/// <summary>
/// A plan that contains a list of alterations to be applied to a set of workflow instances.
/// </summary>
public class Request
{
/// <summary>
/// The alterations to be applied.
/// </summary>
public ICollection<IAlteration> Alterations { get; set; } = new List<IAlteration>();
/// <summary>
/// The IDs of the workflow instances that this plan applies to.
/// </summary>
public ICollection<string> WorkflowInstanceIds { get; set; } = new List<string>();
}
@@ -0,0 +1,8 @@
using Elsa.Alterations.Core.Results;
namespace Elsa.Alterations.Endpoints.Alterations.Run;
/// <summary>
/// The response from the <see cref="Run"/> endpoint.
/// </summary>
public record Response(ICollection<RunAlterationsResult> Results);
@@ -0,0 +1,45 @@
using Elsa.Abstractions;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Models;
using Elsa.Common;
using Elsa.Workflows;
using JetBrains.Annotations;
namespace Elsa.Alterations.Endpoints.Alterations.Submit;
/// <summary>
/// Submits an alteration plan to be executed targeting workflow instances by a filter.
/// </summary>
[PublicAPI]
public class Submit : ElsaEndpoint<AlterationPlanParams, Response>
{
private readonly IAlterationPlanScheduler _alterationPlanScheduler;
private readonly IIdentityGenerator _identityGenerator;
private readonly ISystemClock _systemClock;
/// <inheritdoc />
public Submit(IAlterationPlanScheduler alterationPlanScheduler, IIdentityGenerator identityGenerator, ISystemClock systemClock)
{
_alterationPlanScheduler = alterationPlanScheduler;
_identityGenerator = identityGenerator;
_systemClock = systemClock;
}
/// <inheritdoc />
public override void Configure()
{
Post("/alterations/submit");
ConfigurePermissions("run:alterations");
}
/// <inheritdoc />
public override async Task HandleAsync(AlterationPlanParams planParams, CancellationToken cancellationToken)
{
// Submit the plan.
var planId = await _alterationPlanScheduler.SubmitAsync(planParams, cancellationToken);
// Write response.
var response = new Response(planId);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Alterations.Endpoints.Alterations.Submit;
/// <summary>
/// The response from the <see cref="Execute"/> endpoint.
/// </summary>
public record Response(string PlanId);
@@ -0,0 +1,77 @@
using Elsa.Abstractions;
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Results;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Requests;
using JetBrains.Annotations;
namespace Elsa.Alterations.Endpoints.Workflows.Retry;
/// <summary>
/// Retries the specified workflow instances.
/// </summary>
[PublicAPI]
public class Retry : ElsaEndpoint<Request, Response>
{
private readonly IAlterationRunner _alterationRunner;
private readonly IWorkflowDispatcher _workflowDispatcher;
private readonly IWorkflowInstanceStore _workflowInstanceStore;
/// <inheritdoc />
public Retry(IAlterationRunner alterationRunner, IWorkflowDispatcher workflowDispatcher, IWorkflowInstanceStore workflowInstanceStore)
{
_alterationRunner = alterationRunner;
_workflowDispatcher = workflowDispatcher;
_workflowInstanceStore = workflowInstanceStore;
}
/// <inheritdoc />
public override void Configure()
{
Routes("/alterations/workflows/retry");
Verbs(FastEndpoints.Http.GET, FastEndpoints.Http.POST);
ConfigurePermissions("run:alterations");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var allResults = new List<RunAlterationsResult>();
// Load each workflow instance.
var workflowInstances = (await _workflowInstanceStore.FindManyAsync(new WorkflowInstanceFilter { Ids = request.WorkflowInstanceIds }, cancellationToken)).ToList();
foreach (var workflowInstance in workflowInstances)
{
// Setup an alteration plan.
var activityIds = GetActivityIds(request, workflowInstance).ToList();
var alterations = activityIds.Select(activityId => new ScheduleActivity { ActivityId = activityId }).Cast<IAlteration>().ToList();
// Run the plan.
var results = await _alterationRunner.RunAsync(request.WorkflowInstanceIds, alterations, cancellationToken);
allResults.AddRange(results);
// Schedule updated workflow.
await _workflowDispatcher.DispatchAsync(new DispatchWorkflowInstanceRequest(workflowInstance.Id), cancellationToken: cancellationToken);
}
// Write response.
var response = new Response(allResults);
await SendOkAsync(response, cancellationToken);
}
private IEnumerable<string> GetActivityIds(Request request, WorkflowInstance workflowInstance)
{
// If activity IDs are explicitly specified, use them.
if (request.ActivityIds?.Any() == true)
return request.ActivityIds;
// Otherwise, select IDs of all faulted activities.
return workflowInstance.WorkflowState.Incidents.Select(x => x.ActivityId).Distinct().ToList();
}
}
@@ -0,0 +1,17 @@
namespace Elsa.Alterations.Endpoints.Workflows.Retry;
/// <summary>
/// A request to retry one or more faulted workflow instances.
/// </summary>
public class Request
{
/// <summary>
/// The IDs of the workflow instances that have incidents to be retried.
/// </summary>
public ICollection<string> WorkflowInstanceIds { get; set; } = new List<string>();
/// <summary>
/// An optional list of explicitly specified activity IDs to retry. If omitted, all faulted activities will be retried.
/// </summary>
public ICollection<string>? ActivityIds { get; set; }
}
@@ -0,0 +1,8 @@
using Elsa.Alterations.Core.Results;
namespace Elsa.Alterations.Endpoints.Workflows.Retry;
/// <summary>
/// The response from the <see cref="Retry"/> endpoint.
/// </summary>
public record Response(ICollection<RunAlterationsResult> Results);
@@ -0,0 +1,19 @@
using Elsa.Alterations.Features;
using Elsa.Extensions;
using Elsa.Features.Services;
namespace Elsa.Alterations.Extensions;
/// <summary>
/// Adds the <see cref="AlterationsFeature"/>.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Adds the <see cref="AlterationsFeature"/>.
/// </summary>
public static IModule UseAlterations(this IModule module, Action<AlterationsFeature>? configure = default)
{
return module.Use(configure);
}
}
@@ -0,0 +1,27 @@
using Elsa.Alterations.AlterationHandlers;
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Extensions;
using Elsa.Alterations.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Alterations.Extensions;
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds various alteration handlers.
/// </summary>
public static IServiceCollection AddAlterations(this IServiceCollection services)
{
services.AddAlteration<Migrate, MigrateHandler>();
services.AddAlteration<ModifyVariable, ModifyVariableHandler>();
services.AddAlteration<ScheduleActivity, ScheduleActivityHandler>();
services.AddAlteration<CancelActivity, CancelActivityHandler>();
services.AddAlteration<Cancel, CancelHandler>();
services.AddNotificationHandlersFrom<AlterationsFeature>();
return services;
}
}
@@ -0,0 +1,76 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Extensions;
using Elsa.Alterations.Core.Stores;
using Elsa.Alterations.Extensions;
using Elsa.Alterations.Services;
using Elsa.Alterations.Workflows;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Alterations.Features;
/// <summary>
/// Adds the Elsa alterations services.
/// </summary>
public class AlterationsFeature : FeatureBase
{
/// <inheritdoc />
public AlterationsFeature(IModule module) : base(module)
{
}
/// <summary>
/// Gets or sets the factory for the alteration plan store.
/// </summary>
public Func<IServiceProvider, IAlterationPlanStore> AlterationPlanStoreFactory { get; set; } = sp => sp.GetRequiredService<MemoryAlterationPlanStore>();
/// <summary>
/// Gets or sets the factory for the alteration job store.
/// </summary>
public Func<IServiceProvider, IAlterationJobStore> AlterationJobStoreFactory { get; set; } = sp => sp.GetRequiredService<MemoryAlterationJobStore>();
/// <summary>
/// Gets or sets the factory for the alteration job dispatcher.
/// </summary>
public Func<IServiceProvider, IAlterationJobDispatcher> AlterationJobDispatcherFactory { get; set; } = sp => sp.GetRequiredService<BackgroundAlterationJobDispatcher>();
/// <summary>
/// Adds an alteration and its handler.
/// </summary>
/// <typeparam name="T">The type of alteration.</typeparam>
/// <typeparam name="THandler">The type of alteration handler.</typeparam>
public AlterationsFeature AddAlteration<T, THandler>() where T : class, IAlteration where THandler : class, IAlterationHandler
{
Services.AddAlteration<T, THandler>();
return this;
}
/// <inheritdoc />
public override void Configure()
{
Module.AddFastEndpointsAssembly<AlterationsFeature>();
Module.AddWorkflow<ExecuteAlterationPlanWorkflow>();
}
/// <inheritdoc />
public override void Apply()
{
Services.AddScoped<IAlterationPlanManager, AlterationPlanManager>();
Services.AddAlterations();
Services.AddAlterationsCore();
Services.AddScoped<BackgroundAlterationJobDispatcher>();
Services.AddScoped<IAlterationPlanScheduler, DefaultAlterationPlanScheduler>();
Services.AddScoped<IAlterationJobRunner, DefaultAlterationJobRunner>();
Services.AddScoped<IAlterationRunner, DefaultAlterationRunner>();
Services.AddMemoryStore<AlterationPlan, MemoryAlterationPlanStore>();
Services.AddMemoryStore<AlterationJob, MemoryAlterationJobStore>();
Services.AddScoped(AlterationPlanStoreFactory);
Services.AddScoped(AlterationJobStoreFactory);
Services.AddScoped(AlterationJobDispatcherFactory);
}
}
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,27 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Notifications;
using Elsa.Mediator.Contracts;
using JetBrains.Annotations;
namespace Elsa.Alterations.Handlers;
/// <summary>
/// Handles <see cref="AlterationJobCompleted"/> notifications and updates the plan status if all jobs are completed.
/// </summary>
[UsedImplicitly]
public class AlterationJobCompletedHandler(IAlterationPlanManager manager) : INotificationHandler<AlterationJobCompleted>
{
/// <inheritdoc />
public async Task HandleAsync(AlterationJobCompleted notification, CancellationToken cancellationToken)
{
var job = notification.Job;
var planId = job.PlanId;
var plan = (await manager.GetPlanAsync(planId, cancellationToken))!;
var allJobsCompleted = await manager.GetIsAllJobsCompletedAsync(planId, cancellationToken);
if(!allJobsCompleted)
return;
await manager.CompletePlanAsync(plan, cancellationToken);
}
}
@@ -0,0 +1,31 @@
using Elsa.Alterations.Bookmarks;
using Elsa.Alterations.Core.Notifications;
using Elsa.Mediator.Contracts;
using Elsa.Workflows;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Alterations.Handlers;
/// <summary>
/// Handles <see cref="AlterationPlanCompleted"/> notifications and triggers any workflows that are waiting for the plan to complete.
/// </summary>
[UsedImplicitly]
public class AlterationPlanCompletedHandler(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler<AlterationPlanCompleted>
{
/// <inheritdoc />
public async Task HandleAsync(AlterationPlanCompleted notification, CancellationToken cancellationToken)
{
// Trigger any workflow instances that are waiting for the plan to complete.
var planId = notification.Plan.Id;
var bookmarkPayload = new AlterationPlanCompletedPayload(planId);
var activityTypeName = ActivityTypeNameHelper.GenerateTypeName<Activities.AlterationPlanCompleted>();
var item = new NewBookmarkQueueItem
{
ActivityTypeName = activityTypeName,
StimulusHash = stimulusHasher.Hash(activityTypeName, bookmarkPayload)
};
await bookmarkQueue.EnqueueAsync(item, cancellationToken);
}
}
@@ -0,0 +1,43 @@
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Notifications;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Requests;
using JetBrains.Annotations;
namespace Elsa.Alterations.Handlers;
/// <summary>
/// Resumes a workflow instance when an alteration job has been completed and the workflow contains scheduled work.
/// </summary>
[UsedImplicitly]
public class ResumeWorkflowInstance : INotificationHandler<AlterationJobCompleted>
{
private readonly IWorkflowDispatcher _workflowDispatcher;
/// <summary>
/// Initializes a new instance of the <see cref="ResumeWorkflowInstance"/> class.
/// </summary>
public ResumeWorkflowInstance(IWorkflowDispatcher workflowDispatcher)
{
_workflowDispatcher = workflowDispatcher;
}
/// <inheritdoc />
public async Task HandleAsync(AlterationJobCompleted notification, CancellationToken cancellationToken)
{
var job = notification.Job;
// Check if job is completed.
if(job.Status != AlterationJobStatus.Completed)
return;
// Check if the workflow contains scheduled work.
if(!notification.WorkflowContainsScheduledWork)
return;
// Resume workflow instance.
await _workflowDispatcher.DispatchAsync(new DispatchWorkflowInstanceRequest(job.WorkflowInstanceId), cancellationToken: cancellationToken);
}
}
@@ -0,0 +1,58 @@
using Elsa.Alterations.Core.Contexts;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Models;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Pipelines.WorkflowExecution;
namespace Elsa.Alterations.Middleware.Workflows;
/// <summary>
/// Middleware that runs alterations.
/// </summary>
internal class RunAlterationsMiddleware(WorkflowMiddlewareDelegate next, IEnumerable<IAlterationHandler> handlers) : WorkflowExecutionMiddleware(next)
{
public static readonly object AlterationsPropertyKey = new();
public static readonly object AlterationsLogPropertyKey = new();
public override async ValueTask InvokeAsync(WorkflowExecutionContext context)
{
var alterations = (IEnumerable<IAlteration>)(context.TransientProperties.GetValue(AlterationsPropertyKey) ?? throw new InvalidOperationException("No alterations found in the transient properties."));
var log = (AlterationLog)(context.TransientProperties.GetValue(AlterationsLogPropertyKey) ?? throw new InvalidOperationException("No alteration log found in the transient properties."));
await RunAsync(context, alterations, log, context.CancellationToken);
}
private async Task RunAsync(WorkflowExecutionContext workflowExecutionContext, IEnumerable<IAlteration> alterations, AlterationLog log, CancellationToken cancellationToken = default)
{
var commitActions = new List<Func<Task>>();
foreach (var alteration in alterations)
{
// Find handlers.
var supportedHandlers = handlers.Where(x => x.CanHandle(alteration)).ToList();
foreach (var handler in supportedHandlers)
{
// Execute handler.
var alterationContext = new AlterationContext(alteration, workflowExecutionContext, log, cancellationToken);
await handler.HandleAsync(alterationContext);
// If the handler has failed, exit.
if (alterationContext.HasFailed)
return;
// Collect the commit handler, if any.
if (alterationContext.CommitAction != null)
commitActions.Add(alterationContext.CommitAction);
}
}
// Execute commit handlers.
foreach (var commitAction in commitActions)
await commitAction();
// Add alteration logs to the workflow execution log.
foreach (var alterationLogEntry in log.LogEntries)
workflowExecutionContext.AddExecutionLogEntry(alterationLogEntry.EventName ?? alterationLogEntry.Message, alterationLogEntry.Message);
}
}
@@ -0,0 +1,43 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Filters;
using Elsa.Alterations.Core.Notifications;
using Elsa.Common;
using Elsa.Mediator.Contracts;
namespace Elsa.Alterations.Services;
/// <inheritdoc />
public class AlterationPlanManager(IAlterationPlanStore planStore, IAlterationJobStore jobStore, ISystemClock systemClock, INotificationSender notificationSender) : IAlterationPlanManager
{
/// <inheritdoc />
public async Task<AlterationPlan?> GetPlanAsync(string planId, CancellationToken cancellationToken = default)
{
var planFilter = new AlterationPlanFilter { Id = planId };
return await planStore.FindAsync(planFilter, cancellationToken);
}
/// <inheritdoc />
public async Task<bool> GetIsAllJobsCompletedAsync(string planId, CancellationToken cancellationToken = default)
{
// Check if all jobs are completed.
var jobFilter = new AlterationJobFilter
{
PlanId = planId,
Statuses = [AlterationJobStatus.Pending, AlterationJobStatus.Running]
};
return await jobStore.CountAsync(jobFilter, cancellationToken) == 0;
}
/// <inheritdoc />
public async Task CompletePlanAsync(AlterationPlan plan, CancellationToken cancellationToken = default)
{
plan.Status = AlterationPlanStatus.Completed;
plan.CompletedAt = systemClock.UtcNow;
await planStore.SaveAsync(plan, cancellationToken);
await notificationSender.SendAsync(new AlterationPlanCompleted(plan), cancellationToken);
}
}
@@ -0,0 +1,25 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Mediator.Contracts;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Alterations.Services;
/// <summary>
/// Dispatches an alteration job for execution using an in-memory channel.
/// </summary>
public class BackgroundAlterationJobDispatcher(IJobQueue jobQueue, IServiceScopeFactory scopeFactory) : IAlterationJobDispatcher
{
/// <inheritdoc />
public ValueTask DispatchAsync(string jobId, CancellationToken cancellationToken = default)
{
jobQueue.Enqueue(ct => ExecuteJobAsync(jobId, ct));
return default;
}
private async Task ExecuteJobAsync(string alterationJobId, CancellationToken cancellationToken)
{
using var scope = scopeFactory.CreateScope();
var alterationJobRunner = scope.ServiceProvider.GetRequiredService<IAlterationJobRunner>();
await alterationJobRunner.RunAsync(alterationJobId, cancellationToken);
}
}
@@ -0,0 +1,58 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Filters;
using Elsa.Alterations.Core.Notifications;
using Elsa.Common;
using Elsa.Mediator.Contracts;
namespace Elsa.Alterations.Services;
/// <inheritdoc />
public class DefaultAlterationJobRunner : IAlterationJobRunner
{
private readonly IAlterationPlanStore _alterationPlanStore;
private readonly IAlterationJobStore _alterationJobStore;
private readonly IAlterationRunner _alterationRunner;
private readonly INotificationSender _notificationSender;
private readonly ISystemClock _systemClock;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAlterationJobRunner"/> class.
/// </summary>
public DefaultAlterationJobRunner(
IAlterationPlanStore alterationPlanStore,
IAlterationJobStore alterationJobStore,
IAlterationRunner alterationRunner,
INotificationSender notificationSender,
ISystemClock systemClock)
{
_alterationPlanStore = alterationPlanStore;
_alterationJobStore = alterationJobStore;
_alterationRunner = alterationRunner;
_notificationSender = notificationSender;
_systemClock = systemClock;
}
/// <inheritdoc />
public async Task<AlterationJob> RunAsync(string jobId, CancellationToken cancellationToken = default)
{
var job = (await _alterationJobStore.FindAsync(new AlterationJobFilter { Id = jobId }, cancellationToken))!;
var plan = (await _alterationPlanStore.FindAsync(new AlterationPlanFilter { Id = job.PlanId }, cancellationToken))!;
var workflowInstanceId = job.WorkflowInstanceId;
job.Status = AlterationJobStatus.Running;
job.StartedAt = _systemClock.UtcNow;
await _alterationJobStore.SaveAsync(job, cancellationToken);
var result = await _alterationRunner.RunAsync(workflowInstanceId, plan.Alterations, cancellationToken);
job.Status = result.IsSuccessful ? AlterationJobStatus.Completed : AlterationJobStatus.Failed;
job.Log = result.Log.LogEntries.ToList();
job.CompletedAt = _systemClock.UtcNow;
await _alterationJobStore.SaveAsync(job, cancellationToken);
await _notificationSender.SendAsync(new AlterationJobCompleted(job, result.WorkflowHasScheduledWork), cancellationToken);
return job;
}
}
@@ -0,0 +1,59 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Models;
using Elsa.Alterations.Workflows;
using Elsa.Common;
using Elsa.Common.Models;
using Elsa.Workflows;
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Requests;
namespace Elsa.Alterations.Services;
/// <summary>
/// Stores the new plan and schedules it for immediate execution.
/// </summary>
public class DefaultAlterationPlanScheduler : IAlterationPlanScheduler
{
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly IWorkflowDispatcher _workflowDispatcher;
private readonly IIdentityGenerator _identityGenerator;
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAlterationPlanScheduler"/> class.
/// </summary>
public DefaultAlterationPlanScheduler(IWorkflowDefinitionService workflowDefinitionService, IWorkflowDispatcher workflowDispatcher, IIdentityGenerator identityGenerator, IJsonSerializer jsonSerializer)
{
_workflowDefinitionService = workflowDefinitionService;
_workflowDispatcher = workflowDispatcher;
_identityGenerator = identityGenerator;
_jsonSerializer = jsonSerializer;
}
/// <inheritdoc />
public async Task<string> SubmitAsync(AlterationPlanParams planParams, CancellationToken cancellationToken = default)
{
if(string.IsNullOrWhiteSpace(planParams.Id))
planParams.Id = _identityGenerator.GenerateId();
var definitionId = ExecuteAlterationPlanWorkflow.WorkflowDefinitionId;
var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Published, cancellationToken);
if (workflowGraph == null)
throw new($"Workflow definition with ID '{definitionId}' not found");
var serializedPlan = _jsonSerializer.Serialize(planParams);
var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id)
{
Input = new Dictionary<string, object>
{
["Plan"] = serializedPlan
}
};
await _workflowDispatcher.DispatchAsync(request, cancellationToken);
return planParams.Id;
}
}
@@ -0,0 +1,100 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Models;
using Elsa.Alterations.Core.Results;
using Elsa.Alterations.Middleware.Workflows;
using Elsa.Common;
using Elsa.Workflows;
using Elsa.Workflows.CommitStates;
using Elsa.Workflows.Management;
using Elsa.Workflows.Pipelines.WorkflowExecution;
using Elsa.Workflows.Runtime;
using Microsoft.Extensions.Logging;
namespace Elsa.Alterations.Services;
/// <inheritdoc />
public class DefaultAlterationRunner(
IWorkflowRuntime workflowRuntime,
IWorkflowExecutionPipeline workflowExecutionPipeline,
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowStateExtractor workflowStateExtractor,
ICommitStateHandler commitStateHandler,
ISystemClock systemClock,
IServiceProvider serviceProvider)
: IAlterationRunner
{
/// <inheritdoc />
public async Task<ICollection<RunAlterationsResult>> RunAsync(IEnumerable<string> workflowInstanceIds, IEnumerable<IAlteration> alterations, CancellationToken cancellationToken = default)
{
var results = new List<RunAlterationsResult>();
var alterationList = alterations as ICollection<IAlteration> ?? alterations.ToList();
foreach (var workflowInstanceId in workflowInstanceIds)
{
var result = await RunAsync(workflowInstanceId, alterationList, cancellationToken);
results.Add(result);
}
return results;
}
/// <inheritdoc />
public async Task<RunAlterationsResult> RunAsync(string workflowInstanceId, IEnumerable<IAlteration> alterations, CancellationToken cancellationToken = default)
{
var log = new AlterationLog(systemClock);
var result = new RunAlterationsResult(workflowInstanceId, log);
var workflowClient = await workflowRuntime.CreateClientAsync(workflowInstanceId, cancellationToken: cancellationToken);
// Load workflow instance.
var workflowState = await workflowClient.ExportStateAsync(cancellationToken);
if (workflowState == null)
{
log.Add($"Workflow instance with ID '{workflowInstanceId}' not found.", LogLevel.Error);
return result;
}
// Load workflow definition.
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowState.DefinitionVersionId, cancellationToken);
if (workflowGraph == null)
{
log.Add($"Workflow definition with ID '{workflowState.DefinitionVersionId}' not found.", LogLevel.Error);
return result;
}
// Create workflow execution context.
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(serviceProvider, workflowGraph, workflowState, cancellationToken: cancellationToken);
workflowExecutionContext.TransientProperties.Add(RunAlterationsMiddleware.AlterationsPropertyKey, alterations);
workflowExecutionContext.TransientProperties.Add(RunAlterationsMiddleware.AlterationsLogPropertyKey, log);
// Build a new workflow execution pipeline.
var pipelineBuilder = new WorkflowExecutionPipelineBuilder(serviceProvider);
workflowExecutionPipeline.ConfigurePipelineBuilder(pipelineBuilder);
// Replace the terminal DefaultActivitySchedulerMiddleware with the RunAlterationsMiddleware terminal.
pipelineBuilder.ReplaceTerminal<RunAlterationsMiddleware>();
// Build modified pipeline.
var pipeline = pipelineBuilder.Build();
// Execute the pipeline.
await pipeline(workflowExecutionContext);
// Extract workflow state.
workflowState = workflowStateExtractor.Extract(workflowExecutionContext);
// Commit workflow state.
await commitStateHandler.CommitAsync(workflowExecutionContext, workflowState, cancellationToken);
// Apply updated workflow state.
// TODO: Importing back into the workflow runtime makes sense, but this also causes another SAVE ction of the workflow instance in the DB, which also happens in the previous step during the commit action.
// Can we avoid this? Perhaps we need more granular control over when we purge and when we save to DB.
await workflowClient.ImportStateAsync(workflowState, cancellationToken);
// Check if the workflow has scheduled work.
result.WorkflowHasScheduledWork = workflowExecutionContext.Scheduler.HasAny;
return result;
}
}
@@ -0,0 +1,48 @@
using Elsa.Alterations.Activities;
using Elsa.Alterations.Core.Models;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
namespace Elsa.Alterations.Workflows;
/// <summary>
/// Executes an alteration plan.
/// </summary>
public class ExecuteAlterationPlanWorkflow : WorkflowBase
{
internal const string WorkflowDefinitionId = "Elsa.Alterations.ExecuteAlterationPlan";
/// <inheritdoc />
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(WorkflowDefinitionId);
builder.AsSystemWorkflow();
var plan = builder.WithInput<AlterationPlanParams>("Plan", "The parameters for the new plan");
var planId = builder.WithVariable<string>();
var jobCount = builder.WithVariable<int>();
builder.Root = new Sequence
{
Activities =
{
new SubmitAlterationPlan
{
Params = new(context => context.GetInput<AlterationPlanParams>(plan)!),
Result = new(planId)
},
new Correlate(planId),
new GenerateAlterationJobs(planId)
{
Result = new(jobCount)
},
new If(context => jobCount.Get(context) > 0)
{
Then = new DispatchAlterationJobs(planId),
Else = new CompleteAlterationPlan(planId)
},
new AlterationPlanCompleted(planId),
}
};
}
}