Simplify Proto.Actor bookmarks and triggers (#3917)

* Refactor HTTP route table population

* Update workflow definition EF store to use abstract IWorkflowInstanceStore

This decoupling is necessary, because the application might not be using EF Core, but e.g. Elasticsearch

* Move workflow state persistence to the default workflow runtime feature

* Update JS handler to provide access to variables during trigger indexing

* Configure Proto.Actor runtime with different workflow pipeline

* Add sample using Proto.Actor runtime

* Replace BookmarkGrain with DB based persistence
This commit is contained in:
Sipke Schoorstra 2023-04-17 15:23:05 +02:00 committed by GitHub
parent 998fafa75c
commit c8d87cd763
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 615 additions and 326 deletions

View file

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

View file

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

View file

@ -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()
{
}
/// <summary>
/// Configures the specified hosted service using an optional priority to control in which order it will be registered with the service container.
/// </summary>
/// <param name="priority">The priority.</param>
/// <typeparam name="T">The type of hosted service to configure.</typeparam>
protected void ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService
{
Module.ConfigureHostedService<T>(priority);
}
}

View file

@ -48,7 +48,7 @@ public class Module : IModule
}
/// <inheritdoc />
public IModule ConfigureHostedService<T>(int priority = 0)
public IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService
{
_hostedServiceDescriptors.Add(new HostedServiceDescriptor(priority, typeof(T)));
return this;

View file

@ -31,7 +31,7 @@ public interface IModule
/// <summary>
/// Configures a <see cref="IHostedService"/> using an optional priority to control in which order it will be registered with the service container.
/// </summary>
IModule ConfigureHostedService<T>(int priority = 0);
IModule ConfigureHostedService<T>(int priority = 0) where T : class, IHostedService;
/// <summary>
/// Will apply all configured features, causing the <see cref="Services"/> collection to be populated.

View file

@ -118,9 +118,15 @@ public class ElasticWorkflowInstanceStore : IWorkflowInstanceStore
private static QueryDescriptor<WorkflowInstance> Filter(QueryDescriptor<WorkflowInstance> 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()!));

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ namespace Elsa.EntityFrameworkCore.Modules.Management;
public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
{
private readonly EntityStore<ManagementElsaDbContext, WorkflowDefinition> _store;
private readonly EntityStore<ManagementElsaDbContext, WorkflowInstance> _workflowInstanceStore;
private readonly IWorkflowInstanceStore _workflowInstanceStore;
private readonly IActivitySerializer _serializer;
/// <summary>
@ -23,7 +23,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
/// </summary>
public EFCoreWorkflowDefinitionStore(
EntityStore<ManagementElsaDbContext, WorkflowDefinition> store,
EntityStore<ManagementElsaDbContext, WorkflowInstance> 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);
}

View file

@ -21,10 +21,7 @@ public class EFCoreWorkflowInstancePersistenceFeature : PersistenceFeatureBase<M
/// <inheritdoc />
public override void Configure()
{
Module.Configure<WorkflowInstancesFeature>(feature =>
{
feature.WorkflowInstanceStore = sp => sp.GetRequiredService<EFCoreWorkflowInstanceStore>();
});
Module.Configure<WorkflowInstancesFeature>(feature => feature.WorkflowInstanceStore = sp => sp.GetRequiredService<EFCoreWorkflowInstanceStore>());
}
/// <inheritdoc />

View file

@ -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;
/// <summary>
/// Configures the default workflow runtime to use EF Core persistence providers.
/// </summary>
[DependsOn(typeof(WorkflowRuntimeFeature))]
[DependsOn(typeof(DefaultWorkflowRuntimeFeature))]
public class EFCoreDefaultWorkflowRuntimePersistenceFeature : PersistenceFeatureBase<RuntimeElsaDbContext>
{
/// <inheritdoc />
public EFCoreDefaultWorkflowRuntimePersistenceFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Configure()
{
Module.Configure<WorkflowRuntimeFeature>(feature =>
{
feature.WorkflowTriggerStore = sp => sp.GetRequiredService<EFCoreTriggerStore>();
feature.BookmarkStore = sp => sp.GetRequiredService<EFCoreBookmarkStore>();
});
Module.Configure<DefaultWorkflowRuntimeFeature>(feature => { feature.WorkflowStateStore = sp => sp.GetRequiredService<EFCoreWorkflowStateStore>(); });
}
/// <inheritdoc />
public override void Apply()
{
base.Apply();
AddEntityStore<WorkflowState, EFCoreWorkflowStateStore>();
AddEntityStore<StoredTrigger, EFCoreTriggerStore>();
AddStore<StoredBookmark, EFCoreBookmarkStore>();
}
}

View file

@ -8,9 +8,18 @@ namespace Elsa.EntityFrameworkCore.Modules.Runtime;
public static class Extensions
{
/// <summary>
/// Configures the <see cref="DefaultRuntimeFeature"/> to use the <see cref="EFCoreDefaultRuntimePersistenceFeature"/>.
/// Configures the <see cref="WorkflowRuntimeFeature"/> to use the <see cref="EFCoreWorkflowRuntimePersistenceFeature"/>.
/// </summary>
public static DefaultRuntimeFeature UseEntityFrameworkCore(this DefaultRuntimeFeature feature, Action<EFCoreDefaultRuntimePersistenceFeature>? configure = default)
public static WorkflowRuntimeFeature UseEntityFrameworkCore(this WorkflowRuntimeFeature feature, Action<EFCoreWorkflowRuntimePersistenceFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Configures the <see cref="DefaultWorkflowRuntimeFeature"/> to use the <see cref="EFCoreDefaultWorkflowRuntimePersistenceFeature"/>.
/// </summary>
public static DefaultWorkflowRuntimeFeature UseEntityFrameworkCore(this DefaultWorkflowRuntimeFeature feature, Action<EFCoreDefaultWorkflowRuntimePersistenceFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;

View file

@ -8,28 +8,32 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.EntityFrameworkCore.Modules.Runtime;
/// <summary>
/// Configures the default workflow runtime to use EF Core persistence providers.
/// </summary>
[DependsOn(typeof(WorkflowRuntimeFeature))]
public class EFCoreDefaultRuntimePersistenceFeature : PersistenceFeatureBase<RuntimeElsaDbContext>
public class EFCoreWorkflowRuntimePersistenceFeature : PersistenceFeatureBase<RuntimeElsaDbContext>
{
public EFCoreDefaultRuntimePersistenceFeature(IModule module) : base(module)
/// <inheritdoc />
public EFCoreWorkflowRuntimePersistenceFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Configure()
{
Module.Configure<WorkflowRuntimeFeature>(feature =>
{
feature.WorkflowStateStore = sp => sp.GetRequiredService<EFCoreWorkflowStateStore>();
feature.WorkflowTriggerStore = sp => sp.GetRequiredService<EFCoreTriggerStore>();
feature.BookmarkStore = sp => sp.GetRequiredService<EFCoreBookmarkStore>();
});
}
/// <inheritdoc />
public override void Apply()
{
base.Apply();
AddEntityStore<WorkflowState, EFCoreWorkflowStateStore>();
AddEntityStore<StoredTrigger, EFCoreTriggerStore>();
AddStore<StoredBookmark, EFCoreBookmarkStore>();
}

View file

@ -22,6 +22,15 @@ public static class RouteTableExtensions
routeTable.AddRange(paths);
}
/// <summary>
/// Adds routes from the specified set of bookmarks.
/// </summary>
public static void AddRoutes(this IRouteTable routeTable, IEnumerable<StoredBookmark> bookmarks)
{
var paths = Filter(bookmarks).Select(x => x.GetPayload<HttpEndpointBookmarkPayload>().Path).ToList();
routeTable.AddRange(paths);
}
/// <summary>
/// Adds routes from the specified set of bookmarks.
/// </summary>
@ -49,6 +58,21 @@ public static class RouteTableExtensions
routeTable.RemoveRange(paths);
}
private static IEnumerable<StoredTrigger> Filter(IEnumerable<StoredTrigger> triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>() && x.Payload != null);
private static IEnumerable<Bookmark> Filter(IEnumerable<Bookmark> triggers) => triggers.Where(x => x.Name == ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>() && x.Payload != null);
private static IEnumerable<StoredTrigger> Filter(IEnumerable<StoredTrigger> triggers)
{
var triggerName = ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>();
return triggers.Where(x => x.Name == triggerName && x.Payload != null);
}
private static IEnumerable<StoredBookmark> Filter(IEnumerable<StoredBookmark> bookmarks)
{
var activityTypeName = ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>();
return bookmarks.Where(x => x.ActivityTypeName == activityTypeName && x.Payload != null);
}
private static IEnumerable<Bookmark> Filter(IEnumerable<Bookmark> bookmarks)
{
var bookmarkName = ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>();
return bookmarks.Where(x => x.Name == bookmarkName && x.Payload != null);
}
}

View file

@ -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
});
}
/// <inheritdoc />
public override void ConfigureHostedServices()
{
ConfigureHostedService<UpdateRouteTableHostedService>();
}
/// <inheritdoc />
public override void Apply()
{

View file

@ -6,7 +6,7 @@ using Elsa.Workflows.Runtime.Notifications;
namespace Elsa.Http.Handlers;
/// <summary>
/// A handler that updates the route table.
/// A handler that updates the route table when workflow triggers and bookmarks are indexed.
/// </summary>
public class UpdateRouteTable :
INotificationHandler<WorkflowTriggersIndexed>,

View file

@ -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;
/// <summary>
/// Update the route table based on workflow triggers and bookmarks.
/// </summary>
public class UpdateRouteTableHostedService : BackgroundService
{
private readonly IRouteTable _routeTable;
private readonly ITriggerStore _triggerStore;
private readonly IBookmarkStore _bookmarkStore;
/// <summary>
/// Initializes a new instance of the <see cref="UpdateRouteTableHostedService"/> class.
/// </summary>
public UpdateRouteTableHostedService(IRouteTable routeTable, ITriggerStore triggerStore, IBookmarkStore bookmarkStore)
{
_routeTable = routeTable;
_triggerStore = triggerStore;
_bookmarkStore = bookmarkStore;
}
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var bookmarkName = ActivityTypeNameHelper.GenerateTypeName<HttpEndpoint>();
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);
}
}

View file

@ -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<object?>)(() => inputValue));
var input = workflowExecutionContext.Input;
foreach (var inputEntry in input)
{
var inputPascalName = inputEntry.Key.Pascalize();
var inputValue = inputEntry.Value;
engine.SetValue($"get{inputPascalName}", (Func<object?>)(() => 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<object?>)(() => block.Value));
}
}
}
}

View file

@ -1,7 +0,0 @@
using Elsa.ProtoActor.Protos;
namespace Elsa.ProtoActor;
internal record BookmarksStored(ICollection<StoredBookmark> Bookmarks);
internal record BookmarksRemovedByWorkflow(string WorkflowInstanceId);

View file

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

View file

@ -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;
/// <summary>
/// Provides extensions to <see cref="IWorkflowExecutionPipelineBuilder"/> that add various middleware components.
/// </summary>
public static class WorkflowExecutionPipelineBuilderExtensions
{
/// <summary>
/// Configures the workflow execution pipeline with commonly used components for Proto Actor.
/// </summary>
public static IWorkflowExecutionPipelineBuilder UseProtoActorRuntimePipeline(this IWorkflowExecutionPipelineBuilder pipelineBuilder) =>
pipelineBuilder
.Reset()
.UsePersistentVariables()
.UseBookmarkPersistence()
.UseWorkflowExecutionLogPersistence()
.UseDefaultActivityScheduler();
}

View file

@ -0,0 +1,18 @@
using Elsa.Workflows.Core.Features;
using Elsa.Workflows.Core.Middleware.Activities;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds an extension method to the <see cref="WorkflowsFeature"/> that installs a default workflow runtime execution pipeline.
/// </summary>
public static class WorkflowsFeatureExtensions
{
/// <summary>
/// Installs a default workflow runtime execution pipeline.
/// </summary>
public static WorkflowsFeature WithProtoActorRuntimeWorkflowExecutionPipeline(this WorkflowsFeature workflowsFeature) =>
workflowsFeature.WithWorkflowExecutionPipeline(pipeline =>
pipeline.UseProtoActorRuntimePipeline());
}

View file

@ -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;
/// <summary>
/// Installs the Proto Actor feature to host &amp; execute workflow instances.
/// </summary>
[DependsOn(typeof(WorkflowsFeature))]
[DependsOn(typeof(WorkflowRuntimeFeature))]
public class ProtoActorFeature : FeatureBase
{
@ -34,6 +37,9 @@ public class ProtoActorFeature : FeatureBase
/// <inheritdoc />
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<WorkflowRuntimeFeature>().WorkflowRuntime = sp => ActivatorUtilities.CreateInstance<ProtoActorWorkflowRuntime>(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<WorkflowGrainActor>();
var bookmarkGrainProps = system.DI().PropsFor<BookmarkGrainActor>();
var workflowRegistryGrainProps = system.DI().PropsFor<RunningWorkflowsGrainActor>();
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<WorkflowGrain>(sp, context)))
.AddTransient(sp => new BookmarkGrainActor((context, _) => ActivatorUtilities.CreateInstance<BookmarkGrain>(sp, context)))
.AddTransient(sp => new RunningWorkflowsGrainActor((context, _) => ActivatorUtilities.CreateInstance<RunningWorkflowsGrain>(sp, context)))
;
}

View file

@ -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;
/// <summary>
/// Represents a registry of bookmarks.
/// </summary>
public class BookmarkGrain : BookmarkGrainBase
{
private const int EventsPerSnapshot = 100;
private ICollection<StoredBookmark> _bookmarks = new List<StoredBookmark>();
private readonly Persistence _persistence;
/// <inheritdoc />
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;
/// <inheritdoc />
public override async Task OnStarted() => await _persistence.RecoverStateAsync();
/// <inheritdoc />
public override async Task<Empty> 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();
}
/// <inheritdoc />
public override async Task<Empty> RemoveByWorkflow(RemoveBookmarksByWorkflowRequest request)
{
await _persistence.PersistRollingEventAsync(new BookmarksRemovedByWorkflow(request.WorkflowInstanceId), EventsPerSnapshot);
return new Empty();
}
/// <inheritdoc />
public override Task<ResolveBookmarksResponse> 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);
}

View file

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

View file

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

View file

@ -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<ICollection<WorkflowExecutionResult>> 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);
}
/// <inheritdoc />
public async Task<TriggerWorkflowsResult> TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsRuntimeOptions options, CancellationToken cancellationToken = default)
{
@ -243,18 +241,9 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -292,36 +281,21 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
return resumedWorkflows;
}
private async Task StoreBookmarksAsync(string instanceId, ICollection<Bookmark> bookmarks, string? correlationId, CancellationToken cancellationToken = default)
private async Task StoreBookmarksAsync(string workflowInstanceId, ICollection<Bookmark> 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<Bookmark> bookmarks, CancellationToken cancellationToken = default)
private async Task RemoveBookmarksAsync(string workflowInstanceId, IEnumerable<Bookmark> 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<IEnumerable<WorkflowMatch>> 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;
}
}

View file

@ -6,6 +6,4 @@ namespace Elsa.ProtoActor;
internal record WorkflowSnapshot(string DefinitionId, string InstanceId, int Version, WorkflowState WorkflowState, IDictionary<string, object>? Input);
internal record BookmarkSnapshot(ICollection<StoredBookmark> Bookmarks);
internal record WorkflowRegistrySnapshot(ICollection<WorkflowInstanceEntry> Entries);

View file

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

View file

