diff --git a/Elsa.sln b/Elsa.sln
index 4711db729..851dc810a 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -172,6 +172,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.QuartzIntegrat
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.HangfireIntegration", "src\samples\aspnet\Elsa.Samples.HangfireIntegration\Elsa.Samples.HangfireIntegration.csproj", "{D2614FC7-102F-4F78-BB06-7C87304A10BA}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.ProtoActorRuntime", "src\samples\aspnet\Elsa.Samples.ProtoActorRuntime\Elsa.Samples.ProtoActorRuntime.csproj", "{A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -426,6 +428,10 @@ Global
{D2614FC7-102F-4F78-BB06-7C87304A10BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2614FC7-102F-4F78-BB06-7C87304A10BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2614FC7-102F-4F78-BB06-7C87304A10BA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@@ -501,5 +507,6 @@ Global
{1FCB2200-28B8-4703-8E89-73241AAED047} = {89608AA5-5ADE-4832-AC7B-871C4AE64210}
{B0312D9E-FA30-43E9-B666-40A8782D6E1C} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{D2614FC7-102F-4F78-BB06-7C87304A10BA} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
+ {A41E24C6-B16D-4C6A-A9A9-2C5AF424F20F} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
EndGlobalSection
EndGlobal
diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs
index 8b2623019..12dd27e8e 100644
--- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs
+++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs
@@ -70,9 +70,13 @@ app.MapHealthChecks("/");
app.UseAuthentication();
app.UseAuthorization();
-// Register Elsa middleware.
+// Elsa API endpoints for designer.
app.UseWorkflowsApi();
+
+// Captures unhandled exceptions and returns a JSON response.
app.UseJsonSerializationErrorHandler();
+
+// Elsa HTTP Endpoint activities
app.UseWorkflows();
// Run.
diff --git a/src/common/Elsa.Features/Abstractions/FeatureBase.cs b/src/common/Elsa.Features/Abstractions/FeatureBase.cs
index df354e23f..141ab359e 100644
--- a/src/common/Elsa.Features/Abstractions/FeatureBase.cs
+++ b/src/common/Elsa.Features/Abstractions/FeatureBase.cs
@@ -1,5 +1,6 @@
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
namespace Elsa.Features.Abstractions;
@@ -46,4 +47,14 @@ public abstract class FeatureBase : IFeature
public virtual void Apply()
{
}
+
+ ///
+ /// Configures the specified hosted service using an optional priority to control in which order it will be registered with the service container.
+ ///
+ /// The priority.
+ /// The type of hosted service to configure.
+ protected void ConfigureHostedService(int priority = 0) where T : class, IHostedService
+ {
+ Module.ConfigureHostedService(priority);
+ }
}
\ No newline at end of file
diff --git a/src/common/Elsa.Features/Implementations/Module.cs b/src/common/Elsa.Features/Implementations/Module.cs
index ce15f82ed..6c15a34d3 100644
--- a/src/common/Elsa.Features/Implementations/Module.cs
+++ b/src/common/Elsa.Features/Implementations/Module.cs
@@ -48,7 +48,7 @@ public class Module : IModule
}
///
- public IModule ConfigureHostedService(int priority = 0)
+ public IModule ConfigureHostedService(int priority = 0) where T : class, IHostedService
{
_hostedServiceDescriptors.Add(new HostedServiceDescriptor(priority, typeof(T)));
return this;
diff --git a/src/common/Elsa.Features/Services/IModule.cs b/src/common/Elsa.Features/Services/IModule.cs
index 0bee045f7..94be83a9c 100644
--- a/src/common/Elsa.Features/Services/IModule.cs
+++ b/src/common/Elsa.Features/Services/IModule.cs
@@ -31,7 +31,7 @@ public interface IModule
///
/// Configures a using an optional priority to control in which order it will be registered with the service container.
///
- IModule ConfigureHostedService(int priority = 0);
+ IModule ConfigureHostedService(int priority = 0) where T : class, IHostedService;
///
/// Will apply all configured features, causing the collection to be populated.
diff --git a/src/modules/Elsa.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs b/src/modules/Elsa.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs
index 6516fe8d7..8b0fc1416 100644
--- a/src/modules/Elsa.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs
+++ b/src/modules/Elsa.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs
@@ -118,9 +118,15 @@ public class ElasticWorkflowInstanceStore : IWorkflowInstanceStore
private static QueryDescriptor Filter(QueryDescriptor descriptor, WorkflowInstanceFilter filter)
{
- // TODO: Implement remaining filters.
-
+ if (!string.IsNullOrWhiteSpace(filter.Id)) descriptor = descriptor.Match(m => m.Field(f => f.Id).Query(filter.Id));
if (!string.IsNullOrWhiteSpace(filter.DefinitionId)) descriptor = descriptor.Match(m => m.Field(f => f.DefinitionId).Query(filter.DefinitionId));
+ if (!string.IsNullOrWhiteSpace(filter.DefinitionVersionId)) descriptor = descriptor.Match(m => m.Field(f => f.DefinitionVersionId).Query(filter.DefinitionVersionId));
+
+ // TODO: filter by IDs
+ // TODO: filter by DefinitionIDs
+ // TODO: filter by DefinitionVersionIDs
+ // TODO: filter by CorrelationIDs
+
if (filter.Version != null) descriptor = descriptor.Match(m => m.Field(f => f.Version).Query(filter.Version.ToString()!));
if (!string.IsNullOrWhiteSpace(filter.CorrelationId)) descriptor = descriptor.Match(m => m.Field(f => f.CorrelationId).Query(filter.CorrelationId));
if (filter.WorkflowStatus != null) descriptor = descriptor.Match(m => m.Field(f => f.Status).Query(filter.WorkflowStatus.ToString()!));
diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs
index b5d34079e..3b10929cd 100644
--- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs
@@ -5,7 +5,7 @@ namespace Elsa.EntityFrameworkCore.Extensions;
public static partial class Extensions
{
- public static EFCoreDefaultRuntimePersistenceFeature UsePostgreSql(this EFCoreDefaultRuntimePersistenceFeature feature, string connectionString)
+ public static EFCoreDefaultWorkflowRuntimePersistenceFeature UsePostgreSql(this EFCoreDefaultWorkflowRuntimePersistenceFeature feature, string connectionString)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(connectionString);
return feature;
diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Modules/Runtime/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Modules/Runtime/Extensions.cs
index ae481aede..24d01fe28 100644
--- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Modules/Runtime/Extensions.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Modules/Runtime/Extensions.cs
@@ -5,7 +5,7 @@ namespace Elsa.EntityFrameworkCore.Extensions;
public static partial class Extensions
{
- public static EFCoreDefaultRuntimePersistenceFeature UseSqlServer(this EFCoreDefaultRuntimePersistenceFeature feature, string connectionString)
+ public static EFCoreDefaultWorkflowRuntimePersistenceFeature UseSqlServer(this EFCoreDefaultWorkflowRuntimePersistenceFeature feature, string connectionString)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlServer(connectionString);
return feature;
diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Runtime/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Runtime/Extensions.cs
index 4db325a5c..db3f0e5f1 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Runtime/Extensions.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Runtime/Extensions.cs
@@ -6,7 +6,13 @@ namespace Elsa.EntityFrameworkCore.Extensions;
public static partial class Extensions
{
- public static EFCoreDefaultRuntimePersistenceFeature UseSqlite(this EFCoreDefaultRuntimePersistenceFeature feature, string connectionString = Constants.DefaultConnectionString)
+ public static EFCoreWorkflowRuntimePersistenceFeature UseSqlite(this EFCoreWorkflowRuntimePersistenceFeature feature, string connectionString = Constants.DefaultConnectionString)
+ {
+ feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlite(connectionString);
+ return feature;
+
+ }
+ public static EFCoreDefaultWorkflowRuntimePersistenceFeature UseSqlite(this EFCoreDefaultWorkflowRuntimePersistenceFeature feature, string connectionString = Constants.DefaultConnectionString)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlite(connectionString);
return feature;
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs
index 334e09b82..251284eda 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs
@@ -15,7 +15,7 @@ namespace Elsa.EntityFrameworkCore.Modules.Management;
public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
{
private readonly EntityStore _store;
- private readonly EntityStore _workflowInstanceStore;
+ private readonly IWorkflowInstanceStore _workflowInstanceStore;
private readonly IActivitySerializer _serializer;
///
@@ -23,7 +23,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
///
public EFCoreWorkflowDefinitionStore(
EntityStore store,
- EntityStore workflowInstanceStore,
+ IWorkflowInstanceStore workflowInstanceStore,
IActivitySerializer serializer)
{
_store = store;
@@ -130,7 +130,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
var set = dbContext.WorkflowDefinitions;
var queryable = set.AsQueryable();
var ids = await Filter(queryable, filter).Select(x => x.Id).Distinct().ToListAsync(cancellationToken);
- await _workflowInstanceStore.DeleteWhereAsync(x => ids.Contains(x.DefinitionVersionId), cancellationToken);
+ await _workflowInstanceStore.DeleteManyAsync(new WorkflowInstanceFilter { DefinitionVersionIds = ids }, cancellationToken);
return await _store.DeleteWhereAsync(x => ids.Contains(x.Id), cancellationToken);
}
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs
index 41c3ac044..d1188a726 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs
@@ -21,10 +21,7 @@ public class EFCoreWorkflowInstancePersistenceFeature : PersistenceFeatureBase
public override void Configure()
{
- Module.Configure(feature =>
- {
- feature.WorkflowInstanceStore = sp => sp.GetRequiredService();
- });
+ Module.Configure(feature => feature.WorkflowInstanceStore = sp => sp.GetRequiredService());
}
///
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultWorkflowRuntimePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultWorkflowRuntimePersistenceFeature.cs
new file mode 100644
index 000000000..445f3fb8d
--- /dev/null
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultWorkflowRuntimePersistenceFeature.cs
@@ -0,0 +1,44 @@
+using Elsa.EntityFrameworkCore.Common;
+using Elsa.Features.Attributes;
+using Elsa.Features.Services;
+using Elsa.Workflows.Core.State;
+using Elsa.Workflows.Runtime.Entities;
+using Elsa.Workflows.Runtime.Features;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.EntityFrameworkCore.Modules.Runtime;
+
+///
+/// Configures the default workflow runtime to use EF Core persistence providers.
+///
+[DependsOn(typeof(WorkflowRuntimeFeature))]
+[DependsOn(typeof(DefaultWorkflowRuntimeFeature))]
+public class EFCoreDefaultWorkflowRuntimePersistenceFeature : PersistenceFeatureBase
+{
+ ///
+ public EFCoreDefaultWorkflowRuntimePersistenceFeature(IModule module) : base(module)
+ {
+ }
+
+ ///
+ public override void Configure()
+ {
+ Module.Configure(feature =>
+ {
+ feature.WorkflowTriggerStore = sp => sp.GetRequiredService();
+ feature.BookmarkStore = sp => sp.GetRequiredService();
+ });
+
+ Module.Configure(feature => { feature.WorkflowStateStore = sp => sp.GetRequiredService(); });
+ }
+
+ ///
+ public override void Apply()
+ {
+ base.Apply();
+
+ AddEntityStore();
+ AddEntityStore();
+ AddStore();
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Extensions.cs
index 40beb7b49..f36eb4c63 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Extensions.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Extensions.cs
@@ -8,9 +8,18 @@ namespace Elsa.EntityFrameworkCore.Modules.Runtime;
public static class Extensions
{
///
- /// Configures the to use the .
+ /// Configures the to use the .
///
- public static DefaultRuntimeFeature UseEntityFrameworkCore(this DefaultRuntimeFeature feature, Action? configure = default)
+ public static WorkflowRuntimeFeature UseEntityFrameworkCore(this WorkflowRuntimeFeature feature, Action? configure = default)
+ {
+ feature.Module.Configure(configure);
+ return feature;
+ }
+
+ ///
+ /// Configures the to use the .
+ ///
+ public static DefaultWorkflowRuntimeFeature UseEntityFrameworkCore(this DefaultWorkflowRuntimeFeature feature, Action? configure = default)
{
feature.Module.Configure(configure);
return feature;
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultRuntimePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs
similarity index 70%
rename from src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultRuntimePersistenceFeature.cs
rename to src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs
index 2722da16b..d9ea70cd2 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultRuntimePersistenceFeature.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs
@@ -8,28 +8,32 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.EntityFrameworkCore.Modules.Runtime;
+///
+/// Configures the default workflow runtime to use EF Core persistence providers.
+///
[DependsOn(typeof(WorkflowRuntimeFeature))]
-public class EFCoreDefaultRuntimePersistenceFeature : PersistenceFeatureBase
+public class EFCoreWorkflowRuntimePersistenceFeature : PersistenceFeatureBase
{
- public EFCoreDefaultRuntimePersistenceFeature(IModule module) : base(module)
+ ///
+ public EFCoreWorkflowRuntimePersistenceFeature(IModule module) : base(module)
{
}
+ ///
public override void Configure()
{
Module.Configure(feature =>
{
- feature.WorkflowStateStore = sp => sp.GetRequiredService();
feature.WorkflowTriggerStore = sp => sp.GetRequiredService();
feature.BookmarkStore = sp => sp.GetRequiredService();
});
}
+ ///
public override void Apply()
{
base.Apply();
-
- AddEntityStore();
+
AddEntityStore();
AddStore();
}
diff --git a/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs b/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs
index be297ce40..96371587f 100644
--- a/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs
+++ b/src/modules/Elsa.Http/Extensions/RouteTableExtensions.cs
@@ -22,6 +22,15 @@ public static class RouteTableExtensions
routeTable.AddRange(paths);
}
+ ///
+ /// Adds routes from the specified set of bookmarks.
+ ///
+ public static void AddRoutes(this IRouteTable routeTable, IEnumerable bookmarks)
+ {
+ var paths = Filter(bookmarks).Select(x => x.GetPayload().Path).ToList();
+ routeTable.AddRange(paths);
+ }
+
///
/// Adds routes from the specified set of bookmarks.
///
@@ -49,6 +58,21 @@ public static class RouteTableExtensions
routeTable.RemoveRange(paths);
}
- private static IEnumerable Filter(IEnumerable triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName() && x.Payload != null);
- private static IEnumerable Filter(IEnumerable triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName() && x.Payload != null);
+ private static IEnumerable Filter(IEnumerable triggers)
+ {
+ var triggerName = ActivityTypeNameHelper.GenerateTypeName();
+ return triggers.Where(x => x.Name == triggerName && x.Payload != null);
+ }
+
+ private static IEnumerable Filter(IEnumerable bookmarks)
+ {
+ var activityTypeName = ActivityTypeNameHelper.GenerateTypeName();
+ return bookmarks.Where(x => x.ActivityTypeName == activityTypeName && x.Payload != null);
+ }
+
+ private static IEnumerable Filter(IEnumerable bookmarks)
+ {
+ var bookmarkName = ActivityTypeNameHelper.GenerateTypeName();
+ return bookmarks.Where(x => x.Name == bookmarkName && x.Payload != null);
+ }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs
index 28217c205..46bc6bff8 100644
--- a/src/modules/Elsa.Http/Features/HttpFeature.cs
+++ b/src/modules/Elsa.Http/Features/HttpFeature.cs
@@ -6,6 +6,7 @@ using Elsa.Features.Services;
using Elsa.Http.ContentWriters;
using Elsa.Http.Contracts;
using Elsa.Http.Handlers;
+using Elsa.Http.HostedServices;
using Elsa.Http.Models;
using Elsa.Http.Options;
using Elsa.Http.Parsers;
@@ -77,6 +78,12 @@ public class HttpFeature : FeatureBase
});
}
+ ///
+ public override void ConfigureHostedServices()
+ {
+ ConfigureHostedService();
+ }
+
///
public override void Apply()
{
diff --git a/src/modules/Elsa.Http/Handlers/UpdateRouteTable.cs b/src/modules/Elsa.Http/Handlers/UpdateRouteTable.cs
index 44e098a38..9f47ae2e6 100644
--- a/src/modules/Elsa.Http/Handlers/UpdateRouteTable.cs
+++ b/src/modules/Elsa.Http/Handlers/UpdateRouteTable.cs
@@ -6,7 +6,7 @@ using Elsa.Workflows.Runtime.Notifications;
namespace Elsa.Http.Handlers;
///
-/// A handler that updates the route table.
+/// A handler that updates the route table when workflow triggers and bookmarks are indexed.
///
public class UpdateRouteTable :
INotificationHandler,
diff --git a/src/modules/Elsa.Http/HostedServices/UpdateRouteTableHostedService.cs b/src/modules/Elsa.Http/HostedServices/UpdateRouteTableHostedService.cs
new file mode 100644
index 000000000..058d17912
--- /dev/null
+++ b/src/modules/Elsa.Http/HostedServices/UpdateRouteTableHostedService.cs
@@ -0,0 +1,40 @@
+using Elsa.Extensions;
+using Elsa.Http.Contracts;
+using Elsa.Workflows.Core.Helpers;
+using Elsa.Workflows.Runtime.Contracts;
+using Microsoft.Extensions.Hosting;
+
+namespace Elsa.Http.HostedServices;
+
+///
+/// Update the route table based on workflow triggers and bookmarks.
+///
+public class UpdateRouteTableHostedService : BackgroundService
+{
+ private readonly IRouteTable _routeTable;
+ private readonly ITriggerStore _triggerStore;
+ private readonly IBookmarkStore _bookmarkStore;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UpdateRouteTableHostedService(IRouteTable routeTable, ITriggerStore triggerStore, IBookmarkStore bookmarkStore)
+ {
+ _routeTable = routeTable;
+ _triggerStore = triggerStore;
+ _bookmarkStore = bookmarkStore;
+ }
+
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var bookmarkName = ActivityTypeNameHelper.GenerateTypeName();
+ var triggerFilter = new TriggerFilter { Name = bookmarkName };
+ var bookmarkFilter = new BookmarkFilter { ActivityTypeName = bookmarkName};
+ var triggers = (await _triggerStore.FindManyAsync(triggerFilter, stoppingToken)).ToList();
+ var bookmarks = (await _bookmarkStore.FindManyAsync(bookmarkFilter, stoppingToken)).ToList();
+
+ _routeTable.AddRoutes(triggers);
+ _routeTable.AddRoutes(bookmarks);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs b/src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs
index 230874e9d..f759536f6 100644
--- a/src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs
+++ b/src/modules/Elsa.JavaScript/Handlers/WorkflowDefinitionActivityJavaScriptHandler.cs
@@ -4,6 +4,7 @@ using Elsa.Extensions;
using Elsa.JavaScript.Notifications;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Core.Contracts;
+using Elsa.Workflows.Core.Models;
using Humanizer;
using JetBrains.Annotations;
using Jint;
@@ -32,7 +33,7 @@ public class WorkflowDefinitionActivityJavaScriptHandler : INotificationHandler<
var engine = notification.Engine;
var context = notification.Context;
- // Always create workflow input accessors.
+ // Create workflow input accessors.
CreateWorkflowInputAccessors(engine, context);
return Task.CompletedTask;
@@ -40,13 +41,30 @@ public class WorkflowDefinitionActivityJavaScriptHandler : INotificationHandler<
private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context)
{
- var input = context.GetWorkflowExecutionContext().Input;
-
- foreach (var inputEntry in input)
+ if(context.TryGetWorkflowExecutionContext(out var workflowExecutionContext))
{
- var inputPascalName = inputEntry.Key.Pascalize();
- var inputValue = inputEntry.Value;
- engine.SetValue($"get{inputPascalName}", (Func
public class WorkflowInstanceOrder : OrderDefinition
{
-}
-
-///
-/// Represents a store of workflow instances.
-///
-public interface IWorkflowInstanceStore
-{
- Task FindAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
- Task> FindManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default);
- Task> FindManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, WorkflowInstanceOrder order, CancellationToken cancellationToken = default);
- Task> FindManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
- Task> FindManyAsync(WorkflowInstanceFilter filter, WorkflowInstanceOrder order, CancellationToken cancellationToken = default);
- Task> SummarizeManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default);
- Task> SummarizeManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, WorkflowInstanceOrder order, CancellationToken cancellationToken = default);
- Task> SummarizeManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
- Task> SummarizeManyAsync(WorkflowInstanceFilter filter, WorkflowInstanceOrder order, CancellationToken cancellationToken = default);
- Task SaveAsync(WorkflowInstance record, CancellationToken cancellationToken = default);
- Task SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default);
- Task DeleteAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
- Task DeleteManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/ITriggerIndexer.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/ITriggerIndexer.cs
index ade4ebadf..05f5d314f 100644
--- a/src/modules/Elsa.Workflows.Runtime/Contracts/ITriggerIndexer.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Contracts/ITriggerIndexer.cs
@@ -4,6 +4,9 @@ using Elsa.Workflows.Runtime.Models.Notifications;
namespace Elsa.Workflows.Runtime.Contracts;
+///
+/// Extracts triggers from workflow definitions.
+///
public interface ITriggerIndexer
{
///
@@ -20,9 +23,4 @@ public interface ITriggerIndexer
/// Indexes triggers of the specified workflow.
///
Task IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default);
-
- ///
- /// Send message of all triggers in the trigger store to populate the routetable on startup
- ///
- Task IndexAllTriggersAsync(CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs
index f98a15e58..27b99ae68 100644
--- a/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs
@@ -28,18 +28,36 @@ public static class ModuleExtensions
return module;
}
- public static WorkflowRuntimeFeature UseDefaultRuntime(this WorkflowRuntimeFeature feature, Action? configure = default)
+ ///
+ /// Configures the default workflow runtime.
+ ///
+ /// The workflow runtime feature.
+ /// A callback that configures the default workflow runtime.
+ /// The workflow runtime feature.
+ public static WorkflowRuntimeFeature UseDefaultRuntime(this WorkflowRuntimeFeature feature, Action? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
+ ///
+ /// Configures the execution log records feature.
+ ///
+ /// The workflow runtime feature.
+ /// A callback that configures the execution log records feature.
+ /// The workflow runtime feature.
public static WorkflowRuntimeFeature UseExecutionLogRecords(this WorkflowRuntimeFeature feature, Action? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
+ ///
+ /// Configures the workflow state exporter feature.
+ ///
+ /// The workflow runtime feature.
+ /// A callback that configures the workflow state exporter feature.
+ /// The workflow runtime feature.
public static WorkflowRuntimeFeature UseAsyncWorkflowStateExporter(this WorkflowRuntimeFeature feature, Action? configure = default)
{
feature.Module.Configure(configure);
diff --git a/src/modules/Elsa.Workflows.Runtime/Features/DefaultRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/DefaultRuntimeFeature.cs
deleted file mode 100644
index 8ebf4bf26..000000000
--- a/src/modules/Elsa.Workflows.Runtime/Features/DefaultRuntimeFeature.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Elsa.Features.Abstractions;
-using Elsa.Features.Services;
-
-namespace Elsa.Workflows.Runtime.Features;
-
-public class DefaultRuntimeFeature : FeatureBase
-{
- public DefaultRuntimeFeature(IModule module) : base(module)
- {
- }
-}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Features/DefaultWorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/DefaultWorkflowRuntimeFeature.cs
new file mode 100644
index 000000000..e22c9e1d3
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Runtime/Features/DefaultWorkflowRuntimeFeature.cs
@@ -0,0 +1,38 @@
+using Elsa.Extensions;
+using Elsa.Features.Abstractions;
+using Elsa.Features.Attributes;
+using Elsa.Features.Services;
+using Elsa.Workflows.Core.State;
+using Elsa.Workflows.Runtime.Contracts;
+using Elsa.Workflows.Runtime.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Workflows.Runtime.Features;
+
+///
+/// Installs the default runtime services.
+///
+[DependsOn(typeof(WorkflowRuntimeFeature))]
+public class DefaultWorkflowRuntimeFeature : FeatureBase
+{
+ ///
+ public DefaultWorkflowRuntimeFeature(IModule module) : base(module)
+ {
+ }
+
+ ///
+ /// A factory that instantiates an .
+ ///
+ public Func WorkflowStateStore { get; set; } = sp => ActivatorUtilities.CreateInstance(sp);
+
+ ///
+ public override void Apply()
+ {
+ Services
+ // Replaceable factories
+ .AddSingleton(WorkflowStateStore)
+
+ // Memory stores.
+ .AddMemoryStore();
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs
index 4c4b66913..dcd81e421 100644
--- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs
@@ -47,12 +47,7 @@ public class WorkflowRuntimeFeature : FeatureBase
/// A factory that instantiates an .
///
public Func WorkflowDispatcher { get; set; } = sp => ActivatorUtilities.CreateInstance(sp);
-
- ///
- /// A factory that instantiates an .
- ///
- public Func WorkflowStateStore { get; set; } = sp => ActivatorUtilities.CreateInstance(sp);
-
+
///
/// A factory that instantiates an .
///
@@ -114,8 +109,7 @@ public class WorkflowRuntimeFeature : FeatureBase
Module
.ConfigureHostedService()
.ConfigureHostedService()
- .ConfigureHostedService()
- .ConfigureHostedService();
+ .ConfigureHostedService();
///
public override void Apply()
@@ -133,7 +127,6 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddSingleton()
.AddSingleton(WorkflowRuntime)
.AddSingleton(WorkflowDispatcher)
- .AddSingleton(WorkflowStateStore)
.AddSingleton(BookmarkStore)
.AddSingleton(WorkflowTriggerStore)
.AddSingleton(WorkflowExecutionLogStore)
diff --git a/src/modules/Elsa.Workflows.Runtime/HostedServices/PopulateRouteTable.cs b/src/modules/Elsa.Workflows.Runtime/HostedServices/PopulateRouteTable.cs
deleted file mode 100644
index 65b788e20..000000000
--- a/src/modules/Elsa.Workflows.Runtime/HostedServices/PopulateRouteTable.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Elsa.Workflows.Runtime.Contracts;
-using Microsoft.Extensions.Hosting;
-
-namespace Elsa.Workflows.Runtime.HostedServices;
-
-///
-/// Synchronously updates the routetable from the triggers.
-///
-public class PopulateRouteTable : IHostedService
-{
- private readonly ITriggerIndexer _triggerIndexer;
-
- ///
- /// Constructor.
- ///
- public PopulateRouteTable(ITriggerIndexer triggerIndexer)
- {
- _triggerIndexer = triggerIndexer;
- }
-
- ///
- public async Task StartAsync(CancellationToken cancellationToken)
- {
- await _triggerIndexer.IndexAllTriggersAsync(cancellationToken);
- }
-
- ///
- public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs b/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs
index 2631d4da1..e4ba533dc 100644
--- a/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs
@@ -62,27 +62,6 @@ public class TriggerIndexer : ITriggerIndexer
return await IndexTriggersAsync(workflow, cancellationToken);
}
-
- ///
- public async Task IndexAllTriggersAsync(CancellationToken cancellationToken = default)
- {
- var emptyTriggerList = new List(0);
-
- // Get current triggers
- var filter = new TriggerFilter();
- var allTriggersTriggers = await _triggerStore.FindManyAsync(filter, cancellationToken);
-
-
- //workflow definition already deleted so you do not have one
- var workflow = new Workflow();
-
- var indexedWorkflow = new IndexedWorkflowTriggers(workflow, allTriggersTriggers.ToList(), emptyTriggerList, emptyTriggerList);
-
- // Publish event.
- await _eventPublisher.PublishAsync(new WorkflowTriggersIndexed(indexedWorkflow), cancellationToken);
- return indexedWorkflow;
- }
-
///
public async Task IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default)
{
diff --git a/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Dockerfile b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Dockerfile
new file mode 100644
index 000000000..c92287e3d
--- /dev/null
+++ b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Dockerfile
@@ -0,0 +1,20 @@
+FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
+WORKDIR /app
+EXPOSE 80
+EXPOSE 443
+
+FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
+WORKDIR /src
+COPY ["src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Elsa.Samples.ProtoActorRuntime.csproj", "src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/"]
+RUN dotnet restore "src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Elsa.Samples.ProtoActorRuntime.csproj"
+COPY . .
+WORKDIR "/src/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime"
+RUN dotnet build "Elsa.Samples.ProtoActorRuntime.csproj" -c Release -o /app/build
+
+FROM build AS publish
+RUN dotnet publish "Elsa.Samples.ProtoActorRuntime.csproj" -c Release -o /app/publish
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "Elsa.Samples.ProtoActorRuntime.dll"]
diff --git a/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Elsa.Samples.ProtoActorRuntime.csproj b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Elsa.Samples.ProtoActorRuntime.csproj
new file mode 100644
index 000000000..12360941d
--- /dev/null
+++ b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Elsa.Samples.ProtoActorRuntime.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net7.0
+ enable
+ enable
+ Linux
+
+
+
+
+ .dockerignore
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Program.cs b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Program.cs
new file mode 100644
index 000000000..15a48ec25
--- /dev/null
+++ b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Program.cs
@@ -0,0 +1,91 @@
+using Elsa.EntityFrameworkCore.Extensions;
+using Elsa.EntityFrameworkCore.Modules.Labels;
+using Elsa.EntityFrameworkCore.Modules.Management;
+using Elsa.EntityFrameworkCore.Modules.Runtime;
+using Elsa.Extensions;
+using Elsa.JavaScript.Options;
+using Microsoft.Data.Sqlite;
+using Proto.Persistence.Sqlite;
+
+var builder = WebApplication.CreateBuilder(args);
+var services = builder.Services;
+var configuration = builder.Configuration;
+var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
+var identitySection = configuration.GetSection("Identity");
+var identityTokenSection = identitySection.GetSection("Tokens");
+
+// Add Elsa services.
+services
+ .AddElsa(elsa => elsa
+ .AddActivitiesFrom()
+ .UseIdentity(identity =>
+ {
+ identity.IdentityOptions = options => identitySection.Bind(options);
+ identity.TokenOptions = options => identityTokenSection.Bind(options);
+ identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
+ identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
+ identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
+ })
+ .UseDefaultAuthentication()
+ .UseWorkflowManagement(management =>
+ {
+ // Use EF core for workflow definitions and instances.
+ management.UseEntityFrameworkCore(m => m.UseSqlite(sqliteConnectionString));
+ })
+ .UseWorkflowRuntime(runtime =>
+ {
+ // Use EF core for triggers and bookmarks.
+ runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
+
+ // Use EF core for execution log records.
+ runtime.UseExecutionLogRecords(log => log.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)));
+
+ // Install a workflow state exporter to capture workflow states and store them in IWorkflowInstanceStore.
+ runtime.UseAsyncWorkflowStateExporter();
+
+ // Use Proto.Actor for workflow execution.
+ runtime.UseProtoActor(protoActor =>
+ {
+ protoActor.PersistenceProvider = _ => new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString));
+ });
+ })
+ .UseLabels(labels => labels.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
+ .UseScheduling()
+ .UseWorkflowsApi(api => api.AddFastEndpointsAssembly())
+ .UseJavaScript()
+ .UseLiquid()
+ .UseHttp()
+ );
+
+services.Configure(options => options.AllowClrAccess = true);
+services.AddHandlersFrom();
+services.AddHealthChecks();
+services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
+services.AddHttpContextAccessor();
+
+// Configure middleware pipeline.
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+ app.UseDeveloperExceptionPage();
+
+// CORS.
+app.UseCors();
+
+// Health checks.
+app.MapHealthChecks("/");
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+// Elsa API endpoints for designer.
+app.UseWorkflowsApi();
+
+// Captures unhandled exceptions and returns a JSON response.
+app.UseJsonSerializationErrorHandler();
+
+// Elsa HTTP Endpoint activities
+app.UseWorkflows();
+
+// Run.
+app.Run();
\ No newline at end of file
diff --git a/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Properties/launchSettings.json b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Properties/launchSettings.json
new file mode 100644
index 000000000..d6054c056
--- /dev/null
+++ b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/Properties/launchSettings.json
@@ -0,0 +1,37 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:54982",
+ "sslPort": 44376
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5117",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7180;http://localhost:5117",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/README.md b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/README.md
new file mode 100644
index 000000000..1a514869e
--- /dev/null
+++ b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/README.md
@@ -0,0 +1,10 @@
+# Server
+
+This project represents an Elsa application that hosts workflows and exposes API endpoints to manage & execute workflows.
+
+## Secrets
+The following are the secrets stored in hashed form in appsettings.json:
+
+**API key**: `4E753976726458745954355043687772-e54d5a2c-33a3-4c05-a216-b09569062aed`
+**Admin user**: `admin`
+**Admin password**: `password`
\ No newline at end of file
diff --git a/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/appsettings.json b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/appsettings.json
new file mode 100644
index 000000000..f5baa3a1a
--- /dev/null
+++ b/src/samples/aspnet/Elsa.Samples.ProtoActorRuntime/appsettings.json
@@ -0,0 +1,52 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Elsa.Mediator": "Warning",
+ "MassTransit": "Warning",
+ "Microsoft.Extensions.Http": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "Microsoft.AspNetCore": "Warning",
+ "System.Net.Http": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;"
+ },
+ "Identity": {
+ "Tokens": {
+ "SigningKey": "secret-signing-key",
+ "AccessTokenLifetime": "1:00:00:00",
+ "RefreshTokenLifetime": "1:00:10:00"
+ },
+ "Roles": [{
+ "Id": "admin",
+ "Name": "Administrator",
+ "Permissions": ["*"]
+ }],
+ "Users": [
+ {
+ "Id": "a2323f46-42db-4e15-af8b-94238717d817",
+ "Name": "admin",
+ "HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=",
+ "HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=",
+ "Roles": ["admin"]
+ }
+ ],
+ "Applications": [{
+ "id": "529572c2df854b13807b8bf23f1784cd",
+ "name": "Postman",
+ "roles": [
+ "admin"
+ ],
+ "clientId": "Nu9vrdXtYT5PChwr",
+ "clientSecret": "011pp2C$|j01-qrMZpC9VC0F00XCJq(5",
+ "hashedApiKey": "d0rDld3A+ugKmdctGtMzOLTYjQFkOlUWN+kt0VyW9D0=",
+ "hashedApiKeySalt": "EnutGOyy5MuJWV0fF5jCQiciK7a8PU/DRF+fr6nekSY=",
+ "hashedClientSecret": "ERia2zBcCSWb/9dvB0grQ9yf7fWgFrClNeR8A5RMTzk=",
+ "hashedClientSecretSalt": "z3z8KmzHt+xkAj/zYTXcB8I7y0xAkLm95v4Er/oNqiY="
+ }]
+ }
+}