Refactor workflow management for improved modularization

Reorganized workflow handling to enhance clarity and maintainability, including introducing dedicated request handlers, refactoring middleware, and improving definition publishing. Deprecated `ValidateWorkflowResponse` and updated relevant namespaces for notifications handlers.
This commit is contained in:
Sipke Schoorstra 2025-01-20 00:24:01 +01:00
parent 7e06feffc2
commit ad8eeecdce
25 changed files with 226 additions and 189 deletions

View file

@ -12,5 +12,5 @@ public interface IRequestSender
/// <param name="cancellationToken">The cancellation token.</param>
/// <typeparam name="T">The type of the response.</typeparam>
/// <returns>The response.</returns>
Task<IEnumerable<T>> SendAsync<T>(IRequest<T> request, CancellationToken cancellationToken = default);
Task<T?> SendAsync<T>(IRequest<T> request, CancellationToken cancellationToken = default);
}

View file

@ -6,24 +6,10 @@ namespace Elsa.Mediator.Middleware.Request.Components;
/// <summary>
/// A middleware component that invokes request handlers.
/// </summary>
public class RequestHandlerInvokerMiddleware : IRequestMiddleware
public class RequestHandlerInvokerMiddleware(
RequestMiddlewareDelegate next,
IEnumerable<IRequestHandler> requestHandlers) : IRequestMiddleware
{
private readonly RequestMiddlewareDelegate _next;
private readonly IEnumerable<IRequestHandler> _requestHandlers;
/// <summary>
/// Initializes a new instance of the <see cref="RequestHandlerInvokerMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the pipeline.</param>
/// <param name="requestHandlers"></param>
public RequestHandlerInvokerMiddleware(
RequestMiddlewareDelegate next,
IEnumerable<IRequestHandler> requestHandlers)
{
_next = next;
_requestHandlers = requestHandlers;
}
/// <inheritdoc />
public async ValueTask InvokeAsync(RequestContext context)
{
@ -33,22 +19,26 @@ public class RequestHandlerInvokerMiddleware : IRequestMiddleware
var requestType = request.GetType();
var responseType = context.ResponseType;
var handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, responseType);
var handlers = _requestHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray();
var handlers = requestHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray();
if (handlers.Length == 0)
throw new InvalidOperationException($"There is no handler to handle the {requestType.FullName} request");
foreach (var handler in handlers)
{
var handleMethod = handlerType.GetMethod("HandleAsync")!;
var cancellationToken = context.CancellationToken;
var task = (Task)handleMethod.Invoke(handler, new object?[] { request, cancellationToken })!;
await task;
if (handlers.Length > 1)
throw new InvalidOperationException($"Multiple handlers were found to handle the {requestType.FullName} request");
// Get result of task.
var taskWithReturnType = typeof(Task<>).MakeGenericType(responseType);
var resultProperty = taskWithReturnType.GetProperty(nameof(Task<object>.Result))!;
context.Responses.Add(resultProperty.GetValue(task)!);
}
var handler = handlers.First();
var handleMethod = handlerType.GetMethod("HandleAsync")!;
var cancellationToken = context.CancellationToken;
var task = (Task)handleMethod.Invoke(handler, [request, cancellationToken])!;
await task;
// Get result of task.
var taskWithReturnType = typeof(Task<>).MakeGenericType(responseType);
var resultProperty = taskWithReturnType.GetProperty(nameof(Task<object>.Result))!;
context.Response = resultProperty.GetValue(task)!;
// Invoke next middleware.
await _next(context);
await next(context);
}
}

View file

@ -36,7 +36,7 @@ public class RequestContext
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets the responses from each request handler.
/// Gets the response the request handler.
/// </summary>
public ICollection<object> Responses { get; set; } = new List<object>();
public object? Response { get; set; }
}

View file

