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)(() => inputValue)); + var input = workflowExecutionContext.Input; + + foreach (var inputEntry in input) + { + var inputPascalName = inputEntry.Key.Pascalize(); + var inputValue = inputEntry.Value; + engine.SetValue($"get{inputPascalName}", (Func)(() => inputValue)); + } + } + else + { + // We end up here when we are evaluating an expression during trigger indexing. + // Typically, a workflow definition might have variables declared, that we want to be able to access from JavaScript expressions. + foreach(var block in context.Memory.Blocks.Values) + { + if(block.Metadata is not VariableBlockMetadata variableBlockMetadata) + continue; + + var variable = variableBlockMetadata.Variable; + var variablePascaleName = variable.Name.Pascalize(); + engine.SetValue($"get{variablePascaleName}", (Func)(() => block.Value)); + } } } } \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Events.cs b/src/modules/Elsa.ProtoActor/Events.cs deleted file mode 100644 index 121f2d8f1..000000000 --- a/src/modules/Elsa.ProtoActor/Events.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Elsa.ProtoActor.Protos; - -namespace Elsa.ProtoActor; - -internal record BookmarksStored(ICollection Bookmarks); - -internal record BookmarksRemovedByWorkflow(string WorkflowInstanceId); \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Extensions/ClusterExtensions.cs b/src/modules/Elsa.ProtoActor/Extensions/ClusterExtensions.cs index bf4243941..2613d3fdc 100644 --- a/src/modules/Elsa.ProtoActor/Extensions/ClusterExtensions.cs +++ b/src/modules/Elsa.ProtoActor/Extensions/ClusterExtensions.cs @@ -9,5 +9,4 @@ internal static class ClusterExtensions { public static RunningWorkflowsGrainClient GetNamedRunningWorkflowsGrain(this Cluster cluster) => cluster.GetRunningWorkflowsGrain(nameof(RunningWorkflowsGrain)); public static WorkflowGrainClient GetNamedWorkflowGrain(this Cluster cluster, string workflowInstanceId) => cluster.GetWorkflowGrain($"{nameof(WorkflowGrain)}-{workflowInstanceId}"); - public static BookmarkGrainClient GetNamedBookmarkGrain(this Cluster cluster, string hash) => cluster.GetBookmarkGrain($"{nameof(BookmarkGrain)}-{hash}"); } \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.ProtoActor/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs new file mode 100644 index 000000000..0b127c4cd --- /dev/null +++ b/src/modules/Elsa.ProtoActor/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs @@ -0,0 +1,24 @@ +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Middleware.Workflows; +using Elsa.Workflows.Core.Pipelines.WorkflowExecution; +using Elsa.Workflows.Runtime.Middleware.Workflows; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Provides extensions to that add various middleware components. +/// +public static class WorkflowExecutionPipelineBuilderExtensions +{ + /// + /// Configures the workflow execution pipeline with commonly used components for Proto Actor. + /// + public static IWorkflowExecutionPipelineBuilder UseProtoActorRuntimePipeline(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => + pipelineBuilder + .Reset() + .UsePersistentVariables() + .UseBookmarkPersistence() + .UseWorkflowExecutionLogPersistence() + .UseDefaultActivityScheduler(); +} \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Extensions/WorkflowsFeatureExtensions.cs b/src/modules/Elsa.ProtoActor/Extensions/WorkflowsFeatureExtensions.cs new file mode 100644 index 000000000..66875f11f --- /dev/null +++ b/src/modules/Elsa.ProtoActor/Extensions/WorkflowsFeatureExtensions.cs @@ -0,0 +1,18 @@ +using Elsa.Workflows.Core.Features; +using Elsa.Workflows.Core.Middleware.Activities; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Adds an extension method to the that installs a default workflow runtime execution pipeline. +/// +public static class WorkflowsFeatureExtensions +{ + /// + /// Installs a default workflow runtime execution pipeline. + /// + public static WorkflowsFeature WithProtoActorRuntimeWorkflowExecutionPipeline(this WorkflowsFeature workflowsFeature) => + workflowsFeature.WithWorkflowExecutionPipeline(pipeline => + pipeline.UseProtoActorRuntimePipeline()); +} \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Features/ProtoActorFeature.cs b/src/modules/Elsa.ProtoActor/Features/ProtoActorFeature.cs index 2d4c434d5..c33a290c9 100644 --- a/src/modules/Elsa.ProtoActor/Features/ProtoActorFeature.cs +++ b/src/modules/Elsa.ProtoActor/Features/ProtoActorFeature.cs @@ -1,3 +1,4 @@ +using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; @@ -5,6 +6,7 @@ using Elsa.ProtoActor.Grains; using Elsa.ProtoActor.HostedServices; using Elsa.ProtoActor.Protos; using Elsa.ProtoActor.Services; +using Elsa.Workflows.Core.Features; using Elsa.Workflows.Runtime.Features; using Google.Protobuf.WellKnownTypes; using Microsoft.Extensions.DependencyInjection; @@ -23,6 +25,7 @@ namespace Elsa.ProtoActor.Features; /// /// Installs the Proto Actor feature to host & execute workflow instances. /// +[DependsOn(typeof(WorkflowsFeature))] [DependsOn(typeof(WorkflowRuntimeFeature))] public class ProtoActorFeature : FeatureBase { @@ -34,6 +37,9 @@ public class ProtoActorFeature : FeatureBase /// public override void Configure() { + // Configure default workflow execution pipeline suitable for Proto Actor. + Module.UseWorkflows(workflows => workflows.WithProtoActorRuntimeWorkflowExecutionPipeline()); + // Configure runtime with ProtoActor workflow runtime. Module.Configure().WorkflowRuntime = sp => ActivatorUtilities.CreateInstance(sp); } @@ -89,7 +95,6 @@ public class ProtoActorFeature : FeatureBase var clusterProvider = ClusterProvider(sp); var system = new ActorSystem(systemConfig).WithServiceProvider(sp); var workflowGrainProps = system.DI().PropsFor(); - var bookmarkGrainProps = system.DI().PropsFor(); var workflowRegistryGrainProps = system.DI().PropsFor(); var clusterConfig = ClusterConfig @@ -99,7 +104,6 @@ public class ProtoActorFeature : FeatureBase .WithActorActivationTimeout(TimeSpan.FromHours(1)) .WithActorSpawnVerificationTimeout(TimeSpan.FromHours(1)) .WithClusterKind(WorkflowGrainActor.Kind, workflowGrainProps) - .WithClusterKind(BookmarkGrainActor.Kind, bookmarkGrainProps) .WithClusterKind(RunningWorkflowsGrainActor.Kind, workflowRegistryGrainProps) ; @@ -130,7 +134,6 @@ public class ProtoActorFeature : FeatureBase // Actors. services .AddTransient(sp => new WorkflowGrainActor((context, _) => ActivatorUtilities.CreateInstance(sp, context))) - .AddTransient(sp => new BookmarkGrainActor((context, _) => ActivatorUtilities.CreateInstance(sp, context))) .AddTransient(sp => new RunningWorkflowsGrainActor((context, _) => ActivatorUtilities.CreateInstance(sp, context))) ; } diff --git a/src/modules/Elsa.ProtoActor/Grains/BookmarkGrain.cs b/src/modules/Elsa.ProtoActor/Grains/BookmarkGrain.cs deleted file mode 100644 index 4a3a78107..000000000 --- a/src/modules/Elsa.ProtoActor/Grains/BookmarkGrain.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Elsa.Extensions; -using Elsa.ProtoActor.Extensions; -using Elsa.ProtoActor.Protos; -using Google.Protobuf.WellKnownTypes; -using Proto; -using Proto.Cluster; -using Proto.Persistence; -using Proto.Persistence.SnapshotStrategies; - -namespace Elsa.ProtoActor.Grains; - -/// -/// Represents a registry of bookmarks. -/// -public class BookmarkGrain : BookmarkGrainBase -{ - private const int EventsPerSnapshot = 100; - private ICollection _bookmarks = new List(); - private readonly Persistence _persistence; - - /// - public BookmarkGrain(IProvider provider, IContext context) : base(context) - { - _persistence = Persistence.WithEventSourcingAndSnapshotting( - provider, - provider, - BookmarkHash, - ApplyEvent, - ApplySnapshot, - new IntervalStrategy(EventsPerSnapshot), - GetState); - } - - private string BookmarkHash => Context.ClusterIdentity()!.Identity; - - /// - public override async Task OnStarted() => await _persistence.RecoverStateAsync(); - - /// - public override async Task Store(StoreBookmarksRequest request) - { - var bookmarks = request.BookmarkIds.Select(x => new StoredBookmark - { - WorkflowInstanceId = request.WorkflowInstanceId, - CorrelationId = request.CorrelationId, - BookmarkId = x - }).ToList(); - - await _persistence.PersistRollingEventAsync(new BookmarksStored(bookmarks), EventsPerSnapshot); - return new Empty(); - } - - /// - public override async Task RemoveByWorkflow(RemoveBookmarksByWorkflowRequest request) - { - await _persistence.PersistRollingEventAsync(new BookmarksRemovedByWorkflow(request.WorkflowInstanceId), EventsPerSnapshot); - - return new Empty(); - } - - /// - public override Task Resolve(ResolveBookmarksRequest request) - { - var response = new ResolveBookmarksResponse(); - var query = _bookmarks.AsQueryable(); - - if (!string.IsNullOrEmpty(request.CorrelationId)) - query = query.Where(x => x.CorrelationId == request.CorrelationId); - - response.Bookmarks.AddRange(query); - - return Task.FromResult(response); - } - - private void ApplySnapshot(Snapshot snapshot) - { - var bookmarkSnapshot = (BookmarkSnapshot)snapshot.State; - _bookmarks = bookmarkSnapshot.Bookmarks; - } - - private void ApplyEvent(Event @event) - { - switch (@event.Data) - { - case BookmarksStored bookmarksStored: - _bookmarks.AddRange(bookmarksStored.Bookmarks); - break; - case BookmarksRemovedByWorkflow bookmarksRemovedByWorkflow: - _bookmarks.RemoveWhere(x => x.WorkflowInstanceId == bookmarksRemovedByWorkflow.WorkflowInstanceId); - break; - } - } - - private object GetState() => new BookmarkSnapshot(_bookmarks); -} \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Protos/Grains.proto b/src/modules/Elsa.ProtoActor/Protos/Grains.proto index cd10b1ba9..a684f42af 100644 --- a/src/modules/Elsa.ProtoActor/Protos/Grains.proto +++ b/src/modules/Elsa.ProtoActor/Protos/Grains.proto @@ -13,12 +13,6 @@ service WorkflowGrain { rpc ImportState(ImportWorkflowStateRequest) returns (ImportWorkflowStateResponse); } -service BookmarkGrain { - rpc Store(StoreBookmarksRequest) returns (google.protobuf.Empty); - rpc RemoveByWorkflow(RemoveBookmarksByWorkflowRequest) returns (google.protobuf.Empty); - rpc Resolve (ResolveBookmarksRequest) returns (ResolveBookmarksResponse); -} - service RunningWorkflowsGrain { rpc Register(RegisterRunningWorkflowRequest) returns (google.protobuf.Empty); rpc Unregister(UnregisterRunningWorkflowRequest) returns (google.protobuf.Empty); diff --git a/src/modules/Elsa.ProtoActor/Protos/Messages.proto b/src/modules/Elsa.ProtoActor/Protos/Messages.proto index 53e52efa3..23f7901a8 100644 --- a/src/modules/Elsa.ProtoActor/Protos/Messages.proto +++ b/src/modules/Elsa.ProtoActor/Protos/Messages.proto @@ -76,32 +76,6 @@ message BookmarkDto { optional string CallbackMethodName = 8; } -// BookmarkGrain. -message StoreBookmarksRequest { - string WorkflowInstanceId = 1; - string CorrelationId = 2; - repeated string BookmarkIds = 3; -} - -message RemoveBookmarksByWorkflowRequest { - string WorkflowInstanceId = 1; -} - -message ResolveBookmarksRequest { - string ActivityTypeName = 1; - string CorrelationId = 2; -} - -message ResolveBookmarksResponse { - repeated StoredBookmark Bookmarks = 1; -} - -message StoredBookmark { - string WorkflowInstanceId = 1; - string CorrelationId = 2; - string BookmarkId = 3; -} - // RunningWorkflowsGrain. message RegisterRunningWorkflowRequest { string DefinitionId = 1; diff --git a/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs index 6f7e3bcda..26135929f 100644 --- a/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs @@ -6,6 +6,7 @@ using Elsa.Workflows.Core.Contracts; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.State; using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Entities; using Proto.Cluster; namespace Elsa.ProtoActor.Services; @@ -18,6 +19,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime private readonly Cluster _cluster; private readonly IWorkflowStateSerializer _workflowStateSerializer; private readonly ITriggerStore _triggerStore; + private readonly IBookmarkStore _bookmarkStore; private readonly IIdentityGenerator _identityGenerator; private readonly IBookmarkHasher _hasher; private readonly IWorkflowDefinitionService _workflowDefinitionService; @@ -30,6 +32,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime Cluster cluster, IWorkflowStateSerializer workflowStateSerializer, ITriggerStore triggerStore, + IBookmarkStore bookmarkStore, IIdentityGenerator identityGenerator, IBookmarkHasher hasher, IWorkflowDefinitionService workflowDefinitionService, @@ -38,6 +41,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime _cluster = cluster; _workflowStateSerializer = workflowStateSerializer; _triggerStore = triggerStore; + _bookmarkStore = bookmarkStore; _identityGenerator = identityGenerator; _hasher = hasher; _workflowDefinitionService = workflowDefinitionService; @@ -154,19 +158,13 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsRuntimeOptions options, CancellationToken cancellationToken = default) { var hash = _hasher.Hash(activityTypeName, bookmarkPayload); - var client = _cluster.GetNamedBookmarkGrain(hash); - - var request = new ResolveBookmarksRequest - { - ActivityTypeName = activityTypeName, - CorrelationId = options.CorrelationId.EmptyIfNull(), - }; - - var bookmarksResponse = await client.Resolve(request, cancellationToken); - var bookmarks = bookmarksResponse!.Bookmarks; - return await ResumeWorkflowsAsync(bookmarks, new ResumeWorkflowRuntimeOptions(options.CorrelationId, options.WorkflowInstanceId, Input: options.Input), cancellationToken); + var correlationId = options.CorrelationId; + var workflowInstanceId = options.WorkflowInstanceId; + var filter = new BookmarkFilter { Hash = hash, CorrelationId = correlationId, WorkflowInstanceId = workflowInstanceId }; + var bookmarks = await _bookmarkStore.FindManyAsync(filter, cancellationToken); + return await ResumeWorkflowsAsync(bookmarks, new ResumeWorkflowRuntimeOptions(correlationId, Input: options.Input), cancellationToken); } - + /// public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsRuntimeOptions options, CancellationToken cancellationToken = default) { @@ -243,18 +241,9 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime } /// - public async Task UpdateBookmarkAsync(Workflows.Runtime.Entities.StoredBookmark bookmark, CancellationToken cancellationToken = default) + public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) { - var bookmarkClient = _cluster.GetNamedBookmarkGrain(bookmark.Hash); - - var storeBookmarkRequest = new StoreBookmarksRequest - { - WorkflowInstanceId = bookmark.WorkflowInstanceId, - CorrelationId = bookmark.CorrelationId.EmptyIfNull() - }; - - storeBookmarkRequest.BookmarkIds.Add(bookmark.BookmarkId); - await bookmarkClient.Store(storeBookmarkRequest, cancellationToken); + await _bookmarkStore.SaveAsync(bookmark, cancellationToken); } /// @@ -292,36 +281,21 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime return resumedWorkflows; } - private async Task StoreBookmarksAsync(string instanceId, ICollection bookmarks, string? correlationId, CancellationToken cancellationToken = default) + private async Task StoreBookmarksAsync(string workflowInstanceId, ICollection bookmarks, string? correlationId, CancellationToken cancellationToken = default) { - var groupedBookmarks = bookmarks.GroupBy(x => x.Hash); - - foreach (var groupedBookmark in groupedBookmarks) + foreach (var bookmark in bookmarks) { - var bookmarkClient = _cluster.GetNamedBookmarkGrain(groupedBookmark.Key); - - var storeBookmarkRequest = new StoreBookmarksRequest - { - WorkflowInstanceId = instanceId, - CorrelationId = correlationId.EmptyIfNull() - }; - - storeBookmarkRequest.BookmarkIds.AddRange(groupedBookmark.Select(x => x.Id)); - await bookmarkClient.Store(storeBookmarkRequest, cancellationToken); + var storedBookmark = new StoredBookmark(bookmark.Name, bookmark.Hash, workflowInstanceId, bookmark.Id, correlationId, bookmark.Payload); + await _bookmarkStore.SaveAsync(storedBookmark, cancellationToken); } } - private async Task RemoveBookmarksAsync(string instanceId, IEnumerable bookmarks, CancellationToken cancellationToken = default) + private async Task RemoveBookmarksAsync(string workflowInstanceId, IEnumerable bookmarks, CancellationToken cancellationToken = default) { - var groupedBookmarks = bookmarks.GroupBy(x => x.Hash); - - foreach (var groupedBookmark in groupedBookmarks) + foreach (var bookmark in bookmarks) { - var bookmarkClient = _cluster.GetNamedBookmarkGrain(groupedBookmark.Key); - await bookmarkClient.RemoveByWorkflow(new RemoveBookmarksByWorkflowRequest - { - WorkflowInstanceId = instanceId - }, cancellationToken); + var filter = new BookmarkFilter { Hash = bookmark.Hash, WorkflowInstanceId = workflowInstanceId }; + await _bookmarkStore.DeleteAsync(filter, cancellationToken); } } @@ -364,18 +338,11 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime private async Task> FindResumableWorkflowsAsync(WorkflowsFilter workflowsFilter, CancellationToken cancellationToken) { var hash = _hasher.Hash(workflowsFilter.ActivityTypeName, workflowsFilter.BookmarkPayload); - var client = _cluster.GetNamedBookmarkGrain(hash); - - var request = new ResolveBookmarksRequest - { - ActivityTypeName = workflowsFilter.ActivityTypeName, - CorrelationId = workflowsFilter.Options.CorrelationId.EmptyIfNull() - }; - - var bookmarksResponse = await client.Resolve(request, cancellationToken); - var bookmarks = bookmarksResponse!.Bookmarks; - - var collectedWorkflows = bookmarks.Select(b => new ResumableWorkflowMatch(b.WorkflowInstanceId, default, workflowsFilter.Options.CorrelationId, b.BookmarkId)).ToList(); + var correlationId = workflowsFilter.Options.CorrelationId; + var workflowInstanceId = workflowsFilter.Options.WorkflowInstanceId; + var filter = new BookmarkFilter { Hash = hash, CorrelationId = correlationId, WorkflowInstanceId = workflowInstanceId }; + var bookmarks = await _bookmarkStore.FindManyAsync(filter, cancellationToken); + var collectedWorkflows = bookmarks.Select(b => new ResumableWorkflowMatch(b.WorkflowInstanceId, default, correlationId, b.BookmarkId)).ToList(); return collectedWorkflows; } } \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Snapshots.cs b/src/modules/Elsa.ProtoActor/Snapshots.cs index e89d4da6c..48be1401a 100644 --- a/src/modules/Elsa.ProtoActor/Snapshots.cs +++ b/src/modules/Elsa.ProtoActor/Snapshots.cs @@ -6,6 +6,4 @@ namespace Elsa.ProtoActor; internal record WorkflowSnapshot(string DefinitionId, string InstanceId, int Version, WorkflowState WorkflowState, IDictionary? Input); -internal record BookmarkSnapshot(ICollection Bookmarks); - internal record WorkflowRegistrySnapshot(ICollection Entries); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index 11a5bbb3e..b9634bdf8 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -27,6 +27,9 @@ public static class ExpressionExecutionContextExtensions [InputKey] = input }; + public static bool TryGetWorkflowExecutionContext(this ExpressionExecutionContext context, out WorkflowExecutionContext workflowExecutionContext) => + context.TransientProperties.TryGetValue(WorkflowExecutionContextKey, out workflowExecutionContext!); + public static WorkflowExecutionContext GetWorkflowExecutionContext(this ExpressionExecutionContext context) => (WorkflowExecutionContext)context.TransientProperties[WorkflowExecutionContextKey]; public static ActivityExecutionContext GetActivityExecutionContext(this ExpressionExecutionContext context) => (ActivityExecutionContext)context.TransientProperties[ActivityExecutionContextKey]; public static bool TryGetActivityExecutionContext(this ExpressionExecutionContext context, out ActivityExecutionContext activityExecutionContext) => context.TransientProperties.TryGetValue(ActivityExecutionContextKey, out activityExecutionContext!); diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs index 2400f4459..cead0a2b0 100644 --- a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs +++ b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs @@ -6,13 +6,38 @@ using Elsa.Workflows.Management.Models; namespace Elsa.Workflows.Management.Contracts; +/// +/// 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); +} + +/// +/// A filter for querying workflow instances. +/// public class WorkflowInstanceFilter { public string? Id { get; set; } public ICollection? Ids { get; set; } public string? SearchTerm { get; set; } public string? DefinitionId { get; set; } + public string? DefinitionVersionId { get; set; } public ICollection? DefinitionIds { get; set; } + public ICollection? DefinitionVersionIds { get; set; } public int? Version { get; set; } public string? CorrelationId { get; set; } public ICollection? CorrelationIds { get; set; } @@ -25,7 +50,9 @@ public class WorkflowInstanceFilter if (!string.IsNullOrWhiteSpace(filter.Id)) query = query.Where(x => x.Id == filter.Id); if (filter.Ids != null) query = query.Where(x => filter.Ids.Contains(x.Id)); if (!string.IsNullOrWhiteSpace(filter.DefinitionId)) query = query.Where(x => x.DefinitionId == filter.DefinitionId); + if (!string.IsNullOrWhiteSpace(filter.DefinitionVersionId)) query = query.Where(x => x.DefinitionVersionId == filter.DefinitionVersionId); if (filter.DefinitionIds != null) query = query.Where(x => filter.DefinitionIds.Contains(x.DefinitionId)); + if (filter.DefinitionVersionIds != null) query = query.Where(x => filter.DefinitionVersionIds.Contains(x.DefinitionVersionId)); if (filter.Version != null) query = query.Where(x => x.Version == filter.Version); if (!string.IsNullOrWhiteSpace(filter.CorrelationId)) query = query.Where(x => x.CorrelationId == filter.CorrelationId); if (filter.CorrelationIds != null) query = query.Where(x => filter.CorrelationIds.Contains(x.CorrelationId!)); @@ -53,24 +80,4 @@ public class WorkflowInstanceFilter /// 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=" + }] + } +}