Add JSON workflow provider (#3955)

* Refactor import models

* Update API endpoints

* Add console sample to run workflow from JSON

* Add FluentStorage provider for JSON workflows + sample
This commit is contained in:
Sipke Schoorstra 2023-04-25 22:15:11 +02:00 committed by GitHub
parent 799e9e02ed
commit 0ea463fdd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 847 additions and 268 deletions

View file

@ -188,6 +188,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Onboarding.Web
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Onboarding.WorkflowServer", "src\samples\aspnet\Elsa.Samples.Onboarding.WorkflowServer\Elsa.Samples.Onboarding.WorkflowServer.csproj", "{199FC97A-682B-4C7B-A560-1032B16AEAC4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.JsonWorkflows", "src\samples\console\Elsa.Samples.JsonWorkflows\Elsa.Samples.JsonWorkflows.csproj", "{4FA6955C-6860-493F-ABD4-CE327A33EEA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.JsonWorkflowProvider", "src\samples\aspnet\Elsa.Samples.JsonWorkflowProvider\Elsa.Samples.JsonWorkflowProvider.csproj", "{C5043453-5FB8-4796-9A80-C4C766F2CB62}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.WorkflowProviders.FluentStorage", "src\modules\Elsa.WorkflowProviders.FluentStorage\Elsa.WorkflowProviders.FluentStorage.csproj", "{044C3108-FE79-460A-9C31-A03C30228836}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -474,6 +480,18 @@ Global
{199FC97A-682B-4C7B-A560-1032B16AEAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{199FC97A-682B-4C7B-A560-1032B16AEAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{199FC97A-682B-4C7B-A560-1032B16AEAC4}.Release|Any CPU.Build.0 = Release|Any CPU
{4FA6955C-6860-493F-ABD4-CE327A33EEA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FA6955C-6860-493F-ABD4-CE327A33EEA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FA6955C-6860-493F-ABD4-CE327A33EEA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FA6955C-6860-493F-ABD4-CE327A33EEA3}.Release|Any CPU.Build.0 = Release|Any CPU
{C5043453-5FB8-4796-9A80-C4C766F2CB62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5043453-5FB8-4796-9A80-C4C766F2CB62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5043453-5FB8-4796-9A80-C4C766F2CB62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5043453-5FB8-4796-9A80-C4C766F2CB62}.Release|Any CPU.Build.0 = Release|Any CPU
{044C3108-FE79-460A-9C31-A03C30228836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{044C3108-FE79-460A-9C31-A03C30228836}.Debug|Any CPU.Build.0 = Debug|Any CPU
{044C3108-FE79-460A-9C31-A03C30228836}.Release|Any CPU.ActiveCfg = Release|Any CPU
{044C3108-FE79-460A-9C31-A03C30228836}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@ -557,5 +575,8 @@ Global
{FAF438E1-B771-418B-83DD-A0F959C82D97} = {873BFC3E-63C2-4495-A503-5EC05DCD84E4}
{D64F1240-0110-42E1-AE59-E95AEC8A2900} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{199FC97A-682B-4C7B-A560-1032B16AEAC4} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{4FA6955C-6860-493F-ABD4-CE327A33EEA3} = {873BFC3E-63C2-4495-A503-5EC05DCD84E4}
{C5043453-5FB8-4796-9A80-C4C766F2CB62} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{044C3108-FE79-460A-9C31-A03C30228836} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
EndGlobalSection
EndGlobal

View file

@ -38,9 +38,14 @@ public static class ServiceProviderExtensions
{
var json = await File.ReadAllTextAsync(fileName);
var serializer = services.GetRequiredService<IActivitySerializer>();
var workflowDefinitionRequest = serializer.Deserialize<SaveWorkflowDefinitionRequest>(json);
var model = serializer.Deserialize<WorkflowDefinitionModel>(json);
workflowDefinitionRequest.Publish = true;
var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest
{
Model = model,
Publish = true
};
var workflowDefinitionImporter = services.GetRequiredService<IWorkflowDefinitionImporter>();
return await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
}

View file

@ -49,7 +49,7 @@ export class WorkflowDefinitionsApi {
//TODO: Written as a workaround for different server and client models.
//To be deleted after the port model on backend is updated.
const requestClone = cloneDeep(request);
removeGuidsFromPortNames(requestClone.root);
removeGuidsFromPortNames(requestClone.model.root);
const httpClient = await this.getHttpClient();
const response = await httpClient.post<WorkflowDefinition>('workflow-definitions', requestClone);
@ -179,19 +179,8 @@ export class WorkflowDefinitionsApi {
export interface SaveWorkflowDefinitionRequest {
definitionId: string;
version?: number;
name?: string;
description?: string;
model: WorkflowDefinition;
publish: boolean;
root?: Activity;
variables?: Array<Variable>;
inputs?: Array<InputDefinition>;
outputs?: Array<InputDefinition>;
outcomes?: Array<string>;
options?: WorkflowOptions;
usableAsActivity?: boolean;
customProperties?: Map<string, any>;
}
export interface BaseManyWorkflowDefinitionRequest {

View file

@ -16,19 +16,8 @@ export class WorkflowDefinitionManager {
public saveWorkflow = async (definition: WorkflowDefinition, publish: boolean): Promise<WorkflowDefinition> => {
const request: SaveWorkflowDefinitionRequest = {
definitionId: definition.definitionId,
version: definition.version,
name: definition.name,
description: definition.description,
publish: publish,
root: definition.root,
variables: definition.variables,
customProperties: definition.customProperties,
inputs: definition.inputs,
outputs: definition.outputs,
outcomes: definition.outcomes,
options: definition.options,
usableAsActivity: definition.usableAsActivity
model: definition,
publish: publish
};
return await this.api.post(request);

View file

@ -2,6 +2,7 @@ using Elsa.Common.Models;
using Elsa.Http.Contracts;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using Microsoft.AspNetCore.Http;

View file

@ -4,6 +4,7 @@ using Elsa.ProtoActor.Mappers;
using Elsa.ProtoActor.Protos;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using Proto;
using Proto.Cluster;

View file

@ -5,6 +5,7 @@ using Elsa.ProtoActor.Mappers;
using Elsa.ProtoActor.Protos;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Proto.Cluster;

View file

@ -0,0 +1,16 @@
using FluentStorage.Blobs;
namespace Elsa.WorkflowProviders.FluentStorage.Contracts;
/// <summary>
/// A provider of <see cref="IBlobStorage"/>. The point of this interface is to provide a wrapper for actual <see cref="IBlobStorage"/> implementations.
/// This prevents collisions when the application uses multiple <see cref="IBlobStorage"/> implementations.
/// </summary>
public interface IBlobStorageProvider
{
/// <summary>
/// Gets the <see cref="IBlobStorage"/>.
/// </summary>
/// <returns>The <see cref="IBlobStorage"/>.</returns>
IBlobStorage GetBlobStorage();
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<Description>
Provides a workflow definition provider that loads workflow definitions from files.
</Description>
<PackageTags>elsa module fluentstorage json blob</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentStorage" Version="5.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,37 @@
using Elsa.Features.Services;
using Elsa.WorkflowProviders.FluentStorage.Features;
using FluentStorage.Blobs;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Extension methods for <see cref="IModule"/> to add the fluent storage workflow definition provider.
/// </summary>
[PublicAPI]
public static class ModuleExtensions
{
/// <summary>
/// Adds the fluent storage workflow definition provider.
/// </summary>
/// <param name="module">The module.</param>
/// <param name="blobStorage">A callback that creates an <see cref="IBlobStorage"/>.</param>
/// <returns>The module.</returns>
public static IModule UseFluentStorageProvider(this IModule module, Func<IServiceProvider, IBlobStorage> blobStorage)
{
return module.UseFluentStorageProvider(feature => feature.BlobStorage = blobStorage);
}
/// <summary>
/// Adds the fluent storage workflow definition provider.
/// </summary>
/// <param name="module">The module.</param>
/// <param name="configure">The configuration delegate.</param>
/// <returns>The module.</returns>
public static IModule UseFluentStorageProvider(this IModule module, Action<FluentStorageFeature>? configure = default)
{
module.Use(configure);
return module;
}
}

View file

@ -0,0 +1,46 @@
using System.Reflection;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.WorkflowProviders.FluentStorage.Contracts;
using Elsa.WorkflowProviders.FluentStorage.Services;
using Elsa.Workflows.Management.Features;
using FluentStorage;
using FluentStorage.Blobs;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.WorkflowProviders.FluentStorage.Features;
/// <summary>
/// A feature that enables the FluentStorage workflow definition provider.
/// </summary>
[DependsOn(typeof(WorkflowManagementFeature))]
[PublicAPI]
public class FluentStorageFeature : FeatureBase
{
/// <inheritdoc />
public FluentStorageFeature(IModule module) : base(module)
{
}
/// <summary>
/// The blob storage to use.
/// </summary>
public Func<IServiceProvider, IBlobStorage> BlobStorage { get; set; } = _ => StorageFactory.Blobs.DirectoryFiles(GetDefaultWorkflowsDirectory());
/// <inheritdoc />
public override void Apply()
{
Services.AddSingleton<IBlobStorageProvider>(sp => new BlobStorageProvider(BlobStorage(sp)));
Services.AddWorkflowDefinitionProvider<FluentStorageWorkflowDefinitionProvider>();
}
private static string GetDefaultWorkflowsDirectory()
{
var entryAssemblyDir = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!;
var directory = Path.Combine(entryAssemblyDir, "Workflows");
return directory;
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,24 @@
using Elsa.WorkflowProviders.FluentStorage.Contracts;
using FluentStorage.Blobs;
namespace Elsa.WorkflowProviders.FluentStorage.Services;
/// <summary>
/// A provider of <see cref="IBlobStorage"/>.
/// </summary>
public class BlobStorageProvider : IBlobStorageProvider
{
private readonly IBlobStorage _blobStorage;
/// <summary>
/// Initializes a new instance of the <see cref="BlobStorageProvider"/> class.
/// </summary>
/// <param name="blobStorage">The <see cref="IBlobStorage"/>.</param>
public BlobStorageProvider(IBlobStorage blobStorage)
{
_blobStorage = blobStorage;
}
/// <inheritdoc />
public IBlobStorage GetBlobStorage() => _blobStorage;
}

View file

@ -0,0 +1,96 @@
using Elsa.Common.Contracts;
using Elsa.WorkflowProviders.FluentStorage.Contracts;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Models;
using FluentStorage.Blobs;
using JetBrains.Annotations;
namespace Elsa.WorkflowProviders.FluentStorage.Services;
/// <summary>
/// A workflow definition provider that loads workflow definitions from a storage using FluentStorage (See https://github.com/robinrodricks/FluentStorage).
/// </summary>
[PublicAPI]
public class FluentStorageWorkflowDefinitionProvider : IWorkflowDefinitionProvider
{
private readonly IBlobStorageProvider _blobStorageProvider;
private readonly IActivitySerializer _activitySerializer;
private readonly ISystemClock _systemClock;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
/// <summary>
/// Initializes a new instance of the <see cref="FluentStorageWorkflowDefinitionProvider"/> class.
/// </summary>
public FluentStorageWorkflowDefinitionProvider(
IBlobStorageProvider blobStorageProvider,
IActivitySerializer activitySerializer,
ISystemClock systemClock,
WorkflowDefinitionMapper workflowDefinitionMapper,
VariableDefinitionMapper variableDefinitionMapper)
{
_blobStorageProvider = blobStorageProvider;
_activitySerializer = activitySerializer;
_systemClock = systemClock;
_workflowDefinitionMapper = workflowDefinitionMapper;
_variableDefinitionMapper = variableDefinitionMapper;
}
/// <inheritdoc />
public string Name => "FluentStorage";
/// <inheritdoc />
public async ValueTask<IEnumerable<WorkflowDefinitionResult>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default)
{
var options = new ListOptions
{
Recurse = true,
};
var blobStorage = _blobStorageProvider.GetBlobStorage();
var blobs = await blobStorage.ListFilesAsync(options, cancellationToken);
var results = new List<WorkflowDefinitionResult>();
foreach (var blob in blobs)
{
var result = await ReadWorkflowDefinitionAsync(blob, cancellationToken);
results.Add(result);
}
return results;
}
private async Task<WorkflowDefinitionResult> ReadWorkflowDefinitionAsync(Blob blob, CancellationToken cancellationToken)
{
var blobStorage = _blobStorageProvider.GetBlobStorage();
var workflowJson = await blobStorage.ReadTextAsync(blob.FullPath, cancellationToken: cancellationToken);
var workflowDefinitionModel = _activitySerializer.Deserialize<WorkflowDefinitionModel>(workflowJson);
var variables = _variableDefinitionMapper.Map(workflowDefinitionModel.Variables).ToList();
var rootJson = _activitySerializer.Serialize(workflowDefinitionModel.Root!);
var definition = new WorkflowDefinition
{
Id = workflowDefinitionModel.Id,
DefinitionId = workflowDefinitionModel.DefinitionId,
Version = workflowDefinitionModel.Version,
Name = workflowDefinitionModel.Name,
Description = workflowDefinitionModel.Description,
CustomProperties = workflowDefinitionModel.CustomProperties ?? new Dictionary<string, object>(),
Variables = variables,
IsLatest = workflowDefinitionModel.IsLatest,
IsPublished = workflowDefinitionModel.IsPublished,
CreatedAt = workflowDefinitionModel.CreatedAt == default ? _systemClock.UtcNow : workflowDefinitionModel.CreatedAt,
MaterializerName = JsonWorkflowMaterializer.MaterializerName,
StringData = rootJson
};
var workflow = _workflowDefinitionMapper.Map(definition);
return new WorkflowDefinitionResult(definition, workflow);
}
}

View file

@ -2,6 +2,7 @@ using System.Text;
using Elsa.Common.Models;
using Elsa.JavaScript.TypeDefinitions.Contracts;
using Elsa.JavaScript.TypeDefinitions.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Contracts;
using FastEndpoints;

View file

@ -1,11 +1,11 @@
using System.Text.Json;
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
using Humanizer;
@ -71,6 +71,7 @@ public class Export : ElsaEndpoint<Request>
definition.UsableAsActivity,
definition.IsLatest,
definition.IsPublished,
definition.Options,
workflow.Root);
var serializerOptions = _serializer.CreateOptions();

View file

@ -1,10 +1,10 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Mappers;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Get;
@ -13,11 +13,13 @@ internal class Get : ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IApiSerializer _apiSerializer;
private readonly WorkflowDefinitionMapper _mapper;
public Get(IWorkflowDefinitionStore store, IApiSerializer apiSerializer)
public Get(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper)
{
_store = store;
_apiSerializer = apiSerializer;
_mapper = mapper;
}
public override void Configure()
@ -44,15 +46,14 @@ internal class Get : ElsaEndpoint<Request>
await SendNotFoundAsync(cancellationToken);
return;
}
var mapper = new WorkflowDefinitionMapper();
var response = await mapper.FromEntityAsync(definition, cancellationToken);
var model = await _mapper.MapAsync(definition, cancellationToken);
var serializerOptions = _apiSerializer.CreateOptions();
// If the root of composite activities is not requested, exclude them from being serialized.
if (!request.IncludeCompositeRoot)
serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory());
await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken);
await HttpContext.Response.WriteAsJsonAsync(model, serializerOptions, cancellationToken);
}
}

View file

@ -1,26 +1,30 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Runtime.Contracts;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import;
/// <summary>
/// Imports a JSON file containing a workflow definition.
/// </summary>
internal class Import : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefinitionResponse>
[PublicAPI]
internal class Import : ElsaEndpoint<WorkflowDefinitionModel, WorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
/// <inheritdoc />
public Import(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowDefinitionImporter workflowDefinitionImporter)
IWorkflowDefinitionImporter workflowDefinitionImporter,
WorkflowDefinitionMapper workflowDefinitionMapper)
{
_workflowDefinitionService = workflowDefinitionService;
_workflowDefinitionImporter = workflowDefinitionImporter;
_workflowDefinitionMapper = workflowDefinitionMapper;
}
/// <inheritdoc />
@ -32,38 +36,25 @@ internal class Import : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefi
}
/// <inheritdoc />
public override async Task HandleAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken)
public override async Task HandleAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
var definitionId = model.DefinitionId;
var isNew = string.IsNullOrWhiteSpace(definitionId);
// Import workflow
var draft = await _workflowDefinitionImporter.ImportAsync(request, cancellationToken);
var saveWorkflowRequest = new SaveWorkflowDefinitionRequest
{
Model = model,
Publish = false,
};
var draft = await _workflowDefinitionImporter.ImportAsync(saveWorkflowRequest, cancellationToken);
// Materialize the workflow definition for serialization.
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(draft, cancellationToken);
var response = new WorkflowDefinitionResponse(
draft.Id,
draft.DefinitionId,
draft.Name,
draft.Description,
draft.CreatedAt,
draft.Version,
request.Variables ?? new List<VariableDefinition>(),
draft.Inputs,
draft.Outputs,
draft.Outcomes,
draft.CustomProperties,
draft.IsLatest,
draft.IsPublished,
draft.UsableAsActivity,
workflow.Root,
draft.Options);
// Map the workflow definition for serialization.
var updatedModel = await _workflowDefinitionMapper.MapAsync(draft, cancellationToken);
if (isNew)
await SendCreatedAtAsync<Get.Get>(new { DefinitionId = definitionId }, response, cancellation: cancellationToken);
await SendCreatedAtAsync<Get.Get>(new { DefinitionId = definitionId }, updatedModel, cancellation: cancellationToken);
else
await SendOkAsync(response, cancellationToken);
await SendOkAsync(updatedModel, cancellationToken);
}
}

View file

@ -1,6 +1,5 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
@ -8,7 +7,6 @@ using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Medallion.Threading;
using System.Text.Json;
using Elsa.Workflows.Api.Mappers;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Management.Contracts;
@ -17,22 +15,25 @@ using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post;
[PublicAPI]
internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefinitionResponse, WorkflowDefinitionMapper>
internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefinitionModel>
{
private readonly IApiSerializer _serializer;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
private readonly IDistributedLockProvider _distributedLockProvider;
public Post(
IApiSerializer serializer,
IWorkflowDefinitionPublisher workflowDefinitionPublisher,
VariableDefinitionMapper variableDefinitionMapper,
WorkflowDefinitionMapper workflowDefinitionMapper,
IDistributedLockProvider distributedLockProvider)
{
_serializer = serializer;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
_variableDefinitionMapper = variableDefinitionMapper;
_workflowDefinitionMapper = workflowDefinitionMapper;
_distributedLockProvider = distributedLockProvider;
}
@ -44,7 +45,8 @@ internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefini
public override async Task HandleAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
var model = request.Model;
var definitionId = model.DefinitionId;
var resourceName = $"{GetType().FullName}:{(!string.IsNullOrWhiteSpace(definitionId) ? definitionId : Guid.NewGuid().ToString())}";
await using var handle = await _distributedLockProvider.AcquireLockAsync(resourceName, TimeSpan.FromMinutes(1), cancellationToken);
@ -65,32 +67,32 @@ internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefini
}
// Update the draft with the received model.
var root = request.Root ?? new Sequence();
var root = model.Root ?? new Sequence();
var serializerOptions = _serializer.CreateOptions();
// Ignore the root activity when serializing the workflow definition.
serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory());
var stringData = JsonSerializer.Serialize(root, serializerOptions);
var variables = _variableDefinitionMapper.Map(request.Variables).ToList();
var inputs = request.Inputs ?? new List<InputDefinition>();
var outputs = request.Outputs ?? new List<OutputDefinition>();
var outcomes = request.Outcomes ?? new List<string>();
var variables = _variableDefinitionMapper.Map(model.Variables).ToList();
var inputs = model.Inputs ?? new List<InputDefinition>();
var outputs = model.Outputs ?? new List<OutputDefinition>();
var outcomes = model.Outcomes ?? new List<string>();
draft!.StringData = stringData;
draft.MaterializerName = JsonWorkflowMaterializer.MaterializerName;
draft.Name = request.Name?.Trim();
draft.Description = request.Description?.Trim();
draft.CustomProperties = request.CustomProperties ?? new Dictionary<string, object>();
draft.Name = model.Name?.Trim();
draft.Description = model.Description?.Trim();
draft.CustomProperties = model.CustomProperties ?? new Dictionary<string, object>();
draft.Variables = variables;
draft.Inputs = inputs;
draft.Outputs = outputs;
draft.Outcomes = outcomes;
draft.Options = request.Options;
draft.UsableAsActivity = request.UsableAsActivity;
draft.Options = model.Options;
draft.UsableAsActivity = model.UsableAsActivity;
draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
var response = await Map.FromEntityAsync(draft, cancellationToken);
var response = await _workflowDefinitionMapper.MapAsync(draft, cancellationToken);
if (isNew)
await SendCreatedAtAsync<Get.Get>(new { definitionId }, response, cancellation: cancellationToken);

View file

@ -1,27 +1,29 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Mappers;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Publish;
[PublicAPI]
internal class Publish : ElsaEndpoint<Request, WorkflowDefinitionResponse, WorkflowDefinitionMapper>
internal class Publish : ElsaEndpoint<Request, WorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly IApiSerializer _serializer;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
public Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IApiSerializer serializer)
public Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IApiSerializer serializer, WorkflowDefinitionMapper workflowDefinitionMapper)
{
_store = store;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
_serializer = serializer;
_workflowDefinitionMapper = workflowDefinitionMapper;
}
public override void Configure()
@ -55,7 +57,7 @@ internal class Publish : ElsaEndpoint<Request, WorkflowDefinitionResponse, Workf
await _workflowDefinitionPublisher.PublishAsync(definition, cancellationToken);
var response = await Map.FromEntityAsync(definition, cancellationToken);
var response = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken);
// We do not want to include composite root activities in the response.
var serializerOptions = _serializer.CreateOptions();