@ -41,23 +41,20 @@ public class DefaultMediator : IMediator
}
/// <inheritdoc />
public async Task<IEnumerable<T>> SendAsync<T>(IRequest<T> request, CancellationToken cancellationToken = default)
public async Task<T?> SendAsync<T>(IRequest<T> request, CancellationToken cancellationToken = default)
{
var responseType = typeof(T);
var context = new RequestContext(request, responseType, cancellationToken);
await _requestPipeline.ExecuteAsync(context);
if (context.Responses.All(x => x is T))
return context.Responses.Cast<T>().AsEnumerable();
throw new InvalidCastException($"Unable to cast objects in Responses property to type {typeof(T)}");
return (T?)context.Response;
}
/// <inheritdoc />
public async Task SendAsync(ICommand command, CancellationToken cancellationToken = default) => await SendAsync(command, _defaultCommandStrategy, cancellationToken);
/// <inheritdoc />
public async Task SendAsync(ICommand command, ICommandStrategy? strategy = default, CancellationToken cancellationToken = default)
public async Task SendAsync(ICommand command, ICommandStrategy? strategy = null, CancellationToken cancellationToken = default)
{
var resultType = typeof(Unit);
strategy ??= _defaultCommandStrategy;
@ -69,7 +66,7 @@ public class DefaultMediator : IMediator
public async Task<T> SendAsync<T>(ICommand<T> command, CancellationToken cancellationToken = default) => await SendAsync(command, _defaultCommandStrategy, cancellationToken);
/// <inheritdoc />
public async Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy strategy, CancellationToken cancellationToken = default)
public async Task<T> SendAsync<T>(ICommand<T> command, ICommandStrategy? strategy, CancellationToken cancellationToken = default)
{
var resultType = typeof(T);
strategy ??= _defaultCommandStrategy;
@ -83,7 +80,7 @@ public class DefaultMediator : IMediator
public async Task SendAsync(INotification notification, CancellationToken cancellationToken = default) => await SendAsync(notification, _defaultPublishingStrategy, cancellationToken);
/// <inheritdoc />
public async Task SendAsync(INotification notification, IEventPublishingStrategy? strategy = default, CancellationToken cancellationToken = default)
public async Task SendAsync(INotification notification, IEventPublishingStrategy? strategy = null, CancellationToken cancellationToken = default)
{
strategy ??= _defaultPublishingStrategy;
var context = new NotificationContext(notification, strategy, cancellationToken);

View file

@ -13,7 +13,6 @@
<ItemGroup>
<ProjectReference Include="..\Elsa.Liquid\Elsa.Liquid.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime.ProtoActor\Elsa.Workflows.Runtime.ProtoActor.csproj" />
<ProjectReference Include="..\Elsa.SasTokens\Elsa.SasTokens.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />

View file

@ -16,7 +16,6 @@ using Elsa.Http.Tasks;
using Elsa.Http.UIHints;
using Elsa.Workflows;
using Elsa.Workflows.Management.Requests;
using Elsa.Workflows.Management.Responses;
using FluentStorage;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
@ -168,7 +167,7 @@ public class HttpFeature(IModule module) : FeatureBase(module)
.AddHttpContextAccessor()
// Handlers.
.AddRequestHandler<ValidateWorkflowRequestHandler, ValidateWorkflowRequest, ValidateWorkflowResponse>()
.AddNotificationHandler<ValidateWorkflowRequestHandler>()
.AddNotificationHandler<UpdateRouteTable>()
// Content parsers.

View file

@ -2,9 +2,7 @@ using Elsa.Extensions;
using Elsa.Http.Bookmarks;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Requests;
using Elsa.Workflows.Management.Responses;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Filters;
using JetBrains.Annotations;
@ -15,7 +13,7 @@ namespace Elsa.Http.Handlers;
/// A <see cref="ValidateWorkflowRequest"/> handler that validates a workflow path and return any errors.
/// </summary>
[UsedImplicitly]
public class ValidateWorkflowRequestHandler : IRequestHandler<ValidateWorkflowRequest, ValidateWorkflowResponse>
public class ValidateWorkflowRequestHandler : INotificationHandler<WorkflowDefinitionValidating>
{
private readonly ITriggerStore _triggerStore;
private readonly ITriggerIndexer _triggerIndexer;
@ -28,14 +26,17 @@ public class ValidateWorkflowRequestHandler : IRequestHandler<ValidateWorkflowRe
_triggerStore = triggerStore;
_triggerIndexer = triggerIndexer;
}
/// <inheritdoc />
public async Task<ValidateWorkflowResponse> HandleAsync(ValidateWorkflowRequest request, CancellationToken cancellationToken)
public async Task HandleAsync(WorkflowDefinitionValidating notification, CancellationToken cancellationToken)
{
var workflow = request.Workflow;
var workflow = notification.Workflow;
var httpEndpointTriggers = (await _triggerIndexer.GetTriggersAsync(workflow, cancellationToken)).Where(x => x.Payload is HttpEndpointBookmarkPayload).ToList();
var publishedWorkflowsTriggers = (await _triggerStore.FindManyAsync(new TriggerFilter { Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint)) }, cancellationToken)).ToList();
var validationErrors = new List<WorkflowValidationError>();
var filter = new TriggerFilter
{
Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint))
};
var publishedWorkflowsTriggers = (await _triggerStore.FindManyAsync(filter, cancellationToken)).ToList();
var validationErrors = notification.ValidationErrors;
foreach (var httpEndpointTrigger in httpEndpointTriggers)
{
@ -53,9 +54,7 @@ public class ValidateWorkflowRequestHandler : IRequestHandler<ValidateWorkflowRe
continue;
var message = $"The {triggerPayload.Path} path and {triggerPayload.Method} method are already in use by another workflow!";
validationErrors.Add(new WorkflowValidationError(message, httpEndpointTrigger.ActivityId));
validationErrors.Add(new(message, httpEndpointTrigger.ActivityId));
}
return new ValidateWorkflowResponse(validationErrors);
}
}

View file

@ -1,14 +1,13 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Management.Requests;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetByDefinitionId;
[PublicAPI]
internal class GetByDefinitionId(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request>
internal class GetByDefinitionId(IMediator mediator, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request>
{
public override void Configure()
{
@ -19,16 +18,9 @@ internal class GetByDefinitionId(IWorkflowDefinitionStore store, IWorkflowDefini
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var versionOptions = request.VersionOptions != null ? VersionOptions.FromString(request.VersionOptions) : VersionOptions.Latest;
var filter = new WorkflowDefinitionFilter
{
DefinitionId = request.DefinitionId,
VersionOptions = versionOptions
};
var order = new WorkflowDefinitionOrder<int>(x => x.Version, OrderDirection.Descending);
var definition = (await store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault();
var findRequest = new FindWorkflowDefinitionRequest(request.DefinitionId, versionOptions);
var definition = await mediator.SendAsync(findRequest, cancellationToken);
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);

View file

@ -14,7 +14,7 @@ public static class ModuleExtensions
/// <summary>
/// Adds the workflow management feature to the specified module.
/// </summary>
public static IModule UseWorkflowManagement(this IModule module, Action<WorkflowManagementFeature>? configure = default)
public static IModule UseWorkflowManagement(this IModule module, Action<WorkflowManagementFeature>? configure = null)
{
module.Configure<WorkflowManagementFeature>(management =>
{
@ -27,7 +27,7 @@ public static class ModuleExtensions
/// <summary>
/// Adds the default workflow management feature to the specified module.
/// </summary>
public static WorkflowManagementFeature UseWorkflowDefinitions(this WorkflowManagementFeature feature, Action<WorkflowDefinitionsFeature>? configure = default)
public static WorkflowManagementFeature UseWorkflowDefinitions(this WorkflowManagementFeature feature, Action<WorkflowDefinitionsFeature>? configure = null)
{
feature.Module.Configure(configure);
return feature;
@ -36,7 +36,7 @@ public static class ModuleExtensions
/// <summary>
/// Adds the workflow instance feature to workflow management module.
/// </summary>
public static WorkflowManagementFeature UseWorkflowInstances(this WorkflowManagementFeature feature, Action<WorkflowInstancesFeature>? configure = default)
public static WorkflowManagementFeature UseWorkflowInstances(this WorkflowManagementFeature feature, Action<WorkflowInstancesFeature>? configure = null)
{
feature.Module.Configure(configure);
return feature;
@ -45,7 +45,7 @@ public static class ModuleExtensions
/// <summary>
/// Adds the Elsa DSL integration feature.
/// </summary>
public static WorkflowManagementFeature UseDslIntegration(this WorkflowManagementFeature feature, Action<DslIntegrationFeature>? configure = default)
public static WorkflowManagementFeature UseDslIntegration(this WorkflowManagementFeature feature, Action<DslIntegrationFeature>? configure = null)
{
feature.Module.Configure(configure);
return feature;
@ -84,7 +84,7 @@ public static class ModuleExtensions
/// <summary>
/// Adds caching stores feature to the workflow management feature.
/// </summary>
public static WorkflowManagementFeature UseCache(this WorkflowManagementFeature feature, Action<CachingWorkflowDefinitionsFeature>? configure = default)
public static WorkflowManagementFeature UseCache(this WorkflowManagementFeature feature, Action<CachingWorkflowDefinitionsFeature>? configure = null)
{
feature.Module.Configure(configure);
return feature;

View file

@ -1,6 +1,7 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Handlers;
using Elsa.Workflows.Management.Handlers.Notification;
using Elsa.Workflows.Management.Services;
using Elsa.Workflows.Management.Stores;
using Microsoft.Extensions.DependencyInjection;

View file

@ -1,5 +1,9 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Handlers.Request;
using Elsa.Workflows.Management.Requests;
using Elsa.Workflows.Management.Stores;
using Microsoft.Extensions.DependencyInjection;
@ -19,10 +23,14 @@ public class WorkflowDefinitionsFeature : FeatureBase
/// The factory to create new instances of <see cref="IWorkflowDefinitionStore"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowDefinitionStore> WorkflowDefinitionStore { get; set; } = sp => sp.GetRequiredService<MemoryWorkflowDefinitionStore>();
public Func<Type> FindWorkflowDefinitionHandler { get; set; } = () => typeof(FindWorkflowDefinitionHandler);
/// <inheritdoc />
public override void Apply()
{
Services.AddScoped(WorkflowDefinitionStore);
Services
.AddScoped(WorkflowDefinitionStore)
.AddScoped(typeof(IRequestHandler), FindWorkflowDefinitionHandler())
;
}
}

View file

@ -18,6 +18,7 @@ using Elsa.Workflows.Management.Compression;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Handlers;
using Elsa.Workflows.Management.Handlers.Notification;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;

View file

@ -3,7 +3,7 @@ using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Notifications;
using JetBrains.Annotations;
namespace Elsa.Workflows.Management.Handlers;
namespace Elsa.Workflows.Management.Handlers.Notification;
/// <summary>
/// Deletes workflow instances when a workflow definition or version is deleted.

View file

@ -2,7 +2,7 @@ using Elsa.Mediator.Contracts;
using Elsa.Workflows.Management.Notifications;
using JetBrains.Annotations;
namespace Elsa.Workflows.Management.Handlers;
namespace Elsa.Workflows.Management.Handlers.Notification;
/// <summary>
/// A workflow definition notifications handler for evicting the cache of the workflow definition service.

View file

@ -5,7 +5,7 @@ using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Notifications;
using JetBrains.Annotations;
namespace Elsa.Workflows.Management.Handlers;
namespace Elsa.Workflows.Management.Handlers.Notification;
/// <summary>
/// Refreshes the <see cref="IActivityRegistry"/> for the <see cref="WorkflowDefinitionActivityProvider"/> provider whenever an <see cref="WorkflowDefinition"/> is published, retracted or deleted.

View file

@ -1,9 +1,8 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Notifications;
namespace Elsa.Workflows.Management.Handlers;
namespace Elsa.Workflows.Management.Handlers.Notification;
/// <summary>
/// Updates consuming workflows when a workflow definition is published.

View file

@ -0,0 +1,23 @@
using Elsa.Common.Entities;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Requests;
namespace Elsa.Workflows.Management.Handlers.Request;
public class FindWorkflowDefinitionHandler(IWorkflowDefinitionStore store) : IRequestHandler<FindWorkflowDefinitionRequest, WorkflowDefinition?>
{
public async Task<WorkflowDefinition?> HandleAsync(FindWorkflowDefinitionRequest request, CancellationToken cancellationToken)
{
var filter = new WorkflowDefinitionFilter
{
DefinitionId = request.DefinitionId,
VersionOptions = request.VersionOptions
};
var order = new WorkflowDefinitionOrder<int>(x => x.Version, OrderDirection.Descending);
var definition = (await store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault();
return definition;
}
}

View file

@ -1,5 +1,6 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Models;
@ -34,9 +35,9 @@ public class WorkflowDefinitionMapper
var root = _activitySerializer.Deserialize(source.StringData!);
return new(
new WorkflowIdentity(source.DefinitionId, source.Version, source.Id, source.TenantId),
new WorkflowPublication(source.IsLatest, source.IsPublished),
new WorkflowMetadata(source.Name, source.Description, source.CreatedAt, source.ToolVersion),
new(source.DefinitionId, source.Version, source.Id, source.TenantId),
new(source.IsLatest, source.IsPublished),
new(source.Name, source.Description, source.CreatedAt, source.ToolVersion),
source.Options,
root,
source.Variables,
@ -66,9 +67,9 @@ public class WorkflowDefinitionMapper
#pragma warning restore CS0618
return new(
new WorkflowIdentity(source.DefinitionId, source.Version, source.Id, source.TenantId),
new WorkflowPublication(source.IsLatest, source.IsPublished),
new WorkflowMetadata(source.Name, source.Description, source.CreatedAt, source.ToolVersion),
new(source.DefinitionId, source.Version, source.Id, source.TenantId),
new(source.IsLatest, source.IsPublished),
new(source.Name, source.Description, source.CreatedAt, source.ToolVersion),
options,
root,
variables,
@ -79,6 +80,38 @@ public class WorkflowDefinitionMapper
source.IsReadonly,
source.IsSystem);
}
public WorkflowDefinition MapToWorkflowDefinition(WorkflowDefinitionModel source)
{
var root = source.Root!;
var variables = _variableDefinitionMapper.Map(source.Variables).ToList();
var options = source.Options ?? new WorkflowOptions();
var stringData = _activitySerializer.Serialize(root);
return new()
{
IsPublished = source.IsPublished,
Description = source.Description,
Id = source.Id,
Inputs = source.Inputs ?? [],
Name = source.Name,
Options = options,
Outcomes = source.Outcomes ?? [],
Outputs = source.Outputs ?? [],
Variables = variables,
Version = source.Version,
CreatedAt = source.CreatedAt,
CustomProperties = source.CustomProperties ?? new Dictionary<string, object>(),
DefinitionId = source.DefinitionId,
IsLatest = source.IsLatest,
IsReadonly = source.IsReadonly,
IsSystem = source.IsSystem,
TenantId = source.TenantId,
ToolVersion = source.ToolVersion,
StringData = stringData,
MaterializerName = JsonWorkflowMaterializer.MaterializerName
};
}
/// <summary>
/// Maps many <see cref="WorkflowDefinition"/>s to many <see cref="WorkflowDefinitionModel"/>s.
@ -86,8 +119,10 @@ public class WorkflowDefinitionMapper
/// <param name="source">The source <see cref="WorkflowDefinition"/>s.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The mapped <see cref="WorkflowDefinitionModel"/>s.</returns>
public async Task<IEnumerable<WorkflowDefinitionModel>> MapAsync(IEnumerable<WorkflowDefinition> source, CancellationToken cancellationToken = default) =>
await Task.WhenAll(source.Select(async x => await MapAsync(x, cancellationToken)));
public async Task<IEnumerable<WorkflowDefinitionModel>> MapAsync(IEnumerable<WorkflowDefinition> source, CancellationToken cancellationToken = default)
{
return await Task.WhenAll(source.Select(async x => await MapAsync(x, cancellationToken)));
}
/// <summary>
/// Maps a <see cref="WorkflowDefinition"/> to a <see cref="Workflow"/>.

View file

@ -31,7 +31,7 @@ public class JsonWorkflowMaterializer : IWorkflowMaterializer
public ValueTask<Workflow> MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken)
{
var workflow = ToWorkflow(definition);
return new ValueTask<Workflow>(workflow);
return new(workflow);
}
private Workflow ToWorkflow(WorkflowDefinition definition) => _workflowDefinitionMapper.Map(definition);

View file

@ -5,4 +5,4 @@ namespace Elsa.Workflows.Management.Models;
/// </summary>
/// <param name="Message">The error message.</param>
/// <param name="ActivityId">The Id of the activity that caused the error, if any.</param>
public record WorkflowValidationError(string Message, string? ActivityId = default);
public record WorkflowValidationError(string Message, string? ActivityId = null);

View file

@ -1,11 +1,11 @@
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Responses;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Requests;
namespace Elsa.Workflows.Management.Notifications;
/// <summary>
/// A request to validate a workflow definition.
/// </summary>
/// <param name="Workflow">The workflow materialized from the definition.</param>
public record ValidateWorkflowRequest(Workflow Workflow) : IRequest<ValidateWorkflowResponse>;
public record WorkflowDefinitionValidating(Workflow Workflow, ICollection<WorkflowValidationError> ValidationErrors) : INotification;

View file

@ -0,0 +1,12 @@
using Elsa.Common.Models;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Management.Entities;
namespace Elsa.Workflows.Management.Requests;
/// <summary>
/// A request to find a workflow definition.
/// </summary>
/// <param name="DefinitionId">The ID of the workflow definition.</param>
/// <param name="VersionOptions">The version options.</param>
public record FindWorkflowDefinitionRequest(string DefinitionId, VersionOptions VersionOptions) : IRequest<WorkflowDefinition?>;

View file

@ -1,10 +0,0 @@
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Requests;
namespace Elsa.Workflows.Management.Responses;
/// <summary>
/// Provides a response to a <see cref="ValidateWorkflowRequest"/>.
/// </summary>
/// <param name="ValidationErrors">The validation errors, if any.</param>
public record ValidateWorkflowResponse(ICollection<WorkflowValidationError> ValidationErrors);

View file

@ -14,54 +14,33 @@ using Elsa.Workflows.Management.Requests;
namespace Elsa.Workflows.Management.Services;
/// <inheritdoc />
public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
public class WorkflowDefinitionPublisher(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowDefinitionStore workflowDefinitionStore,
IWorkflowValidator workflowValidator,
INotificationSender notificationSender,
IIdentityGenerator identityGenerator,
IActivitySerializer activitySerializer,
ISystemClock systemClock)
: IWorkflowDefinitionPublisher
{
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
private readonly INotificationSender _notificationSender;
private readonly IIdentityGenerator _identityGenerator;
private readonly IActivitySerializer _activitySerializer;
private readonly IRequestSender _requestSender;
private readonly ISystemClock _systemClock;
/// <summary>
/// Constructor.
/// </summary>
public WorkflowDefinitionPublisher(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowDefinitionStore workflowDefinitionStore,
INotificationSender notificationSender,
IIdentityGenerator identityGenerator,
IActivitySerializer activitySerializer,
IRequestSender requestSender,
ISystemClock systemClock)
{
_workflowDefinitionService = workflowDefinitionService;
_workflowDefinitionStore = workflowDefinitionStore;
_notificationSender = notificationSender;
_identityGenerator = identityGenerator;
_activitySerializer = activitySerializer;
_requestSender = requestSender;
_systemClock = systemClock;
}
/// <inheritdoc />
public WorkflowDefinition New(IActivity? root = null)
{
root ??= new Sequence();
var id = _identityGenerator.GenerateId();
var definitionId = _identityGenerator.GenerateId();
var id = identityGenerator.GenerateId();
var definitionId = identityGenerator.GenerateId();
const int version = 1;
return new WorkflowDefinition
return new()
{
Id = id,
DefinitionId = definitionId,
Version = version,
IsLatest = true,
IsPublished = false,
CreatedAt = _systemClock.UtcNow,
StringData = _activitySerializer.Serialize(root),
CreatedAt = systemClock.UtcNow,
StringData = activitySerializer.Serialize(root),
MaterializerName = JsonWorkflowMaterializer.MaterializerName
};
}
@ -69,14 +48,18 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
/// <inheritdoc />
public async Task<PublishWorkflowDefinitionResult> PublishAsync(string definitionId, CancellationToken cancellationToken = default)
{
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.Latest };
var definition = await _workflowDefinitionStore.FindAsync(filter, cancellationToken);
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = VersionOptions.Latest
};
var definition = await workflowDefinitionStore.FindAsync(filter, cancellationToken);
if (definition == null)
return new PublishWorkflowDefinitionResult(false, new List<WorkflowValidationError>
return new(false, new List<WorkflowValidationError>
{
new("Workflow definition not found.")
}, null);
}, new([]));
return await PublishAsync(definition, cancellationToken);
}
@ -84,47 +67,53 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
/// <inheritdoc />
public async Task<PublishWorkflowDefinitionResult> PublishAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
var workflowGraph = await _workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken);
var responses = await _requestSender.SendAsync(new ValidateWorkflowRequest(workflowGraph.Workflow), cancellationToken);
var validationErrors = responses.SelectMany(r => r.ValidationErrors).ToList();
var workflowGraph = await workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken);
var validationErrors = (await workflowValidator.ValidateAsync(workflowGraph.Workflow, cancellationToken)).ToList();
if (validationErrors.Any())
return new PublishWorkflowDefinitionResult(false, validationErrors, null);
await _notificationSender.SendAsync(new WorkflowDefinitionPublishing(definition), cancellationToken);
return new(false, validationErrors, new([]));
await notificationSender.SendAsync(new WorkflowDefinitionPublishing(definition), cancellationToken);
var definitionId = definition.DefinitionId;
// Reset current latest and published definitions.
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.LatestOrPublished };
var publishedWorkflows = await _workflowDefinitionStore.FindManyAsync(filter, cancellationToken);
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = VersionOptions.LatestOrPublished
};
var publishedWorkflows = await workflowDefinitionStore.FindManyAsync(filter, cancellationToken);
foreach (var publishedAndOrLatestWorkflow in publishedWorkflows)
{
var isPublished = publishedAndOrLatestWorkflow.IsPublished;
var isPublished = publishedAndOrLatestWorkflow.IsPublished;
publishedAndOrLatestWorkflow.IsPublished = false;
publishedAndOrLatestWorkflow.IsLatest = false;
await _workflowDefinitionStore.SaveAsync(publishedAndOrLatestWorkflow, cancellationToken);
await workflowDefinitionStore.SaveAsync(publishedAndOrLatestWorkflow, cancellationToken);
if (isPublished)
await _notificationSender.SendAsync(new WorkflowDefinitionVersionRetracted(publishedAndOrLatestWorkflow), cancellationToken);
await notificationSender.SendAsync(new WorkflowDefinitionVersionRetracted(publishedAndOrLatestWorkflow), cancellationToken);
}
// Save the new published definition.
definition.IsPublished = true;
definition = Initialize(definition);
await _workflowDefinitionStore.SaveAsync(definition, cancellationToken);
await workflowDefinitionStore.SaveAsync(definition, cancellationToken);
var affectedWorkflows = new AffectedWorkflows(new List<WorkflowDefinition>());
await _notificationSender.SendAsync(new WorkflowDefinitionPublished(definition, affectedWorkflows), cancellationToken);
return new PublishWorkflowDefinitionResult(true, validationErrors, affectedWorkflows);
await notificationSender.SendAsync(new WorkflowDefinitionPublished(definition, affectedWorkflows), cancellationToken);
return new(true, validationErrors, affectedWorkflows);
}
/// <inheritdoc />
public async Task<WorkflowDefinition?> RetractAsync(string definitionId, CancellationToken cancellationToken = default)
{
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.Published };
var definition = await _workflowDefinitionStore.FindAsync(filter, cancellationToken);
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = VersionOptions.Published
};
var definition = await workflowDefinitionStore.FindAsync(filter, cancellationToken);
if (definition == null)
return null;
@ -140,19 +129,26 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
definition.IsPublished = false;
await _notificationSender.SendAsync(new WorkflowDefinitionRetracting(definition), cancellationToken);
await _workflowDefinitionStore.SaveAsync(definition, cancellationToken);
await _notificationSender.SendAsync(new WorkflowDefinitionRetracted(definition), cancellationToken);
await notificationSender.SendAsync(new WorkflowDefinitionRetracting(definition), cancellationToken);
await workflowDefinitionStore.SaveAsync(definition, cancellationToken);
await notificationSender.SendAsync(new WorkflowDefinitionRetracted(definition), cancellationToken);
return definition;
}
/// <inheritdoc />
public async Task<WorkflowDefinition?> GetDraftAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = versionOptions };
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = versionOptions
};
var order = new WorkflowDefinitionOrder<int>(x => x.Version, OrderDirection.Descending);
var lastVersion = await _workflowDefinitionStore.FindLastVersionAsync(new WorkflowDefinitionFilter { DefinitionId = definitionId }, cancellationToken);
var definition = await _workflowDefinitionStore.FindAsync(filter, order, cancellationToken) ?? lastVersion;
var lastVersion = await workflowDefinitionStore.FindLastVersionAsync(new()
{
DefinitionId = definitionId
}, cancellationToken);
var definition = await workflowDefinitionStore.FindAsync(filter, order, cancellationToken) ?? lastVersion;
if (definition == null!)
return null;
@ -163,8 +159,8 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
var draft = definition.ShallowClone();
draft.Version = lastVersion?.Version + 1 ?? 1;
draft.CreatedAt = _systemClock.UtcNow;
draft.Id = _identityGenerator.GenerateId();
draft.CreatedAt = systemClock.UtcNow;
draft.Id = identityGenerator.GenerateId();
draft.IsLatest = true;
draft.IsPublished = false;
@ -176,24 +172,27 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
{
var draft = definition;
var definitionId = definition.DefinitionId;
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId };
var lastVersion = await _workflowDefinitionStore.FindLastVersionAsync(filter, cancellationToken);
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId
};
var lastVersion = await workflowDefinitionStore.FindLastVersionAsync(filter, cancellationToken);
draft.Version = draft.Id == lastVersion?.Id ? lastVersion.Version : lastVersion?.Version + 1 ?? 1;
draft.IsLatest = true;
draft = Initialize(draft);
await _workflowDefinitionStore.SaveAsync(draft, cancellationToken);
await workflowDefinitionStore.SaveAsync(draft, cancellationToken);
if (lastVersion is null)
{
await _notificationSender.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken);
await notificationSender.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken);
}
if (lastVersion is { IsPublished: true, IsLatest: true })
{
lastVersion.IsLatest = false;
await _workflowDefinitionStore.SaveAsync(lastVersion, cancellationToken);
await workflowDefinitionStore.SaveAsync(lastVersion, cancellationToken);
}
return draft;
@ -202,10 +201,10 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher
private WorkflowDefinition Initialize(WorkflowDefinition definition)
{
if (definition.Id == null!)
definition.Id = _identityGenerator.GenerateId();
definition.Id = identityGenerator.GenerateId();
if (definition.DefinitionId == null!)
definition.DefinitionId = _identityGenerator.GenerateId();
definition.DefinitionId = identityGenerator.GenerateId();
if (definition.Version == 0)
definition.Version = 1;

View file

@ -1,27 +1,20 @@
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Management.Requests;
namespace Elsa.Workflows.Management.Services;
/// <inheritdoc />
public class WorkflowValidator : IWorkflowValidator
public class WorkflowValidator(INotificationSender notificationSender) : IWorkflowValidator
{
private readonly IRequestSender _requestSender;
/// <summary>
/// Initializes a new instance of the <see cref="WorkflowValidator"/> class.
/// </summary>
public WorkflowValidator(IRequestSender requestSender)
{
_requestSender = requestSender;
}
/// <inheritdoc />
public async Task<IEnumerable<WorkflowValidationError>> ValidateAsync(Workflow workflow, CancellationToken cancellationToken = default)
{
var responses = await _requestSender.SendAsync(new ValidateWorkflowRequest(workflow), cancellationToken);
return responses.SelectMany(r => r.ValidationErrors).ToList();
var validationErrors = new List<WorkflowValidationError>();
var notification = new WorkflowDefinitionValidating(workflow, validationErrors);
await notificationSender.SendAsync(notification, cancellationToken);
return validationErrors;
}
}