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,10 @@
using Elsa.Alterations.Core.Contracts;
namespace Elsa.Alterations.Core.Abstractions;
/// <summary>
/// A base class for alterations.
/// </summary>
public abstract class AlterationBase : IAlteration
{
}
@@ -0,0 +1,33 @@
using Elsa.Alterations.Core.Contexts;
using Elsa.Alterations.Core.Contracts;
namespace Elsa.Alterations.Core.Abstractions;
/// <summary>
/// A base class for alterations.
/// </summary>
public abstract class AlterationHandlerBase : IAlterationHandler
{
/// <inheritdoc />
public abstract bool CanHandle(IAlteration alteration);
/// <inheritdoc />
public abstract ValueTask HandleAsync(AlterationContext context);
}
/// <inheritdoc />
public abstract class AlterationHandlerBase<T> : AlterationHandlerBase where T : IAlteration
{
/// <inheritdoc />
public override bool CanHandle(IAlteration alteration) => alteration is T;
/// <inheritdoc />
public override ValueTask HandleAsync(AlterationContext context) => HandleAsync(context, (T)context.Alteration);
/// <summary>
/// Applies the alteration to the specified context.
/// </summary>
/// <param name="context">A context object that contains information for the alteration and provides a way to alter the workflow instance and control the alteration process.</param>
/// <param name="alteration">A strongly typed alteration.</param>
protected abstract ValueTask HandleAsync(AlterationContext context, T alteration);
}
@@ -0,0 +1,155 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Models;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Microsoft.Extensions.Logging;
namespace Elsa.Alterations.Core.Contexts;
/// <summary>
/// Provides contextual information about an alteration.
/// </summary>
public class AlterationContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AlterationContext"/> class.
/// </summary>
public AlterationContext(
IAlteration alteration,
WorkflowExecutionContext workflowExecutionContext,
AlterationLog log,
CancellationToken cancellationToken)
{
Alteration = alteration;
WorkflowExecutionContext = workflowExecutionContext;
AlterationLog = log;
CancellationToken = cancellationToken;
}
/// <summary>
/// The alteration being handled.
/// </summary>
public IAlteration Alteration { get; }
/// <summary>
/// A workflow execution context of the workflow instance being altered. This offers maximum flexibility for altering the workflow state.
/// </summary>
public WorkflowExecutionContext WorkflowExecutionContext { get; set; }
/// <summary>
/// The workflow of the workflow instance being altered.
/// </summary>
public Workflow Workflow => WorkflowExecutionContext.Workflow;
/// <summary>
/// The cancellation token.
/// </summary>
public CancellationToken CancellationToken { get; }
/// <summary>
/// The service provider.
/// </summary>
public IServiceProvider ServiceProvider => WorkflowExecutionContext.ServiceProvider;
/// <summary>
/// The alteration log.
/// </summary>
public AlterationLog AlterationLog { get; }
/// <summary>
/// A flag indicating whether the alteration has succeeded.
/// </summary>
public bool HasSucceeded { get; private set; }
/// <summary>
/// A flag indicating whether the alteration has failed.
/// </summary>
public bool HasFailed { get; private set; }
/// <summary>
/// An optional action to be executed when the alteration is committed. Set this to perform permanent side effects such as deleting records form the database.
/// </summary>
public Func<Task>? CommitAction { get; set; }
/// <summary>
/// Logs a message.
/// </summary>
/// <param name="eventName">The event name to log.</param>
/// <param name="message">The message to log.</param>
/// <param name="logLevel">The log level.</param>
public void Log(string eventName, string message, LogLevel logLevel = LogLevel.Information)
{
AlterationLog.Add(message, logLevel, eventName);
}
/// <summary>
/// Marks the alteration as succeeded.
/// </summary>
public void Succeed()
{
Succeed($"{Alteration.GetType().Name} succeeded");
}
/// <summary>
/// Marks the alteration as succeeded.
/// </summary>
public void Succeed(Func<Task> commitAction)
{
Succeed();
CommitAction = commitAction;
}
/// <summary>
/// Marks the alteration as succeeded.
/// </summary>
public void Succeed(Action commitAction)
{
Succeed();
CommitAction = () =>
{
commitAction();
return Task.CompletedTask;
};
}
/// <summary>
/// Marks the alteration as succeeded.
/// </summary>
public void Succeed(string message)
{
HasSucceeded = true;
Log($"Alteration {Alteration.GetType().Name} succeeded", message);
}
/// <summary>
/// Marks the alteration as succeeded.
/// </summary>
public void Succeed(string message, Func<Task> commitAction)
{
Succeed(message);
CommitAction = commitAction;
}
/// <summary>
/// Marks the alteration as succeeded.
/// </summary>
public void Succeed(string message, Action commitAction)
{
Succeed(message);
CommitAction = () =>
{
commitAction();
return Task.CompletedTask;
};
}
/// <summary>
/// Marks the alteration as failed.
/// </summary>
/// <param name="message">An optional message.</param>
public void Fail(string? message = null)
{
HasFailed = true;
Log($"Alteration {Alteration.GetType().Name} failed", message ?? $"{Alteration.GetType().Name} failed", LogLevel.Error);
}
}
@@ -0,0 +1,8 @@
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Represents an alteration.
/// </summary>
public interface IAlteration
{
}
@@ -0,0 +1,21 @@
using Elsa.Alterations.Core.Contexts;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Represents a change to a given type <c>T</c>.
/// </summary>
public interface IAlterationHandler
{
/// <summary>
/// Returns <c>true</c> if the alteration is supported by this handler; otherwise, <c>false</c>.
/// </summary>
/// <param name="alteration"></param>
bool CanHandle(IAlteration alteration);
/// <summary>
/// Applies the alteration to the specified context.
/// </summary>
/// <param name="context">A context object that contains information for the alteration and provides a way to alter the workflow instance and control the alteration process.</param>
ValueTask HandleAsync(AlterationContext context);
}
@@ -0,0 +1,14 @@
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Dispatches an alteration job for execution.
/// </summary>
public interface IAlterationJobDispatcher
{
/// <summary>
/// Dispatches an alteration job for execution.
/// </summary>
/// <param name="jobId">The ID of the job to dispatch.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
ValueTask DispatchAsync(string jobId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,17 @@
using Elsa.Alterations.Core.Entities;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Executes an alteration job.
/// </summary>
public interface IAlterationJobRunner
{
/// <summary>
/// Executes the specified alteration plan.
/// </summary>
/// <param name="jobId">The Id of the alteration job to execute.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The job with updated state.</returns>
Task<AlterationJob> RunAsync(string jobId, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,41 @@
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Filters;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// A store for alteration jobs.
/// </summary>
public interface IAlterationJobStore
{
/// <summary>
/// Saves the specified alteration job.
/// </summary>
Task SaveAsync(AlterationJob job, CancellationToken cancellationToken = default);
/// <summary>
/// Saves the specified alteration jobs.
/// </summary>
Task SaveManyAsync(IEnumerable<AlterationJob> jobs, CancellationToken cancellationToken = default);
/// <summary>
/// Finds the alteration job matching the specified filter.
/// </summary>
Task<AlterationJob?> FindAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Finds all alteration jobs matching the specified filter.
/// </summary>
Task<IEnumerable<AlterationJob>> FindManyAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Finds the IDs of all alteration jobs matching the specified filter.
/// </summary>
/// <returns></returns>
Task<IEnumerable<string>> FindManyIdsAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Returns the number of alteration jobs matching the specified filter.
/// </summary>
Task<long> CountAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,24 @@
using Elsa.Alterations.Core.Entities;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Represents a manager for alteration plans.
/// </summary>
public interface IAlterationPlanManager
{
/// <summary>
/// Gets an alteration plan by ID.
/// </summary>
Task<AlterationPlan?> GetPlanAsync(string planId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a value indicating whether all jobs in the plan have been completed.
/// </summary>
Task<bool> GetIsAllJobsCompletedAsync(string planId, CancellationToken cancellationToken = default);
/// <summary>
/// Completes an alteration plan.
/// </summary>
Task CompletePlanAsync(AlterationPlan plan, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,14 @@
using Elsa.Alterations.Core.Models;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Scheduler for alteration plans.
/// </summary>
public interface IAlterationPlanScheduler
{
/// <summary>
/// Submits an alteration plan for execution.
/// </summary>
Task<string> SubmitAsync(AlterationPlanParams planParams, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,25 @@
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Filters;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// A store for alteration plans.
/// </summary>
public interface IAlterationPlanStore
{
/// <summary>
/// Saves the specified alteration plan.
/// </summary>
Task SaveAsync(AlterationPlan plan, CancellationToken cancellationToken = default);
/// <summary>
/// Finds the alteration plan matching the specified filter.
/// </summary>
Task<AlterationPlan?> FindAsync(AlterationPlanFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Returns the number of alteration plans matching the specified filter.
/// </summary>
Task<long> CountAsync(AlterationPlanFilter filter, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,25 @@
using Elsa.Alterations.Core.Results;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Runs a series of alterations on the specified workflow execution context.
/// </summary>
public interface IAlterationRunner
{
/// <summary>
/// Runs a series of alterations on the specified workflow instances.
/// </summary>
/// <param name="workflowInstanceIds">The IDs of the workflow instances to alter.</param>
/// <param name="alterations">The alterations to run.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task<ICollection<RunAlterationsResult>> RunAsync(IEnumerable<string> workflowInstanceIds, IEnumerable<IAlteration> alterations, CancellationToken cancellationToken = default);
/// <summary>
/// Runs a series of alterations on the specified workflow instances.
/// </summary>
/// <param name="workflowInstanceId">The ID of the workflow instance to alter.</param>
/// <param name="alterations">The alterations to run.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task<RunAlterationsResult> RunAsync(string workflowInstanceId, IEnumerable<IAlteration> alterations, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,27 @@
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Serializes and deserializes <see cref="IAlteration"/> objects.
/// </summary>
public interface IAlterationSerializer
{
/// <summary>
/// Serializes the specified <see cref="IAlteration"/> object.
/// </summary>
string Serialize(IAlteration alteration);
/// <summary>
/// Serializes the specified set of <see cref="IAlteration"/> objects.
/// </summary>
string SerializeMany(IEnumerable<IAlteration> alterations);
/// <summary>
/// Deserializes the specified JSON string into an <see cref="IAlteration"/> object.
/// </summary>
IAlteration Deserialize(string json);
/// <summary>
/// Deserializes the specified JSON string into a set of <see cref="IAlteration"/> objects.
/// </summary>
IEnumerable<IAlteration> DeserializeMany(string json);
}
@@ -0,0 +1,23 @@
using Elsa.Alterations.Core.Results;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Schedules a workflow instance to be resumed when an alteration job has been completed.
/// </summary>
public interface IAlteredWorkflowDispatcher
{
/// <summary>
/// Schedules the altered workflow instances to be resumed.
/// </summary>
/// <param name="results">The results of running a series of alterations.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task DispatchAsync(IEnumerable<RunAlterationsResult> results, CancellationToken cancellationToken = default);
/// <summary>
/// Schedules the altered workflow instance to be resumed.
/// </summary>
/// <param name="result">The result of running a series of alterations.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task DispatchAsync(RunAlterationsResult result, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,14 @@
using Elsa.Alterations.Core.Models;
namespace Elsa.Alterations.Core.Contracts;
/// <summary>
/// Represents a service that can find workflow instances based on specified filters.
/// </summary>
public interface IWorkflowInstanceFinder
{
/// <summary>
/// Finds workflow instances based on the specified filter.
/// </summary>
Task<IEnumerable<string>> FindAsync(AlterationWorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides core interfaces, models and services that support the alteration engine.
</Description>
<PackageTags>elsa module alterations</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,46 @@
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Models;
using Elsa.Common.Entities;
namespace Elsa.Alterations.Core.Entities;
/// <summary>
/// Represents the execution of the plan for an individual workflow instance.
/// </summary>
public class AlterationJob : Entity
{
/// <summary>
/// The ID of the plan that this job belongs to.
/// </summary>
public string PlanId { get; set; } = default!;
/// <summary>
/// The ID of the workflow instance that this job applies to.
/// </summary>
public string WorkflowInstanceId { get; set; } = default!;
/// <summary>
/// The status of the job.
/// </summary>
public AlterationJobStatus Status { get; set; }
/// <summary>
/// The serialized log of the job.
/// </summary>
public ICollection<AlterationLogEntry>? Log { get; set; } = new List<AlterationLogEntry>();
/// <summary>
/// The date and time at which the job was created.
/// </summary>
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// The date and time at which the job was started.
/// </summary>
public DateTimeOffset? StartedAt { get; set; }
/// <summary>
/// The date and time at which the job was completed.
/// </summary>
public DateTimeOffset? CompletedAt { get; set; }
}
@@ -0,0 +1,42 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Enums;
using Elsa.Alterations.Core.Models;
using Elsa.Common.Entities;
namespace Elsa.Alterations.Core.Entities;
/// <summary>
/// A plan that contains a list of alterations to be applied to a set of workflow instances.
/// </summary>
public class AlterationPlan : Entity
{
/// <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 AlterationWorkflowInstanceFilter WorkflowInstanceFilter { get; set; } = new();
/// <summary>
/// The status of the plan.
/// </summary>
public AlterationPlanStatus Status { get; set; }
/// <summary>
/// The date and time at which the plan was created.
/// </summary>
public DateTimeOffset CreatedAt { get; set; }
/// <summary>
/// The date and time at which the plan was started.
/// </summary>
public DateTimeOffset? StartedAt { get; set; }
/// <summary>
/// The date and time at which the plan was completed.
/// </summary>
public DateTimeOffset? CompletedAt { get; set; }
}
@@ -0,0 +1,27 @@
namespace Elsa.Alterations.Core.Enums;
/// <summary>
/// The status of an alteration plan for a workflow instance.
/// </summary>
public enum AlterationJobStatus
{
/// <summary>
/// The plan is pending execution.
/// </summary>
Pending,
/// <summary>
/// The plan is currently being executed.
/// </summary>
Running,
/// <summary>
/// The plan has been completed.
/// </summary>
Completed,
/// <summary>
/// The job has failed.
/// </summary>
Failed
}
@@ -0,0 +1,37 @@
namespace Elsa.Alterations.Core.Enums;
/// <summary>
/// The status of an alteration plan.
/// </summary>
public enum AlterationPlanStatus
{
/// <summary>
/// The plan is pending execution.
/// </summary>
Pending,
/// <summary>
/// The plan is currently generating jobs.
/// </summary>
Generating,
/// <summary>
/// The plan is currently dispatching jobs.
/// </summary>
Dispatching,
/// <summary>
/// The plan is currently being executed.
/// </summary>
Running,
/// <summary>
/// The plan has been completed.
/// </summary>
Completed,
/// <summary>
/// The plan has failed.
/// </summary>
Failed
}
@@ -0,0 +1,37 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Options;
using Elsa.Alterations.Core.Serialization;
using Elsa.Alterations.Core.Services;
using Elsa.Extensions;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Alterations.Core.Extensions;
/// <summary>
/// Extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the core Elsa alterations services.
/// </summary>
public static IServiceCollection AddAlterationsCore(this IServiceCollection services)
{
services.Configure<AlterationOptions>(_ => { }); // Ensure that the options are configured even if the application doesn't do so.
services.AddScoped<IAlteredWorkflowDispatcher, AlteredWorkflowDispatcher>();
services.AddScoped<IWorkflowInstanceFinder, WorkflowInstanceFinder>();
services.AddSingleton<IAlterationSerializer, AlterationSerializer>();
services.AddSerializationOptionsConfigurator<AlterationSerializationOptionConfigurator>();
return services;
}
/// <summary>
/// Adds an alteration handler.
/// </summary>
public static IServiceCollection AddAlteration<T, THandler>(this IServiceCollection services) where T : IAlteration where THandler : class, IAlterationHandler
{
services.Configure<AlterationOptions>(options => options.AlterationTypes.Add(typeof(T)));
services.AddScoped<IAlterationHandler, THandler>();
return services;
}
}
@@ -0,0 +1,50 @@
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Enums;
namespace Elsa.Alterations.Core.Filters;
/// <summary>
/// A filter for querying alteration jobs.
/// </summary>
public class AlterationJobFilter
{
/// <summary>
/// The ID of the job.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// The ID of the plan.
/// </summary>
public string? PlanId { get; set; }
/// <summary>
/// The status of the job.
/// </summary>
public AlterationJobStatus? Status { get; set; }
/// <summary>
/// The statuses of the job to match.
/// </summary>
public ICollection<AlterationJobStatus>? Statuses { get; set; }
/// <summary>
/// Applies the filter to the specified query.
/// </summary>
public IQueryable<AlterationJob> Apply(IQueryable<AlterationJob> query)
{
if (!string.IsNullOrWhiteSpace(Id))
query = query.Where(x => x.Id == Id);
if (!string.IsNullOrWhiteSpace(PlanId))
query = query.Where(x => x.PlanId == PlanId);
if (Status != null)
query = query.Where(x => x.Status == Status);
if (Statuses != null)
query = query.Where(x => Statuses.Contains(x.Status));
return query;
}
}
@@ -0,0 +1,25 @@
using Elsa.Alterations.Core.Entities;
namespace Elsa.Alterations.Core.Filters;
/// <summary>
/// A filter for querying alteration plans.
/// </summary>
public class AlterationPlanFilter
{
/// <summary>
/// The ID of the plan.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Applies the filter to the specified query.
/// </summary>
public IQueryable<AlterationPlan> Apply(IQueryable<AlterationPlan> query)
{
if (!string.IsNullOrWhiteSpace(Id))
query = query.Where(x => x.Id == Id);
return query;
}
}
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,36 @@
using Elsa.Workflows;
using JetBrains.Annotations;
namespace Elsa.Alterations.Core.Models;
/// <summary>
/// Represents a filter for activities.
/// </summary>
[UsedImplicitly]
public class ActivityFilter
{
/// <summary>
/// The ID of the activity.
/// </summary>
public string? ActivityId { get; set; }
/// <summary>
/// The ID of the activity instance.
/// </summary>
public string? ActivityInstanceId { get; set; }
/// <summary>
/// The node ID of the activity.
/// </summary>
public string? NodeId { get; set; }
/// <summary>
/// The name of the activity.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The status of the activity.
/// </summary>
public ActivityStatus? Status { get; set; }
}
@@ -0,0 +1,46 @@
using Elsa.Common;
using Microsoft.Extensions.Logging;
namespace Elsa.Alterations.Core.Models;
/// <summary>
/// Represents a log of alterations.
/// </summary>
public class AlterationLog
{
private readonly ISystemClock _systemClock;
private readonly List<AlterationLogEntry> _logEntries = new();
/// <summary>
/// Initializes a new instance of the <see cref="AlterationLog"/> class.
/// </summary>
public AlterationLog(ISystemClock systemClock)
{
_systemClock = systemClock;
}
/// <summary>
/// Gets the log entries.
/// </summary>
public IReadOnlyCollection<AlterationLogEntry> LogEntries => _logEntries.ToList().AsReadOnly();
/// <summary>
/// Adds a log entry.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="logLevel">The log level.</param>
/// <param name="eventName">The event that generated the log entry.</param>
public void Add(string message, LogLevel logLevel = LogLevel.Information, string? eventName = null)
{
var entry = new AlterationLogEntry(message, logLevel, _systemClock.UtcNow, eventName);
_logEntries.Add(entry);
}
/// <summary>
/// Adds a set of log entries.
/// </summary>
/// <param name="entries"></param>
public void AddRange(IEnumerable<AlterationLogEntry> entries) => _logEntries.AddRange(entries);
}
@@ -0,0 +1,12 @@
using Microsoft.Extensions.Logging;
namespace Elsa.Alterations.Core.Models;
/// <summary>
/// A log entry for an alteration.
/// </summary>
/// <param name="Message">The log message.</param>
/// <param name="LogLevel">The log level.</param>
/// <param name="Timestamp">The timestamp when the log entry was created.</param>
/// <param name="EventName">The event that generated the log entry.</param>
public record AlterationLogEntry(string Message, LogLevel LogLevel, DateTimeOffset Timestamp, string? EventName = null);
@@ -0,0 +1,24 @@
using Elsa.Alterations.Core.Contracts;
namespace Elsa.Alterations.Core.Models;
/// <summary>
/// Represents a new alteration plan.
/// </summary>
public class AlterationPlanParams
{
/// <summary>
/// The unique identifier for the alteration plan. If not specified, a new ID will be generated.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// The alterations to be applied.
/// </summary>
public ICollection<IAlteration> Alterations { get; set; } = new List<IAlteration>();
/// <summary>
/// The filter used to determine which workflow instances that this plan applies to.
/// </summary>
public AlterationWorkflowInstanceFilter Filter { get; set; } = new();
}
@@ -0,0 +1,78 @@
using Elsa.Workflows;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
namespace Elsa.Alterations.Core.Models;
/// <summary>
/// Represents a filter for workflow instances.
/// </summary>
[UsedImplicitly]
public class AlterationWorkflowInstanceFilter
{
/// <summary>
/// If the filter is empty, all records are matched.
/// </summary>
public bool EmptyFilterSelectsAll { get; set; }
/// <summary>
/// The IDs of the workflow instances that this plan applies to.
/// </summary>
public IEnumerable<string>? WorkflowInstanceIds { get; set; }
/// <summary>
/// The correlation IDs of the workflow instances that this plan applies to.
/// </summary>
public IEnumerable<string>? CorrelationIds { get; set; }
/// <summary>
/// A collection of names associated with the workflow instances being filtered.
/// </summary>
public ICollection<string>? Names { get; set; }
/// <summary>
/// A search term used to filter workflow instances based on matching criteria.
/// </summary>
public string? SearchTerm { get; set; }
/// <summary>
/// A collection of timestamp filters used for filtering data based on specified timestamp columns and operators.
/// </summary>
public IEnumerable<TimestampFilter>? TimestampFilters { get; set; }
/// <summary>
/// The IDs of the workflow definitions that this plan applies to.
/// </summary>
public ICollection<string>? DefinitionIds { get; set; }
/// <summary>
/// The IDs of the workflow definitions that this plan applies to.
/// </summary>
public IEnumerable<string>? DefinitionVersionIds { get; set; }
/// <summary>
/// Whether the workflow instances to match have incidents.
/// </summary>
public bool? HasIncidents { get; set; }
/// <summary>
/// Whether the workflow instances to match are system workflows. Defaults to <c>false</c>.
/// </summary>
public bool? IsSystem { get; set; } = false;
/// <summary>
/// Represents the workflow statuses included in the filter.
/// </summary>
public ICollection<WorkflowStatus>? Statuses { get; set; }
/// <summary>
/// A collection of sub-statuses used to filter workflow instances by their specific sub-state.
/// </summary>
public ICollection<WorkflowSubStatus>? SubStatuses { get; set; }
/// <summary>
/// Represents a collection of filters for activities.
/// </summary>
public IEnumerable<ActivityFilter>? ActivityFilters { get; set; }
}
@@ -0,0 +1,7 @@
namespace Elsa.Alterations.Core.Models;
public class NewAlterationJob
{
public string PlanId { get; set; }
public string WorkflowInstanceId { get; set; }
}
@@ -0,0 +1,9 @@
using Elsa.Alterations.Core.Entities;
using Elsa.Mediator.Contracts;
namespace Elsa.Alterations.Core.Notifications;
/// <summary>
/// A notification that is published when an alteration job is completed.
/// </summary>
public record AlterationJobCompleted(AlterationJob Job, bool WorkflowContainsScheduledWork) : INotification;
@@ -0,0 +1,9 @@
using Elsa.Alterations.Core.Entities;
using Elsa.Mediator.Contracts;
namespace Elsa.Alterations.Core.Notifications;
/// <summary>
/// A notification that is published when an alteration plan is completed.
/// </summary>
public record AlterationPlanCompleted(AlterationPlan Plan) : INotification;
@@ -0,0 +1,9 @@
using Elsa.Alterations.Core.Entities;
using Elsa.Mediator.Contracts;
namespace Elsa.Alterations.Core.Notifications;
/// <summary>
/// A notification that is published when an alteration plan is submitted.
/// </summary>
public record AlterationPlanSubmitted(AlterationPlan Plan) : INotification;
@@ -0,0 +1,12 @@
namespace Elsa.Alterations.Core.Options;
/// <summary>
/// Options for the Alteration module.
/// </summary>
public class AlterationOptions
{
/// <summary>
/// The types of alterations that are supported.
/// </summary>
public ISet<Type> AlterationTypes { get; set; } = new HashSet<Type>();
}
@@ -0,0 +1,39 @@
using Elsa.Alterations.Core.Models;
using Microsoft.Extensions.Logging;
namespace Elsa.Alterations.Core.Results;
/// <summary>
/// The result of running a series of alterations.
/// </summary>
public class RunAlterationsResult
{
/// <summary>
/// Initializes a new instance of the <see cref="RunAlterationsResult"/> class.
/// </summary>
public RunAlterationsResult(string workflowInstanceId, AlterationLog log)
{
WorkflowInstanceId = workflowInstanceId;
Log = log;
}
/// <summary>
/// The ID of the workflow instance that was altered.
/// </summary>
public string WorkflowInstanceId { get; }
/// <summary>
/// A log of the alterations that were run.
/// </summary>
public AlterationLog Log { get; set; }
/// <summary>
/// A flag indicating whether the workflow has scheduled work.
/// </summary>
public bool WorkflowHasScheduledWork { get; set; }
/// <summary>
/// A flag indicating whether the alterations have succeeded.
/// </summary>
public bool IsSuccessful => Log.LogEntries.Any(x => x.LogLevel <= LogLevel.Warning);
}
@@ -0,0 +1,43 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Options;
using Elsa.Workflows;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
namespace Elsa.Alterations.Core.Serialization;
/// <summary>
/// Add additional <see cref="JsonConverter"/> objects.
/// </summary>
[UsedImplicitly]
public class AlterationSerializationOptionConfigurator(IOptions<AlterationOptions> options) : SerializationOptionsConfiguratorBase
{
/// <inheritdoc />
public override IEnumerable<Action<JsonTypeInfo>> GetModifiers()
{
var alterationTypes = options.Value.AlterationTypes;
yield return typeInfo =>
{
if (typeInfo.Type != typeof(IAlteration))
return;
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
var polymorphismOptions = new JsonPolymorphismOptions
{
TypeDiscriminatorPropertyName = "type"
};
foreach (var alterationType in alterationTypes.ToList())
{
polymorphismOptions.DerivedTypes.Add(new(alterationType, alterationType.Name));
}
typeInfo.PolymorphismOptions = polymorphismOptions;
};
}
}
@@ -0,0 +1,49 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Elsa.Alterations.Core.Contracts;
using Elsa.Common.Serialization;
namespace Elsa.Alterations.Core.Serialization;
/// <summary>
/// A serializer for <see cref="IAlteration"/> objects.
/// </summary>
public class AlterationSerializer : ConfigurableSerializer, IAlterationSerializer
{
/// <inheritdoc />
public AlterationSerializer(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type of the alteration must be known at compile time.")]
public string Serialize(IAlteration alteration)
{
var options = GetOptions();
return JsonSerializer.Serialize(alteration, options);
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type of the alteration must be known at compile time.")]
public string SerializeMany(IEnumerable<IAlteration> alterations)
{
var options = GetOptions();
return JsonSerializer.Serialize(alterations.ToArray(), options);
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type of the alteration must be known at compile time.")]
public IAlteration Deserialize(string json)
{
var options = GetOptions();
return JsonSerializer.Deserialize<IAlteration>(json, options)!;
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type of the alteration must be known at compile time.")]
public IEnumerable<IAlteration> DeserializeMany(string json)
{
var options = GetOptions();
return JsonSerializer.Deserialize<IAlteration[]>(json, options)!;
}
}
@@ -0,0 +1,34 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Results;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Requests;
namespace Elsa.Alterations.Core.Services;
/// <inheritdoc />
public class AlteredWorkflowDispatcher : IAlteredWorkflowDispatcher
{
private readonly IWorkflowDispatcher _workflowDispatcher;
/// <summary>
/// Initializes a new instance of the <see cref="AlteredWorkflowDispatcher"/> class.
/// </summary>
public AlteredWorkflowDispatcher(IWorkflowDispatcher workflowDispatcher)
{
_workflowDispatcher = workflowDispatcher;
}
/// <inheritdoc />
public async Task DispatchAsync(IEnumerable<RunAlterationsResult> results, CancellationToken cancellationToken = default)
{
foreach (var result in results.Where(x => x is { IsSuccessful: true, WorkflowHasScheduledWork: true }))
await DispatchAsync(result, cancellationToken);
}
/// <inheritdoc />
public async Task DispatchAsync(RunAlterationsResult result, CancellationToken cancellationToken = default)
{
await _workflowDispatcher.DispatchAsync(new DispatchWorkflowInstanceRequest(result.WorkflowInstanceId), cancellationToken: cancellationToken);
}
}
@@ -0,0 +1,93 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Models;
using Elsa.Workflows;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Filters;
namespace Elsa.Alterations.Core.Services;
/// <inheritdoc />
public class WorkflowInstanceFinder(IWorkflowInstanceStore workflowInstanceStore, IActivityExecutionStore activityExecutionStore) : IWorkflowInstanceFinder
{
/// <inheritdoc />
public async Task<IEnumerable<string>> FindAsync(AlterationWorkflowInstanceFilter filter, CancellationToken cancellationToken = default)
{
var workflowInstanceFilter = new WorkflowInstanceFilter
{
Ids = filter.WorkflowInstanceIds?.ToList(),
DefinitionIds = filter.DefinitionIds,
DefinitionVersionIds = filter.DefinitionVersionIds?.ToList(),
CorrelationIds = filter.CorrelationIds?.ToList(),
HasIncidents = filter.HasIncidents,
IsSystem = filter.IsSystem,
TimestampFilters = filter.TimestampFilters?.ToList(),
WorkflowStatuses = filter.Statuses?.ToList(),
WorkflowSubStatuses = filter.SubStatuses?.ToList(),
Names = filter.Names?.ToList(),
SearchTerm = filter.SearchTerm,
};
var activityExecutionFilters = filter.ActivityFilters?.Select(x => new ActivityExecutionRecordFilter
{
ActivityId = x.ActivityId,
Id = x.ActivityInstanceId,
ActivityNodeId = x.NodeId,
Name = x.Name,
Status = x.Status,
}).ToList();
var emptyFilterSelectsAll = filter.EmptyFilterSelectsAll;
var workflowInstanceFilterIsEmpty = WorkflowFilterIsEmpty(workflowInstanceFilter);
var workflowInstanceIds = workflowInstanceFilterIsEmpty && !emptyFilterSelectsAll
? Enumerable.Empty<string>().ToHashSet()
: (await workflowInstanceStore.FindManyIdsAsync(workflowInstanceFilter, cancellationToken)).ToHashSet();
if (activityExecutionFilters == null)
return workflowInstanceIds;
foreach (ActivityExecutionRecordFilter activityExecutionFilter in activityExecutionFilters.Where(x => !x.IsEmpty))
{
var activityExecutionRecords = await activityExecutionStore.FindManySummariesAsync(activityExecutionFilter, cancellationToken);
var matchingWorkflowInstanceIds = activityExecutionRecords.Select(x => x.WorkflowInstanceId).ToHashSet();
if (workflowInstanceFilterIsEmpty)
workflowInstanceIds = matchingWorkflowInstanceIds;
else
workflowInstanceIds.IntersectWith(matchingWorkflowInstanceIds);
}
// Alterations must apply only to running workflows.
workflowInstanceIds = (await FilterRunningWorkflowInstancesAsync(workflowInstanceIds, cancellationToken)).ToHashSet();
return workflowInstanceIds;
}
private async Task<IEnumerable<string>> FilterRunningWorkflowInstancesAsync(IEnumerable<string> workflowInstanceIds, CancellationToken cancellationToken)
{
var filter = new WorkflowInstanceFilter
{
Ids = workflowInstanceIds.ToList(),
WorkflowStatus = WorkflowStatus.Running
};
return await workflowInstanceStore.FindManyIdsAsync(filter, cancellationToken);
}
private bool WorkflowFilterIsEmpty(WorkflowInstanceFilter filter)
{
return filter.Id == null &&
filter.Ids == null &&
filter.DefinitionId == null &&
filter.DefinitionVersionId == null &&
filter.DefinitionIds == null &&
filter.DefinitionVersionIds == null &&
filter.Version == null &&
filter.CorrelationId == null &&
filter.CorrelationIds == null &&
filter.HasIncidents == null &&
filter.TimestampFilters == null
&& string.IsNullOrWhiteSpace(filter.SearchTerm);
}
}
@@ -0,0 +1,67 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Filters;
using Elsa.Common.Services;
namespace Elsa.Alterations.Core.Stores;
/// <summary>
/// A memory-based store for alteration jobs.
/// </summary>
public class MemoryAlterationJobStore : IAlterationJobStore
{
private readonly MemoryStore<AlterationJob> _store;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryAlterationJobStore"/> class.
/// </summary>
public MemoryAlterationJobStore(MemoryStore<AlterationJob> store)
{
_store = store;
}
/// <inheritdoc />
public Task SaveAsync(AlterationJob job, CancellationToken cancellationToken = default)
{
_store.Save(job, x => x.Id);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task SaveManyAsync(IEnumerable<AlterationJob> jobs, CancellationToken cancellationToken = default)
{
_store.SaveMany(jobs, x => x.Id);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<AlterationJob?> FindAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default)
{
var entity = _store.Query(query => Filter(query, filter)).FirstOrDefault();
return Task.FromResult(entity);
}
/// <inheritdoc />
public Task<IEnumerable<AlterationJob>> FindManyAsync(AlterationJobFilter filter, CancellationToken cancellationToken)
{
var entities = _store.Query(query => Filter(query, filter)).ToList().AsEnumerable();
return Task.FromResult(entities);
}
/// <inheritdoc />
public Task<IEnumerable<string>> FindManyIdsAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default)
{
var ids = _store.Query(query => Filter(query, filter)).Select(x => x.Id).ToList().AsEnumerable();
return Task.FromResult(ids);
}
/// <inheritdoc />
public Task<long> CountAsync(AlterationJobFilter filter, CancellationToken cancellationToken = default)
{
var count = _store.Query(query => Filter(query, filter)).LongCount();
return Task.FromResult(count);
}
private static IQueryable<AlterationJob> Filter(IQueryable<AlterationJob> query, AlterationJobFilter filter) => filter.Apply(query);
}
@@ -0,0 +1,45 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Core.Entities;
using Elsa.Alterations.Core.Filters;
using Elsa.Common.Services;
namespace Elsa.Alterations.Core.Stores;
/// <summary>
/// A memory-based store for alteration plans.
/// </summary>
public class MemoryAlterationPlanStore : IAlterationPlanStore
{
private readonly MemoryStore<AlterationPlan> _store;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryAlterationPlanStore"/> class.
/// </summary>
public MemoryAlterationPlanStore(MemoryStore<AlterationPlan> store)
{
_store = store;
}
/// <inheritdoc />
public Task SaveAsync(AlterationPlan plan, CancellationToken cancellationToken = default)
{
_store.Save(plan, x => x.Id);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<AlterationPlan?> FindAsync(AlterationPlanFilter filter, CancellationToken cancellationToken = default)
{
var entity = _store.Query(query => Filter(query, filter)).FirstOrDefault();
return Task.FromResult(entity);
}
/// <inheritdoc />
public Task<long> CountAsync(AlterationPlanFilter filter, CancellationToken cancellationToken = default)
{
var count = _store.Query(query => Filter(query, filter)).LongCount();
return Task.FromResult(count);
}
private static IQueryable<AlterationPlan> Filter(IQueryable<AlterationPlan> query, AlterationPlanFilter filter) => filter.Apply(query);
}
@@ -0,0 +1,27 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.MassTransit.Messages;
using MassTransit;
namespace Elsa.Alterations.MassTransit.Consumers;
/// <summary>
/// Consumes <see cref="RunAlterationJob"/> messages.
/// </summary>
public class RunAlterationJobConsumer : IConsumer<RunAlterationJob>
{
private readonly IAlterationJobRunner _alterationJobRunner;
/// <summary>
/// Initializes a new instance of the <see cref="RunAlterationJobConsumer"/> class.
/// </summary>
public RunAlterationJobConsumer(IAlterationJobRunner alterationJobRunner)
{
_alterationJobRunner = alterationJobRunner;
}
/// <inheritdoc />
public async Task Consume(ConsumeContext<RunAlterationJob> context)
{
await _alterationJobRunner.RunAsync(context.Message.JobId, context.CancellationToken);
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides alteration background job runners using MassTransit.
</Description>
<PackageTags>elsa module alterations masstransit</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Alterations\Elsa.Alterations.csproj" />
<ProjectReference Include="..\Elsa.MassTransit\Elsa.MassTransit.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
using Elsa.Alterations.Features;
using Elsa.Alterations.MassTransit.Features;
using Elsa.Extensions;
using Elsa.Features.Services;
namespace Elsa.Alterations.MassTransit.Extensions;
/// <summary>
/// Adds the <see cref="AlterationsFeature"/>.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Adds the <see cref="MassTransitAlterationsFeature"/>.
/// </summary>
public static IModule UseMassTransitDispatcher(this AlterationsFeature alterations, Action<MassTransitAlterationsFeature>? configure = default)
{
return alterations.Module.Use(configure);
}
}
@@ -0,0 +1,42 @@
using Elsa.Alterations.Features;
using Elsa.Alterations.MassTransit.Consumers;
using Elsa.Alterations.MassTransit.Messages;
using Elsa.Alterations.MassTransit.Services;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.MassTransit.Features;
using MassTransit;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Alterations.MassTransit.Features;
/// <summary>
/// A feature for enabling the Alterations MassTransit feature.
/// </summary>
[DependsOn(typeof(MassTransitFeature))]
[DependsOn(typeof(AlterationsFeature))]
public class MassTransitAlterationsFeature : FeatureBase
{
/// <inheritdoc />
public MassTransitAlterationsFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Configure()
{
Module.Configure<AlterationsFeature>(feature => feature.AlterationJobDispatcherFactory = sp => sp.GetRequiredService<MassTransitAlterationJobDispatcher>());
var queueName = KebabCaseEndpointNameFormatter.Instance.Consumer<RunAlterationJobConsumer>();
var queueAddress = new Uri($"queue:elsa-{queueName}");
EndpointConvention.Map<RunAlterationJob>(queueAddress);
Module.AddMassTransitConsumer<RunAlterationJobConsumer>(queueName);
}
/// <inheritdoc />
public override void Apply()
{
Services.AddScoped<MassTransitAlterationJobDispatcher>();
}
}
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,7 @@
namespace Elsa.Alterations.MassTransit.Messages;
/// <summary>
/// Represents a request to run an alteration job.
/// </summary>
/// <param name="JobId">The job ID.</param>
public record RunAlterationJob(string JobId);
@@ -0,0 +1,4 @@
# MassTransit Alterations Background Runner
This module provides an implementation that processes alteration plans in the background using a MassTransit dispatcher and consumer.
This implementation is more resilient than the in-memory queue implementation provided by **Elsa.Alterations.BackgroundRunner** because it uses a message broker to persist messages.
@@ -0,0 +1,28 @@
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.MassTransit.Messages;
using MassTransit;
namespace Elsa.Alterations.MassTransit.Services;
/// <summary>
/// Dispatches an alteration job for execution using MassTransit.
/// </summary>
public class MassTransitAlterationJobDispatcher : IAlterationJobDispatcher
{
private readonly IBus _bus;
/// <summary>
/// Initializes a new instance of the <see cref="MassTransitAlterationJobDispatcher"/> class.
/// </summary>
public MassTransitAlterationJobDispatcher(IBus bus)
{
_bus = bus;
}
/// <inheritdoc />
public async ValueTask DispatchAsync(string jobId, CancellationToken cancellationToken = default)
{
var message = new RunAlterationJob(jobId);
await _bus.Send(message, cancellationToken);
}
}
@@ -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),
}
};
}
}
@@ -0,0 +1,75 @@
using System.Runtime.CompilerServices;
using Elsa.CSharp.Contracts;
using Elsa.CSharp.Extensions;
using Elsa.CSharp.Models;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.CSharp.Activities;
/// <summary>
/// Executes C# code.
/// </summary>
[Activity("Elsa", "Scripting", "Executes C# code", DisplayName = "Run C#")]
public class RunCSharp : CodeActivity<object?>
{
/// <inheritdoc />
public RunCSharp([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <inheritdoc />
public RunCSharp(string script, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line)
{
Script = new Input<string>(script);
}
/// <summary>
/// The script to run.
/// </summary>
[Input(
Description = "The script to run.",
DefaultSyntax = "CSharp",
UIHint = InputUIHints.CodeEditor,
UIHandler = typeof(RunCSharpOptionsProvider)
)]
public Input<string> Script { get; set; } = new("");
/// <summary>
/// A list of possible outcomes. Use "SetOutcome(string)" to set the outcome. Use "SetOutcomes(params string[])" to set multiple outcomes.
/// </summary>
[Input(Description = "A list of possible outcomes.", UIHint = InputUIHints.DynamicOutcomes)]
public Input<ICollection<string>> PossibleOutcomes { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var script = context.Get(Script);
// If no script was specified, there's nothing to do.
if (string.IsNullOrWhiteSpace(script))
return;
// Get a C# evaluator.
var evaluator = context.GetRequiredService<ICSharpEvaluator>();
// Run the script.
var options = new ExpressionEvaluatorOptions();
var result = await evaluator.EvaluateAsync(script, typeof(object), context.ExpressionExecutionContext, options, context.CancellationToken);
// Set the result as output, if any.
if (result is not null)
context.Set(Result, result);
// Get the outcome or outcomes set by the script, if any. If not set, use "Done".
var outcomes = context.ExpressionExecutionContext.TransientProperties.GetValueOrDefault(OutcomeProxy.OutcomePropertiesKey, () => new[] { "Done" })!;
// Complete the activity with the outcome.
await context.CompleteActivityWithOutcomesAsync(outcomes);
}
}
@@ -0,0 +1,10 @@
using System.Reflection;
using Elsa.Workflows.UIHints.CodeEditor;
// ReSharper disable once CheckNamespace
namespace Elsa.CSharp.Activities;
internal class RunCSharpOptionsProvider : CodeEditorOptionsProviderBase
{
protected override string GetLanguage(PropertyInfo propertyInfo, object? context) => "csharp";
}
@@ -0,0 +1,11 @@
using Elsa.Expressions.Models;
using Microsoft.CodeAnalysis.Scripting;
namespace Elsa.CSharp.Contexts;
/// <summary>
/// Context for configuring the <see cref="ScriptOptions"/>.
/// </summary>
/// <param name="Script">The script.</param>
/// <param name="ExpressionExecutionContext">The expression execution context.</param>
public record ScriptConfigurationContext(Script Script, ExpressionExecutionContext ExpressionExecutionContext);
@@ -0,0 +1,11 @@
using Elsa.Expressions.Models;
using Microsoft.CodeAnalysis.Scripting;
namespace Elsa.CSharp.Contexts;
/// <summary>
/// Context for configuring the <see cref="ScriptOptions"/>.
/// </summary>
/// <param name="ScriptOptions">The script options.</param>
/// <param name="ExpressionExecutionContext">The expression execution context.</param>
public record ScriptOptionsConfigurationContext(ScriptOptions ScriptOptions, ExpressionExecutionContext ExpressionExecutionContext);
@@ -0,0 +1,32 @@
using Elsa.Expressions.Models;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis.Scripting;
namespace Elsa.CSharp.Contracts;
/// <summary>
/// Evaluates C# expressions.
/// </summary>
[PublicAPI]
public interface ICSharpEvaluator
{
/// <summary>
/// Evaluates a C# expression.
/// </summary>
/// <param name="expression">The expression to evaluate.</param>
/// <param name="returnType">The type of the return value.</param>
/// <param name="context">The context in which the expression is evaluated.</param>
/// <param name="options">A set of options.</param>
/// <param name="configureScriptOptions">An optional callback to configure the script options.</param>
/// <param name="configureScript">An optional callback to configure the script.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The result of the evaluation.</returns>
Task<object?> EvaluateAsync(
string expression,
Type returnType,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions options,
Func<ScriptOptions, ScriptOptions>? configureScriptOptions = default,
Func<Script<object>, Script<object>>? configureScript = default,
CancellationToken cancellationToken = default);
}

Some files were not shown because too many files have changed in this diff Show More