View file

@ -1,22 +1,24 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Mappers;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Retract;
[PublicAPI]
internal class Retract : ElsaEndpoint<Request, WorkflowDefinitionResponse, WorkflowDefinitionMapper>
internal class Retract : ElsaEndpoint<Request, WorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
public Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher)
public Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, WorkflowDefinitionMapper workflowDefinitionMapper)
{
_store = store;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
_workflowDefinitionMapper = workflowDefinitionMapper;
}
public override void Configure()
@ -49,7 +51,7 @@ internal class Retract : ElsaEndpoint<Request, WorkflowDefinitionResponse, Workf
}
await _workflowDefinitionPublisher.RetractAsync(definition, cancellationToken);
var response = await Map.FromEntityAsync(definition, cancellationToken);
var response = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken);
await SendOkAsync(response, cancellationToken);
}
}

View file

@ -7,7 +7,6 @@ using Elsa.JavaScript.Features;
using Elsa.Workflows.Api.Serialization;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Features;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Api.Features;

View file

@ -1,36 +0,0 @@
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Runtime.Contracts;
using FastEndpoints;
namespace Elsa.Workflows.Api.Mappers;
internal class WorkflowDefinitionMapper : ResponseMapper<WorkflowDefinitionResponse, WorkflowDefinition>
{
public override async Task<WorkflowDefinitionResponse> FromEntityAsync(WorkflowDefinition entity, CancellationToken cancellationToken = default)
{
var workflowDefinitionService = Resolve<IWorkflowDefinitionService>();
var variableDefinitionMapper = Resolve<VariableDefinitionMapper>();
var workflow = await workflowDefinitionService.MaterializeWorkflowAsync(entity, cancellationToken);
var variables = variableDefinitionMapper.Map(workflow.Variables).ToList();
return new(
entity.Id,
entity.DefinitionId,
entity.Name,
entity.Description,
entity.CreatedAt,
entity.Version,
variables,
entity.Inputs,
entity.Outputs,
entity.Outcomes,
entity.CustomProperties,
entity.IsLatest,
entity.IsPublished,
entity.UsableAsActivity,
workflow.Root,
workflow.Options);
}
}

View file

@ -1,22 +0,0 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Api.Models;
internal record WorkflowDefinitionModel(
string Id,
string DefinitionId,
string? Name,
string? Description,
DateTimeOffset CreatedAt,
int Version,
ICollection<VariableDefinition> Variables,
ICollection<InputDefinition> Inputs,
ICollection<OutputDefinition> Outputs,
ICollection<string> Outcomes,
IDictionary<string, object> Metadata,
bool? UsableAsActivity,
bool IsLatest,
bool IsPublished,
IActivity Root
);

View file

@ -1,61 +1,61 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Api.Models;
internal class WorkflowDefinitionResponse
{
public WorkflowDefinitionResponse(
string id,
string definitionId,
string? name,
string? description,
DateTimeOffset createdAt,
int version,
ICollection<VariableDefinition> variables,
ICollection<InputDefinition> inputs,
ICollection<OutputDefinition> outputs,
ICollection<string> outcomes,
IDictionary<string, object> customProperties,
bool isLatest,
bool isPublished,
bool? usableAsActivity,
IActivity root,
WorkflowOptions? options)
{
Id = id;
DefinitionId = definitionId;
Name = name;
Description = description;
CreatedAt = createdAt;
Version = version;
Variables = variables;
Inputs = inputs;
Outputs = outputs;
Outcomes = outcomes;
CustomProperties = customProperties;
IsLatest = isLatest;
IsPublished = isPublished;
UsableAsActivity = usableAsActivity;
Root = root;
Options = options;
}
public string Id { get; }
public string DefinitionId { get; }
public string? Name { get; }
public string? Description { get; }
public DateTimeOffset CreatedAt { get; }
public int Version { get; }
public ICollection<VariableDefinition> Variables { get; }
public ICollection<InputDefinition> Inputs { get; }
public ICollection<OutputDefinition> Outputs { get; }
public ICollection<string> Outcomes { get; }
public IDictionary<string, object> CustomProperties { get; }
public bool IsLatest { get; }
public bool IsPublished { get; }
public bool? UsableAsActivity { get; }
public IActivity Root { get; }
public WorkflowOptions? Options { get; set; }
}
// using Elsa.Workflows.Core.Contracts;
// using Elsa.Workflows.Core.Models;
// using Elsa.Workflows.Management.Models;
//
// namespace Elsa.Workflows.Api.Models;
//
// internal class WorkflowDefinitionResponse
// {
// public WorkflowDefinitionResponse(
// string id,
// string definitionId,
// string? name,
// string? description,
// DateTimeOffset createdAt,
// int version,
// ICollection<VariableDefinition> variables,
// ICollection<InputDefinition> inputs,
// ICollection<OutputDefinition> outputs,
// ICollection<string> outcomes,
// IDictionary<string, object> customProperties,
// bool isLatest,
// bool isPublished,
// bool? usableAsActivity,
// IActivity root,
// WorkflowOptions? options)
// {
// Id = id;
// DefinitionId = definitionId;
// Name = name;
// Description = description;
// CreatedAt = createdAt;
// Version = version;
// Variables = variables;
// Inputs = inputs;
// Outputs = outputs;
// Outcomes = outcomes;
// CustomProperties = customProperties;
// IsLatest = isLatest;
// IsPublished = isPublished;
// UsableAsActivity = usableAsActivity;
// Root = root;
// Options = options;
// }
//
// public string Id { get; }
// public string DefinitionId { get; }
// public string? Name { get; }
// public string? Description { get; }
// public DateTimeOffset CreatedAt { get; }
// public int Version { get; }
// public ICollection<VariableDefinition> Variables { get; }
// public ICollection<InputDefinition> Inputs { get; }
// public ICollection<OutputDefinition> Outputs { get; }
// public ICollection<string> Outcomes { get; }
// public IDictionary<string, object> CustomProperties { get; }
// public bool IsLatest { get; }
// public bool IsPublished { get; }
// public bool? UsableAsActivity { get; }
// public IActivity Root { get; }
// public WorkflowOptions? Options { get; set; }
// }

