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:
parent
7e06feffc2
commit
ad8eeecdce
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"/>.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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?>;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue