diff --git a/Directory.Packages.props b/Directory.Packages.props index 8b65fbf..03dedda 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,174 +1,178 @@ - - true - true - - - 3.5.0-preview.1092 - 9.0.7 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + 3.5.0-preview.1092 + 9.0.7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Elsa.sln b/Elsa.sln index 29feedd..5baed45 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34003.232 @@ -77,14 +76,14 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E5482-0482-448C-B9E4-EC67A9474B85}" ProjectSection(SolutionItems) = preProject docker\.dockerignore = docker\.dockerignore + docker\docker-compose-datadog+otel-collector.yml = docker\docker-compose-datadog+otel-collector.yml docker\docker-compose.yml = docker\docker-compose.yml docker\ElsaServer-Datadog.Dockerfile = docker\ElsaServer-Datadog.Dockerfile docker\ElsaServer.Dockerfile = docker\ElsaServer.Dockerfile docker\ElsaServerAndStudio.Dockerfile = docker\ElsaServerAndStudio.Dockerfile docker\ElsaStudio.Dockerfile = docker\ElsaStudio.Dockerfile - docker\otel-collector-config.yaml = docker\otel-collector-config.yaml docker\init-db-postgres.sh = docker\init-db-postgres.sh - docker\docker-compose-datadog+otel-collector.yml = docker\docker-compose-datadog+otel-collector.yml + docker\otel-collector-config.yaml = docker\otel-collector-config.yaml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Elasticsearch", "src\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj", "{3246883E-2FA7-4B4A-BDC5-99039A2869BC}" @@ -313,18 +312,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "migrations", "migrations", scripts\migrations\efcore-3.1.sh = scripts\migrations\efcore-3.1.sh scripts\migrations\efcore-3.2.sh = scripts\migrations\efcore-3.2.sh scripts\migrations\efcore-3.3.sh = scripts\migrations\efcore-3.3.sh - scripts\migrations\README.md = scripts\migrations\README.md scripts\migrations\efcore-3.4.sh = scripts\migrations\efcore-3.4.sh scripts\migrations\efcore-3.5.sh = scripts\migrations\efcore-3.5.sh + scripts\migrations\README.md = scripts\migrations\README.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{AA5ACF15-8647-4AC9-B9F5-73FCB92BE7E3}" ProjectSection(SolutionItems) = preProject scripts\docker\build-and-run-all-in-one-web-docker.sh = scripts\docker\build-and-run-all-in-one-web-docker.sh - scripts\docker\docker-compose.yml = scripts\docker\docker-compose.yml - scripts\docker\docker-run-all-in-one-web.ps1 = scripts\docker\docker-run-all-in-one-web.ps1 scripts\docker\docker-compose-citus.yml = scripts\docker\docker-compose-citus.yml scripts\docker\docker-compose-yugabyte.yml = scripts\docker\docker-compose-yugabyte.yml + scripts\docker\docker-compose.yml = scripts\docker\docker-compose.yml + scripts\docker\docker-run-all-in-one-web.ps1 = scripts\docker\docker-run-all-in-one-web.ps1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "elsa-studio", "elsa-studio", "{DFFE6199-B5A7-4BA8-A5E1-77D0426C6468}" @@ -388,11 +387,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adr", "adr", "{0A04B1FD-06C ProjectSection(SolutionItems) = preProject doc\adr\0001-record-architecture-decisions.md = doc\adr\0001-record-architecture-decisions.md doc\adr\0002-fault-propagation-from-child-to-parent-activities.md = doc\adr\0002-fault-propagation-from-child-to-parent-activities.md - doc\adr\toc.md = doc\adr\toc.md - doc\adr\graph.dot = doc\adr\graph.dot doc\adr\0003-direct-bookmark-management-in-workflowexecutioncontext.md = doc\adr\0003-direct-bookmark-management-in-workflowexecutioncontext.md doc\adr\0004-activity-execution-snapshots.md = doc\adr\0004-activity-execution-snapshots.md doc\adr\0005-tenant-deleted-event.md = doc\adr\0005-tenant-deleted-event.md + doc\adr\graph.dot = doc\adr\graph.dot + doc\adr\toc.md = doc\adr\toc.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bounty", "bounty", "{9B80A705-2E31-4012-964A-83963DCDB384}" @@ -416,6 +415,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.IO.Compression", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.IO.Http", "src\modules\Elsa.IO.Http\Elsa.IO.Http.csproj", "{C583AF05-D517-4B7F-8955-6B61500ED3D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Caspian.WorkflowFramework", "src\apps\Caspian.WorkflowFramework\Caspian.WorkflowFramework.csproj", "{A0F413B7-FA0F-472D-8BC2-6193DA440072}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Caspian.WorkflowFramework.Sample", "src\apps\Caspian.WorkflowFramework.Sample\Caspian.WorkflowFramework.Sample.csproj", "{C260D75F-19EB-4982-A98B-D8E126776910}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -709,14 +712,14 @@ Global {99B171E6-0248-4402-836D-98947CD63772}.Release|Any CPU.ActiveCfg = Release|Any CPU {99B171E6-0248-4402-836D-98947CD63772}.Release|Any CPU.Build.0 = Release|Any CPU {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D}.Release|Any CPU.Build.0 = Release|Any CPU - {832675FA-C597-4554-AE6B-18F189198A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {832675FA-C597-4554-AE6B-18F189198A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {832675FA-C597-4554-AE6B-18F189198A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {832675FA-C597-4554-AE6B-18F189198A1F}.Release|Any CPU.Build.0 = Release|Any CPU - {5ADDDFB1-E59B-4097-97B7-8C24E2D60463}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D}.Release|Any CPU.Build.0 = Release|Any CPU + {832675FA-C597-4554-AE6B-18F189198A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {832675FA-C597-4554-AE6B-18F189198A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {832675FA-C597-4554-AE6B-18F189198A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {832675FA-C597-4554-AE6B-18F189198A1F}.Release|Any CPU.Build.0 = Release|Any CPU + {5ADDDFB1-E59B-4097-97B7-8C24E2D60463}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5ADDDFB1-E59B-4097-97B7-8C24E2D60463}.Debug|Any CPU.Build.0 = Debug|Any CPU {5ADDDFB1-E59B-4097-97B7-8C24E2D60463}.Release|Any CPU.ActiveCfg = Release|Any CPU {5ADDDFB1-E59B-4097-97B7-8C24E2D60463}.Release|Any CPU.Build.0 = Release|Any CPU @@ -864,6 +867,14 @@ Global {C583AF05-D517-4B7F-8955-6B61500ED3D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C583AF05-D517-4B7F-8955-6B61500ED3D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {C583AF05-D517-4B7F-8955-6B61500ED3D8}.Release|Any CPU.Build.0 = Release|Any CPU + {A0F413B7-FA0F-472D-8BC2-6193DA440072}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0F413B7-FA0F-472D-8BC2-6193DA440072}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0F413B7-FA0F-472D-8BC2-6193DA440072}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0F413B7-FA0F-472D-8BC2-6193DA440072}.Release|Any CPU.Build.0 = Release|Any CPU + {C260D75F-19EB-4982-A98B-D8E126776910}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C260D75F-19EB-4982-A98B-D8E126776910}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C260D75F-19EB-4982-A98B-D8E126776910}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C260D75F-19EB-4982-A98B-D8E126776910}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -885,6 +896,7 @@ Global {C9539BD8-D2AE-4A8D-8281-71A05B3FBF31} = {B08B4E00-C2AB-48F3-8389-449F42AEF179} {169E2C9B-6687-427F-A278-30BF849BFEDC} = {B08B4E00-C2AB-48F3-8389-449F42AEF179} {2DD5D66B-85E9-4AF9-911C-C9F963234159} = {B08B4E00-C2AB-48F3-8389-449F42AEF179} + {8A050229-DB79-4E0B-9AFF-7565E87F2954} = {B08B4E00-C2AB-48F3-8389-449F42AEF179} {C237BA1A-3A7D-4AB2-BE09-2696F3C082A4} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} {80529478-A383-4FEA-B744-C71264969E9A} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} {39CD855E-83B1-4A96-93F7-01608211EBE3} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} @@ -958,10 +970,10 @@ Global {94A61AD7-2A2B-40DB-81F3-C59D596958A8} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} {4B598AF7-BD7D-4544-A274-2CDDD98F4167} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09} {99B171E6-0248-4402-836D-98947CD63772} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} - {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} - {832675FA-C597-4554-AE6B-18F189198A1F} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} - {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} - {5ADDDFB1-E59B-4097-97B7-8C24E2D60463} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} + {4332A6BC-434A-4AF5-A075-F1BBCDD28F5D} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} + {832675FA-C597-4554-AE6B-18F189198A1F} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} + {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F} + {5ADDDFB1-E59B-4097-97B7-8C24E2D60463} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} {97C7E531-9D5F-43FD-AA19-BF24DA13B612} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {65F2AD97-3ECF-4BA6-8AAA-1E5882FDCE68} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} {690B0274-291F-4D9E-BA76-54EFF7D3E4BC} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} @@ -1007,7 +1019,6 @@ Global {FA2B2B44-7E3E-444B-A157-7526B376E9CA} = {9DF81143-2F39-4CFB-9DA3-902279010622} {DF0CA7E2-F5DF-40ED-91B6-748767084FC7} = {9DF81143-2F39-4CFB-9DA3-902279010622} {DB20B168-CD90-41F2-BE08-3D8C6F142459} = {9DF81143-2F39-4CFB-9DA3-902279010622} - {8A050229-DB79-4E0B-9AFF-7565E87F2954} = {B08B4E00-C2AB-48F3-8389-449F42AEF179} {0A04B1FD-06C0-4271-A910-A08C263DBC44} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6} {9B80A705-2E31-4012-964A-83963DCDB384} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6} {CD7DC0D1-FFDC-417A-89BE-7F32408F583E} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} @@ -1017,6 +1028,8 @@ Global {EB24F9FE-D7BD-4FCC-907E-AE400288C2A5} = {7FD1FD1E-5778-4065-AAA5-1F878129EF77} {9CA02818-F7EB-4A0B-B27B-BC74ACD499C9} = {7FD1FD1E-5778-4065-AAA5-1F878129EF77} {C583AF05-D517-4B7F-8955-6B61500ED3D8} = {7FD1FD1E-5778-4065-AAA5-1F878129EF77} + {A0F413B7-FA0F-472D-8BC2-6193DA440072} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} + {C260D75F-19EB-4982-A98B-D8E126776910} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/apps/Caspian.WorkflowFramework.Sample/Caspian.WorkflowFramework.Sample.csproj b/src/apps/Caspian.WorkflowFramework.Sample/Caspian.WorkflowFramework.Sample.csproj new file mode 100644 index 0000000..9bf6c98 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/Caspian.WorkflowFramework.Sample.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + diff --git a/src/apps/Caspian.WorkflowFramework.Sample/Caspian.WorkflowFramework.Sample.http b/src/apps/Caspian.WorkflowFramework.Sample/Caspian.WorkflowFramework.Sample.http new file mode 100644 index 0000000..c613358 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/Caspian.WorkflowFramework.Sample.http @@ -0,0 +1,6 @@ +@Caspian.WorkflowFramework.Sample_HostAddress = http://localhost:5008 + +GET {{Caspian.WorkflowFramework.Sample_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/apps/Caspian.WorkflowFramework.Sample/Controllers/WorkflowController.cs b/src/apps/Caspian.WorkflowFramework.Sample/Controllers/WorkflowController.cs new file mode 100644 index 0000000..379e2f4 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/Controllers/WorkflowController.cs @@ -0,0 +1,76 @@ +using Caspian.WorkflowFramework; +using Caspian.WorkflowFramework.Contracts; +using Caspian.WorkflowFramework.Models; +using Elsa.Common.Models; +using Elsa.Workflows.Runtime.Matches; +using Microsoft.AspNetCore.Mvc; +using San.MongoLogger; +using Newtonsoft.Json; + +namespace Caspian.WorkflowFramework.Sample.Controllers; +public class WorkflowController : Controller, ILoggable +{ + #region Fields + private readonly IWorkflowService _workflowService; + private readonly IMdbLogger _logger; + #endregion + + #region Ctor + public WorkflowController(IWorkflowService workflowService, + IMdbLogger logger) + { + _workflowService = workflowService; + _logger = logger; + } + #endregion + + #region Start + [HttpPost("Start")] + public async Task Start(CreateAndExecuteWorkflowModel input) + { + _logger.Log("Start of StartWorkflow action", JsonConvert.SerializeObject(input)); + + //var model = _mapper.Map(input); + var result = await _workflowService.CreateAndExecuteAsync(new CreateAndExecuteWorkflowModel() + { + Name = input.Name, + UserId = input.UserId, + Version = VersionOptions.Published + }); + + _logger.Log("End of StartWorkflow action", WFInstanceId: result.Data?.InstanceId, JsonConvert.SerializeObject(result)); + + return Json(result); + } + #endregion + + #region Do + [HttpPost("Do")] + public async Task Do(PublishEventModel input) + { + _logger.Log("Start of Do action", WFInstanceId: input.WorkflowInstanceId, JsonConvert.SerializeObject(input)); + + //var model = _mapper.Map(input); + var result = await _workflowService.PublishEventAsync(input); + + _logger.Log("End of Do action", WFInstanceId: input.WorkflowInstanceId, JsonConvert.SerializeObject(result)); + + return Json(result); + } + #endregion + + #region GetState + [HttpPost("GetState")] + public async Task GetState(InquiryStateModel input) + { + _logger.Log("Start of GetState action", WFInstanceId: input.WorkflowInstanceId, JsonConvert.SerializeObject(input)); + + //var model = _mapper.Map(input); + var result = await _workflowService.InquiryStateAsync(input); + + _logger.Log("End of GetState action", WFInstanceId: input.WorkflowInstanceId, JsonConvert.SerializeObject(result)); + + return Json(result); + } + #endregion +} diff --git a/src/apps/Caspian.WorkflowFramework.Sample/ExcludeExternalControllersFilter.cs b/src/apps/Caspian.WorkflowFramework.Sample/ExcludeExternalControllersFilter.cs new file mode 100644 index 0000000..540df78 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/ExcludeExternalControllersFilter.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Caspian.WorkflowFramework.Sample; +public class ExcludeExternalControllersFilter : IDocumentFilter +{ + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var currentAssembly = Assembly.GetExecutingAssembly(); + + var keysToRemove = context.ApiDescriptions + .Where(apiDesc => + { + var controllerType = apiDesc.ActionDescriptor?.EndpointMetadata?.OfType() + .FirstOrDefault()?.ControllerTypeInfo?.AsType(); + + return controllerType != null && controllerType.Assembly != currentAssembly; + }) + .Select(apiDesc => "/" + apiDesc.RelativePath.TrimEnd('/')) + .ToList(); + + foreach (var path in keysToRemove) + { + swaggerDoc.Paths.Remove(path); + } + } +} diff --git a/src/apps/Caspian.WorkflowFramework.Sample/FodyWeavers.xml b/src/apps/Caspian.WorkflowFramework.Sample/FodyWeavers.xml new file mode 100644 index 0000000..00e1d9a --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/apps/Caspian.WorkflowFramework.Sample/Program.cs b/src/apps/Caspian.WorkflowFramework.Sample/Program.cs new file mode 100644 index 0000000..16d0e68 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/Program.cs @@ -0,0 +1,78 @@ +using Caspian.WorkflowFramework; +using Caspian.WorkflowFramework.Sample; +using Microsoft.OpenApi.Models; +using San.MongoLogger; +using San.MongoLogger.Models; +using Parlot.Fluent; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowSpecificOrigin", + builder => builder.AllowAnyOrigin() // Add your client origin + .AllowAnyMethod() // Allow any HTTP method + .AllowAnyHeader()); // Allow any header +}); + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +//builder.Services.AddOpenApi(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + c.DocumentFilter(); + c.CustomSchemaIds(type => type.FullName); +}); + +//Workflow +//var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +var t = $"appsettings.workflow.{builder.Environment.EnvironmentName}.json"; +builder.AddWorkflowFramework($"appsettings.workflow.json"); + +builder.Services.AddMongoLogger(builder.Configuration); + +var app = builder.Build(); + +app.UseMongoLogger(); + +// Enable CORS before authentication and authorization +app.UseCors("AllowSpecificOrigin"); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + //app.MapOpenApi(); + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + c.RoutePrefix = string.Empty; // Set Swagger UI at the app's root URL + }); +} + +app.UseAuthorization(); + +app.MapControllers(); + +//Workflow +app.UseWorkflowFramework(); + +// Redirect to Swagger URL +app.Use(async (context, next) => +{ + // Check if current request is the root URL ("/") + if (context.Request.Path == "/") + { + // Redirect to Swagger UI + context.Response.Redirect("/swagger/index.html"); + return; + } + + await next(); // Proceed to the next middleware if not redirecting +}); + +app.Run(); diff --git a/src/apps/Caspian.WorkflowFramework.Sample/Properties/launchSettings.json b/src/apps/Caspian.WorkflowFramework.Sample/Properties/launchSettings.json new file mode 100644 index 0000000..afcfdbe --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5008", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/apps/Caspian.WorkflowFramework.Sample/appsettings.Development.json b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.Development.json new file mode 100644 index 0000000..84dfb83 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + //"Microsoft.EntityFrameworkCore.Database.Command": "Warning" // Disable SQL command logging + } + } +} diff --git a/src/apps/Caspian.WorkflowFramework.Sample/appsettings.json b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.json new file mode 100644 index 0000000..eab431b --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.json @@ -0,0 +1,322 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + //"Microsoft.EntityFrameworkCore.Database.Command": "Warning" // Disable SQL command logging + } + }, + "LogDatabaseSettings": { + "ConnectionString": "mongodb://localhost", + "DatabaseName": "Caspian_WorkflowFramework_Sample_DB", + "LogsCollectionName": "Col", + "BatchSize": 0, + "RollingInterval": "Daily", + "MaskList": [ + "code", + "depositNumber", + "username", + "customerNo", + "password", + "sessionId", + "mainAccount", + "accountNumber", + "owners", + "balance", + "accountNo", + "depositUniqueId", + "externalRefList", + "externalref", + "sameSignatureDepositNumber", + "mobileNumber", + "destinationNumber", + "cardNumber" + ] + }, + "AllowedHosts": "*", + "HostBuilder": { + "reloadConfigOnChange": true + }, + "ConnectionStrings": { + "DefaultConnection": "Data Source=.;Initial Catalog=WorkflowFrameworkTest;User ID=sa;Password=123456;Integrated Security=false;TrustServerCertificate=True;", + //"Sqlite": "", //"Data Source=App_Data/elsa.sqlite.db;Cache=Shared;", + ////"SqlServer": "Server=localhost,1433;Initial Catalog=Elsa;User=sa;Password=!Elsa2025@;Encrypt=false;PersistSecurityInfo=false", + //"SqlServer": "Data Source=.;Initial Catalog=WorkflowFrameworkTest;User ID=sa;Password=123456;Integrated Security=false;TrustServerCertificate=True;", + //"MySql": "", //"Server=localhost;Database=elsa;Uid=admin;Pwd=password;", + //"PostgreSql": "", //"Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", + //"Citus": "", //"Server=localhost;Username=citus;Database=citus;Port=9700;Password=citus;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", + //"YugabyteDb": "", //"Host=localhost;Port=5433;Database=elsa;Username=yugabyte;Password=yugabyte;Pooling=true", + //"Oracle": "", //"Data Source=(DESCRIPTION = (ADDRESS_LIST = (FAILOVER =ON) (LOAD_BALANCE = OFF) (ADDRESS = (PROTOCOL =TCP)(HOST=localhost)(PORT=1521))) (CONNECT_DATA = (SID= FREE) ));User Id=ELSA;Password=elsa;", + //"CockroachDb": "", //"Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true", + //"MongoDb": "", //"mongodb://localhost:27017/elsa-workflows", + //"AzureServiceBus": "", + //"RabbitMq": "", //"amqp://guest:guest@localhost:5672", + //"Redis": "" //"localhost:6379,abortConnect=false" + } + //"DatabaseProvider": "SqlServer", //"Sqlite", + //"Multitenancy": { + // "Tenants": [ + // { + // "Id": null, + // "Name": "Default", + // "Configuration": { + // "Http": { + // "Prefix": "", + // "Host": "localhost:5001" + // }, + // "ConnectionStrings": { + // "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;" + // } + // } + // }, + // { + // "Id": "tenant-1", + // "Name": "Tenant 1", + // "Configuration": { + // "Http": { + // "Prefix": "tenant-1", + // "Host": "localhost:5001" + // }, + // "ConnectionStrings": { + // "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;" + // } + // } + // } + // ] + //}, + //"Smtp": { + // "Host": "localhost", + // "Port": 2525, + // "DefaultSender": "noreply@crmservices.com" + //}, + //"Http": { + // "BaseUrl": "https://localhost:5001", + // "BasePath": "/workflows", + // "ApiRoutePrefix": "elsa/api", + // "AvailableContentTypes": [ + // "application/json", + // "application/xml", + // "text/plain", + // "text/html" + // ] + //}, + //"Identity": { + // "Tokens": { + // "SigningKey": "sufficiently-large-secret-signing-key", + // "AccessTokenLifetime": "1:00:00:00", + // "RefreshTokenLifetime": "7:00:00:00" + // }, + // "Roles": [ + // { + // "Id": "admin", + // "Name": "Administrator", + // "Permissions": [ + // "*" + // ], + // "TenantId": "default" + // } + // ], + // "Users": [ + // { + // "Id": "a2323f46-42db-4e15-af8b-94238717d817", + // "Name": "admin", + // //"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=", + // //"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=", + // "HashedPassword": "p90gXKnVpt5Q2VZ4KkeOaM5GZN3bqjczoznWbnFMrvo=", + // "HashedPasswordSalt": "QmVzbWVsbGFoQHJSYWhtYW5AcnJhaDFt", + // "Roles": [ + // "admin" + // ], + // "TenantId": "" + // }, + // { + // "Id": "b0cd0e506e713a9d", + // "Name": "alice", + // "Roles": [ + // "admin" + // ], + // "HashedPassword": "8B0fFK/f/kk9GkVtzXfRJ2Y6cNyYVvLTfKouWcAcuPg=", + // "HashedPasswordSalt": "xlNWvEng8fRvo0McyJopbRJ2MJ9NIYV/4IY5dOZeiiw=", + // "TenantId": "tenant-2" + // }, + // { + // "Id": "789d5056cb3bb8e9", + // "Name": "bob", + // "Roles": [ + // "admin" + // ], + // "HashedPassword": "GMHHvUw7RfIIBq+GAWZ/AlHR50TbgONYfGQ92Xge0EI=", + // "HashedPasswordSalt": "V4uHKg1PFBc1bsngCQoptibOu14faj9obSn5B23h1wE=", + // "TenantId": "tenant-1" + // } + // ], + // "Applications": [ + // { + // "Id": "d57030226341448daff5a2935aba2d3f", + // "Name": "Postman", + // "Roles": [ + // "admin" + // ], + // "ClientId": "HXr0Vzdm9KCZbwsJ", + // "ClientSecret": "a<~QGGHTEA%u4;CU&'Wga5ED:_&Gd1C)", + // "HashedApiKey": "Z5ClHs3mbzx8Pnw3+PxbMq8A/Y+VKMCCDTGYtax8JFM=", + // "HashedApiKeySalt": "kBisa1X8FwBfN2zmyGMFRgIVVBleghhQAJ4WGyTkaD0=", + // "HashedClientSecret": "jEv58d0SVbGQ3nBZM0lkzHghG4Y+lMKW80wipz+9vHk=", + // "HashedClientSecretSalt": "xRKy14Ok1/tU3kLf/8V1fcbLIegy9vcM90Peu2tzohU=", + // "TenantId": "default" + // } + // ] + //}, + //"Environments": { + // "Environments": [ + // { + // "Name": "Local", + // "ServerUrl": "https://localhost:5001/elsa/api" + // }, + // { + // "Name": "Staging", + // "ServerUrl": "https://staging.acme.com/elsa/api" + // }, + // { + // "Name": "Production", + // "ServerUrl": "https://production.acme.com/elsa/api" + // } + // ] + //}, + //"AppRole": "Default", + //"Runtime": { + // "DistributedLocking": { + // "Provider": "File", + // "LockAcquisitionTimeout": "00:00:10" + // }, + // "DistributedLockProvider": "File", + // "WorkflowDispatcher": { + // "Channels": [ + // { + // "Name": "Low" + // }, + // { + // "Name": "Medium" + // }, + // { + // "Name": "High" + // } + // ] + // } + //}, + //"Scripting": { + // "Python": { + // "PythonDllPath": "", + // "Scripts": [ + // "def greet(name): return f'Hello {name}!'", + // "def say_hello_world(): return greet('World')" + // ] + // } + //}, + //"AzureServiceBus": { + // "Queues": [ + // { + // "Name": "queue1" + // }, + // { + // "Name": "queue2" + // } + // ], + // "Topics": [ + // { + // "Name": "topic1", + // "Subscriptions": [ + // { + // "Name": "subscription1" + // }, + // { + // "Name": "subscription2" + // } + // ] + // } + // ] + //}, + //"Kafka": { + // "Topics": [ + // { + // "Id": "topic-1", + // "Name": "topic-1" + // }, + // { + // "Id": "topic-2", + // "Name": "topic-2" + // } + // ], + // "SchemaRegistries": [ + // { + // "Id": "schema-registry-1", + // "Name": "Schema Registry 1", + // "Config": { + // "Url": "http://localhost:8081" + // } + // } + // ], + // "Producers": [ + // { + // "Id": "producer-1", + // "Name": "Producer 1", + // "FactoryType": "Elsa.Kafka.Factories.GenericProducerFactory`2[[System.String, System.Private.CoreLib], [Elsa.Server.Web.Messages.OrderReceived, Elsa.Server.Web]], Elsa.Kafka", + // "Config": { + // "BootstrapServers": "localhost:9092" + // }, + // "SchemaRegistryId": "schema-registry-1" + // } + // ], + // "Consumers": [ + // { + // "Id": "consumer-1", + // "Name": "Consumer 1", + // "FactoryType": "Elsa.Kafka.Factories.GenericConsumerFactory`2[[System.String, System.Private.CoreLib], [Elsa.Server.Web.Messages.OrderReceived, Elsa.Server.Web]], Elsa.Kafka", + // "Config": { + // "BootstrapServers": "localhost:9092", + // "GroupId": "group-1", + // "AutoOffsetReset": "earliest", + // "EnableAutoCommit": "true" + // }, + // "SchemaRegistryId": "schema-registry-1" + // } + // ] + //}, + //"Secrets": { + // "Management": { + // "SweepInterval": "04:00:00" + // } + //}, + //"Webhooks": { + // "Sinks": [ + // { + // "Id": "1", + // "Name": "Run Task", + // "Filters": [ + // { + // "EventType": "Elsa.RunTask" + // } + // ], + // "Url": "https://localhost:6001/webhooks/run-task" + // } + // ] + //}, + //"Resilience": { + // "Strategies": [ + // { + // "$type": "HttpResilienceStrategy", + // "Id": "1", + // "DisplayName": "Retry 3 times, exponential", + // "MaxRetryAttempts": 3, + // "BackoffType": "Exponential" + // }, + // { + // "$type": "HttpResilienceStrategy", + // "Id": "2", + // "DisplayName": "Retry 10 times, linear", + // "MaxRetryAttempts": 10, + // "BackoffType": "Linear" + // } + // ] + //} +} diff --git a/src/apps/Caspian.WorkflowFramework.Sample/appsettings.workflow.Development.json b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.workflow.Development.json new file mode 100644 index 0000000..84dfb83 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.workflow.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + //"Microsoft.EntityFrameworkCore.Database.Command": "Warning" // Disable SQL command logging + } + } +} diff --git a/src/apps/Caspian.WorkflowFramework.Sample/appsettings.workflow.json b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.workflow.json new file mode 100644 index 0000000..5ab381e --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework.Sample/appsettings.workflow.json @@ -0,0 +1,285 @@ +{ + "HostBuilder": { + "reloadConfigOnChange": true + }, + "ConnectionStrings": { + //"Sqlite": "", //"Data Source=App_Data/elsa.sqlite.db;Cache=Shared;", + "SqlServer": "Data Source=.;Initial Catalog=WorkflowDb_3-5-0;User ID=sa;Password=123456;Integrated Security=false;TrustServerCertificate=True;" + //"MySql": "", //"Server=localhost;Database=elsa;Uid=admin;Pwd=password;", + //"PostgreSql": "", //"Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", + //"Citus": "", //"Server=localhost;Username=citus;Database=citus;Port=9700;Password=citus;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", + //"YugabyteDb": "", //"Host=localhost;Port=5433;Database=elsa;Username=yugabyte;Password=yugabyte;Pooling=true", + //"Oracle": "", //"Data Source=(DESCRIPTION = (ADDRESS_LIST = (FAILOVER =ON) (LOAD_BALANCE = OFF) (ADDRESS = (PROTOCOL =TCP)(HOST=localhost)(PORT=1521))) (CONNECT_DATA = (SID= FREE) ));User Id=ELSA;Password=elsa;", + //"CockroachDb": "", //"Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true", + //"MongoDb": "", //"mongodb://localhost:27017/elsa-workflows", + //"AzureServiceBus": "", + //"RabbitMq": "", //"amqp://guest:guest@localhost:5672", + //"Redis": "" //"localhost:6379,abortConnect=false" + }, + "DatabaseProvider": "SqlServer", //"Sqlite", + "Multitenancy": { + "Tenants": [ + { + "Id": null, + "Name": "Default", + "Configuration": { + "Http": { + "Prefix": "", + "Host": "localhost:5001" + }, + "ConnectionStrings": { + "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;" + } + } + }, + { + "Id": "tenant-1", + "Name": "Tenant 1", + "Configuration": { + "Http": { + "Prefix": "tenant-1", + "Host": "localhost:5001" + }, + "ConnectionStrings": { + "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;" + } + } + } + ] + }, + "Smtp": { + "Host": "localhost", + "Port": 2525, + "DefaultSender": "noreply@crmservices.com" + }, + "Http": { + "BaseUrl": "https://localhost:5001", + "BasePath": "/workflows", + "ApiRoutePrefix": "elsa/api", + "AvailableContentTypes": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ] + }, + "Identity": { + "Tokens": { + "SigningKey": "sufficiently-large-secret-signing-key", + "AccessTokenLifetime": "1:00:00:00", + "RefreshTokenLifetime": "7:00:00:00" + }, + "Roles": [ + { + "Id": "admin", + "Name": "Administrator", + "Permissions": [ + "*" + ], + "TenantId": "default" + } + ], + "Users": [ + { + "Id": "a2323f46-42db-4e15-af8b-94238717d817", + "Name": "admin", + //"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=", + //"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=", + "HashedPassword": "p90gXKnVpt5Q2VZ4KkeOaM5GZN3bqjczoznWbnFMrvo=", + "HashedPasswordSalt": "QmVzbWVsbGFoQHJSYWhtYW5AcnJhaDFt", + "Roles": [ + "admin" + ], + "TenantId": "" + }, + { + "Id": "b0cd0e506e713a9d", + "Name": "alice", + "Roles": [ + "admin" + ], + "HashedPassword": "8B0fFK/f/kk9GkVtzXfRJ2Y6cNyYVvLTfKouWcAcuPg=", + "HashedPasswordSalt": "xlNWvEng8fRvo0McyJopbRJ2MJ9NIYV/4IY5dOZeiiw=", + "TenantId": "tenant-2" + }, + { + "Id": "789d5056cb3bb8e9", + "Name": "bob", + "Roles": [ + "admin" + ], + "HashedPassword": "GMHHvUw7RfIIBq+GAWZ/AlHR50TbgONYfGQ92Xge0EI=", + "HashedPasswordSalt": "V4uHKg1PFBc1bsngCQoptibOu14faj9obSn5B23h1wE=", + "TenantId": "tenant-1" + } + ], + "Applications": [ + { + "Id": "d57030226341448daff5a2935aba2d3f", + "Name": "Postman", + "Roles": [ + "admin" + ], + "ClientId": "HXr0Vzdm9KCZbwsJ", + "ClientSecret": "a<~QGGHTEA%u4;CU&'Wga5ED:_&Gd1C)", + "HashedApiKey": "Z5ClHs3mbzx8Pnw3+PxbMq8A/Y+VKMCCDTGYtax8JFM=", + "HashedApiKeySalt": "kBisa1X8FwBfN2zmyGMFRgIVVBleghhQAJ4WGyTkaD0=", + "HashedClientSecret": "jEv58d0SVbGQ3nBZM0lkzHghG4Y+lMKW80wipz+9vHk=", + "HashedClientSecretSalt": "xRKy14Ok1/tU3kLf/8V1fcbLIegy9vcM90Peu2tzohU=", + "TenantId": "default" + } + ] + }, + "Environments": { + "Environments": [ + { + "Name": "Local", + "ServerUrl": "https://localhost:5001/elsa/api" + }, + { + "Name": "Staging", + "ServerUrl": "https://staging.acme.com/elsa/api" + }, + { + "Name": "Production", + "ServerUrl": "https://production.acme.com/elsa/api" + } + ] + }, + "AppRole": "Default", + "Runtime": { + "DistributedLocking": { + "Provider": "File", + "LockAcquisitionTimeout": "00:00:10" + }, + "DistributedLockProvider": "File", + "WorkflowDispatcher": { + "Channels": [ + { + "Name": "Low" + }, + { + "Name": "Medium" + }, + { + "Name": "High" + } + ] + } + }, + "Scripting": { + "Python": { + "PythonDllPath": "", + "Scripts": [ + "def greet(name): return f'Hello {name}!'", + "def say_hello_world(): return greet('World')" + ] + } + }, + "AzureServiceBus": { + "Queues": [ + { + "Name": "queue1" + }, + { + "Name": "queue2" + } + ], + "Topics": [ + { + "Name": "topic1", + "Subscriptions": [ + { + "Name": "subscription1" + }, + { + "Name": "subscription2" + } + ] + } + ] + }, + "Kafka": { + "Topics": [ + { + "Id": "topic-1", + "Name": "topic-1" + }, + { + "Id": "topic-2", + "Name": "topic-2" + } + ], + "SchemaRegistries": [ + { + "Id": "schema-registry-1", + "Name": "Schema Registry 1", + "Config": { + "Url": "http://localhost:8081" + } + } + ], + "Producers": [ + { + "Id": "producer-1", + "Name": "Producer 1", + "FactoryType": "Elsa.Kafka.Factories.GenericProducerFactory`2[[System.String, System.Private.CoreLib], [Elsa.Server.Web.Messages.OrderReceived, Elsa.Server.Web]], Elsa.Kafka", + "Config": { + "BootstrapServers": "localhost:9092" + }, + "SchemaRegistryId": "schema-registry-1" + } + ], + "Consumers": [ + { + "Id": "consumer-1", + "Name": "Consumer 1", + "FactoryType": "Elsa.Kafka.Factories.GenericConsumerFactory`2[[System.String, System.Private.CoreLib], [Elsa.Server.Web.Messages.OrderReceived, Elsa.Server.Web]], Elsa.Kafka", + "Config": { + "BootstrapServers": "localhost:9092", + "GroupId": "group-1", + "AutoOffsetReset": "earliest", + "EnableAutoCommit": "true" + }, + "SchemaRegistryId": "schema-registry-1" + } + ] + }, + "Secrets": { + "Management": { + "SweepInterval": "04:00:00" + } + }, + "Webhooks": { + "Sinks": [ + { + "Id": "1", + "Name": "Run Task", + "Filters": [ + { + "EventType": "Elsa.RunTask" + } + ], + "Url": "https://localhost:6001/webhooks/run-task" + } + ] + }, + "Resilience": { + "Strategies": [ + { + "$type": "HttpResilienceStrategy", + "Id": "1", + "DisplayName": "Retry 3 times, exponential", + "MaxRetryAttempts": 3, + "BackoffType": "Exponential" + }, + { + "$type": "HttpResilienceStrategy", + "Id": "2", + "DisplayName": "Retry 10 times, linear", + "MaxRetryAttempts": 10, + "BackoffType": "Linear" + } + ] + } +} diff --git a/src/apps/Caspian.WorkflowFramework/Caspian.WorkflowFramework.csproj b/src/apps/Caspian.WorkflowFramework/Caspian.WorkflowFramework.csproj new file mode 100644 index 0000000..d9a20dd --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Caspian.WorkflowFramework.csproj @@ -0,0 +1,72 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/apps/Caspian.WorkflowFramework/Caspian.WorkflowFramework.nuspec b/src/apps/Caspian.WorkflowFramework/Caspian.WorkflowFramework.nuspec new file mode 100644 index 0000000..0c750ba --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Caspian.WorkflowFramework.nuspec @@ -0,0 +1,130 @@ + + + + Caspian.WorkflowFramework + 1.0.0-preview34 + Digital Banking Team + Caspian + Caspian workflow framework + https://caspco.ir + package-icon.png + readme.md + caspian, nextbank, workflow + LICENSE.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/Caspian.WorkflowFramework/Config/DependencyInjection.cs b/src/apps/Caspian.WorkflowFramework/Config/DependencyInjection.cs new file mode 100644 index 0000000..1c0dbb4 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Config/DependencyInjection.cs @@ -0,0 +1,685 @@ +using System.IO; +using System.Text.Encodings.Web; +using Caspian.WorkflowFramework.Contracts; +using Caspian.WorkflowFramework.Services; +using Elsa.Alterations.Extensions; +using Elsa.Alterations.MassTransit.Extensions; +using Elsa.Caching.Options; +using Elsa.Common.Codecs; +using Elsa.Common.DistributedHosting.DistributedLocks; +using Elsa.Common.RecurringTasks; +using Elsa.Common.Serialization; +using Elsa.Dapper.Extensions; +using Elsa.Dapper.Services; +using Elsa.DropIns.Extensions; +using Elsa.EntityFrameworkCore.Extensions; +using Elsa.EntityFrameworkCore.Modules.Alterations; +using Elsa.EntityFrameworkCore.Modules.Identity; +using Elsa.EntityFrameworkCore.Modules.Management; +using Elsa.EntityFrameworkCore.Modules.Runtime; +using Elsa.EntityFrameworkCore.Modules.Tenants; +using Elsa.Expressions.Helpers; +using Elsa.Extensions; +using Elsa.Features.Services; +using Elsa.Identity.Multitenancy; +using Elsa.Kafka; +using Elsa.Kafka.Factories; +using Elsa.MassTransit.Extensions; +using Elsa.MongoDb.Extensions; +using Elsa.MongoDb.Modules.Alterations; +using Elsa.MongoDb.Modules.Identity; +using Elsa.MongoDb.Modules.Management; +using Elsa.MongoDb.Modules.Runtime; +using Elsa.MongoDb.Modules.Tenants; +using Elsa.OpenTelemetry.Middleware; +using Elsa.Retention.Extensions; +using Elsa.Retention.Models; +using Elsa.Secrets.Extensions; +using Elsa.Secrets.Management.Tasks; +using Elsa.Secrets.Persistence; +using Elsa.Server.Web; +using Elsa.Server.Web.Extensions; +using Elsa.Server.Web.Filters; +using Elsa.Server.Web.Messages; +using Elsa.Sql.Extensions; +using Elsa.Sql.MySql; +using Elsa.Sql.PostgreSql; +using Elsa.Sql.Sqlite; +using Elsa.Sql.SqlServer; +using Elsa.Tenants.AspNetCore; +using Elsa.Tenants.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Api; +using Elsa.Workflows.CommitStates.Strategies; +using Elsa.Workflows.IncidentStrategies; +using Elsa.Workflows.LogPersistence; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Stores; +using Elsa.Workflows.Options; +using Elsa.Workflows.Runtime.Distributed.Extensions; +using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Stores; +using Elsa.Workflows.Runtime.Tasks; +using Hangfire; +using Hangfire.MemoryStorage; +using Hangfire.PostgreSql; +using Hangfire.PostgreSql.Factories; +using Hangfire.SqlServer; +using Hangfire.Storage.SQLite; +using JetBrains.Annotations; +using Medallion.Threading.FileSystem; +using Medallion.Threading.Postgres; +using Medallion.Threading.Redis; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Proto.Cluster.Kubernetes; +using Proto.Persistence.Sqlite; +using Proto.Persistence.SqlServer; +using Proto.Remote; +using Proto.Remote.GrpcNet; +using San.MongoLogger; +using San.MongoLogger.Models; +using StackExchange.Redis; + +namespace Caspian.WorkflowFramework; + +public static class DependencyInjection +{ + const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore; + const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; + const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.Memory; + const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; + const bool useDbContextPooling = false; + const bool useHangfire = false; + const bool useQuartz = true; + const bool useMassTransit = false; + const bool useZipCompression = false; + const bool runEFCoreMigrations = true; + const bool useMemoryStores = false; + const bool useCaching = true; + const bool useKafka = false; + const bool useReadOnlyMode = false; + const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests. + const bool useMultitenancy = false; + const bool useTenantsFromConfiguration = true; + const bool useSecrets = false; + const bool disableVariableWrappers = false; + const bool disableVariableCopying = false; + const bool useManualOtelInstrumentation = false; + + public static void AddWorkflowFramework(this IHostApplicationBuilder builder, string appsettingName) + { + var configuration = new ConfigurationBuilder() + .AddJsonFile(appsettingName, optional: false, reloadOnChange: true) + .Build(); + //var configuration = builder.Configuration; + var heartbeatSection = configuration.GetSection("Heartbeat"); + var identitySection = configuration.GetSection("Identity"); + var identityTokenSection = identitySection.GetSection("Tokens"); + var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!; + var sqlServerConnectionString = configuration.GetConnectionString("SqlServer")!; + var postgresConnectionString = configuration.GetConnectionString("PostgreSql")!; + var citusConnectionString = configuration.GetConnectionString("Citus")!; + var yugabyteDbConnectionString = configuration.GetConnectionString("YugabyteDb")!; + var oracleConnectionString = configuration.GetConnectionString("Oracle")!; + var mySqlConnectionString = configuration.GetConnectionString("MySql")!; + var cockroachDbConnectionString = configuration.GetConnectionString("CockroachDb")!; + var mongoDbConnectionString = configuration.GetConnectionString("MongoDb")!; + var azureServiceBusConnectionString = configuration.GetConnectionString("AzureServiceBus")!; + var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!; + var redisConnectionString = configuration.GetConnectionString("Redis")!; + var distributedLockProviderName = configuration.GetSection("Runtime:DistributedLocking")["Provider"]; + var appRole = Enum.Parse(configuration["AppRole"]! ?? "Default"); + var sqlDatabaseProvider = Enum.Parse(configuration["DatabaseProvider"] ?? "SqlServer"); + const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore; + const bool useDbContextPooling = false; + const bool useHangfire = false; + const bool useQuartz = false;//true; + const bool useMassTransit = false; + const bool useZipCompression = false; + const bool runEFCoreMigrations = true; + const bool useMemoryStores = false; + const bool useCaching = true; + const bool useKafka = false; + const bool useReadOnlyMode = false; + const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests. + const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; + const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.Memory; + const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; + const bool useMultitenancy = false; + const bool useTenantsFromConfiguration = false;// true; + const bool useSecrets = false; + const bool disableVariableWrappers = false; + const bool disableVariableCopying = false; + const bool useManualOtelInstrumentation = false; + + // Optionally create type aliases for easier configuration. + TypeAliasRegistry.RegisterAlias("OrderReceivedProducerFactory", typeof(GenericProducerFactory)); + TypeAliasRegistry.RegisterAlias("OrderReceivedConsumerFactory", typeof(GenericConsumerFactory)); + + if (useManualOtelInstrumentation) + { + builder.Services + .AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService("elsa-workflows", serviceVersion: "3.5.0").AddTelemetrySdk()) + .WithTracing(tracing => + { + tracing + .AddSource("*") + .SetSampler(new AlwaysOnSampler()) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddSqlClientInstrumentation() + .AddConsoleExporter() + .AddOtlpExporter() + ; + }) + .WithMetrics(metrics => + { + metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddConsoleExporter() + .AddOtlpExporter() + ; + }); + + // Enable OpenTelemetry Logging (optional) + builder.Logging.AddOpenTelemetry(options => + { + options.IncludeFormattedMessage = true; + options.IncludeScopes = true; + options.ParseStateValues = true; + }); + } + + // Add Elsa services. + builder.Services.AddScoped(); + + builder.Services.AddMongoLogger(builder.Configuration); + + builder.Services.AddElsa(elsa => + { + elsa + .UseWorkflows(workflows => + { + workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing()); + workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing()); + workflows.UseCommitStrategies(strategies => + { + strategies.AddStandardStrategies(); + strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10))); + }); + }) + .UseWorkflowManagement(management => + { + management.UseEntityFrameworkCore(ef => + { + ef.UseSqlServer(sqlServerConnectionString); + }); + + if (useZipCompression) + management.SetCompressionAlgorithm(nameof(Zstd)); + + if (useMemoryStores) + management.UseWorkflowInstances(feature => feature.WorkflowInstanceStore = sp => sp.GetRequiredService()); + + if (useMassTransit) + management.UseMassTransitDispatcher(); + + if (useCaching) + management.UseCache(); + + management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit); + management.UseReadOnlyMode(useReadOnlyMode); + management.AddVariableTypeAndAlias("Application"); + }) + .UseWorkflowRuntime(runtime => + { + runtime.UseEntityFrameworkCore(ef => + { + ef.UseContextPooling = useDbContextPooling; + + //ef.UseSqlServer(sqlServerConnectionString, new ElsaDbContextOptions); + var migrationsAssembly = typeof(Elsa.EntityFrameworkCore.SqlServer.IdentityDbContextFactory).Assembly; + var connectionString = sqlServerConnectionString; + ef.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlServer(migrationsAssembly, connectionString, null, configure => configure.CommandTimeout(60000)); + + ef.RunMigrations = runEFCoreMigrations; + }); + + if (workflowRuntime == WorkflowRuntime.Distributed) + { + runtime.UseDistributedRuntime(); + } + + if (workflowRuntime == WorkflowRuntime.ProtoActor) + { + runtime.UseProtoActor(); + } + + if (useMassTransit) + runtime.UseMassTransitDispatcher(); + + runtime.WorkflowDispatcherOptions = options => configuration.GetSection("Runtime:WorkflowDispatcher").Bind(options); + + if (useMemoryStores) + { + runtime.ActivityExecutionLogStore = sp => sp.GetRequiredService(); + runtime.WorkflowExecutionLogStore = sp => sp.GetRequiredService(); + } + + if (useCaching) + runtime.UseCache(); + + runtime.DistributedLockingOptions = options => configuration.GetSection("Runtime:DistributedLocking").Bind(options); + + runtime.DistributedLockProvider = _ => + { + switch (distributedLockProviderName) + { + case "Postgres": + return new PostgresDistributedSynchronizationProvider(postgresConnectionString, options => + { + options.KeepaliveCadence(TimeSpan.FromMinutes(5)); + options.UseMultiplexing(); + }); + case "Redis": + { + var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConnectionString); + var database = connectionMultiplexer.GetDatabase(); + return new RedisDistributedSynchronizationProvider(database); + } + case "File": + return new FileDistributedSynchronizationProvider(new(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks"))); + case "Noop": + default: + return new NoopDistributedSynchronizationProvider(); + } + }; + }) + .UseEnvironments(environments => environments.EnvironmentsOptions = options => configuration.GetSection("Environments").Bind(options)) + .UseScheduling(scheduling => + { + if (useHangfire) + scheduling.UseHangfireScheduler(); + + if (useQuartz) + scheduling.UseQuartzScheduler(); + }) + .UseWorkflowsApi(api => + { + api.AddFastEndpointsAssembly(); + }) + .UseCSharp(options => + { + options.DisableWrappers = disableVariableWrappers; + options.AppendScript("string Greet(string name) => $\"Hello {name}!\";"); + options.AppendScript("string SayHelloWorld() => Greet(\"World\");"); + }) + .UseJavaScript(options => + { + options.AllowClrAccess = true; + options.DisableWrappers = disableVariableWrappers; + options.DisableVariableCopying = disableVariableCopying; + options.RegisterType(); + options.ConfigureEngine(engine => + { + engine.Execute("function greet(name) { return `Hello ${name}!`; }"); + engine.Execute("function sayHelloWorld() { return greet('World'); }"); + }); + }) + .UsePython(python => + { + python.PythonOptions += options => + { + // Make sure to configure the path to the python DLL. E.g. /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11 + // alternatively, you can set the PYTHONNET_PYDLL environment variable. + configuration.GetSection("Scripting:Python").Bind(options); + + options.AddScript(sb => + { + sb.AppendLine("def greet():"); + sb.AppendLine(" return \"Hello, welcome to Python!\""); + }); + }; + }) + .UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default) + .UseHttp(http => + { + http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options); + + if (useCaching) + http.UseCache(); + }) + .UseSql(options => + { + options.Clients = client => + { + client.Register("MySql"); + client.Register("PostgreSql"); + client.Register("Sqlite"); + client.Register("Sql Server"); + }; + }) + .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) + .UseAlterations(alterations => + { + if (persistenceProvider == PersistenceProvider.MongoDb) + { + alterations.UseMongoDb(); + } + else if (persistenceProvider == PersistenceProvider.Dapper) + { + // TODO: alterations.UseDapper(); + } + else + { + alterations.UseEntityFrameworkCore(ef => + { + ef.UseContextPooling = useDbContextPooling; + ef.UseSqlServer(sqlServerConnectionString); + ef.RunMigrations = runEFCoreMigrations; + }); + } + + if (useMassTransit) + { + alterations.UseMassTransitDispatcher(); + } + }) + .UseOpenTelemetry(otel => otel.UseNewRootActivityForRemoteParent = true) + .UseWorkflowContexts(); + + if (useQuartz) + { + elsa.UseQuartz(quartz => + { + if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) + quartz.UseSqlite(sqliteConnectionString); + else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) + quartz.UsePostgreSql(postgresConnectionString); + }); + } + + if (useSignalR) + { + elsa.UseRealTimeWorkflows(); + } + + if (useMassTransit) + { + elsa.UseMassTransit(massTransit => + { + massTransit.DisableConsumers = appRole == ApplicationRole.Api; + massTransit.AddMessageType(); + + if (massTransitBroker == MassTransitBroker.AzureServiceBus) + { + massTransit.UseAzureServiceBus(azureServiceBusConnectionString, serviceBusFeature => serviceBusFeature.ConfigureTransportBus = (context, bus) => + { + bus.PrefetchCount = 50; + bus.LockDuration = TimeSpan.FromMinutes(5); + bus.MaxConcurrentCalls = 32; + bus.MaxDeliveryCount = 8; + // etc. + }); + } + + if (massTransitBroker == MassTransitBroker.RabbitMq) + { + massTransit.UseRabbitMq(rabbitMqConnectionString, rabbit => rabbit.ConfigureTransportBus = (context, bus) => + { + bus.PrefetchCount = 50; + bus.Durable = true; + bus.AutoDelete = false; + bus.ConcurrentMessageLimit = 32; + // etc. + }); + } + }); + } + + if (distributedCachingTransport != DistributedCachingTransport.None) + { + elsa.UseDistributedCache(distributedCaching => + { + if (distributedCachingTransport == DistributedCachingTransport.MassTransit) distributedCaching.UseMassTransit(); + if (distributedCachingTransport == DistributedCachingTransport.ProtoActor) distributedCaching.UseProtoActor(); + }); + } + + if (distributedCachingTransport == DistributedCachingTransport.ProtoActor || workflowRuntime == WorkflowRuntime.ProtoActor) + { + elsa.UseProtoActor(proto => + { + proto + .EnableMetrics() + .EnableTracing(); + + proto.PersistenceProvider = _ => + { + if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) + return new SqlServerProvider(sqlServerConnectionString, true, "", "proto_actor"); + return new SqliteProvider(new(sqliteConnectionString)); + }; + + if (configuration["KUBERNETES_SERVICE_HOST"] != null) + { + var kubernetesConfig = new KubernetesProviderConfig(); + var clusterProvider = new KubernetesProvider(kubernetesConfig); + + var remoteConfig = GrpcNetRemoteConfig + .BindToAllInterfaces(advertisedHost: configuration["ProtoActor:AdvertisedHost"]) // Environment variable to be provided by Kubernetes using pod.status.podIP. + .WithLogLevelForDeserializationErrors(LogLevel.Critical) + .WithRemoteDiagnostics(true); + + proto.CreateClusterProvider = _ => clusterProvider; + proto.ConfigureRemoteConfig = _ => remoteConfig; + } + }); + } + + if (useKafka) + { + elsa.UseKafka(kafka => + { + kafka.ConfigureOptions(options => configuration.GetSection("Kafka").Bind(options)); + }); + } + + if (useSecrets) + { + elsa + .UseSecrets() + .UseSecretsManagement(management => + { + management.ConfigureOptions(options => configuration.GetSection("Secrets:Management").Bind(options)); + if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) + management.UseEntityFrameworkCore(ef => + { + ef.UseContextPooling = useDbContextPooling; + ef.UseSqlServer(sqlServerConnectionString); + }); + else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) + management.UseEntityFrameworkCore(ef => + { + ef.UseContextPooling = useDbContextPooling; + ef.UsePostgreSql(postgresConnectionString); + }); + else + management.UseEntityFrameworkCore(ef => + { + ef.UseContextPooling = useDbContextPooling; + ef.UseSqlite(sp => sp.GetSqliteConnectionString()); + }); + }) + .UseSecretsApi() + .UseSecretsScripting(); + } + + //elsa.UseRetention(r => + //{ + // r.SweepInterval = TimeSpan.FromHours(5); + // r.AddDeletePolicy("Delete all finished workflows", sp => + // { + // var filter = new RetentionWorkflowInstanceFilter + // { + // WorkflowStatus = WorkflowStatus.Finished + // }; + // return filter; + // }); + //}); + + if (useMultitenancy) + { + elsa.UseTenants(tenants => + { + tenants.ConfigureMultitenancy(options => + { + options.TenantResolverPipelineBuilder + .Append() + .Append() + .Append() + .Append(); + }); + + if (useTenantsFromConfiguration) + { + tenants.UseConfigurationBasedTenantsProvider(options => configuration.GetSection("Multitenancy").Bind(options)); + } + else + { + tenants.UseStoreBasedTenantsProvider(); + + tenants.UseTenantManagement(management => + { + if (persistenceProvider == PersistenceProvider.MongoDb) + management.UseMongoDb(); + if (persistenceProvider == PersistenceProvider.Dapper) + throw new NotSupportedException("Dapper is not supported for tenant management."); + if (persistenceProvider == PersistenceProvider.EntityFrameworkCore) + { + management.UseEntityFrameworkCore(ef => + { + ef.UseContextPooling = useDbContextPooling; + if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) ef.UseSqlite(sqliteConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.Citus) ef.UsePostgreSql(citusConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.YugabyteDb) ef.UsePostgreSql(yugabyteDbConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) + ef.UseOracle(oracleConnectionString, new() + { + SchemaName = "ELSA" + }); +#if !NET9_0 + if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) + ef.UseMySql(mySqlConnectionString); +#endif + if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString); + }); + } + }); + + tenants.UseTenantManagementEndpoints(); + } + }); + + elsa.UseTenantHttpRouting(tenantHttpRouting => + { + // Override the tenant header name with a custom one. + tenantHttpRouting.WithTenantHeader("X-Tenant-ID"); + }); + } + + elsa.UseWebhooks(webhooks => webhooks.ConfigureSinks += options => builder.Configuration.GetSection("Webhooks").Bind(options)); + elsa.InstallDropIns(options => options.DropInRootDirectory = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "DropIns")); + elsa.AddSwagger(); + elsa.AddFastEndpointsAssembly(); + //ConfigureForTest?.Invoke(elsa); + }); + + // Obfuscate HTTP request headers. + builder.Services.AddActivityStateFilter(); + + // Optionally configure recurring tasks using alternative schedules. + builder.Services.Configure(options => + { + options.Schedule.ConfigureTask(TimeSpan.FromSeconds(300)); + options.Schedule.ConfigureTask(TimeSpan.FromSeconds(300)); + options.Schedule.ConfigureTask(TimeSpan.FromHours(4)); + options.Schedule.ConfigureTask(TimeSpan.FromSeconds(15)); + }); + + builder.Services.Configure(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); }); + builder.Services.Configure(options => options.Ttl = TimeSpan.FromSeconds(10)); + builder.Services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1)); + builder.Services.Configure(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy)); + builder.Services.AddHealthChecks(); + builder.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*"))); + + + builder.Services.AddScoped(); + } + + ///// + ///// The main entry point for the application made public for end to end testing. + ///// + //[UsedImplicitly] + //public partial class Program + //{ + // /// + // /// Set by the test runner to configure the module for testing. + // /// + // public static Action? ConfigureForTest { get; set; } + //} + + public static void UseWorkflowFramework(this IApplicationBuilder app) + { + //try + //{ + // app.UseMongoLogger(); + //} + //catch (Exception ex) + //{ + // var d = ex; + //} + + // Configure the pipeline. + var env = app.ApplicationServices.GetRequiredService(); + if (env.IsDevelopment()) + app.UseDeveloperExceptionPage(); + + // CORS. + app.UseCors(); + + // Health checks. + //app.MapHealthChecks("/"); + + // Routing used for SignalR. + app.UseRouting(); + + // Multitenancy. + if (useMultitenancy) + app.UseTenants(); + + // Elsa API endpoints for designer. + var routePrefix = app.ApplicationServices.GetRequiredService>().Value.RoutePrefix; + app.UseWorkflowsApi(routePrefix); + + // Captures unhandled exceptions and returns a JSON response. + app.UseJsonSerializationErrorHandler(); + + app.UseWorkflows(); + } +} + diff --git a/src/apps/Caspian.WorkflowFramework/Contracts/IWorkflowService.cs b/src/apps/Caspian.WorkflowFramework/Contracts/IWorkflowService.cs new file mode 100644 index 0000000..0b2db48 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Contracts/IWorkflowService.cs @@ -0,0 +1,27 @@ +using Caspian.WorkflowFramework.Models; + +namespace Caspian.WorkflowFramework.Contracts; +public interface IWorkflowService +{ + /// + /// Create and executes a new workflow instance. + /// + /// + /// A new object. + /// Create a new workflow instance and executes it and waits for it to complete or reach a suspend point. + Task> CreateAndExecuteAsync(CreateAndExecuteWorkflowModel input); + + /// + /// Publishes the specified event. + /// + /// + /// A new object. + Task> PublishEventAsync(PublishEventModel input); + + /// + /// Inquiry last state of workflow instance. + /// + /// + /// A new object. + Task> InquiryStateAsync(InquiryStateModel input); +} diff --git a/src/apps/Caspian.WorkflowFramework/FodyWeavers.xml b/src/apps/Caspian.WorkflowFramework/FodyWeavers.xml new file mode 100644 index 0000000..00e1d9a --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/apps/Caspian.WorkflowFramework/LICENSE.txt b/src/apps/Caspian.WorkflowFramework/LICENSE.txt new file mode 100644 index 0000000..df73626 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/LICENSE.txt @@ -0,0 +1,3 @@ +Terms and Conditions of Use, Reproduction and Distribution are permitted only for Caspian Company. + +Copyright [2025] Caspian Company \ No newline at end of file diff --git a/src/apps/Caspian.WorkflowFramework/Models/CreateAndExecuteWorkflowModel.cs b/src/apps/Caspian.WorkflowFramework/Models/CreateAndExecuteWorkflowModel.cs new file mode 100644 index 0000000..917adc5 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Models/CreateAndExecuteWorkflowModel.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Elsa.Common.Models; +using JetBrains.Annotations; + +namespace Caspian.WorkflowFramework.Models; +[PublicAPI] +public class CreateAndExecuteWorkflowModel +{ + /// + /// Workflow name + /// + [Required] + public string? Name { get; set; } + + /// + /// Workflow version + /// + [Required] + public VersionOptions Version { get; set; } + + /// + /// Current user's id + /// + [Required] + public long? UserId { get; set; } +} diff --git a/src/apps/Caspian.WorkflowFramework/Models/InquiryStateModel.cs b/src/apps/Caspian.WorkflowFramework/Models/InquiryStateModel.cs new file mode 100644 index 0000000..a549da8 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Models/InquiryStateModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Caspian.WorkflowFramework.Models; +public class InquiryStateModel +{ + [Required] + public string WorkflowInstanceId { get; set; } +} diff --git a/src/apps/Caspian.WorkflowFramework/Models/PublishEventModel.cs b/src/apps/Caspian.WorkflowFramework/Models/PublishEventModel.cs new file mode 100644 index 0000000..10cd5ad --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Models/PublishEventModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Caspian.WorkflowFramework.Models; +[PublicAPI] +public class PublishEventModel +{ + [Required] + public string WorkflowInstanceId { get; set; } + + [Required] + public string EventName { get; set; } + + [Required] + public string? EventData { get; set; } +} diff --git a/src/apps/Caspian.WorkflowFramework/Models/WorkflowModel.cs b/src/apps/Caspian.WorkflowFramework/Models/WorkflowModel.cs new file mode 100644 index 0000000..36fadfe --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Models/WorkflowModel.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Caspian.WorkflowFramework.Models; +[PublicAPI] +public class WorkflowModel +{ + /// + /// شناسه گردش کار + /// + public string? InstanceId { get; set; } + + /// + /// نام گردش کار + /// + public string? Name { get; set; } + + /// + /// اتمام گردش کار؟ + /// + public bool IsComplete { get; set; } + + /// + /// برای کدام کاربر؟ + /// + public long? UserId { get; set; } + + /// + /// داده صفحه جاری + /// + public string? CurrentPageData { get; set; } + + /// + /// داده صفحه بعد + /// + /// فعلا استفاده ای نداریم + /// + /// + public string? NextPageData { get; set; } + + /// + /// آدرس صفحه بعدی + /// + public string? NextPageUrl { get; set; } + + /// + /// رویدادی که باید پس از سابمیت صفحه بعدی منتشر شود + /// + public string? NextPageEventName { get; set; } +} diff --git a/src/apps/Caspian.WorkflowFramework/Models/WorkflowResponse.cs b/src/apps/Caspian.WorkflowFramework/Models/WorkflowResponse.cs new file mode 100644 index 0000000..494b5b3 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Models/WorkflowResponse.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Caspian.WorkflowFramework.Models; +[PublicAPI] +public class WorkflowResponse : WorkflowResponseBase where T : class, new() +{ + public WorkflowResponse() + { + Data = new T(); + } + + /// + /// پاسخ + /// + public T? Data { get; set; } +} + +[PublicAPI] +public class WorkflowResponseBase +{ + /// + /// وضعیت پاسخ + /// + public int? Status { get; set; } + + /// + /// پیام پاسخ + /// + public string? Message { get; set; } +} diff --git a/src/apps/Caspian.WorkflowFramework/Services/WorkflowService.cs b/src/apps/Caspian.WorkflowFramework/Services/WorkflowService.cs new file mode 100644 index 0000000..7ca42ac --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Services/WorkflowService.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure; +using Caspian.WorkflowFramework.Contracts; +using Caspian.WorkflowFramework.Models; +using Caspian.WorkflowFramework.Utility; +using Elsa.Common.Models; +using Elsa.JavaScript.Handlers; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Messages; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Win32; +using San.MongoLogger; +using Newtonsoft.Json; +using System.Diagnostics; + +namespace Caspian.WorkflowFramework.Services; +public class WorkflowService : IWorkflowService, ILoggable +{ + #region Fields + private readonly IServiceProvider _serviceProvider; + private readonly IWorkflowRuntime _workflowRuntime; + private readonly IWorkflowDefinitionService _workflowDefinitionService; + private readonly IWorkflowInstanceVariableManager _workflowInstanceVariableManager; + private readonly IWorkflowInstanceManager _workflowInstanceManager; + private readonly IEventPublisher _eventPublisher; + private readonly IMdbLogger _logger; + #endregion + + #region Ctor + public WorkflowService(IServiceProvider serviceProvider, + IMdbLogger logger) + { + _serviceProvider = serviceProvider; + _workflowRuntime = _serviceProvider.GetRequiredService(); + _workflowDefinitionService = _serviceProvider.GetRequiredService(); + _workflowInstanceVariableManager = _serviceProvider.GetRequiredService(); + _workflowInstanceManager = _serviceProvider.GetRequiredService(); + _eventPublisher = _serviceProvider.GetRequiredService(); + _logger = logger; + } + #endregion + + #region CreateAndExecuteAsync + /// + public async Task> CreateAndExecuteAsync(CreateAndExecuteWorkflowModel input) + { + var sw = Stopwatch.StartNew(); + try + { + _logger.Log("Start of CreateAndExecuteAsync", input); + + WorkflowResponse response; + + var workflowClient = await _workflowRuntime.CreateClientAsync(); + + var definition = await _workflowDefinitionService + .FindWorkflowDefinitionAsync(new WorkflowDefinitionFilter() + { + Name = input.Name + }); + + if (definition == null) + { + var msg = $"Workflow '{input.Name}' defintion not found"; + response = new WorkflowResponse() + { + Status = 1, + Message = msg, + Data = null + }; + + sw.Stop(); + + _logger.Log("End of CreateAndExecuteAsync", WFInstanceId: response.Data?.InstanceId, response, sw.ElapsedMilliseconds); + + return response; + } + + var instanceInput = new Dictionary { ["UserId"] = input.UserId }; + input.Version = VersionOptions.Published; + var newInstance = await workflowClient.CreateInstanceAsync(new CreateWorkflowInstanceRequest + { + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definition.DefinitionId, input.Version), + Input = instanceInput, + ExternalUserId = input.UserId + }); + + var result = await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty); + + var variables = await _workflowInstanceVariableManager.GetVariablesAsync(result.WorkflowInstanceId); + + // تبدیل به دیکشنری + var dictionary = variables.ToDictionary(a => a.Variable.Name, a => a.Value); + + var workflowModel = dictionary.ConvertToWorkflowModel(); + workflowModel.InstanceId = workflowClient.WorkflowInstanceId; + workflowModel.UserId = input.UserId; + + response = new WorkflowResponse() + { + Status = 0, + Message = "Success", + Data = workflowModel + }; + + sw.Stop(); + + _logger.Log("End of CreateAndExecuteAsync", WFInstanceId: response.Data?.InstanceId, response, sw.ElapsedMilliseconds); + + return response; + } + catch (Exception ex) + { + var response = new WorkflowResponse() + { + Status = 100, + Message = ex.Message, + Data = null + }; + + sw.Stop(); + + _logger.Log("Exception occurs in CreateAndExecuteAsync", WFInstanceId: response.Data?.InstanceId, response, sw.ElapsedMilliseconds, ex); + + return response; + } + } + #endregion + + #region PublishEventAsync + /// + public async Task> PublishEventAsync(PublishEventModel input) + { + var sw = Stopwatch.StartNew(); + try + { + _logger.Log("Start of PublishEventAsync", WFInstanceId: input.WorkflowInstanceId, input); + + WorkflowResponse response; + + var instance = await _workflowInstanceManager.FindByIdAsync(input.WorkflowInstanceId); + + if (instance == null) + { + var msg = $"Workflow instance with id '{input.WorkflowInstanceId}' not found"; + response = new WorkflowResponse() + { + Status = 1, + Message = msg, + Data = null + }; + + sw.Stop(); + + _logger.Log("End of PublishEventAsync", WFInstanceId: input.WorkflowInstanceId, response, sw.ElapsedMilliseconds); + + return response; + } + + await _eventPublisher.PublishAsync(input.EventName, null, input.WorkflowInstanceId, null, input.EventData); + + var variables = await _workflowInstanceVariableManager.GetVariablesAsync(input.WorkflowInstanceId); + + // تبدیل به دیکشنری + var dictionary = variables.ToDictionary(a => a.Variable.Name, a => a.Value); + + var workflowModel = dictionary.ConvertToWorkflowModel(); + workflowModel.InstanceId = instance.Id; + workflowModel.UserId = instance.ExternalUserId; + + response = new WorkflowResponse() + { + Status = 0, + Message = "Success", + Data = workflowModel + }; + + sw.Stop(); + + _logger.Log("End of PublishEventAsync", WFInstanceId: input.WorkflowInstanceId, response, sw.ElapsedMilliseconds); + + return response; + } + catch (Exception ex) + { + var response = new WorkflowResponse() + { + Status = 100, + Message = ex.Message, + Data = null + }; + + sw.Stop(); + + _logger.Log("Exception occurs in PublishEventAsync", WFInstanceId: input.WorkflowInstanceId, response, sw.ElapsedMilliseconds, ex); + + return response; + } + } + #endregion + + #region InquiryStateAsync + /// + public async Task> InquiryStateAsync(InquiryStateModel input) + { + var sw = Stopwatch.StartNew(); + try + { + _logger.Log("Start of InquiryStateAsync", WFInstanceId: input.WorkflowInstanceId, input); + + WorkflowResponse response; + + var instance = await _workflowInstanceManager.FindByIdAsync(input.WorkflowInstanceId); + + if (instance == null) + { + var msg = $"Workflow instance with id '{input.WorkflowInstanceId}' not found"; + response = new WorkflowResponse() + { + Status = 1, + Message = msg, + Data = null + }; + + sw.Stop(); + + _logger.Log("End of InquiryStateAsync", WFInstanceId: input.WorkflowInstanceId, response, sw.ElapsedMilliseconds); + + return response; + } + + var variables = await _workflowInstanceVariableManager.GetVariablesAsync(input.WorkflowInstanceId); + + // تبدیل به دیکشنری + var dictionary = variables.ToDictionary(a => a.Variable.Name, a => a.Value); + + var workflowModel = dictionary.ConvertToWorkflowModel(); + workflowModel.InstanceId = instance.Id; + workflowModel.UserId = instance.ExternalUserId; + + response = new WorkflowResponse() + { + Status = 0, + Message = "Success", + Data = workflowModel + }; + + sw.Stop(); + + _logger.Log("End of InquiryStateAsync", WFInstanceId: input.WorkflowInstanceId, response, sw.ElapsedMilliseconds); + + return response; + } + catch (Exception ex) + { + var response = new WorkflowResponse() + { + Status = 100, + Message = ex.Message, + Data = null + }; + + sw.Stop(); + + _logger.Log("Exception occurs in InquiryStateAsync", WFInstanceId: input.WorkflowInstanceId, response, sw.ElapsedMilliseconds, ex); + + return response; + } + } + #endregion +} diff --git a/src/apps/Caspian.WorkflowFramework/Utility/ConversionExtensions.cs b/src/apps/Caspian.WorkflowFramework/Utility/ConversionExtensions.cs new file mode 100644 index 0000000..b917ae1 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/Utility/ConversionExtensions.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Caspian.WorkflowFramework.Models; + +namespace Caspian.WorkflowFramework.Utility; +public static class ConversionExtensions +{ + public static WorkflowModel ConvertToWorkflowModel(this Dictionary dictionary) + { + var workflowModel = new WorkflowModel(); + + if (dictionary == null || dictionary.Count == 0) + return workflowModel; + + //Map dictionary values to WorkflowModel properties + workflowModel.InstanceId = dictionary.ContainsKey(nameof(workflowModel.InstanceId)) + ? dictionary[nameof(workflowModel.InstanceId)]?.ToString() + : null; + + workflowModel.Name = dictionary.ContainsKey(nameof(workflowModel.Name)) + ? dictionary[nameof(workflowModel.Name)]?.ToString() + : null; + + workflowModel.IsComplete = dictionary.ContainsKey(nameof(workflowModel.IsComplete)) + ? bool.TryParse(dictionary[nameof(workflowModel.IsComplete)]?.ToString(), out var isComplete) && isComplete + : false; + + workflowModel.UserId = dictionary.ContainsKey(nameof(workflowModel.UserId)) + ? long.TryParse(dictionary[nameof(workflowModel.UserId)]?.ToString(), out var userId) ? userId : (long?)null + : null; + + //workflowModel.CurrentPageData = dictionary.ContainsKey(nameof(workflowModel.CurrentPageData)) + // ? (object?)dictionary[nameof(workflowModel.CurrentPageData)] + // : null; + + workflowModel.CurrentPageData = dictionary.ContainsKey(nameof(workflowModel.CurrentPageData)) + ? dictionary[nameof(workflowModel.CurrentPageData)]?.ToString() + : null; + + //workflowModel.NextPageData = dictionary.ContainsKey(nameof(workflowModel.NextPageData)) + // ? (object?)dictionary[nameof(workflowModel.NextPageData)] + // : null; + + workflowModel.NextPageData = dictionary.ContainsKey(nameof(workflowModel.NextPageData)) + ? dictionary[nameof(workflowModel.NextPageData)]?.ToString() + : null; + + workflowModel.NextPageUrl = dictionary.ContainsKey(nameof(workflowModel.NextPageUrl)) + ? dictionary[nameof(workflowModel.NextPageUrl)]?.ToString() + : null; + + workflowModel.NextPageEventName = dictionary.ContainsKey(nameof(workflowModel.NextPageEventName)) + ? dictionary[nameof(workflowModel.NextPageEventName)]?.ToString() + : null; + + return workflowModel; + } +} diff --git a/src/apps/Caspian.WorkflowFramework/favicon.ico b/src/apps/Caspian.WorkflowFramework/favicon.ico new file mode 100644 index 0000000..f39a7b9 Binary files /dev/null and b/src/apps/Caspian.WorkflowFramework/favicon.ico differ diff --git a/src/apps/Caspian.WorkflowFramework/gallery-icon.png b/src/apps/Caspian.WorkflowFramework/gallery-icon.png new file mode 100644 index 0000000..c401de4 Binary files /dev/null and b/src/apps/Caspian.WorkflowFramework/gallery-icon.png differ diff --git a/src/apps/Caspian.WorkflowFramework/nuget_command.txt b/src/apps/Caspian.WorkflowFramework/nuget_command.txt new file mode 100644 index 0000000..7e09d5e --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/nuget_command.txt @@ -0,0 +1 @@ +nuget pack Caspian.WorkflowFramework.nuspec -IncludeReferencedProjects \ No newline at end of file diff --git a/src/apps/Caspian.WorkflowFramework/package-icon.png b/src/apps/Caspian.WorkflowFramework/package-icon.png new file mode 100644 index 0000000..05adb12 Binary files /dev/null and b/src/apps/Caspian.WorkflowFramework/package-icon.png differ diff --git a/src/apps/Caspian.WorkflowFramework/readme.md b/src/apps/Caspian.WorkflowFramework/readme.md new file mode 100644 index 0000000..c1954a1 --- /dev/null +++ b/src/apps/Caspian.WorkflowFramework/readme.md @@ -0,0 +1,35 @@ +# Caspian.NextBank.Workflow + +Workflows for project "NextBankParsian" + +## Installation +```bash +dotnet add package Caspian.NextBank.Workflow +``` +```bash +install-package Caspian.NextBank.Workflow +``` + +## Usage Example +In your Program.cs : +```csharp +builder.Services.AddMongoLogger(builder.Configuration); + +builder.AddWorkflowServices("DefaultConnection"); + +var app = builder.Build(); + +app.UseMongoLogger(); + +app.UseWorkflow(); +``` +Then use service "WfService" + +## Features +- Start new instance of every workflow +- Publish event and get next step information +- Get last status of workflow instance + +## Version History +### 1.0.0-alfa +- Initial prerelease \ No newline at end of file diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index 737250e..818f03e 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -5,6 +5,33 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + "LogDatabaseSettings": { + "ConnectionString": "mongodb://localhost", + "DatabaseName": "Elsa.Server.Web.DB", + "LogsCollectionName": "Col", + "BatchSize": 0, + "RollingInterval": "Daily", + "MaskList": [ + "code", + "depositNumber", + "username", + "customerNo", + "password", + "sessionId", + "mainAccount", + "accountNumber", + "owners", + "balance", + "accountNo", + "depositUniqueId", + "externalRefList", + "externalref", + "sameSignatureDepositNumber", + "mobileNumber", + "destinationNumber", + "cardNumber" + ] + }, "HostBuilder": { "reloadConfigOnChange": true }, @@ -24,7 +51,7 @@ "RabbitMq": "", //"amqp://guest:guest@localhost:5672", "Redis": "" //"localhost:6379,abortConnect=false" }, - "DatabaseProvider": "SqlServer",//"Sqlite", + "DatabaseProvider": "SqlServer", //"Sqlite", "Multitenancy": { "Tenants": [ { diff --git a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json index 6878f72..c2f42a4 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json +++ b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json @@ -7,6 +7,33 @@ "OpenTelemetry": "Debug" } }, + "LogDatabaseSettings": { + "ConnectionString": "mongodb://localhost", + "DatabaseName": "Elsa.ServerAndStudio.Web.DB", + "LogsCollectionName": "Col", + "BatchSize": 0, + "RollingInterval": "Daily", + "MaskList": [ + "code", + "depositNumber", + "username", + "customerNo", + "password", + "sessionId", + "mainAccount", + "accountNumber", + "owners", + "balance", + "accountNo", + "depositUniqueId", + "externalRefList", + "externalref", + "sameSignatureDepositNumber", + "mobileNumber", + "destinationNumber", + "cardNumber" + ] + }, "AllowedHosts": "*", "ConnectionStrings": { "Sqlite": "", //"Data Source=elsa.sqlite.db;Cache=Shared;",