View file

@ -11,7 +11,7 @@ namespace Elsa.Workflows.Management.Contracts
/// <summary>
/// Imports a workflow definition.
/// </summary>
/// <param name="request">A DTO representing the workflow definition to import.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The imported workflow definition.</returns>
Task<WorkflowDefinition> ImportAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);

View file

@ -2,7 +2,7 @@ using Elsa.Common.Models;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Entities;
namespace Elsa.Workflows.Runtime.Contracts;
namespace Elsa.Workflows.Management.Contracts;
/// <summary>
/// Manages materialization of <see cref="WorkflowDefinition"/> to <see cref="Workflow"/> objects.

View file

@ -148,6 +148,7 @@ public class WorkflowManagementFeature : FeatureBase
.AddSingleton<IWorkflowMaterializer, JsonWorkflowMaterializer>()
.AddSingleton<IActivityPortResolver, WorkflowDefinitionActivityPortResolver>()
.AddActivityProvider<WorkflowDefinitionActivityProvider>()
.AddSingleton<WorkflowDefinitionMapper>()
.AddSingleton<VariableDefinitionMapper>()
;

View file

@ -0,0 +1,76 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Mappers;
/// <summary>
/// Maps <see cref="WorkflowDefinition"/> to and from <see cref="Workflow"/> and <see cref="WorkflowDefinitionModel"/>.
/// </summary>
public class WorkflowDefinitionMapper
{
private readonly IActivitySerializer _activitySerializer;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
/// <summary>
/// Initializes a new instance of the <see cref="WorkflowDefinitionMapper"/> class.
/// </summary>
public WorkflowDefinitionMapper(IActivitySerializer activitySerializer, IWorkflowDefinitionService workflowDefinitionService, VariableDefinitionMapper variableDefinitionMapper)
{
_activitySerializer = activitySerializer;
_workflowDefinitionService = workflowDefinitionService;
_variableDefinitionMapper = variableDefinitionMapper;
}
/// <summary>
/// Maps a <see cref="WorkflowDefinition"/> to a <see cref="Workflow"/>.
/// </summary>
/// <param name="source">The source <see cref="WorkflowDefinition"/>.</param>
/// <returns>The mapped <see cref="Workflow"/>.</returns>
public Workflow Map(WorkflowDefinition source)
{
var root = _activitySerializer.Deserialize(source.StringData!);
return new(
new WorkflowIdentity(source.DefinitionId, source.Version, source.Id),
new WorkflowPublication(source.IsLatest, source.IsPublished),
new WorkflowMetadata(source.Name, source.Description, source.CreatedAt),
source.Options,
root,
source.Variables,
source.CustomProperties);
}
/// <summary>
/// Maps a <see cref="WorkflowDefinition"/> to a <see cref="Workflow"/>.
/// </summary>
/// <param name="workflowDefinition">The source <see cref="WorkflowDefinition"/>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The mapped <see cref="Workflow"/>.</returns>
public async Task<WorkflowDefinitionModel> MapAsync(WorkflowDefinition workflowDefinition, CancellationToken cancellationToken = default)
{
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken);
var variables = _variableDefinitionMapper.Map(workflow.Variables).ToList();
return new(
workflowDefinition.Id,
workflowDefinition.DefinitionId,
workflowDefinition.Name,
workflowDefinition.Description,
workflowDefinition.CreatedAt,
workflowDefinition.Version,
variables,
workflowDefinition.Inputs,
workflowDefinition.Outputs,
workflowDefinition.Outcomes,
workflowDefinition.CustomProperties,
workflowDefinition.UsableAsActivity,
workflowDefinition.IsLatest,
workflowDefinition.IsPublished,
workflow.Options,
workflow.Root);
}
}

