Merge remote-tracking branch 'origin/patch/3.5.1' into develop/3.6.0

This commit is contained in:
Sipke Schoorstra 2025-09-15 18:46:48 +02:00
commit 525f21bcc8
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
14 changed files with 155 additions and 119 deletions

View file

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

View file

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

View file

@ -24,7 +24,6 @@ internal class PostEndpoint(
ConfigurePermissions("exec:workflow-definitions");
Verbs(FastEndpoints.Http.POST);
}
/// <inheritdoc />
public override async Task HandleAsync(CancellationToken cancellationToken)
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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