diff --git a/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs b/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs
new file mode 100644
index 000000000..a16571941
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Elsa.IntegrationTests")]
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs
index b55ef7339..645e365c7 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs
@@ -1,11 +1,6 @@
-using System.Text.Json;
using Elsa.Abstractions;
-using Elsa.Common.Models;
using Elsa.Workflows.Api.Models;
-using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Management.Contracts;
-using Elsa.Workflows.Management.Mappers;
-using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
@@ -14,24 +9,18 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import;
///
/// Imports a JSON file containing a workflow definition.
///
-internal class Import : ElsaEndpoint
+internal class Import : ElsaEndpoint
{
- private readonly SerializerOptionsProvider _serializerOptionsProvider;
- private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
- private readonly VariableDefinitionMapper _variableDefinitionMapper;
+ private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
///
public Import(
- SerializerOptionsProvider serializerOptionsProvider,
- IWorkflowDefinitionPublisher workflowDefinitionPublisher,
IWorkflowDefinitionService workflowDefinitionService,
- VariableDefinitionMapper variableDefinitionMapper)
+ IWorkflowDefinitionImporter workflowDefinitionImporter)
{
- _serializerOptionsProvider = serializerOptionsProvider;
- _workflowDefinitionPublisher = workflowDefinitionPublisher;
_workflowDefinitionService = workflowDefinitionService;
- _variableDefinitionMapper = variableDefinitionMapper;
+ _workflowDefinitionImporter = workflowDefinitionImporter;
}
///
@@ -43,44 +32,13 @@ internal class Import : ElsaEndpoint
- public override async Task HandleAsync(WorkflowDefinitionRequest request, CancellationToken cancellationToken)
+ public override async Task HandleAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
+ var isNew = string.IsNullOrWhiteSpace(definitionId);
- // Get a workflow draft version.
- var draft = !string.IsNullOrWhiteSpace(definitionId)
- ? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken)
- : default;
-
- var isNew = draft == null;
-
- // Create a new workflow in case no existing definition was found.
- if (isNew)
- {
- draft = _workflowDefinitionPublisher.New();
-
- if (!string.IsNullOrWhiteSpace(definitionId))
- draft.DefinitionId = definitionId;
- }
-
- // Update the draft with the received model.
- var root = request.Root;
- var serializerOptions = _serializerOptionsProvider.CreateApiOptions();
- var stringData = JsonSerializer.Serialize(root, serializerOptions);
- var variables = _variableDefinitionMapper.Map(request.Variables).ToList();
-
- draft!.StringData = stringData;
- draft.MaterializerName = JsonWorkflowMaterializer.MaterializerName;
- draft.Name = request.Name?.Trim();
- draft.Description = request.Description?.Trim();
- draft.CustomProperties = request.CustomProperties ?? new Dictionary();
- draft.Variables = variables;
- draft.Inputs = request.Inputs ?? new List();
- draft.Outputs = request.Outputs ?? new List();
- draft.Outcomes = request.Outcomes ?? new List();
- draft.Options = request.Options;
- draft.UsableAsActivity = request.UsableAsActivity;
- draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
+ // Import workflow
+ var draft = await _workflowDefinitionImporter.ImportAsync(request, cancellationToken);
// Materialize the workflow definition for serialization.
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(draft, cancellationToken);
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs
index 2a7419087..cf7627fd0 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs
@@ -17,7 +17,7 @@ using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post;
[PublicAPI]
-internal class Post : ElsaEndpoint
+internal class Post : ElsaEndpoint
{
private readonly SerializerOptionsProvider _serializerOptionsProvider;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
@@ -42,7 +42,7 @@ internal class Post : ElsaEndpoint? Variables { get; set; }
- public ICollection? Inputs { get; set; }
- public ICollection? Outputs { get; set; }
- public ICollection? Outcomes { get; set; }
- public IDictionary? CustomProperties { get; set; }
- public IActivity? Root { get; set; }
- public WorkflowOptions? Options { get; set; }
- public bool Publish { get; set; }
- public bool? UsableAsActivity { get; set; }
-}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionImporter.cs b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionImporter.cs
new file mode 100644
index 000000000..638ab309f
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionImporter.cs
@@ -0,0 +1,10 @@
+using Elsa.Workflows.Management.Entities;
+using Elsa.Workflows.Management.Models;
+
+namespace Elsa.Workflows.Management.Contracts
+{
+ public interface IWorkflowDefinitionImporter
+ {
+ Task ImportAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs
index 1e8fefb42..51e7fc869 100644
--- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs
+++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs
@@ -103,7 +103,7 @@ public class WorkflowManagementFeature : FeatureBase
/// Adds the specified variable type to the system.
///
public WorkflowManagementFeature AddVariableType(string category) => AddVariableType(typeof(T), category);
-
+
///
/// Adds the specified variable type to the system.
///
@@ -138,6 +138,7 @@ public class WorkflowManagementFeature : FeatureBase
.AddMemoryStore()
.AddActivityProvider()
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
@@ -151,7 +152,7 @@ public class WorkflowManagementFeature : FeatureBase
.AddSingleton()
.AddSingleton()
;
-
+
Services.AddNotificationHandlersFrom(GetType());
Services.Configure(options =>
diff --git a/src/modules/Elsa.Workflows.Management/Models/SaveWorkflowDefinitionRequest.cs b/src/modules/Elsa.Workflows.Management/Models/SaveWorkflowDefinitionRequest.cs
new file mode 100644
index 000000000..35b834990
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Management/Models/SaveWorkflowDefinitionRequest.cs
@@ -0,0 +1,21 @@
+using Elsa.Workflows.Core.Contracts;
+using Elsa.Workflows.Core.Models;
+
+namespace Elsa.Workflows.Management.Models
+{
+ public class SaveWorkflowDefinitionRequest
+ {
+ public string? DefinitionId { get; set; }
+ public string? Name { get; set; }
+ public string? Description { get; set; }
+ public ICollection? Variables { get; set; }
+ public ICollection? Inputs { get; set; }
+ public ICollection? Outputs { get; set; }
+ public ICollection? Outcomes { get; set; }
+ public IDictionary? CustomProperties { get; set; }
+ public IActivity? Root { get; set; }
+ public WorkflowOptions? Options { get; set; }
+ public bool Publish { get; set; }
+ public bool? UsableAsActivity { get; set; }
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs
new file mode 100644
index 000000000..259221a63
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs
@@ -0,0 +1,70 @@
+using Elsa.Common.Models;
+using Elsa.Workflows.Core.Serialization;
+using Elsa.Workflows.Management.Contracts;
+using Elsa.Workflows.Management.Entities;
+using Elsa.Workflows.Management.Mappers;
+using Elsa.Workflows.Management.Materializers;
+using Elsa.Workflows.Management.Models;
+using System.Text.Json;
+
+namespace Elsa.Workflows.Management.Services
+{
+ public class WorkflowDefinitionImporter : IWorkflowDefinitionImporter
+ {
+ private readonly SerializerOptionsProvider _serializerOptionsProvider;
+ private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
+ private readonly VariableDefinitionMapper _variableDefinitionMapper;
+
+ public WorkflowDefinitionImporter(
+ SerializerOptionsProvider serializerOptionsProvider,
+ IWorkflowDefinitionPublisher workflowDefinitionPublisher,
+ VariableDefinitionMapper variableDefinitionMapper)
+ {
+ _serializerOptionsProvider = serializerOptionsProvider;
+ _workflowDefinitionPublisher = workflowDefinitionPublisher;
+ _variableDefinitionMapper = variableDefinitionMapper;
+ }
+
+ public async Task ImportAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
+ {
+ var definitionId = request.DefinitionId;
+
+ // Get a workflow draft version.
+ var draft = !string.IsNullOrWhiteSpace(definitionId)
+ ? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken)
+ : default;
+
+ var isNew = draft == null;
+
+ // Create a new workflow in case no existing definition was found.
+ if (isNew)
+ {
+ draft = _workflowDefinitionPublisher.New();
+
+ if (!string.IsNullOrWhiteSpace(definitionId))
+ draft.DefinitionId = definitionId;
+ }
+
+ // Update the draft with the received model.
+ var root = request.Root;
+ var serializerOptions = _serializerOptionsProvider.CreateApiOptions();
+ var stringData = JsonSerializer.Serialize(root, serializerOptions);
+ var variables = _variableDefinitionMapper.Map(request.Variables).ToList();
+
+ draft!.StringData = stringData;
+ draft.MaterializerName = JsonWorkflowMaterializer.MaterializerName;
+ draft.Name = request.Name?.Trim();
+ draft.Description = request.Description?.Trim();
+ draft.CustomProperties = request.CustomProperties ?? new Dictionary();
+ draft.Variables = variables;
+ draft.Inputs = request.Inputs ?? new List();
+ draft.Outputs = request.Outputs ?? new List();
+ draft.Outcomes = request.Outcomes ?? new List();
+ draft.Options = request.Options;
+ draft.UsableAsActivity = request.UsableAsActivity;
+ draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
+
+ return draft;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj
index 7eed97924..f255dbd3f 100644
--- a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj
+++ b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj
@@ -23,7 +23,14 @@
+
+
+
+ PreserveNewest
+
+
+
diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/Tests.cs
new file mode 100644
index 000000000..b6ab1d50c
--- /dev/null
+++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/Tests.cs
@@ -0,0 +1,82 @@
+using Elsa.Common.Models;
+using Elsa.Expressions.Contracts;
+using Elsa.Extensions;
+using Elsa.Testing.Shared;
+using Elsa.Workflows.Api.Models;
+using Elsa.Workflows.Core.Activities;
+using Elsa.Workflows.Core.Activities.Flowchart.Activities;
+using Elsa.Workflows.Core.Contracts;
+using Elsa.Workflows.Core.Models;
+using Elsa.Workflows.Core.Serialization;
+using Elsa.Workflows.Management.Contracts;
+using Elsa.Workflows.Management.Models;
+using Elsa.Workflows.Runtime.Contracts;
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Elsa.IntegrationTests.Scenarios.ImportAndExecute;
+
+public class Tests
+{
+ private readonly CapturingTextWriter _capturingTextWriter = new();
+ private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
+ private readonly SerializerOptionsProvider _serializerOptionsProvider;
+ private readonly IWorkflowRuntime _workflowRuntime;
+ private readonly IActivityRegistry _activityRegistry;
+ private readonly IExpressionSyntaxRegistry _expressionSyntaxRegistry;
+ private readonly IEnumerable _expressionSyntaxProviders;
+
+ public Tests(ITestOutputHelper testOutputHelper)
+ {
+ var services = new TestApplicationBuilder(testOutputHelper)
+ .WithCapturingTextWriter(_capturingTextWriter)
+ .ConfigureElsa(elsa => elsa.UseWorkflowsApi())
+ .Build();
+ _serializerOptionsProvider = services.GetRequiredService();
+ _workflowRuntime = services.GetRequiredService();
+ _activityRegistry = services.GetRequiredService();
+ _expressionSyntaxRegistry = services.GetRequiredService();
+ _expressionSyntaxProviders = services.GetServices();
+ _workflowDefinitionImporter = services.GetRequiredService();
+ }
+
+ [Fact(DisplayName = "Workflow imported from file should execute successfully.")]
+ public async Task Test1()
+ {
+ // Register activities.
+ await _activityRegistry.RegisterAsync(typeof(Workflow));
+ await _activityRegistry.RegisterAsync(typeof(Flowchart));
+ await _activityRegistry.RegisterAsync(typeof(WriteLine));
+
+ // Register expression syntaxes.
+ foreach (var syntaxProvider in _expressionSyntaxProviders)
+ {
+ var syntaxes = await syntaxProvider.GetDescriptorsAsync();
+ _expressionSyntaxRegistry.AddMany(syntaxes);
+ }
+
+ // Import and publish workflow.
+ var fileName = @"Scenarios/ImportAndExecute/workflow.json";
+ await using var openStream = File.OpenRead(fileName);
+ var options = _serializerOptionsProvider.CreateApiOptions();
+ var workflowDefinitionRequest = (await JsonSerializer.DeserializeAsync(openStream, options))!;
+
+ workflowDefinitionRequest.Publish = true;
+ var workflowDefinition = await _workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
+
+ // Execute.
+ var startWorkflowOptions = new StartWorkflowRuntimeOptions(null, new Dictionary(), VersionOptions.Published);
+ await _workflowRuntime.StartWorkflowAsync(workflowDefinition.DefinitionId, startWorkflowOptions);
+
+ // Assert.
+ var lines = _capturingTextWriter.Lines.ToList();
+
+ Assert.Equal(new[] { "Dummy Text" }, lines);
+ }
+}
diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/workflow.json b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/workflow.json
new file mode 100644
index 000000000..45c905c9c
--- /dev/null
+++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/workflow.json
@@ -0,0 +1,55 @@
+{
+ "id": "3a793167cf6948c899d2f40c3c05cf0b",
+ "definitionId": "11e2b350dae8485498c16d1ce3843829",
+ "name": "Dummy workflow",
+ "createdAt": "2023-04-04T08:35:22.841043+00:00",
+ "version": 1,
+ "variables": [],
+ "inputs": [],
+ "outputs": [],
+ "outcomes": [],
+ "metadata": {},
+ "isLatest": true,
+ "isPublished": true,
+ "root": {
+ "type": "Elsa.Flowchart",
+ "version": 1,
+ "id": "Flowchart1",
+ "metadata": {},
+ "applicationProperties": {
+ "NotFoundConnectionsKey": []
+ },
+ "start": "WriteLine1",
+ "activities": [
+ {
+ "text": {
+ "typeName": "String",
+ "expression": {
+ "type": "Literal",
+ "value": "Dummy Text"
+ },
+ "memoryReference": {
+ "id": "WriteLine1:input-1"
+ }
+ },
+ "id": "WriteLine1",
+ "type": "Elsa.WriteLine",
+ "version": 1,
+ "canStartWorkflow": false,
+ "runAsynchronously": false,
+ "customProperties": {},
+ "source": null,
+ "line": null,
+ "metadata": {
+ "designer": {
+ "position": {
+ "x": 2256,
+ "y": 2462
+ }
+ }
+ }
+ }
+ ],
+ "connections": []
+ }
+}
\ No newline at end of file