View file

@ -1,7 +1,7 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Mappers;
namespace Elsa.Workflows.Management.Materializers;
@ -10,7 +10,7 @@ namespace Elsa.Workflows.Management.Materializers;
/// </summary>
public class JsonWorkflowMaterializer : IWorkflowMaterializer
{
private readonly IActivitySerializer _activitySerializer;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
/// <summary>
/// The name of the materializer.
@ -23,9 +23,9 @@ public class JsonWorkflowMaterializer : IWorkflowMaterializer
/// <summary>
/// Initializes a new instance of the <see cref="JsonWorkflowMaterializer"/> class.
/// </summary>
public JsonWorkflowMaterializer(IActivitySerializer activitySerializer)
public JsonWorkflowMaterializer(WorkflowDefinitionMapper workflowDefinitionMapper)
{
_activitySerializer = activitySerializer;
_workflowDefinitionMapper = workflowDefinitionMapper;
}
/// <inheritdoc />
@ -35,17 +35,5 @@ public class JsonWorkflowMaterializer : IWorkflowMaterializer
return new ValueTask<Workflow>(workflow);
}
private Workflow ToWorkflow(WorkflowDefinition definition)
{
var root = _activitySerializer.Deserialize(definition.StringData!);
return new(
new WorkflowIdentity(definition.DefinitionId, definition.Version, definition.Id),
new WorkflowPublication(definition.IsLatest, definition.IsPublished),
new WorkflowMetadata(definition.Name, definition.Description, definition.CreatedAt),
definition.Options,
root,
definition.Variables,
definition.CustomProperties);
}
private Workflow ToWorkflow(WorkflowDefinition definition) => _workflowDefinitionMapper.Map(definition);
}

View file

@ -1,21 +1,18 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Management.Models
namespace Elsa.Workflows.Management.Models
{
/// <summary>
/// Represents a request to save a workflow definition.
/// </summary>
public class SaveWorkflowDefinitionRequest
{
public string? DefinitionId { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public ICollection<VariableDefinition>? Variables { get; set; }
public ICollection<InputDefinition>? Inputs { get; set; }
public ICollection<OutputDefinition>? Outputs { get; set; }
public ICollection<string>? Outcomes { get; set; }
public IDictionary<string, object>? CustomProperties { get; set; }
public IActivity? Root { get; set; }
public WorkflowOptions? Options { get; set; }
/// <summary>
/// The workflow definition to save.
/// </summary>
public WorkflowDefinitionModel Model { get; set; } = default!;
/// <summary>
/// Whether the workflow definition should be published.
/// </summary>
public bool Publish { get; set; }
public bool? UsableAsActivity { get; set; }
}
}

View file

@ -0,0 +1,66 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Management.Models;
/// <summary>
/// Represents a serializable workflow definition.
/// </summary>
/// <param name="Id"></param>
/// <param name="DefinitionId"></param>
/// <param name="Name"></param>
/// <param name="Description"></param>
/// <param name="CreatedAt"></param>
/// <param name="Version"></param>
/// <param name="Variables"></param>
/// <param name="Inputs"></param>
/// <param name="Outputs"></param>
/// <param name="Outcomes"></param>
/// <param name="CustomProperties"></param>
/// <param name="UsableAsActivity"></param>
/// <param name="IsLatest"></param>
/// <param name="IsPublished"></param>
/// <param name="Options">The type of <see cref="IWorkflowActivationStrategy"/> to apply when new instances are requested to be created.</param>
/// <param name="Root"></param>
[PublicAPI]
public record WorkflowDefinitionModel(
string Id,
string DefinitionId,
string? Name,
string? Description,
DateTimeOffset CreatedAt,
int Version,
ICollection<VariableDefinition>? Variables,
ICollection<InputDefinition>? Inputs,
ICollection<OutputDefinition>? Outputs,
ICollection<string>? Outcomes,
IDictionary<string, object>? CustomProperties,
bool? UsableAsActivity,
bool IsLatest,
bool IsPublished,
WorkflowOptions? Options,
IActivity? Root
)
{
/// <inheritdoc />
public WorkflowDefinitionModel() : this(
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!,
default!)
{
}
}

View file

@ -1,4 +1,5 @@
using Elsa.Common.Models;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Mappers;
@ -31,7 +32,8 @@ namespace Elsa.Workflows.Management.Services
/// <inheritdoc />
public async Task<WorkflowDefinition> ImportAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
{
var definitionId = request.DefinitionId;
var model = request.Model;
var definitionId = model.DefinitionId;
// Get a workflow draft version.
var draft = !string.IsNullOrWhiteSpace(definitionId)
@ -50,21 +52,21 @@ namespace Elsa.Workflows.Management.Services
}
// Update the draft with the received model.
var root = request.Root!;
var root = model.Root ?? new Sequence();
var stringData = _serializer.Serialize(root);
var variables = _variableDefinitionMapper.Map(request.Variables).ToList();
var variables = _variableDefinitionMapper.Map(model.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<string, object>();
draft.Name = model.Name?.Trim();
draft.Description = model.Description?.Trim();
draft.CustomProperties = model.CustomProperties ?? new Dictionary<string, object>();
draft.Variables = variables;
draft.Inputs = request.Inputs ?? new List<InputDefinition>();
draft.Outputs = request.Outputs ?? new List<OutputDefinition>();
draft.Outcomes = request.Outcomes ?? new List<string>();
draft.Options = request.Options;
draft.UsableAsActivity = request.UsableAsActivity;
draft.Inputs = model.Inputs ?? new List<InputDefinition>();
draft.Outputs = model.Outputs ?? new List<OutputDefinition>();
draft.Outcomes = model.Outcomes ?? new List<string>();
draft.Options = model.Options;
draft.UsableAsActivity = model.UsableAsActivity;
draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
return draft;

View file

@ -4,9 +4,8 @@ using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Contracts;
namespace Elsa.Workflows.Runtime.Services;
namespace Elsa.Workflows.Management.Services;
/// <inheritdoc />
public class WorkflowDefinitionService : IWorkflowDefinitionService
@ -14,7 +13,7 @@ public class WorkflowDefinitionService : IWorkflowDefinitionService
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
private readonly IActivityVisitor _activityVisitor;
private readonly IIdentityGraphService _identityGraphService;
private readonly IEnumerable<IWorkflowMaterializer> _materializers;
private readonly Func<IEnumerable<IWorkflowMaterializer>> _materializers;
/// <summary>
/// Constructor.
@ -23,7 +22,7 @@ public class WorkflowDefinitionService : IWorkflowDefinitionService
IWorkflowDefinitionStore workflowDefinitionStore,
IActivityVisitor activityVisitor,
IIdentityGraphService identityGraphService,
IEnumerable<IWorkflowMaterializer> materializers)
Func<IEnumerable<IWorkflowMaterializer>> materializers)
{
_workflowDefinitionStore = workflowDefinitionStore;
_activityVisitor = activityVisitor;
@ -34,7 +33,8 @@ public class WorkflowDefinitionService : IWorkflowDefinitionService
/// <inheritdoc />
public async Task<Workflow> MaterializeWorkflowAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
var provider = _materializers.FirstOrDefault(x => x.Name == definition.MaterializerName);
var materializers = _materializers();
var provider = materializers.FirstOrDefault(x => x.Name == definition.MaterializerName);
if (provider == null)
throw new Exception("Provider not found");

View file

@ -1,5 +1,4 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Models;
namespace Elsa.Workflows.Runtime.Contracts;
@ -8,9 +7,15 @@ namespace Elsa.Workflows.Runtime.Contracts;
/// </summary>
public interface IWorkflowDefinitionProvider
{
/// <summary>
/// Gets the name of the provider.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the workflow definitions.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The workflow definitions.</returns>
ValueTask<IEnumerable<WorkflowDefinitionResult>> GetWorkflowDefinitionsAsync(CancellationToken cancellationToken = default);
}
public record WorkflowDefinitionResult(WorkflowDefinition Definition, Workflow Workflow);
}

View file

@ -1,9 +1,19 @@
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Services;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Provides extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class DependencyInjectionExtensions
{
/// <summary>
/// Adds the <see cref="ClrWorkflowDefinitionProvider"/> 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>
public static IServiceCollection AddWorkflowDefinitionProvider<T>(this IServiceCollection services) where T : class, IWorkflowDefinitionProvider => services.AddSingleton<IWorkflowDefinitionProvider, T>();
}

View file

@ -6,7 +6,9 @@ using Elsa.Features.Services;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Notifications;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Management.Services;
using Elsa.Workflows.Runtime.ActivationValidators;
using Elsa.Workflows.Runtime.Commands;
using Elsa.Workflows.Runtime.Contracts;
@ -136,6 +138,10 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddSingleton<SynchronousTaskDispatcher>()
.AddSingleton<AsynchronousTaskDispatcher>()
.AddSingleton<IEventPublisher, EventPublisher>()
// Lazy services.
.AddSingleton<Func<IEnumerable<IWorkflowDefinitionProvider>>>(sp => sp.GetServices<IWorkflowDefinitionProvider>)
.AddSingleton<Func<IEnumerable<IWorkflowMaterializer>>>(sp => sp.GetServices<IWorkflowMaterializer>)
// Memory stores.
.AddMemoryStore<WorkflowState, MemoryWorkflowStateStore>()

