Merge remote-tracking branch 'origin/patch/3.5.1' into develop/3.6.0
This commit is contained in:
commit
525f21bcc8
3
Elsa.sln
3
Elsa.sln
|
|
@ -64,6 +64,9 @@ EndProject
|
|||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{18453B51-25EB-4317-A4B3-B10518252E92}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration", "{1B8D5897-902E-4632-8698-E89CAF3DDF54}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\integration\Elsa.Logging.Core.LoggerSinkTests.cs = test\integration\Elsa.Logging.Core.LoggerSinkTests.cs
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "component", "component", "{08B41FFA-CEE3-46A7-B5C0-3EB65D37A16C}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
using System.Net.Mime;
|
||||
using Elsa.Abstractions;
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Execute;
|
||||
|
||||
/// <summary>
|
||||
/// This abstract class provides the necessary infrastructure to handle the execution of workflows, including setup of routes, permissions,
|
||||
/// and processing of HTTP requests to execute workflows.
|
||||
/// </summary>
|
||||
internal abstract class EndpointBase<T>(
|
||||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
IWorkflowRuntime workflowRuntime,
|
||||
IWorkflowStarter workflowStarter,
|
||||
IApiSerializer apiSerializer)
|
||||
: ElsaEndpoint<T, Response> where T : IExecutionRequest, new()
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Routes("/workflow-definitions/{definitionId}/execute");
|
||||
ConfigurePermissions("exec:workflow-definitions");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(T request, CancellationToken cancellationToken)
|
||||
{
|
||||
var definitionId = request.DefinitionId;
|
||||
var versionOptions = request.VersionOptions ?? VersionOptions.Published;
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken);
|
||||
|
||||
if (workflowGraph == null)
|
||||
{
|
||||
await Send.NotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var startRequest = new StartWorkflowRequest
|
||||
{
|
||||
Workflow = workflowGraph.Workflow,
|
||||
CorrelationId = request.CorrelationId,
|
||||
Name = request.Name,
|
||||
Input = request.GetInputAsDictionary(),
|
||||
Variables = request.GetVariablesAsDictionary(),
|
||||
TriggerActivityId = request.TriggerActivityId,
|
||||
ActivityHandle = request.ActivityHandle
|
||||
};
|
||||
|
||||
var startResponse = await workflowStarter.StartWorkflowAsync(startRequest, cancellationToken);
|
||||
|
||||
if(!HttpContext.Response.HasStarted)
|
||||
HttpContext.Response.Headers.Append("x-elsa-workflow-cannot-start", startResponse.CannotStart.ToString());
|
||||
|
||||
if (startResponse.CannotStart)
|
||||
{
|
||||
await Send.OkAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceId = startResponse.WorkflowInstanceId!;
|
||||
|
||||
// Write the workflow instance ID to the response header.
|
||||
// This allows clients to read the header even if the workflow writes a response body
|
||||
// (in which case, we can't transmit a JSON body that includes the instance ID).
|
||||
if(!HttpContext.Response.HasStarted)
|
||||
HttpContext.Response.Headers.Append("x-elsa-workflow-instance-id", instanceId);
|
||||
|
||||
var workflowClient = await workflowRuntime.CreateClientAsync(instanceId, cancellationToken);
|
||||
|
||||
// If a workflow fault occurred, respond appropriately with a 500 internal server error.
|
||||
if (startResponse.SubStatus == WorkflowSubStatus.Faulted)
|
||||
{
|
||||
var workflowState = await workflowClient.ExportStateAsync(cancellationToken);
|
||||
await HandleFaultAsync(workflowState, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!HttpContext.Response.HasStarted)
|
||||
{
|
||||
// Write a response header to indicate that the response is a workflow state response.
|
||||
// This is used by tools like Elsa Studio to determine if the response is in response to a workflow execution manually triggered by the user.
|
||||
HttpContext.Response.Headers.Append("x-elsa-response", "true");
|
||||
|
||||
// Only write a response if the workflow didn't change the HTTP status code.
|
||||
if (HttpContext.Response.StatusCode == StatusCodes.Status200OK)
|
||||
{
|
||||
var workflowState = await workflowClient.ExportStateAsync(cancellationToken);
|
||||
await Send.OkAsync(new(workflowState), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleFaultAsync(WorkflowState workflowState, CancellationToken cancellationToken)
|
||||
{
|
||||
var faultedResponse = apiSerializer.Serialize(new Response(workflowState));
|
||||
|
||||
HttpContext.Response.ContentType = MediaTypeNames.Application.Json;
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
await HttpContext.Response.WriteAsync(faultedResponse, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ internal class PostEndpoint(
|
|||
ConfigurePermissions("exec:workflow-definitions");
|
||||
Verbs(FastEndpoints.Http.POST);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer
|
|||
DefinitionId = definitionId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IWorkflowBuilder WithId(string id)
|
||||
{
|
||||
Id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWorkflowBuilder WithTenantId(string tenantId)
|
||||
|
|
|
|||
|
|
@ -96,6 +96,13 @@ public interface IWorkflowBuilder
|
|||
/// <param name="definitionId">The definition ID to use for the workflow being built.</param>
|
||||
IWorkflowBuilder WithDefinitionId(string definitionId);
|
||||
|
||||
/// <summary>
|
||||
/// A fluent method for setting the <see cref="Id"/> property.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier to use for the workflow being built.</param>
|
||||
/// <returns>The current <see cref="IWorkflowBuilder"/> instance for method chaining.</returns>
|
||||
IWorkflowBuilder WithId(string id);
|
||||
|
||||
/// <summary>
|
||||
/// A fluent method for setting the <see cref="TenantId"/> property.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,12 @@ public class InputDescriptor : PropertyDescriptor
|
|||
/// True if the expression should be evaluated automatically, false otherwise. Defaults to true.
|
||||
/// </summary>
|
||||
public bool AutoEvaluate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of a custom evaluator to use for evaluating the input property value.
|
||||
/// The evaluator type determines how the value for the property is resolved at runtime.
|
||||
/// </summary>
|
||||
public Type? EvaluatorType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of a custom evaluator to use for evaluating the input property value.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Elsa.Workflows.Runtime.Activities;
|
|||
[UsedImplicitly]
|
||||
public class Event : Trigger<object?>
|
||||
{
|
||||
internal const string EventInputWorkflowInputKey = "__EventPayloadWorkflowInput";
|
||||
public const string EventInputWorkflowInputKey = "__EventPayloadWorkflowInput";
|
||||
|
||||
/// <inheritdoc />
|
||||
internal Event([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,11 @@ namespace Microsoft.Extensions.DependencyInjection;
|
|||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="ClrWorkflowsProvider"/> to the service collection.
|
||||
/// Adds the specified workflow provider type to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <typeparam name="T">The type of the workflow definition provider.</typeparam>
|
||||
/// <returns>The service collection.</returns>
|
||||
/// <typeparam name="T">The type of the workflow provider to add. Must implement <see cref="IWorkflowsProvider"/>.</typeparam>
|
||||
[Obsolete("Use AddWorkflowsProvider instead.", false)]
|
||||
public static IServiceCollection AddWorkflowDefinitionProvider<T>(this IServiceCollection services) where T : class, IWorkflowsProvider => services.AddScoped<IWorkflowsProvider, T>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="ITriggerPayloadValidator{TPayload}"/> with the service container.
|
||||
/// </summary>
|
||||
|
|
@ -29,4 +27,10 @@ public static class DependencyInjectionExtensions
|
|||
{
|
||||
return services.AddScoped<ITriggerPayloadValidator<TPayload>, TValidator>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified workflows provider type to the service collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the workflow provider to add. Must implement <see cref="IWorkflowsProvider"/>.</typeparam>
|
||||
public static IServiceCollection AddWorkflowsProvider<T>(this IServiceCollection services) where T : class, IWorkflowsProvider => services.AddScoped<IWorkflowsProvider, T>();
|
||||
}
|
||||
|
|
@ -9,4 +9,4 @@ namespace Elsa.Workflows.Runtime;
|
|||
/// <param name="ProviderName">The name of the provider that provided the workflow definition.</param>
|
||||
/// <param name="MaterializerName">The name of the materializer that materialized the workflow.</param>
|
||||
/// <param name="MaterializerContext">The context of the materializer that materialized the workflow.</param>
|
||||
public record MaterializedWorkflow(Workflow Workflow, string ProviderName, string MaterializerName, object? MaterializerContext = default);
|
||||
public record MaterializedWorkflow(Workflow Workflow, string ProviderName, string MaterializerName, object? MaterializerContext = null);
|
||||
|
|
@ -56,7 +56,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
|
|||
{
|
||||
var providers = _workflowDefinitionProviders();
|
||||
var workflowDefinitions = new List<WorkflowDefinition>();
|
||||
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
var results = await provider.GetWorkflowsAsync(cancellationToken).AsTask().ToList();
|
||||
|
|
@ -84,7 +84,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
|
|||
var workflowDefinition = await AddOrUpdateAsync(materializedWorkflow, cancellationToken);
|
||||
|
||||
if (indexTriggers)
|
||||
await IndexTriggersAsync(materializedWorkflow, cancellationToken);
|
||||
await IndexTriggersAsync(workflowDefinition, cancellationToken);
|
||||
|
||||
return workflowDefinition;
|
||||
}
|
||||
|
|
@ -260,7 +260,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
|
|||
}
|
||||
}
|
||||
|
||||
private async Task IndexTriggersAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(materializedWorkflow.Workflow, cancellationToken);
|
||||
private async Task IndexTriggersAsync(WorkflowDefinition workflowDefinition, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(workflowDefinition, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Syncs the items in the primary list with existing items in the secondary list, even when the object instances are not the same (but their IDs are).
|
||||
|
|
|
|||
|
|
@ -29,16 +29,17 @@ public class EventPublisher(IStimulusSender stimulusSender, IStimulusDispatcher
|
|||
WorkflowInstanceId = workflowInstanceId,
|
||||
Input = workflowInput
|
||||
};
|
||||
var triggerName = ActivityTypeNameHelper.GenerateTypeName<Event>();
|
||||
if (asynchronous)
|
||||
{
|
||||
await stimulusDispatcher.SendAsync(new()
|
||||
{
|
||||
ActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<Event>(),
|
||||
ActivityTypeName = triggerName,
|
||||
Stimulus = stimulus,
|
||||
Metadata = metadata
|
||||
}, cancellationToken);
|
||||
}
|
||||
else
|
||||
await stimulusSender.SendAsync<Event>(stimulus, metadata, cancellationToken);
|
||||
await stimulusSender.SendAsync(triggerName, stimulus, metadata, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionStorePopulation;
|
||||
|
||||
public class InMemoryWorkflowMaterializer(Workflow workflow) : IWorkflowMaterializer
|
||||
{
|
||||
public string Name => "InMemory";
|
||||
|
||||
public ValueTask<Workflow> MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var materializedWorkflow = new Workflow
|
||||
{
|
||||
Identity = new(definition.DefinitionId, definition.Version, definition.Id),
|
||||
Root = workflow.Root
|
||||
};
|
||||
return new(materializedWorkflow);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionStorePopulation;
|
||||
|
||||
public class InMemoryWorkflowsProvider(Workflow workflow) : IWorkflowsProvider
|
||||
{
|
||||
public string Name => "InMemory";
|
||||
|
||||
public ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var materializedWorkflow = new MaterializedWorkflow(
|
||||
Workflow: workflow,
|
||||
ProviderName: "InMemory",
|
||||
MaterializerName: "InMemory",
|
||||
MaterializerContext: null
|
||||
);
|
||||
|
||||
return new([materializedWorkflow]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
using Elsa.Workflows.Runtime.Stimuli;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionStorePopulation;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a test class for integration testing various scenarios related to the
|
||||
/// `WorkflowDefinitionStorePopulation`. This class primarily focuses
|
||||
/// on ensuring correct behavior when workflow definitions are published or updated, and their effects
|
||||
/// on consuming workflows and triggers.
|
||||
/// </summary>
|
||||
public class Tests
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly Workflow _shiftyWorkflow;
|
||||
|
||||
public Tests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_shiftyWorkflow = new()
|
||||
{
|
||||
Identity = new(
|
||||
DefinitionId: "WorkflowWithTrigger",
|
||||
Version: 1,
|
||||
Id: "1",
|
||||
TenantId: "default"
|
||||
),
|
||||
Root = new Event("Foo")
|
||||
{
|
||||
CanStartWorkflow = true
|
||||
}
|
||||
};
|
||||
|
||||
_services = new TestApplicationBuilder(testOutputHelper)
|
||||
.ConfigureServices(services => services
|
||||
.AddScoped<IWorkflowsProvider>(_ => new InMemoryWorkflowsProvider(_shiftyWorkflow))
|
||||
.AddScoped<IWorkflowMaterializer>(_ => new InMemoryWorkflowMaterializer(_shiftyWorkflow)))
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a dependency workflow is published, all consuming workflows are updated to point to the new version of the dependency.
|
||||
/// </summary>
|
||||
[Fact(DisplayName = "When a workflow definition from a given source has a different Id than the one in the store, the trigger should still point to the workflow definition version ID in the store.")]
|
||||
public async Task Test1()
|
||||
{
|
||||
// Initial population of the store from workflow providers.
|
||||
await _services.PopulateRegistriesAsync();
|
||||
|
||||
// Artificially change the workflow definition version ID.
|
||||
_shiftyWorkflow.Identity = _shiftyWorkflow.Identity with
|
||||
{
|
||||
Id = ":1"
|
||||
};
|
||||
|
||||
// Emulate reloading of workflow definitions.
|
||||
await _services.PopulateRegistriesAsync();
|
||||
|
||||
// Triggering the workflow should still work.
|
||||
var stimulusSender = _services.GetRequiredService<IStimulusSender>();
|
||||
var stimulus = new EventStimulus("Foo");
|
||||
var triggerName = ActivityTypeNameHelper.GenerateTypeName<Event>();
|
||||
var result = await stimulusSender.SendAsync(triggerName, stimulus);
|
||||
|
||||
Assert.NotEmpty(result.WorkflowInstanceResponses);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue