diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 19cac840c..ed205dae8 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -24,11 +24,6 @@ public static class ObjectConverter return value; var options = serializerOptions ?? new JsonSerializerOptions(); - options.SetupExtensions().SetReferenceHandling(ReferenceHandling.Preserve); - var registry = options.GetDiscriminatorConventionRegistry(); - registry.ClearConventions(); - registry.RegisterConvention(new DefaultDiscriminatorConvention(options, "_type")); - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.ReferenceHandler = ReferenceHandler.Preserve; options.PropertyNameCaseInsensitive = true; diff --git a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs index 9160c4039..6849af1c8 100644 --- a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs +++ b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs @@ -38,23 +38,28 @@ public class HttpEndpoint : Trigger )] public Input Policy { get; set; } = new(default(string?)); - protected override IEnumerable GetTriggerPayload(TriggerIndexingContext context) => GetBookmarkPayload(context.ExpressionExecutionContext); + /// + protected override IEnumerable GetTriggerPayloads(TriggerIndexingContext context) => GetBookmarkPayloads(context.ExpressionExecutionContext); - protected override void Execute(ActivityExecutionContext context) + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { // If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark. if (!context.TryGetInput(InputKey, out var request)) { // Create bookmarks for when we receive the expected HTTP request. - context.CreateBookmarks(GetBookmarkPayload(context.ExpressionExecutionContext)); + context.CreateBookmarks(GetBookmarkPayloads(context.ExpressionExecutionContext)); return; } // Provide the received HTTP request as output. context.Set(Result, request); + + // Complete. + await context.CompleteActivityAsync(); } - private IEnumerable GetBookmarkPayload(ExpressionExecutionContext context) + private IEnumerable GetBookmarkPayloads(ExpressionExecutionContext context) { // Generate bookmark data for path and selected methods. var path = context.Get(Path); diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index 211c0baf0..3584e3a0a 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -11,7 +11,7 @@ using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders; namespace Elsa.Http; -[Activity("Elsa", "HTTP", "Send Http Request.", DisplayName = "Send HTTP Request", Kind = ActivityKind.Task)] +[Activity("Elsa", "HTTP", "Send Http Request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)] public class SendHttpRequest : Activity { [Input] public Input Url { get; set; } = default!; diff --git a/src/modules/Elsa.Http/Elsa.Http.csproj b/src/modules/Elsa.Http/Elsa.Http.csproj index 294346280..7e0ec05c0 100644 --- a/src/modules/Elsa.Http/Elsa.Http.csproj +++ b/src/modules/Elsa.Http/Elsa.Http.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/modules/Elsa.Http/Extensions/DependencyInjectionExtensions.cs b/src/modules/Elsa.Http/Extensions/ModuleExtensions.cs similarity index 86% rename from src/modules/Elsa.Http/Extensions/DependencyInjectionExtensions.cs rename to src/modules/Elsa.Http/Extensions/ModuleExtensions.cs index e3136bc53..650a17703 100644 --- a/src/modules/Elsa.Http/Extensions/DependencyInjectionExtensions.cs +++ b/src/modules/Elsa.Http/Extensions/ModuleExtensions.cs @@ -4,7 +4,7 @@ using Elsa.Http.Features; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; -public static class DependencyInjectionExtensions +public static class ModuleExtensions { public static IModule UseHttp(this IModule module, Action? configure = default) { diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index bd3f79a78..c6572a7e9 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -1,4 +1,6 @@ +using Elsa.Common.Features; using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; using Elsa.Features.Services; using Elsa.Http.ContentWriters; using Elsa.Http.Handlers; @@ -11,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Http.Features; +[DependsOn(typeof(MemoryCacheFeature))] public class HttpFeature : FeatureBase { public HttpFeature(IModule module) : base(module) diff --git a/src/modules/Elsa.Http/Models/HttpEndpointBookmarkPayload.cs b/src/modules/Elsa.Http/Models/HttpEndpointBookmarkPayload.cs index f546aa859..666316bdf 100644 --- a/src/modules/Elsa.Http/Models/HttpEndpointBookmarkPayload.cs +++ b/src/modules/Elsa.Http/Models/HttpEndpointBookmarkPayload.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Elsa.Http.Models; public record HttpEndpointBookmarkPayload @@ -5,6 +7,11 @@ public record HttpEndpointBookmarkPayload private readonly string _path = default!; private readonly string _method = default!; + [JsonConstructor] + public HttpEndpointBookmarkPayload() + { + } + public HttpEndpointBookmarkPayload(string path, string method) { Path = path; diff --git a/src/modules/Elsa.Http/Models/HttpRequestModel.cs b/src/modules/Elsa.Http/Models/HttpRequestModel.cs index 2d56d60d8..ceb8c1174 100644 --- a/src/modules/Elsa.Http/Models/HttpRequestModel.cs +++ b/src/modules/Elsa.Http/Models/HttpRequestModel.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Elsa.Http.Models; public record HttpRequestModel( @@ -7,4 +9,14 @@ public record HttpRequestModel( IDictionary QueryString, IDictionary RouteValues, IDictionary Headers -); \ No newline at end of file +) +{ + + /// + /// Constructor used for deserialization. + /// + [JsonConstructor] + public HttpRequestModel() : this(default!, default!, default!, default!, default!, default!) + { + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Identity/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Identity/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..3af6e8451 --- /dev/null +++ b/src/modules/Elsa.Identity/Extensions/ModuleExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Features.Services; +using Elsa.Identity.Features; + +namespace Elsa.Identity.Extensions; + +/// +/// Extensions for that installs the feature. +/// +public static class ModuleExtensions +{ + /// + /// Installs & configures the feature. + /// + public static IModule UseIdentity(this IModule module, Action? configure = default) + { + module.Configure(configure); + return module; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs index 4166f91ce..75f9c865f 100644 --- a/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs @@ -213,7 +213,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime var storeBookmarkRequest = new StoreBookmarksRequest { WorkflowInstanceId = instanceId, - CorrelationId = correlationId + CorrelationId = correlationId.EmptyIfNull() }; storeBookmarkRequest.BookmarkIds.AddRange(groupedBookmark.Select(x => x.Id)); diff --git a/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs b/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs index a1bef8058..5667467c9 100644 --- a/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs +++ b/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs @@ -35,23 +35,5 @@ public class ScheduledChildCallbackBehavior : Behavior { await callbackEntry.CompletionCallback(activityExecutionContext, childActivityExecutionContext); } - else - { - var ports = Owner.GetType().GetProperties().Where(x => typeof(IActivity).IsAssignableFrom(x.PropertyType)).ToList(); - - var portQuery = - from p in ports - let i = (IActivity)p.GetValue(Owner) - where i == childActivity - select new { PortProperty = p, PortActivity = i }; - - var port = portQuery.FirstOrDefault(); - - if (port == null) - return; - - var portName = port.PortProperty.GetCustomAttribute()?.Name ?? port.PortProperty.Name; - await activityExecutionContext.CompleteActivityWithOutcomesAsync(portName); - } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index a95f2f824..62766d57a 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -218,13 +218,18 @@ public static class ActivityExecutionContextExtensions await context.SendSignalAsync(new ActivityCompleted(result)); // Remove the context. - context.WorkflowExecutionContext.ActivityExecutionContexts.Remove(context); + context.WorkflowExecutionContext.RemoveActivityExecutionContext(context); } /// /// Complete the current activity with the specified outcome. /// public static ValueTask CompleteActivityWithOutcomesAsync(this ActivityExecutionContext context, params string[] outcomes) => context.CompleteActivityAsync(new Outcomes(outcomes)); + + /// + /// Complete the current composite activity with the specified outcome. + /// + public static async ValueTask CompleteCompositeAsync(this ActivityExecutionContext context, params string[] outcomes) => await context.SendSignalAsync(new CompleteCompositeSignal(new Outcomes(outcomes))); /// /// Cancel the activity. For blocking activities, it means their bookmarks will be removed. For job activities, the background work will be cancelled. diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index 635ac98a7..23c5736b7 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -15,7 +15,7 @@ public static class WorkflowExecutionContextExtensions var list = contexts.ToList(); // Remove each context. - foreach (var context in list) workflowExecutionContext.ActivityExecutionContexts.Remove(context); + foreach (var context in list) workflowExecutionContext.RemoveActivityExecutionContext(context); } /// diff --git a/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs b/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs index 2f983ef0d..516c40a96 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs @@ -31,7 +31,7 @@ public class ActivityInvoker : IActivityInvoker } // Add the activity context to the workflow context. - workflowExecutionContext.ActivityExecutionContexts.Add(activityExecutionContext); + workflowExecutionContext.AddActivityExecutionContext(activityExecutionContext); // Execute the activity execution pipeline. await InvokeAsync(activityExecutionContext); diff --git a/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs b/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs index c7b13d1d0..ee9fdd944 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs @@ -47,7 +47,18 @@ public class DefaultWorkflowExecutionContextFactory : IWorkflowExecutionContextF var scheduler = _schedulerFactory.CreateScheduler(); // Setup a workflow execution context. - var workflowExecutionContext = new WorkflowExecutionContext(_serviceProvider, instanceId, correlationId, workflow, graph, scheduler, input, executeActivityDelegate, triggerActivityId, cancellationToken); + var workflowExecutionContext = new WorkflowExecutionContext( + _serviceProvider, + instanceId, + correlationId, + workflow, + graph, + scheduler, + input, + executeActivityDelegate, + triggerActivityId, + default, + cancellationToken); // Restore workflow execution context from state, if provided. if (workflowState != null) _workflowStateSerializer.DeserializeState(workflowExecutionContext, workflowState); diff --git a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs index 6a8dc8493..583d729dd 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs @@ -97,14 +97,14 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer var owner = workflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == completionCallbackEntry.OwnerId); var child = workflowExecutionContext.FindNodeById(completionCallbackEntry.ChildId).Activity; var callbackName = completionCallbackEntry.MethodName; - var callbackDelegate = owner.Activity.GetActivityCompletionCallback(callbackName); + var callbackDelegate = !string.IsNullOrEmpty(callbackName) ? owner.Activity.GetActivityCompletionCallback(callbackName) : default; workflowExecutionContext.AddCompletionCallback(owner, child, callbackDelegate); } } private void SerializeCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { - var completionCallbacks = workflowExecutionContext.CompletionCallbacks.Select(x => new CompletionCallbackState(x.Owner.Id, x.Child.Id, x.CompletionCallback.Method.Name)); + var completionCallbacks = workflowExecutionContext.CompletionCallbacks.Select(x => new CompletionCallbackState(x.Owner.Id, x.Child.Id, x.CompletionCallback?.Method.Name)); state.CompletionCallbacks = completionCallbacks.ToList(); } diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs index 9359510f6..6e1a1c086 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs @@ -14,8 +14,10 @@ public class WorkflowExecutionContext private readonly IServiceProvider _serviceProvider; private readonly IList _nodes; private readonly IList _completionCallbackEntries = new List(); + private IList _activityExecutionContexts; - public WorkflowExecutionContext(IServiceProvider serviceProvider, + public WorkflowExecutionContext( + IServiceProvider serviceProvider, string id, string? correlationId, Workflow workflow, @@ -24,6 +26,7 @@ public class WorkflowExecutionContext IDictionary? input, ExecuteActivityDelegate? executeDelegate, string? triggerActivityId, + IEnumerable? activityExecutionContexts, CancellationToken cancellationToken) { _serviceProvider = serviceProvider; @@ -33,6 +36,7 @@ public class WorkflowExecutionContext Id = id; CorrelationId = correlationId; _nodes = graph.Flatten().Distinct().ToList(); + _activityExecutionContexts = activityExecutionContexts?.ToList() ?? new List(); Scheduler = scheduler; Input = input ?? new Dictionary(); ExecuteDelegate = executeDelegate; @@ -72,7 +76,12 @@ public class WorkflowExecutionContext public string? TriggerActivityId { get; set; } public CancellationToken CancellationToken { get; } public ICollection CompletionCallbacks => new ReadOnlyCollection(_completionCallbackEntries); - public ICollection ActivityExecutionContexts { get; set; } = new List(); + + public IReadOnlyCollection ActivityExecutionContexts + { + get => _activityExecutionContexts.ToList(); + internal set => _activityExecutionContexts = value.ToList(); + } /// /// A volatile collection of executed activity instance IDs. This collection is reset when workflow execution starts. @@ -148,6 +157,9 @@ public class WorkflowExecutionContext expressionExecutionContext.TransientProperties[ExpressionExecutionContextExtensions.ActivityExecutionContextKey] = activityExecutionContext; return activityExecutionContext; } + + public void RemoveActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Remove(context); + public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context); public async Task CancelActivityAsync(string activityId) { diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs index d5f3dcbd5..7ddb706a8 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs @@ -39,7 +39,7 @@ public class TypeJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) { // Handle collection types. - if (value.IsGenericType) + if (value.IsGenericType && value.GenericTypeArguments.Length == 1) { var elementType = value.GenericTypeArguments.First(); var typedEnumerable = typeof(IEnumerable<>).MakeGenericType(elementType); diff --git a/src/modules/Elsa.Workflows.Core/State/CompletionCallbackState.cs b/src/modules/Elsa.Workflows.Core/State/CompletionCallbackState.cs index 756d3d685..199af1b74 100644 --- a/src/modules/Elsa.Workflows.Core/State/CompletionCallbackState.cs +++ b/src/modules/Elsa.Workflows.Core/State/CompletionCallbackState.cs @@ -13,7 +13,7 @@ public class CompletionCallbackState { } - public CompletionCallbackState(string ownerId, string childId, string methodName) + public CompletionCallbackState(string ownerId, string childId, string? methodName) { OwnerId = ownerId; ChildId = childId; @@ -22,5 +22,5 @@ public class CompletionCallbackState public string OwnerId { get; init; } = default!; public string ChildId { get; init; } = default!; - public string MethodName { get; init; } = default!; + public string? MethodName { get; init; } = default!; } \ No newline at end of file