Fix HTTP activity issues

This commit is contained in:
Sipke Schoorstra 2022-11-23 11:20:00 +01:00
parent 651a7a76d5
commit 0f358e72f9
19 changed files with 94 additions and 43 deletions

View file

@ -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<string>(options, "_type"));
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.ReferenceHandler = ReferenceHandler.Preserve;
options.PropertyNameCaseInsensitive = true;

View file

@ -38,23 +38,28 @@ public class HttpEndpoint : Trigger<HttpRequestModel>
)]
public Input<string?> Policy { get; set; } = new(default(string?));
protected override IEnumerable<object> GetTriggerPayload(TriggerIndexingContext context) => GetBookmarkPayload(context.ExpressionExecutionContext);
/// <inheritdoc />
protected override IEnumerable<object> GetTriggerPayloads(TriggerIndexingContext context) => GetBookmarkPayloads(context.ExpressionExecutionContext);
protected override void Execute(ActivityExecutionContext context)
/// <inheritdoc />
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<HttpRequestModel>(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<object> GetBookmarkPayload(ExpressionExecutionContext context)
private IEnumerable<object> GetBookmarkPayloads(ExpressionExecutionContext context)
{
// Generate bookmark data for path and selected methods.
var path = context.Get(Path);

View file

@ -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<Uri?> Url { get; set; } = default!;

View file

@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="2.2.2" />

View file

@ -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<HttpFeature>? configure = default)
{

View file

@ -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)

View file

@ -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;

View file

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace Elsa.Http.Models;
public record HttpRequestModel(
@ -7,4 +9,14 @@ public record HttpRequestModel(
IDictionary<string, string> QueryString,
IDictionary<string, object> RouteValues,
IDictionary<string, string> Headers
);
)
{
/// <summary>
/// Constructor used for deserialization.
/// </summary>
[JsonConstructor]
public HttpRequestModel() : this(default!, default!, default!, default!, default!, default!)
{
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Features.Services;
using Elsa.Identity.Features;
namespace Elsa.Identity.Extensions;
/// <summary>
/// Extensions for <see cref="IModule"/> that installs the <see cref="IdentityFeature"/> feature.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Installs & configures the <see cref="IdentityFeature"/> feature.
/// </summary>
public static IModule UseIdentity(this IModule module, Action<IdentityFeature>? configure = default)
{
module.Configure(configure);
return module;
}
}

View file

@ -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));

View file

@ -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<PortAttribute>()?.Name ?? port.PortProperty.Name;
await activityExecutionContext.CompleteActivityWithOutcomesAsync(portName);
}
}
}

View file

@ -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);
}
/// <summary>
/// Complete the current activity with the specified outcome.
/// </summary>
public static ValueTask CompleteActivityWithOutcomesAsync(this ActivityExecutionContext context, params string[] outcomes) => context.CompleteActivityAsync(new Outcomes(outcomes));
/// <summary>
/// Complete the current composite activity with the specified outcome.
/// </summary>
public static async ValueTask CompleteCompositeAsync(this ActivityExecutionContext context, params string[] outcomes) => await context.SendSignalAsync(new CompleteCompositeSignal(new Outcomes(outcomes)));
/// <summary>
/// Cancel the activity. For blocking activities, it means their bookmarks will be removed. For job activities, the background work will be cancelled.

View file

@ -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);
}
/// <summary>

View file

@ -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);

View file

@ -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);

View file

@ -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();
}

View file

@ -14,8 +14,10 @@ public class WorkflowExecutionContext
private readonly IServiceProvider _serviceProvider;
private readonly IList<ActivityNode> _nodes;
private readonly IList<ActivityCompletionCallbackEntry> _completionCallbackEntries = new List<ActivityCompletionCallbackEntry>();
private IList<ActivityExecutionContext> _activityExecutionContexts;
public WorkflowExecutionContext(IServiceProvider serviceProvider,
public WorkflowExecutionContext(
IServiceProvider serviceProvider,
string id,
string? correlationId,
Workflow workflow,
@ -24,6 +26,7 @@ public class WorkflowExecutionContext
IDictionary<string, object>? input,
ExecuteActivityDelegate? executeDelegate,
string? triggerActivityId,
IEnumerable<ActivityExecutionContext>? 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<ActivityExecutionContext>();
Scheduler = scheduler;
Input = input ?? new Dictionary<string, object>();
ExecuteDelegate = executeDelegate;
@ -72,7 +76,12 @@ public class WorkflowExecutionContext
public string? TriggerActivityId { get; set; }
public CancellationToken CancellationToken { get; }
public ICollection<ActivityCompletionCallbackEntry> CompletionCallbacks => new ReadOnlyCollection<ActivityCompletionCallbackEntry>(_completionCallbackEntries);
public ICollection<ActivityExecutionContext> ActivityExecutionContexts { get; set; } = new List<ActivityExecutionContext>();
public IReadOnlyCollection<ActivityExecutionContext> ActivityExecutionContexts
{
get => _activityExecutionContexts.ToList();
internal set => _activityExecutionContexts = value.ToList();
}
/// <summary>
/// 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)
{

View file

@ -39,7 +39,7 @@ public class TypeJsonConverter : JsonConverter<Type>
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);

View file

@ -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!;
}