From 6577b0cd3889aa248168569a66194602f95e0038 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 30 Oct 2020 21:55:27 +0100 Subject: [PATCH] Improve trigger API and implement HTTP correlation --- Samples.sln | 7 +++ .../Extensions/HttpRequestExtensions.cs | 19 +++++++ .../Middleware/HttpRequestMiddleware.cs | 46 +++++++++++++--- .../Triggers/ReceiveHttpRequestTrigger.cs | 4 +- .../Events/WorkflowInstanceDeleted.cs | 15 +++++ .../Events/WorkflowInstanceSaved.cs | 15 +++++ .../Elsa.Abstractions/Models/Variables.cs | 14 ++++- .../Models/ActivityExecutionContext.cs | 1 + .../Models/WorkflowExecutionContext.cs | 8 ++- .../Triggers/TriggerDescriptor.cs | 5 +- .../Triggers/WorkflowSelectorResult.cs | 6 +- .../Primitives/Correlate/Correlate.cs | 2 +- .../Correlate/SetVariableBuilderExtensions.cs | 19 +++++++ .../Primitives/SetVariable/SetVariable.cs | 3 +- .../SetVariableBuilderExtensions.cs | 19 +++++++ .../ActivityResults/CorrelateResult.cs | 3 +- .../Elsa.Core/Builders/ActivityBuilder.cs | 4 +- src/core/Elsa.Core/Handlers/UpdateTriggers.cs | 20 ++++++- .../Services/WorkflowInstanceManager.cs | 8 ++- src/core/Elsa.Core/Services/WorkflowRunner.cs | 3 +- .../Elsa.Core/Services/WorkflowScheduler.cs | 4 +- .../Elsa.Core/Triggers/WorkflowSelector.cs | 8 +-- .../Elsa.Samples.CorrelationHttp.csproj | 11 ++++ .../Models/Registration.cs | 8 +++ .../Elsa.Samples.CorrelationHttp/Program.cs | 14 +++++ .../Properties/launchSettings.json | 12 ++++ .../Elsa.Samples.CorrelationHttp/Startup.cs | 29 ++++++++++ .../Workflows/RegistrationWorkflow.cs | 55 +++++++++++++++++++ .../workflows.http | 28 ++++++++++ 29 files changed, 356 insertions(+), 34 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Events/WorkflowInstanceDeleted.cs create mode 100644 src/core/Elsa.Abstractions/Events/WorkflowInstanceSaved.cs create mode 100644 src/core/Elsa.Core/Activities/Primitives/Correlate/SetVariableBuilderExtensions.cs create mode 100644 src/samples/Elsa.Samples.CorrelationHttp/Elsa.Samples.CorrelationHttp.csproj create mode 100644 src/samples/Elsa.Samples.CorrelationHttp/Models/Registration.cs create mode 100644 src/samples/Elsa.Samples.CorrelationHttp/Program.cs create mode 100644 src/samples/Elsa.Samples.CorrelationHttp/Properties/launchSettings.json create mode 100644 src/samples/Elsa.Samples.CorrelationHttp/Startup.cs create mode 100644 src/samples/Elsa.Samples.CorrelationHttp/Workflows/RegistrationWorkflow.cs create mode 100644 src/samples/Elsa.Samples.CorrelationHttp/workflows.http diff --git a/Samples.sln b/Samples.sln index 10b60aaed..43e05f59d 100644 --- a/Samples.sln +++ b/Samples.sln @@ -106,6 +106,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.WhileLoopWorke EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.IfElseConsole", "src\samples\Elsa.Samples.IfElseConsole\Elsa.Samples.IfElseConsole.csproj", "{E508C1B6-916F-40F4-8731-58279B8F45B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.CorrelationHttp", "src\samples\Elsa.Samples.CorrelationHttp\Elsa.Samples.CorrelationHttp.csproj", "{565E2F7D-1F5F-45EF-B130-BC09CB3465B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -249,6 +251,10 @@ Global {E508C1B6-916F-40F4-8731-58279B8F45B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {E508C1B6-916F-40F4-8731-58279B8F45B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {E508C1B6-916F-40F4-8731-58279B8F45B2}.Release|Any CPU.Build.0 = Release|Any CPU + {565E2F7D-1F5F-45EF-B130-BC09CB3465B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {565E2F7D-1F5F-45EF-B130-BC09CB3465B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {565E2F7D-1F5F-45EF-B130-BC09CB3465B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {565E2F7D-1F5F-45EF-B130-BC09CB3465B1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -298,6 +304,7 @@ Global {6B83CCE1-D523-4E3F-84EB-8D79AAF6F7C2} = {5E5E1E84-DDBC-40D6-B891-0D563A15A44A} {85A70E7C-A4AC-49BA-942B-635F974DE255} = {5E5E1E84-DDBC-40D6-B891-0D563A15A44A} {E508C1B6-916F-40F4-8731-58279B8F45B2} = {5E5E1E84-DDBC-40D6-B891-0D563A15A44A} + {565E2F7D-1F5F-45EF-B130-BC09CB3465B1} = {5E5E1E84-DDBC-40D6-B891-0D563A15A44A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158} diff --git a/src/activities/Elsa.Activities.Http/Extensions/HttpRequestExtensions.cs b/src/activities/Elsa.Activities.Http/Extensions/HttpRequestExtensions.cs index e6945e6dc..9e41fb6f0 100644 --- a/src/activities/Elsa.Activities.Http/Extensions/HttpRequestExtensions.cs +++ b/src/activities/Elsa.Activities.Http/Extensions/HttpRequestExtensions.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -38,5 +39,23 @@ namespace Elsa.Activities.Http.Extensions var absoluteUrl = $"{request.Scheme}://{request.Host}{relativePath}"; return new Uri(absoluteUrl, UriKind.Absolute); } + + public static bool TryGetCorrelationId(this HttpRequest request, out string? correlationId) + { + if (request.Query.ContainsKey("correlation")) + { + correlationId = request.Query["correlation"]; + return true; + } + + if (request.Headers.ContainsKey("X-Correlation-Id")) + { + correlationId = request.Headers["X-Correlation-Id"].ToString(); + return true; + } + + correlationId = null; + return false; + } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Middleware/HttpRequestMiddleware.cs b/src/activities/Elsa.Activities.Http/Middleware/HttpRequestMiddleware.cs index 893a19815..c8d7cffef 100644 --- a/src/activities/Elsa.Activities.Http/Middleware/HttpRequestMiddleware.cs +++ b/src/activities/Elsa.Activities.Http/Middleware/HttpRequestMiddleware.cs @@ -1,9 +1,14 @@ +using System; using System.Linq; +using System.Net; using System.Threading.Tasks; +using Elsa.Activities.Http.Extensions; using Elsa.Activities.Http.Triggers; +using Elsa.Extensions; using Elsa.Services; using Elsa.Triggers; using Microsoft.AspNetCore.Http; +using Open.Linq.AsyncExtensions; namespace Elsa.Activities.Http.Middleware { @@ -16,22 +21,49 @@ namespace Elsa.Activities.Http.Middleware _next = next; } - public async Task InvokeAsync(HttpContext httpContext, IWorkflowSelector workflowSelector, IWorkflowRunner workflowRunner) + public async Task InvokeAsync(HttpContext httpContext, IWorkflowSelector workflowSelector, IWorkflowRunner workflowRunner, IWorkflowInstanceManager workflowInstanceManager) { var path = httpContext.Request.Path; var method = httpContext.Request.Method; var cancellationToken = httpContext.RequestAborted; + httpContext.Request.TryGetCorrelationId(out var correlationId); var results = await workflowSelector.SelectWorkflowsAsync( - x => x.Path == path && x.Method == null || x.Method == method, - cancellationToken); + x => (x.Path == path) && (x.Method == null || x.Method == method) && (x.CorrelationId == null || x.CorrelationId == correlationId), + cancellationToken) + .ToList(); - var result = results.FirstOrDefault(); - - if (result == null) + if (!results.Any()) + { await _next(httpContext); + return; + } + + if (results.Count > 1) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await httpContext.Response.WriteAsync("Request matches multiple workflows.", cancellationToken); + return; + } + + var result = results.Single(); + + if (result.WorkflowInstanceId == null) + { + await workflowRunner.RunWorkflowAsync(result.WorkflowBlueprint, result.ActivityId, cancellationToken: cancellationToken); + } else - await workflowRunner.RunWorkflowAsync(result.WorkflowBlueprint, result.WorkflowInstance, result.ActivityId, cancellationToken: cancellationToken); + { + var workflowInstance = await workflowInstanceManager.GetByIdAsync(result.WorkflowInstanceId, cancellationToken); + + if (workflowInstance == null) + { + httpContext.Response.StatusCode = 404; + return; + } + + await workflowRunner.RunWorkflowAsync(result.WorkflowBlueprint, workflowInstance, result.ActivityId, cancellationToken: cancellationToken); + } } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Triggers/ReceiveHttpRequestTrigger.cs b/src/activities/Elsa.Activities.Http/Triggers/ReceiveHttpRequestTrigger.cs index f6034d00d..21103de70 100644 --- a/src/activities/Elsa.Activities.Http/Triggers/ReceiveHttpRequestTrigger.cs +++ b/src/activities/Elsa.Activities.Http/Triggers/ReceiveHttpRequestTrigger.cs @@ -9,6 +9,7 @@ namespace Elsa.Activities.Http.Triggers { public PathString Path { get; set; } public string? Method { get; set; } + public string? CorrelationId { get; set; } } public class ReceiveHttpRequestTriggerProvider : TriggerProvider @@ -17,7 +18,8 @@ namespace Elsa.Activities.Http.Triggers new ReceiveHttpRequestTrigger { Path = await context.Activity.GetPropertyValueAsync(x => x.Path, cancellationToken), - Method = await context.Activity.GetPropertyValueAsync(x => x.Method, cancellationToken) + Method = await context.Activity.GetPropertyValueAsync(x => x.Method, cancellationToken), + CorrelationId = context.ActivityExecutionContext.WorkflowExecutionContext.CorrelationId }; } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Events/WorkflowInstanceDeleted.cs b/src/core/Elsa.Abstractions/Events/WorkflowInstanceDeleted.cs new file mode 100644 index 000000000..bfb5f5263 --- /dev/null +++ b/src/core/Elsa.Abstractions/Events/WorkflowInstanceDeleted.cs @@ -0,0 +1,15 @@ +using Elsa.Models; +using MediatR; + +namespace Elsa.Events +{ + public class WorkflowInstanceDeleted : INotification + { + public WorkflowInstanceDeleted(WorkflowInstance workflowInstance) + { + WorkflowInstance = workflowInstance; + } + + public WorkflowInstance WorkflowInstance { get; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Events/WorkflowInstanceSaved.cs b/src/core/Elsa.Abstractions/Events/WorkflowInstanceSaved.cs new file mode 100644 index 000000000..bf81f7370 --- /dev/null +++ b/src/core/Elsa.Abstractions/Events/WorkflowInstanceSaved.cs @@ -0,0 +1,15 @@ +using Elsa.Models; +using MediatR; + +namespace Elsa.Events +{ + public class WorkflowInstanceSaved : INotification + { + public WorkflowInstanceSaved(WorkflowInstance workflowInstance) + { + WorkflowInstance = workflowInstance; + } + + public WorkflowInstance WorkflowInstance { get; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Models/Variables.cs b/src/core/Elsa.Abstractions/Models/Variables.cs index bb3ab087f..a32e165fd 100644 --- a/src/core/Elsa.Abstractions/Models/Variables.cs +++ b/src/core/Elsa.Abstractions/Models/Variables.cs @@ -22,7 +22,19 @@ namespace Elsa.Models [JsonExtensionData] public JObject Data { get; set; } public JToken? Get(string name) => Has(name) ? Data[name] : default; - public T Get(string name) => Has(name) ? Data.Value(name) : default!; + + public T Get(string name) + { + if (!Has(name)) + return default!; + + var value = Get(name); + + if (value == null) + return default!; + + return value.ToObject()!; + } public Variables Set(string name, JToken value) { diff --git a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs index eb6dcd2d0..02fe2897d 100644 --- a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs @@ -40,6 +40,7 @@ namespace Elsa.Services.Models public void SetVariable(string name, object? value) => WorkflowExecutionContext.SetVariable(name, value); public object? GetVariable(string name) => WorkflowExecutionContext.GetVariable(name); public T GetVariable(string name) => WorkflowExecutionContext.GetVariable(name); + public T GetVariable() => GetVariable(typeof(T).Name); public T GetService() => WorkflowExecutionContext.ServiceProvider.GetService(); public async ValueTask SetActivityPropertiesAsync( diff --git a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs index b59f6421a..725e8a6c9 100644 --- a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs @@ -56,7 +56,13 @@ namespace Elsa.Services.Models public void PostScheduleActivity(ScheduledActivity activity) => WorkflowInstance.PostScheduledActivities.Push(activity); public ScheduledActivity PopScheduledActivity() => WorkflowInstance.ScheduledActivities.Pop(); public ScheduledActivity PeekScheduledActivity() => WorkflowInstance.ScheduledActivities.Peek(); - public string? CorrelationId { get; set; } + + public string? CorrelationId + { + get => WorkflowInstance.CorrelationId; + set => WorkflowInstance.CorrelationId = value; + } + public bool DeleteCompletedInstances { get; set; } public ICollection ExecutionLog => WorkflowInstance.ExecutionLog; public WorkflowStatus Status => WorkflowInstance.Status; diff --git a/src/core/Elsa.Abstractions/Triggers/TriggerDescriptor.cs b/src/core/Elsa.Abstractions/Triggers/TriggerDescriptor.cs index d5c0b670f..dba66f921 100644 --- a/src/core/Elsa.Abstractions/Triggers/TriggerDescriptor.cs +++ b/src/core/Elsa.Abstractions/Triggers/TriggerDescriptor.cs @@ -1,12 +1,11 @@ -using Elsa.Models; -using Elsa.Services.Models; +using Elsa.Services.Models; namespace Elsa.Triggers { public class TriggerDescriptor { public IWorkflowBlueprint WorkflowBlueprint { get; set; } = default!; - public WorkflowInstance WorkflowInstance { get; set; } = default!; + public string? WorkflowInstanceId { get; set; } public string ActivityId { get; set; } = default!; public string ActivityType { get; set; } = default!; public ITrigger Trigger { get; set; } = default!; diff --git a/src/core/Elsa.Abstractions/Triggers/WorkflowSelectorResult.cs b/src/core/Elsa.Abstractions/Triggers/WorkflowSelectorResult.cs index 20da42e02..eafb75957 100644 --- a/src/core/Elsa.Abstractions/Triggers/WorkflowSelectorResult.cs +++ b/src/core/Elsa.Abstractions/Triggers/WorkflowSelectorResult.cs @@ -5,16 +5,16 @@ namespace Elsa.Triggers { public class WorkflowSelectorResult { - public WorkflowSelectorResult(IWorkflowBlueprint workflowBlueprint, WorkflowInstance workflowInstance, string activityId, ITrigger trigger) + public WorkflowSelectorResult(IWorkflowBlueprint workflowBlueprint, string? workflowInstanceId, string activityId, ITrigger trigger) { WorkflowBlueprint = workflowBlueprint; - WorkflowInstance = workflowInstance; + WorkflowInstanceId = workflowInstanceId; ActivityId = activityId; Trigger = trigger; } public IWorkflowBlueprint WorkflowBlueprint { get; } - public WorkflowInstance WorkflowInstance { get; } + public string? WorkflowInstanceId { get; } public string ActivityId { get; } public ITrigger Trigger { get; } } diff --git a/src/core/Elsa.Core/Activities/Primitives/Correlate/Correlate.cs b/src/core/Elsa.Core/Activities/Primitives/Correlate/Correlate.cs index 4827e9629..f9a1428d5 100644 --- a/src/core/Elsa.Core/Activities/Primitives/Correlate/Correlate.cs +++ b/src/core/Elsa.Core/Activities/Primitives/Correlate/Correlate.cs @@ -18,6 +18,6 @@ namespace Elsa.Activities.Primitives [ActivityProperty(Hint = "An expression that evaluates to the value to store as the correlation ID.")] public string Value { get; set; } = default!; - protected override IActivityExecutionResult OnExecute() => new CorrelateResult(Value); + protected override IActivityExecutionResult OnExecute() => Combine(Done(Value), new CorrelateResult(Value)); } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/Correlate/SetVariableBuilderExtensions.cs b/src/core/Elsa.Core/Activities/Primitives/Correlate/SetVariableBuilderExtensions.cs new file mode 100644 index 000000000..5038e4f99 --- /dev/null +++ b/src/core/Elsa.Core/Activities/Primitives/Correlate/SetVariableBuilderExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Elsa.Activities.Primitives; +using Elsa.Builders; +using Elsa.Services.Models; + +// ReSharper disable once CheckNamespace +namespace Elsa.Activities.ControlFlow +{ + public static class CorrelateBuilderExtensions + { + public static IOutcomeBuilder Correlate(this IBuilder builder, Action> setup) => builder.Then(setup).When(OutcomeNames.Done); + public static IOutcomeBuilder Correlate(this IBuilder builder, Func> value) => builder.Correlate(activity => activity.Set(x => x.Value, value)); + public static IOutcomeBuilder Correlate(this IBuilder builder, Func value) => builder.Correlate(activity => activity.Set(x => x.Value, value)); + public static IOutcomeBuilder Correlate(this IBuilder builder, Func> value) => builder.Correlate(activity => activity.Set(x => x.Value, value)); + public static IOutcomeBuilder Correlate(this IBuilder builder, Func value) => builder.Correlate(activity => activity.Set(x => x.Value, value)); + public static IOutcomeBuilder Correlate(this IBuilder builder, string value) => builder.Correlate(activity => activity.Set(x => x.Value, value)); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariable.cs b/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariable.cs index 086180c30..2470c4954 100644 --- a/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariable.cs +++ b/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariable.cs @@ -10,8 +10,7 @@ namespace Elsa.Activities.Primitives DisplayName = "Set Variable", Description = "Set variable on the workflow.", Category = "Primitives", - RuntimeDescription = - "x => !!x.state.variableName ? `${x.state.variableName} = ${x.state.valueExpression.expression}
${x.state.valueExpression.syntax}` : x.definition.description", + RuntimeDescription = "x => !!x.state.variableName ? `${x.state.variableName} = ${x.state.valueExpression.expression}
${x.state.valueExpression.syntax}` : x.definition.description", Outcomes = new[] { OutcomeNames.Done } )] public class SetVariable : Activity diff --git a/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariableBuilderExtensions.cs b/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariableBuilderExtensions.cs index 1f3dae7f0..452de2b3e 100644 --- a/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariableBuilderExtensions.cs +++ b/src/core/Elsa.Core/Activities/Primitives/SetVariable/SetVariableBuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Elsa.Activities.Primitives; using Elsa.Builders; using Elsa.Services.Models; @@ -10,12 +11,24 @@ namespace Elsa.Activities.ControlFlow { public static IOutcomeBuilder SetVariable(this IBuilder builder, Action> setup) => builder.Then(setup).When(OutcomeNames.Done); + public static IOutcomeBuilder SetVariable(this IBuilder builder, string name, Func> value) => + builder.SetVariable( + activity => activity + .Set(x => x.VariableName, name) + .Set(x => x.Value, value)); + public static IOutcomeBuilder SetVariable(this IBuilder builder, string name, Func value) => builder.SetVariable( activity => activity .Set(x => x.VariableName, name) .Set(x => x.Value, value)); + public static IOutcomeBuilder SetVariable(this IBuilder builder, string name, Func> value) => + builder.SetVariable( + activity => activity + .Set(x => x.VariableName, name) + .Set(x => x.Value, value)); + public static IOutcomeBuilder SetVariable(this IBuilder builder, string name, Func value) => builder.SetVariable( activity => activity @@ -27,5 +40,11 @@ namespace Elsa.Activities.ControlFlow activity => activity .Set(x => x.VariableName, name) .Set(x => x.Value, value)); + + public static IOutcomeBuilder SetVariable(this IBuilder builder, Func> value) => builder.SetVariable(typeof(T).Name, async context => await value(context)); + public static IOutcomeBuilder SetVariable(this IBuilder builder, Func value) => builder.SetVariable(typeof(T).Name, context => value(context)); + public static IOutcomeBuilder SetVariable(this IBuilder builder, Func> value) => builder.SetVariable(typeof(T).Name, async () => await value()); + public static IOutcomeBuilder SetVariable(this IBuilder builder, Func value) => builder.SetVariable(typeof(T).Name, () => value()); + public static IOutcomeBuilder SetVariable(this IBuilder builder, T value) => builder.SetVariable(typeof(T).Name, value); } } \ No newline at end of file diff --git a/src/core/Elsa.Core/ActivityResults/CorrelateResult.cs b/src/core/Elsa.Core/ActivityResults/CorrelateResult.cs index f48ed1d25..860447f65 100644 --- a/src/core/Elsa.Core/ActivityResults/CorrelateResult.cs +++ b/src/core/Elsa.Core/ActivityResults/CorrelateResult.cs @@ -7,7 +7,6 @@ namespace Elsa.ActivityResults public CorrelateResult(string correlationId) => CorrelationId = correlationId; public string CorrelationId { get; } - protected override void Execute(ActivityExecutionContext activityExecutionContext) => - activityExecutionContext.WorkflowExecutionContext.CorrelationId = CorrelationId; + protected override void Execute(ActivityExecutionContext activityExecutionContext) => activityExecutionContext.WorkflowExecutionContext.CorrelationId = CorrelationId; } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Builders/ActivityBuilder.cs b/src/core/Elsa.Core/Builders/ActivityBuilder.cs index 7ac4988fb..35a8a8233 100644 --- a/src/core/Elsa.Core/Builders/ActivityBuilder.cs +++ b/src/core/Elsa.Core/Builders/ActivityBuilder.cs @@ -47,9 +47,9 @@ namespace Elsa.Builders return this; } - public IActivityBuilder WithId(string id) + public IActivityBuilder WithId(string? id) { - ActivityId = id; + ActivityId = id!; return this; } diff --git a/src/core/Elsa.Core/Handlers/UpdateTriggers.cs b/src/core/Elsa.Core/Handlers/UpdateTriggers.cs index 8f32f0428..ca6c04f55 100644 --- a/src/core/Elsa.Core/Handlers/UpdateTriggers.cs +++ b/src/core/Elsa.Core/Handlers/UpdateTriggers.cs @@ -1,15 +1,29 @@ using System.Threading; using System.Threading.Tasks; using Elsa.Events; +using Elsa.Models; +using Elsa.Services; using Elsa.Triggers; using MediatR; namespace Elsa.Handlers { - public class UpdateTriggers : INotificationHandler + public class UpdateTriggers : INotificationHandler { private readonly IWorkflowSelector _workflowSelector; - public UpdateTriggers(IWorkflowSelector workflowSelector) => _workflowSelector = workflowSelector; - public async Task Handle(WorkflowExecuted notification, CancellationToken cancellationToken) => await _workflowSelector.UpdateTriggersAsync(notification.WorkflowExecutionContext.WorkflowBlueprint, cancellationToken); + private readonly IWorkflowRegistry _workflowRegistry; + + public UpdateTriggers(IWorkflowSelector workflowSelector, IWorkflowRegistry workflowRegistry) + { + _workflowSelector = workflowSelector; + _workflowRegistry = workflowRegistry; + } + + public async Task Handle(WorkflowInstanceSaved notification, CancellationToken cancellationToken) + { + var workflowInstance = notification.WorkflowInstance; + var workflowBlueprint = await _workflowRegistry.GetWorkflowAsync(workflowInstance.WorkflowDefinitionId, VersionOptions.SpecificVersion(workflowInstance.Version), cancellationToken); + await _workflowSelector.UpdateTriggersAsync(workflowBlueprint!, cancellationToken); + } } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowInstanceManager.cs b/src/core/Elsa.Core/Services/WorkflowInstanceManager.cs index 9cf7eed37..8e49e3014 100644 --- a/src/core/Elsa.Core/Services/WorkflowInstanceManager.cs +++ b/src/core/Elsa.Core/Services/WorkflowInstanceManager.cs @@ -3,7 +3,9 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Elsa.Data; +using Elsa.Events; using Elsa.Models; +using MediatR; using YesSql; using YesSql.Indexes; @@ -12,22 +14,26 @@ namespace Elsa.Services public class WorkflowInstanceManager : IWorkflowInstanceManager { private readonly ISession _session; + private readonly IMediator _mediator; - public WorkflowInstanceManager(ISession session) + public WorkflowInstanceManager(ISession session, IMediator mediator) { _session = session; + _mediator = mediator; } public async ValueTask SaveAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken = default) { _session.Save(workflowInstance, CollectionNames.WorkflowInstances); await _session.CommitAsync(); + await _mediator.Publish(new WorkflowInstanceSaved(workflowInstance), cancellationToken); } public async ValueTask DeleteAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken = default) { _session.Delete(workflowInstance, CollectionNames.WorkflowInstances); await _session.CommitAsync(); + await _mediator.Publish(new WorkflowInstanceDeleted(workflowInstance), cancellationToken); } public IQuery Query() => _session.Query(CollectionNames.WorkflowInstances); diff --git a/src/core/Elsa.Core/Services/WorkflowRunner.cs b/src/core/Elsa.Core/Services/WorkflowRunner.cs index 3eccb1159..3af7a83e2 100644 --- a/src/core/Elsa.Core/Services/WorkflowRunner.cs +++ b/src/core/Elsa.Core/Services/WorkflowRunner.cs @@ -211,12 +211,13 @@ namespace Elsa.Services private async ValueTask RunAsync(WorkflowExecutionContext workflowExecutionContext, ActivityOperation activityOperation, CancellationToken cancellationToken = default) { using var scope = _serviceProvider.CreateScope(); + var serviceProvider = scope.ServiceProvider; while (workflowExecutionContext.HasScheduledActivities) { var scheduledActivity = workflowExecutionContext.PopScheduledActivity(); var currentActivityId = scheduledActivity.ActivityId; var activityBlueprint = workflowExecutionContext.WorkflowBlueprint.GetActivity(currentActivityId)!; - var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, scope.ServiceProvider, activityBlueprint, scheduledActivity.Input); + var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, serviceProvider, activityBlueprint, scheduledActivity.Input); var activity = await activityBlueprint.CreateActivityAsync(activityExecutionContext, cancellationToken); var result = await activityOperation(activityExecutionContext, activity, cancellationToken); await _mediator.Publish(new ActivityExecuting(activityExecutionContext), cancellationToken); diff --git a/src/core/Elsa.Core/Services/WorkflowScheduler.cs b/src/core/Elsa.Core/Services/WorkflowScheduler.cs index e660b7ded..d10779807 100644 --- a/src/core/Elsa.Core/Services/WorkflowScheduler.cs +++ b/src/core/Elsa.Core/Services/WorkflowScheduler.cs @@ -95,8 +95,8 @@ namespace Elsa.Services foreach (var result in results) { - if (result.WorkflowInstance.Id > 0) - await ScheduleWorkflowInstanceAsync(result.WorkflowInstance.WorkflowInstanceId, result.ActivityId, input, cancellationToken); + if (result.WorkflowInstanceId != null) + await ScheduleWorkflowInstanceAsync(result.WorkflowInstanceId, result.ActivityId, input, cancellationToken); else await ScheduleWorkflowAsync(result.WorkflowBlueprint, result.WorkflowBlueprint.GetActivity(result.ActivityId)!, input, correlationId, cancellationToken); diff --git a/src/core/Elsa.Core/Triggers/WorkflowSelector.cs b/src/core/Elsa.Core/Triggers/WorkflowSelector.cs index c8d02f55c..3cd3d8083 100644 --- a/src/core/Elsa.Core/Triggers/WorkflowSelector.cs +++ b/src/core/Elsa.Core/Triggers/WorkflowSelector.cs @@ -74,7 +74,7 @@ namespace Elsa.Triggers where descriptor.Trigger == trigger select (entry.Key, descriptor); - foreach (var (key, descriptor) in query.ToList()) + foreach (var (key, descriptor) in query.ToList()) _descriptors[key].Remove(descriptor); _memoryCache.Set(CacheKey, _descriptors); @@ -94,10 +94,10 @@ namespace Elsa.Triggers return from descriptor in descriptors let workflow = descriptor.WorkflowBlueprint where workflow.IsPublished && workflow.IsEnabled - let workflowInstance = descriptor.WorkflowInstance + let workflowInstanceId = descriptor.WorkflowInstanceId let trigger = descriptor.Trigger where trigger.GetType() == triggerType && evaluate(trigger) - select new WorkflowSelectorResult(workflow, workflowInstance, descriptor.ActivityId, trigger); + select new WorkflowSelectorResult(workflow, workflowInstanceId, descriptor.ActivityId, trigger); } private async Task>> BuildDescriptorsAsync(CancellationToken cancellationToken) @@ -171,7 +171,7 @@ namespace Elsa.Triggers ActivityId = blockingActivity.Id, ActivityType = blockingActivity.Type, WorkflowBlueprint = workflowBlueprint, - WorkflowInstance = workflowInstance, + WorkflowInstanceId = workflowInstance.Id == 0 ? default : workflowInstance.WorkflowInstanceId, Trigger = trigger, }; diff --git a/src/samples/Elsa.Samples.CorrelationHttp/Elsa.Samples.CorrelationHttp.csproj b/src/samples/Elsa.Samples.CorrelationHttp/Elsa.Samples.CorrelationHttp.csproj new file mode 100644 index 000000000..876533012 --- /dev/null +++ b/src/samples/Elsa.Samples.CorrelationHttp/Elsa.Samples.CorrelationHttp.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/src/samples/Elsa.Samples.CorrelationHttp/Models/Registration.cs b/src/samples/Elsa.Samples.CorrelationHttp/Models/Registration.cs new file mode 100644 index 000000000..d173a78e5 --- /dev/null +++ b/src/samples/Elsa.Samples.CorrelationHttp/Models/Registration.cs @@ -0,0 +1,8 @@ +namespace Elsa.Samples.CorrelationHttp.Models +{ + public class Registration + { + public string Name { get; set; } + public string Email { get; set; } + } +} \ No newline at end of file diff --git a/src/samples/Elsa.Samples.CorrelationHttp/Program.cs b/src/samples/Elsa.Samples.CorrelationHttp/Program.cs new file mode 100644 index 000000000..ab70bd6da --- /dev/null +++ b/src/samples/Elsa.Samples.CorrelationHttp/Program.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Elsa.Samples.CorrelationHttp +{ + public class Program + { + public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/src/samples/Elsa.Samples.CorrelationHttp/Properties/launchSettings.json b/src/samples/Elsa.Samples.CorrelationHttp/Properties/launchSettings.json new file mode 100644 index 000000000..8f09bf942 --- /dev/null +++ b/src/samples/Elsa.Samples.CorrelationHttp/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Elsa.Samples.LongRunningHttp": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:8201" + } + } +} \ No newline at end of file diff --git a/src/samples/Elsa.Samples.CorrelationHttp/Startup.cs b/src/samples/Elsa.Samples.CorrelationHttp/Startup.cs new file mode 100644 index 000000000..808baf52d --- /dev/null +++ b/src/samples/Elsa.Samples.CorrelationHttp/Startup.cs @@ -0,0 +1,29 @@ +using System.Data; +using Elsa.Samples.CorrelationHttp.Workflows; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using YesSql.Provider.Sqlite; + +namespace Elsa.Samples.CorrelationHttp +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services + .AddElsa(option => option.UsePersistence(db => db.UseSqLite("Data Source=elsa.db;Cache=Shared", IsolationLevel.ReadUncommitted))) + .AddHttpActivities() + .AddConsoleActivities() + .AddWorkflow(); + } + + public void Configure(IApplicationBuilder app) + { + // Add HTTP activities middleware. + app.UseHttpActivities(); + + // Show welcome page if no routes matched any endpoints. + app.UseWelcomePage(); + } + } +} \ No newline at end of file diff --git a/src/samples/Elsa.Samples.CorrelationHttp/Workflows/RegistrationWorkflow.cs b/src/samples/Elsa.Samples.CorrelationHttp/Workflows/RegistrationWorkflow.cs new file mode 100644 index 000000000..b5098c072 --- /dev/null +++ b/src/samples/Elsa.Samples.CorrelationHttp/Workflows/RegistrationWorkflow.cs @@ -0,0 +1,55 @@ +using System.Net; +using Elsa.Activities.ControlFlow; +using Elsa.Activities.Http; +using Elsa.Activities.Http.Models; +using Elsa.Builders; +using Elsa.Samples.CorrelationHttp.Models; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Samples.CorrelationHttp.Workflows +{ + /// + /// Triggered when POST HTTP requests are made to /register and continued when a subsequent GET HTTP request is made to /confirm to confirm the registration (see workflows.http). + /// + public class RegistrationWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder workflow) + { + workflow + // Configure a Receive HTTP Request trigger that executes on incoming HTTP POST requests. + .ReceiveHttpRequest(activity => activity.WithPath("/register").WithMethod(HttpMethods.Post).WithTargetType()) + + // Store the registration as a workflow variable for easier access. + .SetVariable(context => (Registration)((HttpRequestModel)(context.Input))?.Body) + + // Correlate the workflow by email address. + .Correlate(context => context.GetVariable().Email) + + // Write an HTTP response with a hyperlink to continue the workflow (notice the presence of the "correlation" query string parameter). + .WriteHttpResponse(activity => activity + .WithStatusCode(HttpStatusCode.OK) + .WithContentType("text/html") + .WithContent(context => + { + var registration = context.GetVariable(); + return $"Welcome onboard, {registration.Name}! Please confirm your registration"; + })) + + // Configure another Receive HTTP Request trigger that executes on incoming HTTP GET requests. + // This will cause the workflow to become suspended and executed once the request comes in. + .ReceiveHttpRequest(activity => activity.WithPath("/confirm")) + + // Write an HTTP response with a thank-you note. + // Notice that the correct workflow instance is resumed base on the incoming correlation ID. + .WriteHttpResponse(activity => activity + .WithStatusCode(HttpStatusCode.OK) + .WithContentType("text/html") + .WithContent( + context => + { + var registration = context.GetVariable(); + return $"Thanks for confirming your registration, {registration.Name}!"; + })); + } + } +} \ No newline at end of file diff --git a/src/samples/Elsa.Samples.CorrelationHttp/workflows.http b/src/samples/Elsa.Samples.CorrelationHttp/workflows.http new file mode 100644 index 000000000..67f4517ca --- /dev/null +++ b/src/samples/Elsa.Samples.CorrelationHttp/workflows.http @@ -0,0 +1,28 @@ +# Register John. +POST http://localhost:8201/register +Content-Type: application/json + +{ + "name": "John von Neumann", + "email": "john@gmail.com" +} + +### +# Register Julia. +POST http://localhost:8201/register +Content-Type: application/json + +{ +"name": "Julia Berger", +"email": "julia@gmail.com" +} + +### +# Confirm John's registration +GET http://localhost:8201/confirm?correlation=john@gmail.com +Content-Type: application/json + +### +# Confirm Julia's registration +GET http://localhost:8201/confirm?correlation=julia@gmail.com +Content-Type: application/json \ No newline at end of file