From 70b8582b23e4ece72d12f67c3bf5a35465bb8b26 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 22 Jul 2024 14:36:33 +0200 Subject: [PATCH] Add OpenTelemetry module (#5810) * Add OpenTelemetry integration for workflow tracing Introduced a new module, Elsa.OpenTelemetry, to provide OpenTelemetry sources for tracing workflow and activity execution. Updated various components and pipeline extensions to support OpenTelemetry tracing throughout the workflow execution process. * Refactor workflow execution pipelines Deleted `WorkflowsFeatureExtensions` and migrated methods to `PipelineWorkflowsFeatureExtensions` with added configurability. Enhanced `ActivityExecutionMiddlewareExtensions` and `ActivityExecutionPipelinePipelineBuilder` to support middleware insertion. Added comprehensive tracing to `OpenTelemetryTracingWorkflowExecutionMiddleware`. * Add OpenTelemetry tracing to activity execution Introduced `OpenTelemetryTracingActivityExecutionMiddleware` to capture tracing information for activity execution within workflows. This middleware logs activity execution start and end events, attaching pertinent activity tags. Added extension method to register this middleware in the workflow execution pipeline. * Set `PYTHONNET_PYDLL` consistently and update workflow pipelines Update Dockerfiles to set the `PYTHONNET_PYDLL` environment variable consistently without spaces. Additionally, refactor `Program.cs` to streamline workflow and activity execution pipeline configurations by using the `WithDefaultWorkflowExecutionPipeline` and `WithDefaultActivityExecutionPipeline` methods. * Enhance OpenTelemetry Tracing Middleware Implementation Add missing activity tags and events to improve telemetry data. Simplify middleware installation syntax for both workflow and activity execution tracing pipelines, ensuring consistent and clear tracing across modules. --- Elsa.sln | 7 ++++ docker/ElsaServer-Datadog.Dockerfile | 2 +- docker/ElsaServer.Dockerfile | 2 +- docker/ElsaServerAndStudio.Dockerfile | 2 +- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 1 + src/apps/Elsa.Server.Web/Program.cs | 9 +++++ .../Elsa.OpenTelemetry.csproj | 18 +++++++++ .../Features/OpenTelemetryFeature.cs | 11 ++++++ .../Elsa.OpenTelemetry/FodyWeavers.xml | 3 ++ .../Helpers/OpenTelemetryHelpers.cs | 8 ++++ ...metryTracingActivityExecutionMiddleware.cs | 36 ++++++++++++++++++ ...metryTracingWorkflowExecutionMiddleware.cs | 38 +++++++++++++++++++ .../WorkflowExecutionBuilderExtensions.cs | 1 + .../Contracts/IActivityExecutionMiddleware.cs | 2 +- .../IActivityExecutionPipelineBuilder.cs | 5 ++- .../IWorkflowExecutionPipelineBuilder.cs | 21 +++------- .../ActivityExecutionMiddlewareExtensions.cs | 27 ++++++++++--- ...ctivityExecutionPipelinePipelineBuilder.cs | 20 ++++------ .../WorkflowExecutionMiddlewareExtensions.cs | 22 +++++------ .../WorkflowExecutionPipelineBuilder.cs | 8 +++- ...ivityExecutionPipelineBuilderExtensions.cs | 1 + .../PipelineWorkflowsFeatureExtensions.cs | 36 ++++++++++++++++++ ...kflowExecutionPipelineBuilderExtensions.cs | 2 +- .../Extensions/WorkflowsFeatureExtensions.cs | 30 --------------- .../Extensions/ServiceProviderExtensions.cs | 1 + src/modules/Elsa/Features/ElsaFeature.cs | 2 +- 26 files changed, 231 insertions(+), 84 deletions(-) create mode 100644 src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj create mode 100644 src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs create mode 100644 src/modules/Elsa.OpenTelemetry/FodyWeavers.xml create mode 100644 src/modules/Elsa.OpenTelemetry/Helpers/OpenTelemetryHelpers.cs create mode 100644 src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs create mode 100644 src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs delete mode 100644 src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowsFeatureExtensions.cs diff --git a/Elsa.sln b/Elsa.sln index de7cd82fb..239e86f9b 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -373,6 +373,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.ServerAndStudio.Web", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\apps\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{169A82A5-2DB3-40EA-801E-14C08D743DF7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.OpenTelemetry", "src\modules\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj", "{2CDF3E1C-267D-4198-B1C7-7E1F548FC120}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -885,6 +887,10 @@ Global {169A82A5-2DB3-40EA-801E-14C08D743DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {169A82A5-2DB3-40EA-801E-14C08D743DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU {169A82A5-2DB3-40EA-801E-14C08D743DF7}.Release|Any CPU.Build.0 = Release|Any CPU + {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CDF3E1C-267D-4198-B1C7-7E1F548FC120}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1039,6 +1045,7 @@ Global {690B0274-291F-4D9E-BA76-54EFF7D3E4BC} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} {060FD0BA-BD78-48E1-A8A7-4906A5AD5E39} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} {169A82A5-2DB3-40EA-801E-14C08D743DF7} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} + {2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/docker/ElsaServer-Datadog.Dockerfile b/docker/ElsaServer-Datadog.Dockerfile index 3af4d992a..207e69e5d 100644 --- a/docker/ElsaServer-Datadog.Dockerfile +++ b/docker/ElsaServer-Datadog.Dockerfile @@ -36,7 +36,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* # Set PYTHONNET_PYDLL environment variable -ENV PYTHONNET_PYDLL /usr/lib/aarch64-linux-gnu/libpython3.11.so +ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so # Copy the tracer from build target COPY --from=build /tmp/datadog-dotnet-apm.deb /tmp/datadog-dotnet-apm.deb diff --git a/docker/ElsaServer.Dockerfile b/docker/ElsaServer.Dockerfile index 5c6ca57c1..219a46a74 100644 --- a/docker/ElsaServer.Dockerfile +++ b/docker/ElsaServer.Dockerfile @@ -28,7 +28,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* # Set PYTHONNET_PYDLL environment variable -ENV PYTHONNET_PYDLL /usr/lib/aarch64-linux-gnu/libpython3.11.so +ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so EXPOSE 80/tcp EXPOSE 443/tcp diff --git a/docker/ElsaServerAndStudio.Dockerfile b/docker/ElsaServerAndStudio.Dockerfile index bcfdcba90..2de69c82e 100644 --- a/docker/ElsaServerAndStudio.Dockerfile +++ b/docker/ElsaServerAndStudio.Dockerfile @@ -29,7 +29,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* # Set PYTHONNET_PYDLL environment variable -ENV PYTHONNET_PYDLL /usr/lib/aarch64-linux-gnu/libpython3.11.so +ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so EXPOSE 80/tcp EXPOSE 443/tcp diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index 7622f41c1..1e3e878f0 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -5,6 +5,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 3ea373136..c77607f0e 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -21,13 +21,17 @@ using Elsa.MongoDb.Modules.Alterations; using Elsa.MongoDb.Modules.Identity; using Elsa.MongoDb.Modules.Management; using Elsa.MongoDb.Modules.Runtime; +using Elsa.OpenTelemetry.Middleware; using Elsa.Server.Web; using Elsa.Tenants.Extensions; using Elsa.Workflows; using Elsa.Workflows.Api; using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Stores; +using Elsa.Workflows.Pipelines.ActivityExecution; +using Elsa.Workflows.Pipelines.WorkflowExecution; using Elsa.Workflows.Runtime.Distributed.Extensions; +using Elsa.Workflows.Runtime.Extensions; using Elsa.Workflows.Runtime.Stores; using JetBrains.Annotations; using Medallion.Threading.FileSystem; @@ -133,6 +137,11 @@ services identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options)); }) .UseDefaultAuthentication() + .UseWorkflows(workflows => + { + workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing()); + workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing()); + }) .UseWorkflowManagement(management => { if (persistenceProvider == PersistenceProvider.MongoDb) diff --git a/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj b/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj new file mode 100644 index 000000000..8660d883e --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj @@ -0,0 +1,18 @@ + + + + + Provides OpenTelemetry sources for tracing workflow and activity execution. + + elsa module open-telemetry + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs new file mode 100644 index 000000000..67201c44d --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Features/OpenTelemetryFeature.cs @@ -0,0 +1,11 @@ +using Elsa.Features.Abstractions; +using Elsa.Features.Services; + +namespace Elsa.OpenTelemetry.Features; + +public class OpenTelemetryFeature(IModule module) : FeatureBase(module) +{ + public override void Configure() + { + } +} \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/FodyWeavers.xml b/src/modules/Elsa.OpenTelemetry/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Helpers/OpenTelemetryHelpers.cs b/src/modules/Elsa.OpenTelemetry/Helpers/OpenTelemetryHelpers.cs new file mode 100644 index 000000000..302659456 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Helpers/OpenTelemetryHelpers.cs @@ -0,0 +1,8 @@ +using System.Diagnostics; + +namespace Elsa.OpenTelemetry.Helpers; + +public class ElsaOpenTelemetry +{ + public static readonly ActivitySource ActivitySource = new("Elsa.Workflows"); +} \ No newline at end of file diff --git a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs new file mode 100644 index 000000000..4c8e1e1aa --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingActivityExecutionMiddleware.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; +using Elsa.OpenTelemetry.Helpers; +using Elsa.Workflows; +using Elsa.Workflows.Pipelines.ActivityExecution; +using Elsa.Workflows.Pipelines.WorkflowExecution; +using JetBrains.Annotations; +using Activity = System.Diagnostics.Activity; +using ActivityKind = System.Diagnostics.ActivityKind; + +namespace Elsa.OpenTelemetry.Middleware; + +[UsedImplicitly] +public class OpenTelemetryTracingActivityExecutionMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware +{ + public async ValueTask InvokeAsync(ActivityExecutionContext context) + { + var activity = context.Activity; + using var span = ElsaOpenTelemetry.ActivitySource.StartActivity($"ActivityExecution {context.ActivityDescriptor.TypeName}", ActivityKind.Internal, Activity.Current?.Context ?? default); + span?.AddTag("activity.nodeId", activity.NodeId); + span?.AddTag("activity.type", activity.Type); + span?.AddTag("activity.name", activity.Name); + span?.AddTag("activityInstance.id", context.Id); + span?.AddTag("activityInstance.originalStatus", context.Status.ToString()); + span?.AddEvent(new ActivityEvent("Executing")); + await next(context); + span?.AddEvent(new ActivityEvent("Executed")); + span?.AddTag("activityInstance.newStatus", context.Status.ToString()); + } +} + +[UsedImplicitly] +public static class OpenTelemetryTracingActivityExecutionMiddlewareExtensions +{ + /// Installs the component in the workflow execution pipeline. + public static IActivityExecutionPipelineBuilder UseActivityExecutionTracing(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert(0); +} diff --git a/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs new file mode 100644 index 000000000..4876aded3 --- /dev/null +++ b/src/modules/Elsa.OpenTelemetry/Middleware/OpenTelemetryTracingWorkflowExecutionMiddleware.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; +using Elsa.OpenTelemetry.Helpers; +using Elsa.Workflows; +using Elsa.Workflows.Pipelines.WorkflowExecution; +using JetBrains.Annotations; +using Activity = System.Diagnostics.Activity; +using ActivityKind = System.Diagnostics.ActivityKind; + +namespace Elsa.OpenTelemetry.Middleware; + +[UsedImplicitly] +public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next) : WorkflowExecutionMiddleware(next) +{ + public override async ValueTask InvokeAsync(WorkflowExecutionContext context) + { + var workflowInstanceId = context.Id; + var workflow = context.Workflow; + using var activity = ElsaOpenTelemetry.ActivitySource.StartActivity($"WorkflowExecution {workflow.WorkflowMetadata.Name}", ActivityKind.Internal, Activity.Current?.Context ?? default); + activity?.AddTag("workflowInstance.id", workflowInstanceId); + activity?.AddTag("workflowDefinition.definitionId", workflow.Identity.DefinitionId); + activity?.AddTag("workflowDefinition.version", workflow.Identity.Version); + activity?.AddTag("tenantId", workflow.Identity.TenantId); + activity?.AddTag("workflowInstance.originalStatus", context.Status.ToString()); + activity?.AddTag("workflowInstance.originalSubStatus", context.SubStatus.ToString()); + activity?.AddEvent(new ActivityEvent("Executing")); + await Next(context); + activity?.AddEvent(new ActivityEvent("Executed")); + activity?.AddTag("workflowInstance.newStatus", context.Status.ToString()); + activity?.AddTag("workflowInstance.newSubStatus", context.SubStatus.ToString()); + } +} + +[UsedImplicitly] +public static class OpenTelemetryWorkflowExecutionMiddlewareExtensions +{ + /// Installs the component in the workflow execution pipeline. + public static IWorkflowExecutionPipelineBuilder UseWorkflowExecutionTracing(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert(0); +} diff --git a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs index 7f78ccd3b..03fc7c966 100644 --- a/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs +++ b/src/modules/Elsa.WorkflowContexts/Extensions/WorkflowExecutionBuilderExtensions.cs @@ -1,4 +1,5 @@ using Elsa.WorkflowContexts.Middleware; +using Elsa.Workflows; using Elsa.Workflows.Contracts; using Elsa.Workflows.Pipelines.ActivityExecution; using Elsa.Workflows.Pipelines.WorkflowExecution; diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs index 6094ed7e5..bb786f283 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionMiddleware.cs @@ -1,4 +1,4 @@ -namespace Elsa.Workflows.Contracts; +namespace Elsa.Workflows; public interface IActivityExecutionMiddleware { diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs index 7c7968451..3d60e9f68 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityExecutionPipelineBuilder.cs @@ -1,6 +1,6 @@ using Elsa.Workflows.Pipelines.ActivityExecution; -namespace Elsa.Workflows.Contracts; +namespace Elsa.Workflows; /// /// Builds an activity execution pipeline. @@ -19,6 +19,9 @@ public interface IActivityExecutionPipelineBuilder /// The current . IActivityExecutionPipelineBuilder Use(Func middleware); + /// Inserts the middleware component at the specified index. + IActivityExecutionPipelineBuilder Insert(int index, Func middleware); + /// /// Constructs the final delegate that invokes each installed middleware component. /// diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs index a2f37c9b0..f0de095d9 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowExecutionPipelineBuilder.cs @@ -1,41 +1,30 @@ using Elsa.Workflows.Pipelines.WorkflowExecution; -namespace Elsa.Workflows.Contracts; +namespace Elsa.Workflows; -/// /// Builds a workflow execution pipeline. -/// public interface IWorkflowExecutionPipelineBuilder { - /// /// A general-purpose dictionary of values that can be used by middleware components. - /// public IDictionary Properties { get; } - /// /// The middleware components that have been installed. - /// public IEnumerable> Components { get; } - /// /// The current service provider to resolve services from. - /// IServiceProvider ServiceProvider { get; } - /// /// Installs the specified delegate as a middleware component. - /// IWorkflowExecutionPipelineBuilder Use(Func middleware); - /// /// Constructs the final delegate that invokes each installed middleware component. - /// public WorkflowMiddlewareDelegate Build(); - - /// + /// Clears the current pipeline. - /// IWorkflowExecutionPipelineBuilder Reset(); + + /// Inserts the middleware component at the specified index. + IWorkflowExecutionPipelineBuilder Insert(int index, Func middleware); /// /// Replaces the middleware component at the specified index with the specified delegate. diff --git a/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionMiddlewareExtensions.cs b/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionMiddlewareExtensions.cs index 487b41340..c1565a907 100644 --- a/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionMiddlewareExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionMiddlewareExtensions.cs @@ -1,20 +1,37 @@ -using Elsa.Workflows.Contracts; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Pipelines.ActivityExecution; public static class ActivityExecutionMiddlewareExtensions { - public static IActivityExecutionPipelineBuilder UseMiddleware(this IActivityExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IActivityExecutionMiddleware + public static IActivityExecutionPipelineBuilder UseMiddleware<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>(this IActivityExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IActivityExecutionMiddleware + { + var delegateFactory = CreateMiddlewareDelegateFactory(pipelineBuilder, args); + return pipelineBuilder.Use(delegateFactory); + } + + public static IActivityExecutionPipelineBuilder Insert<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]TMiddleware>(this IActivityExecutionPipelineBuilder pipelineBuilder, int index, params object[] args) where TMiddleware : IActivityExecutionMiddleware + { + var delegateFactory = CreateMiddlewareDelegateFactory(pipelineBuilder, args); + return pipelineBuilder.Insert(index, delegateFactory); + } + + /// Creates a middleware delegate for the specified middleware component. + public static Func CreateMiddlewareDelegateFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>( + this IActivityExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IActivityExecutionMiddleware { var middleware = typeof(TMiddleware); - return pipelineBuilder.Use(next => + return next => { var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware); - var ctorArgs = new[] { next }.Concat(args).Select(x => x!).ToArray(); + var ctorArgs = new[] + { + next + }.Concat(args).Select(x => x).ToArray(); var instance = ActivatorUtilities.CreateInstance(pipelineBuilder.ServiceProvider, middleware, ctorArgs); return (ActivityMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(ActivityMiddlewareDelegate), instance); - }); + }; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipelinePipelineBuilder.cs b/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipelinePipelineBuilder.cs index 971ebfbca..76850bbb9 100644 --- a/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipelinePipelineBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipelinePipelineBuilder.cs @@ -1,22 +1,12 @@ -using Elsa.Workflows.Contracts; - namespace Elsa.Workflows.Pipelines.ActivityExecution; /// -public class ActivityExecutionPipelinePipelineBuilder : IActivityExecutionPipelineBuilder +public class ActivityExecutionPipelinePipelineBuilder(IServiceProvider serviceProvider) : IActivityExecutionPipelineBuilder { private readonly IList> _components = new List>(); - /// - /// Initializes a new instance of the class. - /// - public ActivityExecutionPipelinePipelineBuilder(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - /// - public IServiceProvider ServiceProvider { get; } + public IServiceProvider ServiceProvider { get; } = serviceProvider; /// public IActivityExecutionPipelineBuilder Use(Func middleware) @@ -25,6 +15,12 @@ public class ActivityExecutionPipelinePipelineBuilder : IActivityExecutionPipeli return this; } + public IActivityExecutionPipelineBuilder Insert(int index, Func middleware) + { + _components.Insert(index, middleware); + return this; + } + /// public ActivityMiddlewareDelegate Build() { diff --git a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionMiddlewareExtensions.cs b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionMiddlewareExtensions.cs index 4e7221c11..4007c8a16 100644 --- a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionMiddlewareExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionMiddlewareExtensions.cs @@ -4,44 +4,42 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Pipelines.WorkflowExecution; -/// /// Provides extensions to that adds support for installing components. -/// public static class WorkflowExecutionMiddlewareExtensions { - /// /// Installs the specified middleware component into the pipeline being built. - /// public static IWorkflowExecutionPipelineBuilder UseMiddleware<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>( this IWorkflowExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware { var delegateFactory = CreateMiddlewareDelegateFactory(pipelineBuilder, args); return pipelineBuilder.Use(delegateFactory); } + + /// Installs the specified middleware component into the pipeline being built. + public static IWorkflowExecutionPipelineBuilder Insert<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>( + this IWorkflowExecutionPipelineBuilder pipelineBuilder, int index, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware + { + var delegateFactory = CreateMiddlewareDelegateFactory(pipelineBuilder, args); + return pipelineBuilder.Insert(index, delegateFactory); + } - /// /// Replaces the terminal middleware component with the specified middleware component. - /// public static IWorkflowExecutionPipelineBuilder ReplaceTerminal<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>( this IWorkflowExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware { var index = pipelineBuilder.Components.Count() - 1; return pipelineBuilder.Replace(index, args); } - - /// + /// Replaces the middleware component at the specified index with the specified middleware component. - /// public static IWorkflowExecutionPipelineBuilder Replace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>( this IWorkflowExecutionPipelineBuilder pipelineBuilder, int index, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware { var delegateFactory = CreateMiddlewareDelegateFactory(pipelineBuilder, args); return pipelineBuilder.Replace(index, delegateFactory); } - - /// + /// Creates a middleware delegate for the specified middleware component. - /// public static Func CreateMiddlewareDelegateFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TMiddleware>( this IWorkflowExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IWorkflowExecutionMiddleware { diff --git a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipelineBuilder.cs b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipelineBuilder.cs index c61c6c4e5..6215c720c 100644 --- a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipelineBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipelineBuilder.cs @@ -1,5 +1,3 @@ -using Elsa.Workflows.Contracts; - namespace Elsa.Workflows.Pipelines.WorkflowExecution; /// @@ -54,6 +52,12 @@ public class WorkflowExecutionPipelineBuilder : IWorkflowExecutionPipelineBuilde return this; } + public IWorkflowExecutionPipelineBuilder Insert(int index, Func middleware) + { + _components.Insert(index, middleware); + return this; + } + public IWorkflowExecutionPipelineBuilder Replace(int index, Func middleware) { _components[index] = middleware; diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionPipelineBuilderExtensions.cs index e471f2815..6f20c873c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionPipelineBuilderExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionPipelineBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Elsa.Workflows; using Elsa.Workflows.Contracts; using Elsa.Workflows.Pipelines.ActivityExecution; using Elsa.Workflows.Runtime.Middleware.Activities; diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs new file mode 100644 index 000000000..b647f0c7e --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs @@ -0,0 +1,36 @@ +using Elsa.Extensions; +using Elsa.Workflows.Features; +using Elsa.Workflows.Middleware.Activities; + +// ReSharper disable once CheckNamespace + +namespace Elsa.Workflows.Runtime.Extensions; + +/// Adds an extension method to the that installs a default workflow runtime execution pipeline. +public static class PipelineWorkflowsFeatureExtensions +{ + /// Installs a default workflow runtime execution pipeline. + public static WorkflowsFeature WithDefaultWorkflowExecutionPipeline(this WorkflowsFeature workflowsFeature, Action? configurePipeline = null) + { + return workflowsFeature.WithWorkflowExecutionPipeline(pipeline => + { + pipeline.UseDefaultPipeline(); + configurePipeline?.Invoke(pipeline); + }); + } + + /// Installs an activity invoker that can run activities asynchronously in the background. + public static WorkflowsFeature WithDefaultActivityExecutionPipeline(this WorkflowsFeature workflowsFeature, Action? configurePipeline = null) + { + return workflowsFeature.WithActivityExecutionPipeline(pipeline => + { + pipeline + .UseExceptionHandling() + .UseExecutionLogging() + .UseNotifications() + .UseBackgroundActivityInvoker(); + + configurePipeline?.Invoke(pipeline); + }); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs index 4d4d0128d..f65fcf45e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Elsa.Workflows.Contracts; +using Elsa.Workflows; using Elsa.Workflows.Middleware.Workflows; using Elsa.Workflows.Pipelines.WorkflowExecution; using Elsa.Workflows.Runtime.Middleware.Workflows; diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowsFeatureExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowsFeatureExtensions.cs deleted file mode 100644 index ca2eb46c3..000000000 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowsFeatureExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Elsa.Extensions; -using Elsa.Workflows.Features; -using Elsa.Workflows.Middleware.Activities; -// ReSharper disable once CheckNamespace - -namespace Elsa.Workflows.Runtime.Extensions; - -/// -/// Adds an extension method to the that installs a default workflow runtime execution pipeline. -/// -public static class WorkflowsFeatureExtensions -{ - /// - /// Installs a default workflow runtime execution pipeline. - /// - public static WorkflowsFeature WithDefaultWorkflowExecutionPipeline(this WorkflowsFeature workflowsFeature) => - workflowsFeature.WithWorkflowExecutionPipeline(pipeline => - pipeline.UseDefaultPipeline()); - - /// - /// Installs an activity invoker that can run activities asynchronously in the background. - /// - public static WorkflowsFeature WithBackgroundActivityExecutionPipeline(this WorkflowsFeature workflowsFeature) => - workflowsFeature.WithActivityExecutionPipeline(pipeline => - pipeline - .UseExceptionHandling() - .UseExecutionLogging() - .UseNotifications() - .UseBackgroundActivityInvoker()); -} \ No newline at end of file diff --git a/src/modules/Elsa/Extensions/ServiceProviderExtensions.cs b/src/modules/Elsa/Extensions/ServiceProviderExtensions.cs index 734d90a3f..03ec09d5c 100644 --- a/src/modules/Elsa/Extensions/ServiceProviderExtensions.cs +++ b/src/modules/Elsa/Extensions/ServiceProviderExtensions.cs @@ -1,3 +1,4 @@ +using Elsa.Workflows; using Elsa.Workflows.Contracts; using Microsoft.Extensions.DependencyInjection; diff --git a/src/modules/Elsa/Features/ElsaFeature.cs b/src/modules/Elsa/Features/ElsaFeature.cs index 08350b6ec..e4fc95efa 100644 --- a/src/modules/Elsa/Features/ElsaFeature.cs +++ b/src/modules/Elsa/Features/ElsaFeature.cs @@ -37,7 +37,7 @@ public class ElsaFeature : FeatureBase Module .UseWorkflows(workflows => workflows .WithDefaultWorkflowExecutionPipeline() - .WithBackgroundActivityExecutionPipeline()) + .WithDefaultActivityExecutionPipeline()) .UseWorkflowManagement(management => { if (!DisableAutomaticActivityRegistration)