@ -6,13 +6,38 @@ using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Contracts;
/// <summary>
/// Represents a store of workflow instances.
/// </summary>
public interface IWorkflowInstanceStore
{
Task<WorkflowInstance?> FindAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstance>> FindManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstance>> FindManyAsync<TOrderBy>(WorkflowInstanceFilter filter, PageArgs pageArgs, WorkflowInstanceOrder<TOrderBy> order, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstance>> FindManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstance>> FindManyAsync<TOrderBy>(WorkflowInstanceFilter filter, WorkflowInstanceOrder<TOrderBy> order, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstanceSummary>> SummarizeManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstanceSummary>> SummarizeManyAsync<TOrderBy>(WorkflowInstanceFilter filter, PageArgs pageArgs, WorkflowInstanceOrder<TOrderBy> order, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstanceSummary>> SummarizeManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstanceSummary>> SummarizeManyAsync<TOrder>(WorkflowInstanceFilter filter, WorkflowInstanceOrder<TOrder> order, CancellationToken cancellationToken = default);
Task SaveAsync(WorkflowInstance record, CancellationToken cancellationToken = default);
Task SaveManyAsync(IEnumerable<WorkflowInstance> records, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<int> DeleteManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
}
/// <summary>
/// A filter for querying workflow instances.
/// </summary>
public class WorkflowInstanceFilter
{
public string? Id { get; set; }
public ICollection<string>? Ids { get; set; }
public string? SearchTerm { get; set; }
public string? DefinitionId { get; set; }
public string? DefinitionVersionId { get; set; }
public ICollection<string>? DefinitionIds { get; set; }
public ICollection<string>? DefinitionVersionIds { get; set; }
public int? Version { get; set; }
public string? CorrelationId { get; set; }
public ICollection<string>? 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
/// </summary>
public class WorkflowInstanceOrder<TProp> : OrderDefinition<WorkflowInstance, TProp>
{
}
/// <summary>
/// Represents a store of workflow instances.
/// </summary>
public interface IWorkflowInstanceStore
{
Task<WorkflowInstance?> FindAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstance>> FindManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstance>> FindManyAsync<TOrderBy>(WorkflowInstanceFilter filter, PageArgs pageArgs, WorkflowInstanceOrder<TOrderBy> order, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstance>> FindManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstance>> FindManyAsync<TOrderBy>(WorkflowInstanceFilter filter, WorkflowInstanceOrder<TOrderBy> order, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstanceSummary>> SummarizeManyAsync(WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default);
Task<Page<WorkflowInstanceSummary>> SummarizeManyAsync<TOrderBy>(WorkflowInstanceFilter filter, PageArgs pageArgs, WorkflowInstanceOrder<TOrderBy> order, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstanceSummary>> SummarizeManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<IEnumerable<WorkflowInstanceSummary>> SummarizeManyAsync<TOrder>(WorkflowInstanceFilter filter, WorkflowInstanceOrder<TOrder> order, CancellationToken cancellationToken = default);
Task SaveAsync(WorkflowInstance record, CancellationToken cancellationToken = default);
Task SaveManyAsync(IEnumerable<WorkflowInstance> records, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
Task<int> DeleteManyAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default);
}

View file

@ -4,6 +4,9 @@ using Elsa.Workflows.Runtime.Models.Notifications;
namespace Elsa.Workflows.Runtime.Contracts;
/// <summary>
/// Extracts triggers from workflow definitions.
/// </summary>
public interface ITriggerIndexer
{
/// <summary>
@ -20,9 +23,4 @@ public interface ITriggerIndexer
/// Indexes triggers of the specified workflow.
/// </summary>
Task<IndexedWorkflowTriggers> IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default);
/// <summary>
/// Send message of all triggers in the trigger store to populate the routetable on startup
/// </summary>
Task<IndexedWorkflowTriggers> IndexAllTriggersAsync(CancellationToken cancellationToken = default);
}

View file

@ -28,18 +28,36 @@ public static class ModuleExtensions
return module;
}
public static WorkflowRuntimeFeature UseDefaultRuntime(this WorkflowRuntimeFeature feature, Action<DefaultRuntimeFeature>? configure = default)
/// <summary>
/// Configures the default workflow runtime.
/// </summary>
/// <param name="feature">The workflow runtime feature.</param>
/// <param name="configure">A callback that configures the default workflow runtime.</param>
/// <returns>The workflow runtime feature.</returns>
public static WorkflowRuntimeFeature UseDefaultRuntime(this WorkflowRuntimeFeature feature, Action<DefaultWorkflowRuntimeFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Configures the execution log records feature.
/// </summary>
/// <param name="feature">The workflow runtime feature.</param>
/// <param name="configure">A callback that configures the execution log records feature.</param>
/// <returns>The workflow runtime feature.</returns>
public static WorkflowRuntimeFeature UseExecutionLogRecords(this WorkflowRuntimeFeature feature, Action<ExecutionLogRecordFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Configures the workflow state exporter feature.
/// </summary>
/// <param name="feature">The workflow runtime feature.</param>
/// <param name="configure">A callback that configures the workflow state exporter feature.</param>
/// <returns>The workflow runtime feature.</returns>
public static WorkflowRuntimeFeature UseAsyncWorkflowStateExporter(this WorkflowRuntimeFeature feature, Action<AsyncWorkflowStateExporterFeature>? configure = default)
{
feature.Module.Configure(configure);

View file

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

View file

@ -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;
/// <summary>
/// Installs the default runtime services.
/// </summary>
[DependsOn(typeof(WorkflowRuntimeFeature))]
public class DefaultWorkflowRuntimeFeature : FeatureBase
{
/// <inheritdoc />
public DefaultWorkflowRuntimeFeature(IModule module) : base(module)
{
}
/// <summary>
/// A factory that instantiates an <see cref="IWorkflowStateStore"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowStateStore> WorkflowStateStore { get; set; } = sp => ActivatorUtilities.CreateInstance<MemoryWorkflowStateStore>(sp);
/// <inheritdoc />
public override void Apply()
{
Services
// Replaceable factories
.AddSingleton(WorkflowStateStore)
// Memory stores.
.AddMemoryStore<WorkflowState, MemoryWorkflowStateStore>();
}
}

View file

@ -47,12 +47,7 @@ public class WorkflowRuntimeFeature : FeatureBase
/// A factory that instantiates an <see cref="IWorkflowDispatcher"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowDispatcher> WorkflowDispatcher { get; set; } = sp => ActivatorUtilities.CreateInstance<TaskBasedWorkflowDispatcher>(sp);
/// <summary>
/// A factory that instantiates an <see cref="IWorkflowStateStore"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowStateStore> WorkflowStateStore { get; set; } = sp => ActivatorUtilities.CreateInstance<MemoryWorkflowStateStore>(sp);
/// <summary>
/// A factory that instantiates an <see cref="IBookmarkStore"/>.
/// </summary>
@ -114,8 +109,7 @@ public class WorkflowRuntimeFeature : FeatureBase
Module
.ConfigureHostedService<RegisterDescriptors>()
.ConfigureHostedService<RegisterExpressionSyntaxDescriptors>()
.ConfigureHostedService<PopulateWorkflowDefinitionStore>()
.ConfigureHostedService<PopulateRouteTable>();
.ConfigureHostedService<PopulateWorkflowDefinitionStore>();
/// <inheritdoc />
public override void Apply()
@ -133,7 +127,6 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddSingleton<IBackgroundActivityInvoker, DefaultBackgroundActivityInvoker>()
.AddSingleton(WorkflowRuntime)
.AddSingleton(WorkflowDispatcher)
.AddSingleton(WorkflowStateStore)
.AddSingleton(BookmarkStore)
.AddSingleton(WorkflowTriggerStore)
.AddSingleton(WorkflowExecutionLogStore)

View file

@ -1,29 +0,0 @@
using Elsa.Workflows.Runtime.Contracts;
using Microsoft.Extensions.Hosting;
namespace Elsa.Workflows.Runtime.HostedServices;
/// <summary>
/// Synchronously updates the routetable from the triggers.
/// </summary>
public class PopulateRouteTable : IHostedService
{
private readonly ITriggerIndexer _triggerIndexer;
/// <summary>
/// Constructor.
/// </summary>
public PopulateRouteTable(ITriggerIndexer triggerIndexer)
{
_triggerIndexer = triggerIndexer;
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
await _triggerIndexer.IndexAllTriggersAsync(cancellationToken);
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View file

@ -62,27 +62,6 @@ public class TriggerIndexer : ITriggerIndexer
return await IndexTriggersAsync(workflow, cancellationToken);
}
/// <inheritdoc />
public async Task<IndexedWorkflowTriggers> IndexAllTriggersAsync(CancellationToken cancellationToken = default)
{
var emptyTriggerList = new List<StoredTrigger>(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;
}
/// <inheritdoc />
public async Task<IndexedWorkflowTriggers> IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default)
{

View file

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

View file

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.JavaScript\Elsa.JavaScript.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.ProtoActor\Elsa.ProtoActor.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Scheduling\Elsa.Scheduling.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Proto.Persistence.Sqlite" Version="1.1.0" />
</ItemGroup>
</Project>

View file

@ -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<Program>()
.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<Program>())
.UseJavaScript()
.UseLiquid()
.UseHttp()
);
services.Configure<JintOptions>(options => options.AllowClrAccess = true);
services.AddHandlersFrom<Program>();
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();

View file

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

View file

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

View file

@ -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="
}]
}
}