diff --git a/Elsa.sln b/Elsa.sln index d289f5038..525bde857 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -144,6 +144,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.Po EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.RunTaskIntegration", "src\samples\aspnet\Elsa.Samples.RunTaskIntegration\Elsa.Samples.RunTaskIntegration.csproj", "{51050209-EC2F-4DC7-8F46-07E22B7811CD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Elasticsearch", "src\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj", "{3246883E-2FA7-4B4A-BDC5-99039A2869BC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -362,6 +364,10 @@ Global {51050209-EC2F-4DC7-8F46-07E22B7811CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {51050209-EC2F-4DC7-8F46-07E22B7811CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {51050209-EC2F-4DC7-8F46-07E22B7811CD}.Release|Any CPU.Build.0 = Release|Any CPU + {3246883E-2FA7-4B4A-BDC5-99039A2869BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3246883E-2FA7-4B4A-BDC5-99039A2869BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3246883E-2FA7-4B4A-BDC5-99039A2869BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3246883E-2FA7-4B4A-BDC5-99039A2869BC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} @@ -424,5 +430,6 @@ Global {536EFB55-EEA1-4761-9A72-D4EA84A6DB70} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} {AA84DBF7-F70F-4673-8DC2-6EFBE3E9BF83} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {51050209-EC2F-4DC7-8F46-07E22B7811CD} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {3246883E-2FA7-4B4A-BDC5-99039A2869BC} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} EndGlobalSection EndGlobal diff --git a/src/bundles/Elsa.AllInOne.Web/Program.cs b/src/bundles/Elsa.AllInOne.Web/Program.cs index e1a1fda21..86559fcea 100644 --- a/src/bundles/Elsa.AllInOne.Web/Program.cs +++ b/src/bundles/Elsa.AllInOne.Web/Program.cs @@ -30,7 +30,11 @@ services identity.TokenOptions = identityTokenOptions; }) .UseDefaultAuthentication() - .UseWorkflowRuntime(runtime => { runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)); }) + .UseWorkflowRuntime(runtime => + { + runtime.UseDefaultRuntime(dr => dr.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))); + runtime.UseExecutionLogRecords(e => e.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))); + }) .UseLabels(labels => labels.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) .UseActivityDefinitions(feature => feature.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) .UseJobs() diff --git a/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj b/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj index 99e658116..7daa3c414 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj +++ b/src/bundles/Elsa.WorkflowServer.Web/Elsa.WorkflowServer.Web.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index 83e3561da..2dba6f435 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -1,3 +1,6 @@ +using Elsa.Elasticsearch.Modules.Management; +using Elsa.Elasticsearch.Modules.Runtime; +using Elsa.Elasticsearch.Options; using Elsa.EntityFrameworkCore.Extensions; using Elsa.Extensions; using Elsa.Identity; @@ -24,6 +27,9 @@ var identitySection = configuration.GetSection("Identity"); var identityTokenSection = identitySection.GetSection("Tokens"); identitySection.Bind(identityOptions); identityTokenSection.Bind(identityTokenOptions); +var postgreSqlConnectionString = configuration.GetConnectionString("PostgreSql")!; +var elasticOptions = new ElasticsearchOptions(); +configuration.GetSection(ElasticsearchOptions.Elasticsearch).Bind(elasticOptions); // Add Elsa services. services @@ -35,11 +41,16 @@ services identity.TokenOptions = identityTokenOptions; }) .UseDefaultAuthentication() - .UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) + .UseWorkflowManagement(management => + { + management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)); + management.UseWorkflowInstances(w => w.UseElasticsearch(elasticOptions)); + }) .UseWorkflowRuntime(runtime => { runtime.UseProtoActor(proto => proto.PersistenceProvider = _ => new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString))); - runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)); + runtime.UseDefaultRuntime(df => df.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))); + runtime.UseExecutionLogRecords(e => e.UseElasticsearch(elasticOptions)); runtime.UseAsyncWorkflowStateExporter(); runtime.UseMassTransitDispatcher(); }) diff --git a/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json b/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json index ab15a90c8..65a64df73 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json +++ b/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json @@ -12,7 +12,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7072;http://localhost:5072", + "applicationUrl": "https://localhost:5001;http://localhost:5072", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json index a7e3aec5c..a4a513ce9 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json +++ b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json @@ -9,6 +9,7 @@ "AllowedHosts": "*", "ConnectionStrings": { "Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;", + "PostgreSql": "Server=localhost;Port=5455;Database=myPostgresDb;Username=guest;Password=guest", "RabbitMq": "rabbitmq://guest:guest@localhost" }, "Identity": { @@ -17,5 +18,8 @@ "SigningKey": "secret-signing-key", "Lifetime": "8:00:00" } + }, + "Elasticsearch": { + "Endpoint": "http://localhost:9200/" } -} +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Common/Configuration.cs b/src/modules/Elsa.Elasticsearch/Common/Configuration.cs new file mode 100644 index 000000000..d3f91546c --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Common/Configuration.cs @@ -0,0 +1,35 @@ +using Elasticsearch.Net; +using Elsa.Elasticsearch.Options; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Runtime.Entities; +using Nest; + +namespace Elsa.Elasticsearch.Common; + +public static class Configuration +{ + public static ConnectionSettings SetupAuthentication(this ConnectionSettings settings, ElasticsearchOptions options) + { + if (!string.IsNullOrEmpty(options.ApiKey)) + { + settings.ApiKeyAuthentication(new ApiKeyAuthenticationCredentials(options.ApiKey)); + } + else if (!string.IsNullOrEmpty(options.Username) && !string.IsNullOrEmpty(options.Password)) + { + settings.BasicAuthentication(options.Username, options.Password); + } + + return settings; + } + + public static ConnectionSettings SetupMappingsAndIndices(this ConnectionSettings settings) + { + settings.DefaultMappingFor(s => + s.IndexName(ElasticsearchOptions.Indices[typeof(WorkflowInstance)])); + + settings.DefaultMappingFor(s => + s.IndexName(ElasticsearchOptions.Indices[typeof(WorkflowExecutionLogRecord)])); + + return settings; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Common/ElasticFeatureBase.cs b/src/modules/Elsa.Elasticsearch/Common/ElasticFeatureBase.cs new file mode 100644 index 000000000..fe8648eb0 --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Common/ElasticFeatureBase.cs @@ -0,0 +1,39 @@ +using Elasticsearch.Net; +using Elsa.Elasticsearch.Options; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; +using Nest; + +namespace Elsa.Elasticsearch.Common; + +public abstract class ElasticFeatureBase : FeatureBase +{ + public ElasticFeatureBase(IModule module) : base(module) + { + } + + internal ElasticsearchOptions Options { get; set; } = new(); + + public override void Apply() + { + if (Services.All(x => x.ServiceType != typeof(ElasticClient))) + { + Services.AddSingleton(new ElasticClient(GetSettings())); + } + } + + private ConnectionSettings GetSettings() + { + return new ConnectionSettings(new Uri(Options.Endpoint)) + .SetupAuthentication(Options) + .SetupMappingsAndIndices(); + } + + protected void AddStore() where TModel : class where TStore : class + { + Services + .AddSingleton>() + .AddSingleton(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Common/ElasticStore.cs b/src/modules/Elsa.Elasticsearch/Common/ElasticStore.cs new file mode 100644 index 000000000..2dc37a24f --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Common/ElasticStore.cs @@ -0,0 +1,121 @@ +using System.Collections.ObjectModel; +using Elsa.Common.Models; +using Microsoft.Extensions.Logging; +using Nest; + +namespace Elsa.Elasticsearch.Common; + +public class ElasticStore where T : class +{ + private readonly ElasticClient _elasticClient; + private readonly ILogger _logger; + + public ElasticStore(ElasticClient elasticClient, ILogger> logger) + { + _elasticClient = elasticClient; + _logger = logger; + } + + public async Task GetByIdAsync(string id, CancellationToken cancellationToken) + { + var response = await _elasticClient.GetAsync(DocumentPath.Id(id), ct: cancellationToken); + + if (response.IsValid) return response.Source; + + _logger.LogError("Failed to fetch data from Elasticsearch: {message}", response.ServerError?.ToString()); + return null; + } + + public async Task> SearchAsync(Func, QueryContainer> query, PageArgs? pageArgs, CancellationToken cancellationToken) + { + var search = new SearchDescriptor().Query(query); + if (pageArgs != default) + { + search = search.From(pageArgs.Offset).Size(pageArgs.Limit); + } + + var response = await _elasticClient.SearchAsync(search, cancellationToken); + + if (response.IsValid) return new Page(response.Hits.Select(hit => hit.Source).ToList(), response.Total); + + _logger.LogError("Failed to search data in Elasticsearch: {message}", response.ServerError?.ToString()); + return new Page(new Collection(), 0); + } + + public async Task> SearchAsync(SearchDescriptor search, PageArgs? pageArgs, CancellationToken cancellationToken) + { + if (pageArgs != default) + { + search = search.From(pageArgs.Offset).Size(pageArgs.Limit); + } + + var response = await _elasticClient.SearchAsync(search, cancellationToken); + + if (response.IsValid) return new Page(response.Hits.Select(hit => hit.Source).ToList(), response.Total); + + _logger.LogError("Failed to search data in Elasticsearch: {message}", response.ServerError?.ToString()); + return new Page(new Collection(), 0); + } + + public async Task SaveAsync(T model, CancellationToken cancellationToken) + { + var response = await _elasticClient.IndexAsync(model, descriptor => descriptor, cancellationToken); + + if (response.IsValid) return true; + + _logger.LogError("Failed to save data in Elasticsearch: {message}", response.ServerError?.ToString()); + return false; + } + + public async Task SaveManyAsync(IEnumerable documents, CancellationToken cancellationToken) + { + var response = await _elasticClient.IndexManyAsync(documents, cancellationToken: cancellationToken); + + if (response.IsValid) return true; + + _logger.LogError("Failed to save data in Elasticsearch: {message}", response.ServerError?.ToString()); + return false; + } + + public async Task DeleteByIdAsync(string id, CancellationToken cancellationToken) + { + var response = await _elasticClient.DeleteAsync(DocumentPath.Id(id), ct: cancellationToken); + + if (response.IsValid) return true; + + _logger.LogError("Failed to delete data in Elasticsearch: {message}", response.ServerError?.ToString()); + return false; + } + + public async Task DeleteManyAsync(IEnumerable list, CancellationToken cancellationToken) + { + var response = await _elasticClient.DeleteManyAsync(list, cancellationToken: cancellationToken); + + if (response.IsValid) return 0; + + _logger.LogError("Failed to delete data in Elasticsearch: {message}", response.ServerError?.ToString()); + return response.Items.Count; + } + + public async Task DeleteByQueryAsync(Func, QueryContainer> query, CancellationToken cancellationToken) + { + var response = await _elasticClient.DeleteByQueryAsync(q => q + .Query(query), cancellationToken); + + if (response.IsValid) return true; + + _logger.LogError("Failed to delete data in Elasticsearch: {message}", response.ServerError?.ToString()); + return false; + } + + public async Task CountAsync(Func, QueryContainer> query, CancellationToken cancellationToken) + { + var response = await _elasticClient.CountAsync(s => + s.Query(query), cancellationToken); + + if (response.IsValid) return response.Count; + + _logger.LogError("Failed to count data in Elasticsearch: {message}", response.ServerError?.ToString()); + return 0; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Elsa.Elasticsearch.csproj b/src/modules/Elsa.Elasticsearch/Elsa.Elasticsearch.csproj new file mode 100644 index 000000000..dacbc8ca1 --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Elsa.Elasticsearch.csproj @@ -0,0 +1,23 @@ + + + + + + + net6.0;net7.0 + + Provides Elasticsearch implementation for workflow state persistence. + + elsa module + + + + + + + + + + + + diff --git a/src/modules/Elsa.Elasticsearch/FodyWeavers.xml b/src/modules/Elsa.Elasticsearch/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Modules/Management/ElasticWorkflowInstanceFeature.cs b/src/modules/Elsa.Elasticsearch/Modules/Management/ElasticWorkflowInstanceFeature.cs new file mode 100644 index 000000000..55e45a613 --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Modules/Management/ElasticWorkflowInstanceFeature.cs @@ -0,0 +1,31 @@ +using Elsa.Elasticsearch.Common; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Elasticsearch.Modules.Management; + +[DependsOn(typeof(WorkflowManagementFeature))] +public class ElasticWorkflowInstanceFeature : ElasticFeatureBase +{ + public ElasticWorkflowInstanceFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.Configure(feature => + { + feature.WorkflowInstanceStore = sp => sp.GetRequiredService(); + }); + } + + public override void Apply() + { + base.Apply(); + + AddStore(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Modules/Management/Extensions.cs b/src/modules/Elsa.Elasticsearch/Modules/Management/Extensions.cs new file mode 100644 index 000000000..18182aefa --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Modules/Management/Extensions.cs @@ -0,0 +1,17 @@ +using Elsa.Elasticsearch.Options; +using Elsa.Workflows.Management.Features; + +namespace Elsa.Elasticsearch.Modules.Management; + +public static class Extensions +{ + /// + /// Configures the to use the . + /// + public static WorkflowInstanceFeature UseElasticsearch(this WorkflowInstanceFeature feature, ElasticsearchOptions options, Action? configure = default) + { + configure += f => f.Options = options; + feature.Module.Configure(configure); + return feature; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs b/src/modules/Elsa.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs new file mode 100644 index 000000000..c60d76d5f --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Modules/Management/WorkflowInstanceStore.cs @@ -0,0 +1,99 @@ +using Elsa.Common.Entities; +using Elsa.Common.Models; +using Elsa.Elasticsearch.Common; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Management.Services; +using Nest; + +namespace Elsa.Elasticsearch.Modules.Management; + +public class ElasticWorkflowInstanceStore : IWorkflowInstanceStore +{ + private readonly ElasticStore _store; + + public ElasticWorkflowInstanceStore(ElasticStore store) + { + _store = store; + } + + public async Task FindByIdAsync(string id, CancellationToken cancellationToken = default) => + await _store.GetByIdAsync(id, cancellationToken); + + public async Task SaveAsync(WorkflowInstance record, CancellationToken cancellationToken = default) => + await _store.SaveAsync(record, cancellationToken); + + public async Task SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default) => + await _store.SaveManyAsync(records, cancellationToken); + + public async Task DeleteAsync(string id, CancellationToken cancellationToken = default) => + await _store.DeleteByIdAsync(id, cancellationToken); + + public async Task DeleteManyAsync(IEnumerable ids, CancellationToken cancellationToken = default) => + await _store.DeleteManyAsync(ids.Select(id => new WorkflowInstance { Id = id}), cancellationToken); + + public async Task DeleteManyByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default) => + await _store.DeleteByQueryAsync(q => q + .Term(t => t + .Field(f => f.DefinitionId) + .Value(definitionId)), + cancellationToken); + + public async Task> FindManyAsync(FindWorkflowInstancesArgs args, + CancellationToken cancellationToken = default) + { + var queryDescriptor = new QueryContainerDescriptor(); + var searchDescriptor = new SearchDescriptor(); + var query = new QueryContainer(); + + var (searchTerm, definitionId, version, correlationId, workflowStatus, workflowSubStatus, pageArgs, orderBy, + orderDirection) = args; + + if (!string.IsNullOrWhiteSpace(definitionId)) + query = queryDescriptor.Match(m => m.Field(f => f.DefinitionId).Query(args.DefinitionId)); + + if (version != null) + query &= queryDescriptor.Match(m => m.Field(f => f.Version).Query(args.Version.ToString())); + + if (!string.IsNullOrWhiteSpace(correlationId)) + query &= queryDescriptor.Match(m => m.Field(f => f.CorrelationId).Query(args.CorrelationId)); + + if (workflowStatus != null) + query &= queryDescriptor.Match(m => m.Field(f => f.Status).Query(args.WorkflowStatus?.ToString())); + + if (workflowSubStatus != null) + query &= queryDescriptor.Match(m => m.Field(f => f.SubStatus).Query(args.WorkflowSubStatus?.ToString())); + + if(!string.IsNullOrWhiteSpace(searchTerm)) + { + query &= queryDescriptor + .MultiMatch(c => c + .Type(TextQueryType.Phrase) + .Fields(f => f + .Field(p => p.Name) + .Field(p => p.Id) + .Field(p => p.DefinitionId) + .Field(p => p.CorrelationId)) + .Lenient() + .Query(searchTerm)); + } + + searchDescriptor = orderBy switch + { + OrderBy.Finished => orderDirection == OrderDirection.Ascending + ? searchDescriptor.Sort(x => x.Ascending(f => f.FinishedAt)) + : searchDescriptor.Sort(x => x.Descending(f => f.FinishedAt)), + OrderBy.LastExecuted => orderDirection == OrderDirection.Ascending + ? searchDescriptor.Sort(x => x.Ascending(f => f.LastExecutedAt)) + : searchDescriptor.Sort(x => x.Descending(f => f.LastExecutedAt)), + OrderBy.Created => orderDirection == OrderDirection.Ascending + ? searchDescriptor.Sort(x => x.Ascending(f => f.CreatedAt)) + : searchDescriptor.Sort(x => x.Descending(f => f.CreatedAt)), + _ => searchDescriptor + }; + + var result = await _store.SearchAsync(searchDescriptor.Query(_ => query), args.PageArgs, cancellationToken); + return new Page(result.Items.Select(WorkflowInstanceSummary.FromInstance).ToList(), + result.TotalCount); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Modules/Runtime/ElasticExecutionLogRecordFeature.cs b/src/modules/Elsa.Elasticsearch/Modules/Runtime/ElasticExecutionLogRecordFeature.cs new file mode 100644 index 000000000..b98df8686 --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Modules/Runtime/ElasticExecutionLogRecordFeature.cs @@ -0,0 +1,31 @@ +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Features; +using Elsa.Elasticsearch.Common; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Elasticsearch.Modules.Runtime; + +[DependsOn(typeof(WorkflowRuntimeFeature))] +public class ElasticExecutionLogRecordFeature : ElasticFeatureBase +{ + public ElasticExecutionLogRecordFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.Configure(feature => + { + feature.WorkflowExecutionLogStore = sp => sp.GetRequiredService(); + }); + } + + public override void Apply() + { + base.Apply(); + + AddStore(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Modules/Runtime/Extensions.cs b/src/modules/Elsa.Elasticsearch/Modules/Runtime/Extensions.cs new file mode 100644 index 000000000..e6e65367b --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Modules/Runtime/Extensions.cs @@ -0,0 +1,17 @@ +using Elsa.Elasticsearch.Options; +using Elsa.Workflows.Runtime.Features; + +namespace Elsa.Elasticsearch.Modules.Runtime; + +public static class Extensions +{ + /// + /// Configures the to use the . + /// + public static ExecutionLogRecordFeature UseElasticsearch(this ExecutionLogRecordFeature feature, ElasticsearchOptions options, Action? configure = default) + { + configure += f => f.Options = options; + feature.Module.Configure(configure); + return feature; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs b/src/modules/Elsa.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs new file mode 100644 index 000000000..0506c0a50 --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs @@ -0,0 +1,31 @@ +using Elsa.Common.Models; +using Elsa.Elasticsearch.Common; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Services; + +namespace Elsa.Elasticsearch.Modules.Runtime; + +public class ElasticWorkflowExecutionLogStore : IWorkflowExecutionLogStore +{ + private readonly ElasticStore _store; + + public ElasticWorkflowExecutionLogStore(ElasticStore store) + { + _store = store; + } + + public async Task SaveAsync(WorkflowExecutionLogRecord record, CancellationToken cancellationToken = default) => + await _store.SaveAsync(record, cancellationToken); + + public async Task SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default) => + await _store.SaveManyAsync(records, cancellationToken); + + public async Task> FindManyByWorkflowInstanceIdAsync(string workflowInstanceId, PageArgs? pageArgs = default, CancellationToken cancellationToken = default) => + await _store + .SearchAsync(q => q + .Match(m => m + .Field(f => f.WorkflowInstanceId) + .Query(workflowInstanceId)), + pageArgs, + cancellationToken); +} \ No newline at end of file diff --git a/src/modules/Elsa.Elasticsearch/Options/ElasticsearchOptions.cs b/src/modules/Elsa.Elasticsearch/Options/ElasticsearchOptions.cs new file mode 100644 index 000000000..877fd0405 --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Options/ElasticsearchOptions.cs @@ -0,0 +1,20 @@ +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Runtime.Entities; + +namespace Elsa.Elasticsearch.Options; + +public class ElasticsearchOptions +{ + public const string Elasticsearch = "Elasticsearch"; + + public static readonly Dictionary Indices = new() + { + {typeof(WorkflowInstance),"workflow-instances"}, + {typeof(WorkflowExecutionLogRecord),"execution-log-records"} + }; + + public string Endpoint { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string ApiKey { get; set; } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Management/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Management/Extensions.cs index 6f2344c30..196b87147 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Management/Extensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Management/Extensions.cs @@ -1,4 +1,5 @@ using Elsa.EntityFrameworkCore.Modules.Management; +using Elsa.EntityFrameworkCore.Modules.Runtime; // ReSharper disable once CheckNamespace namespace Elsa.EntityFrameworkCore.Extensions; @@ -10,4 +11,10 @@ public static partial class Extensions feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(connectionString); return feature; } + + public static EFCoreWorkflowInstancePersistenceFeature UsePostgreSql(this EFCoreWorkflowInstancePersistenceFeature feature, string connectionString) + { + feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(connectionString); + return feature; + } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs index 559d051da..b5d34079e 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Modules/Runtime/Extensions.cs @@ -5,7 +5,13 @@ namespace Elsa.EntityFrameworkCore.Extensions; public static partial class Extensions { - public static EFCoreRuntimePersistenceFeature UsePostgreSql(this EFCoreRuntimePersistenceFeature feature, string connectionString) + public static EFCoreDefaultRuntimePersistenceFeature UsePostgreSql(this EFCoreDefaultRuntimePersistenceFeature feature, string connectionString) + { + feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(connectionString); + return feature; + } + + public static EFCoreExecutionLogRecordPersistenceFeature UsePostgreSql(this EFCoreExecutionLogRecordPersistenceFeature 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 dca6ad3bb..ae481aede 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 EFCoreRuntimePersistenceFeature UseSqlServer(this EFCoreRuntimePersistenceFeature feature, string connectionString) + public static EFCoreDefaultRuntimePersistenceFeature UseSqlServer(this EFCoreDefaultRuntimePersistenceFeature feature, string connectionString) { feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlServer(connectionString); return feature; diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Management/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Management/Extensions.cs index 67fe7c5e2..ff8424eb5 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Management/Extensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Management/Extensions.cs @@ -11,4 +11,10 @@ public static partial class Extensions feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlite(connectionString); return feature; } + + public static EFCoreWorkflowInstancePersistenceFeature UseSqlite(this EFCoreWorkflowInstancePersistenceFeature feature, string connectionString = Constants.DefaultConnectionString) + { + feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlite(connectionString); + return feature; + } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Runtime/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Modules/Runtime/Extensions.cs index 8450ad54d..4db325a5c 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 EFCoreRuntimePersistenceFeature UseSqlite(this EFCoreRuntimePersistenceFeature feature, string connectionString = Constants.DefaultConnectionString) + public static EFCoreDefaultRuntimePersistenceFeature UseSqlite(this EFCoreDefaultRuntimePersistenceFeature feature, string connectionString = Constants.DefaultConnectionString) + { + feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlite(connectionString); + return feature; + } + + public static EFCoreExecutionLogRecordPersistenceFeature UseSqlite(this EFCoreExecutionLogRecordPersistenceFeature feature, string connectionString = Constants.DefaultConnectionString) { feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlite(connectionString); return feature; diff --git a/src/modules/Elsa.EntityFrameworkCore/Common/Store.cs b/src/modules/Elsa.EntityFrameworkCore/Common/Store.cs index 64a8c9349..c9fd2ea82 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Common/Store.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Common/Store.cs @@ -16,12 +16,6 @@ public class Store where TDbContext : DbContext where TEnti _dbContextFactory = dbContextFactory; } - public async Task GetDbContextAsync(CancellationToken cancellationToken) - { - await using var dbContext = await CreateDbContextAsync(cancellationToken); - return dbContext; - } - public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) => await _dbContextFactory.CreateDbContextAsync(cancellationToken); public async Task SaveAsync(TEntity entity, CancellationToken cancellationToken = default) => await SaveAsync(entity, default, cancellationToken); diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Extensions.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Extensions.cs index 77daef648..c04b379da 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Extensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Extensions.cs @@ -10,7 +10,17 @@ public static class WorkflowManagementFeatureExtensions /// /// Sets up the EF Core persistence provider. /// - public static WorkflowManagementFeature UseEntityFrameworkCore(this WorkflowManagementFeature feature, Action? configure = default) + public static WorkflowManagementFeature UseEntityFrameworkCore(this WorkflowManagementFeature feature, + Action? configure = default) + { + feature.Module.Configure(configure); + return feature; + } + + /// + /// Sets up the EF Core persistence provider. + /// + public static WorkflowInstanceFeature UseEntityFrameworkCore(this WorkflowInstanceFeature feature, Action? configure = default) { feature.Module.Configure(configure); return feature; diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Feature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/ManagementPersistenceFeature.cs similarity index 84% rename from src/modules/Elsa.EntityFrameworkCore/Modules/Management/Feature.cs rename to src/modules/Elsa.EntityFrameworkCore/Modules/Management/ManagementPersistenceFeature.cs index cb562b117..8b80471b5 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/Feature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/ManagementPersistenceFeature.cs @@ -19,7 +19,6 @@ public class EFCoreManagementPersistenceFeature : PersistenceFeatureBase(feature => { feature.WorkflowDefinitionStore = sp => sp.GetRequiredService(); - feature.WorkflowInstanceStore = sp => sp.GetRequiredService(); }); } @@ -28,6 +27,5 @@ public class EFCoreManagementPersistenceFeature : PersistenceFeatureBase(); - AddStore(); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs new file mode 100644 index 000000000..a310a51ed --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs @@ -0,0 +1,31 @@ +using Elsa.EntityFrameworkCore.Common; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.EntityFrameworkCore.Modules.Management; + +[DependsOn(typeof(WorkflowManagementFeature))] +public class EFCoreWorkflowInstancePersistenceFeature : PersistenceFeatureBase +{ + public EFCoreWorkflowInstancePersistenceFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.Configure(feature => + { + feature.WorkflowInstanceStore = sp => sp.GetRequiredService(); + }); + } + + public override void Apply() + { + base.Apply(); + + AddStore(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/RuntimePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultRuntimePersistenceFeature.cs similarity index 87% rename from src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/RuntimePersistenceFeature.cs rename to src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultRuntimePersistenceFeature.cs index c77bd1ba0..d44faccef 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/RuntimePersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DefaultRuntimePersistenceFeature.cs @@ -10,9 +10,9 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.EntityFrameworkCore.Modules.Runtime; [DependsOn(typeof(WorkflowRuntimeFeature))] -public class EFCoreRuntimePersistenceFeature : PersistenceFeatureBase +public class EFCoreDefaultRuntimePersistenceFeature : PersistenceFeatureBase { - public EFCoreRuntimePersistenceFeature(IModule module) : base(module) + public EFCoreDefaultRuntimePersistenceFeature(IModule module) : base(module) { } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ExecutionLogRecordPersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ExecutionLogRecordPersistenceFeature.cs new file mode 100644 index 000000000..7760a0080 --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ExecutionLogRecordPersistenceFeature.cs @@ -0,0 +1,32 @@ +using Elsa.EntityFrameworkCore.Common; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Features; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.EntityFrameworkCore.Modules.Runtime; + +[DependsOn(typeof(WorkflowRuntimeFeature))] +public class EFCoreExecutionLogRecordPersistenceFeature : PersistenceFeatureBase +{ + public EFCoreExecutionLogRecordPersistenceFeature(IModule module) : base(module) + { + } + + public override void Configure() + { + Module.Configure(feature => + { + feature.WorkflowExecutionLogStore = sp => sp.GetRequiredService(); + }); + } + + public override void Apply() + { + base.Apply(); + + 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 09fe9db91..42805a759 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Extensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Extensions.cs @@ -10,9 +10,18 @@ namespace Elsa.EntityFrameworkCore.Modules.Runtime; public static class Extensions { /// - /// Configures the to use the . + /// Configures the to use the . /// - public static WorkflowRuntimeFeature UseEntityFrameworkCore(this WorkflowRuntimeFeature feature, Action? configure = default) + public static DefaultRuntimeFeature UseEntityFrameworkCore(this DefaultRuntimeFeature feature, Action? configure = default) + { + feature.Module.Configure(configure); + return feature; + } + + /// + /// Configures the to use the . + /// + public static ExecutionLogRecordFeature UseEntityFrameworkCore(this ExecutionLogRecordFeature feature, Action? configure = default) { feature.Module.Configure(configure); return feature; diff --git a/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs index ba8c4872a..4eb30e928 100644 --- a/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs @@ -23,6 +23,15 @@ public static class ModuleExtensions }); return module; } + + /// + /// Adds the workflow instance feature to workflow management module. + /// + public static WorkflowManagementFeature UseWorkflowInstances(this WorkflowManagementFeature feature, Action? configure = default) + { + feature.Module.Configure(configure); + return feature; + } /// /// Adds all types implementing to the system. diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowInstanceFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowInstanceFeature.cs new file mode 100644 index 000000000..887d9c789 --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowInstanceFeature.cs @@ -0,0 +1,11 @@ +using Elsa.Features.Abstractions; +using Elsa.Features.Services; + +namespace Elsa.Workflows.Management.Features; + +public class WorkflowInstanceFeature : FeatureBase +{ + public WorkflowInstanceFeature(IModule module) : base(module) + { + } +} \ 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 92af21fca..42d09d2cc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs @@ -27,4 +27,16 @@ public static class ModuleExtensions module.Configure().AddWorkflow(); return module; } + + public static WorkflowRuntimeFeature UseDefaultRuntime(this WorkflowRuntimeFeature feature, Action? configure = default) + { + feature.Module.Configure(configure); + return feature; + } + + public static WorkflowRuntimeFeature UseExecutionLogRecords(this WorkflowRuntimeFeature feature, Action? configure = default) + { + feature.Module.Configure(configure); + return feature; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Features/DefaultRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/DefaultRuntimeFeature.cs new file mode 100644 index 000000000..8ebf4bf26 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Features/DefaultRuntimeFeature.cs @@ -0,0 +1,11 @@ +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/ExecutionLogRecordFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/ExecutionLogRecordFeature.cs new file mode 100644 index 000000000..f9bf0c103 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Features/ExecutionLogRecordFeature.cs @@ -0,0 +1,11 @@ +using Elsa.Features.Abstractions; +using Elsa.Features.Services; + +namespace Elsa.Workflows.Runtime.Features; + +public class ExecutionLogRecordFeature : FeatureBase +{ + public ExecutionLogRecordFeature(IModule module) : base(module) + { + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.TelnyxIntegration/Program.cs b/src/samples/aspnet/Elsa.Samples.TelnyxIntegration/Program.cs index fc0c8119a..6dd8d611d 100644 --- a/src/samples/aspnet/Elsa.Samples.TelnyxIntegration/Program.cs +++ b/src/samples/aspnet/Elsa.Samples.TelnyxIntegration/Program.cs @@ -57,7 +57,8 @@ services { //proto.PersistenceProvider = _ => new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString)); }); - runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)); + runtime.UseDefaultRuntime(dr => dr.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))); + runtime.UseExecutionLogRecords(e => e.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))); runtime.WorkflowStateExporter = sp => sp.GetRequiredService(); }) .UseActivityDefinitions(feature => feature.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))