View file

@ -13,7 +13,7 @@ namespace Elsa.Workflows.Runtime.HostedServices;
/// </summary>
public class PopulateWorkflowDefinitionStore : IHostedService
{
private readonly IEnumerable<IWorkflowDefinitionProvider> _workflowDefinitionProviders;
private readonly Func<IEnumerable<IWorkflowDefinitionProvider>> _workflowDefinitionProviders;
private readonly ITriggerIndexer _triggerIndexer;
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
@ -21,7 +21,7 @@ public class PopulateWorkflowDefinitionStore : IHostedService
/// Constructor.
/// </summary>
public PopulateWorkflowDefinitionStore(
IEnumerable<IWorkflowDefinitionProvider> workflowDefinitionProviders,
Func<IEnumerable<IWorkflowDefinitionProvider>> workflowDefinitionProviders,
ITriggerIndexer triggerIndexer,
IWorkflowDefinitionStore workflowDefinitionStore)
{
@ -33,7 +33,8 @@ public class PopulateWorkflowDefinitionStore : IHostedService
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
foreach (var provider in _workflowDefinitionProviders)
var providers = _workflowDefinitionProviders();
foreach (var provider in providers)
{
var results = await provider.GetWorkflowDefinitionsAsync(cancellationToken).AsTask().ToList();

View file

@ -0,0 +1,11 @@
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Entities;
namespace Elsa.Workflows.Runtime.Models;
/// <summary>
/// Represents a workflow definition and its workflow.
/// </summary>
/// <param name="Definition">The workflow definition.</param>
/// <param name="Workflow">The workflow materialized from its workflow definition.</param>
public record WorkflowDefinitionResult(WorkflowDefinition Definition, Workflow Workflow);

View file

@ -4,6 +4,7 @@ using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Features;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Options;
using Microsoft.Extensions.Options;

View file

@ -2,6 +2,7 @@ using Elsa.Common.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Middleware.Activities;
using Elsa.Workflows.Runtime.Models;

View file

@ -2,6 +2,7 @@ using Elsa.Common.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;

View file

@ -5,6 +5,7 @@ using Elsa.Extensions;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Comparers;
using Elsa.Workflows.Runtime.Contracts;

View file

@ -1,5 +1,6 @@
using Elsa.Common.Models;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Sinks.Contracts;
using Elsa.Workflows.Sinks.Models;

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.WorkflowProviders.FluentStorage\Elsa.WorkflowProviders.FluentStorage.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Workflows\HelloWorld.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,39 @@
using Elsa.Extensions;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
// Add Elsa services.
services.AddElsa(elsa => elsa
// Configure the workflow runtime.
.UseFluentStorageProvider()
// Expose API endpoints.
.UseWorkflowsApi()
// Configure identity so that we can create a default admin user.
.UseIdentity(identity =>
{
identity.UseAdminUserProvider();
identity.TokenOptions = options =>
{
options.SigningKey = "secret-token-signing-key";
options.AccessTokenLifetime = TimeSpan.FromDays(1);
};
})
// Use default authentication (JWT).
.UseDefaultAuthentication(auth => auth.UseAdminApiKey())
);
// Configure middleware pipeline.
var app = builder.Build();
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
// Configure the HTTP request pipeline.
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.Run();

View file

@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55253",
"sslPort": 44338
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5129",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7230;http://localhost:5129",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,28 @@
{
"id": "hello-world-v1",
"definitionId": "hello-world",
"name": "Hello World",
"isLatest": true,
"isPublished": true,
"version": 1,
"root": {
"id": "Flowchart1",
"type": "Elsa.Flowchart",
"activities": [
{
"id": "WriteLine1",
"type": "Elsa.WriteLine",
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello World!"
},
"memoryReference": {
"id": "WriteLine1:input-1"
}
}
}
]
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="HelloWorld.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
{
"definitionId": "11e2b350dae8485498c16d1ce3843829",
"name": "Hello World",
"root": {
"id": "Flowchart1",
"type": "Elsa.Flowchart",
"activities": [
{
"id": "WriteLine1",
"type": "Elsa.WriteLine",
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello World!"
},
"memoryReference": {
"id": "WriteLine1:input-1"
}
}
}
]
}
}

View file

@ -0,0 +1,33 @@
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Microsoft.Extensions.DependencyInjection;
// Setup service container.
var services = new ServiceCollection();
// Add Elsa services.
services.AddElsa();
// Build service container.
var serviceProvider = services.BuildServiceProvider();
// Populate registries. This is only necessary for applications that are not using hosted services.
await serviceProvider.PopulateRegistriesAsync();
// Import a workflow from a JSON file.
var workflowJson = await File.ReadAllTextAsync("HelloWorld.json");
// Get a serializer to deserialize the workflow.
var serializer = serviceProvider.GetRequiredService<IActivitySerializer>();
// Deserialize the workflow.
var workflow = serializer.Deserialize<Workflow>(workflowJson);
// Resolve a workflow runner to run the workflow.
var workflowRunner = serviceProvider.GetRequiredService<IWorkflowRunner>();
// Run the workflow.
await workflowRunner.RunAsync(workflow);

View file

@ -0,0 +1,5 @@
# JSON workflows
This sample demonstrates how to load a JSON workflow from a file and then execute it.
There is another sample in the `aspnet` folder that demonstrates the use of the `JsonWorkflowDefinitionProvider` that provides workflows to the system loaded from JSON files.