using System.Text.Encodings.Web; using Elsa.Agents; using Elsa.Alterations.Extensions; using Elsa.Alterations.MassTransit.Extensions; using Elsa.Caching.Options; 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.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.Tenants.AspNetCore; using Elsa.Tenants.Extensions; using Elsa.Workflows; using Elsa.Workflows.Api; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Management; using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Stores; 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.Data.Sqlite; using Microsoft.Extensions.Options; using Proto.Cluster.Kubernetes; using Proto.Persistence.Sqlite; using Proto.Persistence.SqlServer; using Proto.Remote; using Proto.Remote.GrpcNet; using StackExchange.Redis; // ReSharper disable RedundantAssignment const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore; const bool useHangfire = false; const bool useQuartz = true; const bool useMassTransit = true; const bool useZipCompression = false; const bool runEFCoreMigrations = true; const bool useMemoryStores = false; const bool useCaching = true; const bool useAzureServiceBus = false; 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.MassTransit; const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; const bool useMultitenancy = true; const bool useTenantsFromConfiguration = true; const bool useAgents = false; const bool useSecrets = false; const bool disableVariableWrappers = false; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; var configuration = builder.Configuration; 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 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"] ?? "Sqlite"); // Optionally create type aliases for easier configuration. TypeAliasRegistry.RegisterAlias("OrderReceivedProducerFactory", typeof(GenericProducerFactory)); TypeAliasRegistry.RegisterAlias("OrderReceivedConsumerFactory", typeof(GenericConsumerFactory)); // Add Elsa services. services .AddElsa(elsa => { if (persistenceProvider == PersistenceProvider.MongoDb) elsa.UseMongoDb(mongoDbConnectionString); if (persistenceProvider == PersistenceProvider.Dapper) elsa.UseDapper(dapper => { dapper.UseMigrations(feature => { if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) feature.UseSqlServer(); else feature.UseSqlite(); }); dapper.DbConnectionProvider = sp => { if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) return new SqlServerDbConnectionProvider(sqlServerConnectionString!); else return new SqliteDbConnectionProvider(sqliteConnectionString); }; }); if (useHangfire) { JobStorage jobStorage; if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) { jobStorage = new PostgreSqlStorage(new NpgsqlConnectionFactory(postgresConnectionString, new() { QueuePollInterval = TimeSpan.FromSeconds(1) })); } else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) { jobStorage = new SQLiteStorage(sqliteConnectionString, new() { QueuePollInterval = TimeSpan.FromSeconds(1) }); } else if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) { jobStorage = new SqlServerStorage(sqlServerConnectionString, new() { QueuePollInterval = TimeSpan.FromSeconds(1) }); } else { jobStorage = new MemoryStorage(); } elsa.UseHangfire(hangfire => hangfire.UseJobStorage(jobStorage)); } elsa .AddActivitiesFrom() .AddWorkflowsFrom() .UseFluentStorageProvider() .UseFileStorage() .UseIdentity(identity => { if (persistenceProvider == PersistenceProvider.MongoDb) identity.UseMongoDb(); else if (persistenceProvider == PersistenceProvider.Dapper) identity.UseDapper(); else identity.UseEntityFrameworkCore(ef => { if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString!); else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); ef.RunMigrations = runEFCoreMigrations; }); identity.TokenOptions = options => identityTokenSection.Bind(options); identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options)); identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options)); identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options)); }) .UseDefaultAuthentication() .UseWorkflows(workflows => { workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing()); workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing()); }) .UseWorkflowManagement(management => { if (persistenceProvider == PersistenceProvider.MongoDb) management.UseMongoDb(); else if (persistenceProvider == PersistenceProvider.Dapper) management.UseDapper(); else management.UseEntityFrameworkCore(ef => { if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString!); else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); ef.RunMigrations = runEFCoreMigrations; }); 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 => { if (persistenceProvider == PersistenceProvider.MongoDb) runtime.UseMongoDb(); else if (persistenceProvider == PersistenceProvider.Dapper) runtime.UseDapper(); else runtime.UseEntityFrameworkCore(ef => { if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) { //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)); } else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); 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 DirectoryInfo(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.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); }; }) .UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default) .UseHttp(http => { http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options); if (useCaching) http.UseCache(); }) .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 => { if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString); else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); ef.RunMigrations = runEFCoreMigrations; }); } if (useMassTransit) { alterations.UseMassTransitDispatcher(); } }) .UseWorkflowContexts(); if (useQuartz) { elsa.UseQuartz(quartz => { quartz.UseSqlite(sqliteConnectionString); }); } if (useSignalR) { elsa.UseRealTimeWorkflows(); } if (useMassTransit) { elsa.UseMassTransit(massTransit => { massTransit.DisableConsumers = appRole == ApplicationRole.Api; if (massTransitBroker == MassTransitBroker.AzureServiceBus) { massTransit.UseAzureServiceBus(azureServiceBusConnectionString, serviceBusFeature => serviceBusFeature.ConfigureServiceBus = 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.ConfigureServiceBus = 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 SqliteConnectionStringBuilder(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 (useAzureServiceBus) { elsa.UseAzureServiceBus(azureServiceBusConnectionString, asb => { asb.AzureServiceBusOptions = options => configuration.GetSection("AzureServiceBus").Bind(options); }); } if (useKafka) { elsa.UseKafka(kafka => { kafka.ConfigureOptions(options => configuration.GetSection("Kafka").Bind(options)); }); } if (useAgents) { elsa .UseAgentActivities() .UseAgentPersistence(persistence => persistence.UseEntityFrameworkCore(ef => ef.UseSqlite(sp => sp.GetSqliteConnectionString()))) .UseAgentsApi() ; services.Configure(options => builder.Configuration.GetSection("Agents").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.UseSqlServer(sqlServerConnectionString) ); else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) management.UseEntityFrameworkCore(ef => ef.UsePostgreSql(postgresConnectionString) ); else management.UseEntityFrameworkCore(ef => { 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 => { if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) ef.UseSqlite(sqliteConnectionString); if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString); if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString); #if !NET9_0 if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); #endif if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString); }); } }); tenants.UseTenantManagementEndpoints(); } }); elsa.UseTenantHttpRouting(); } elsa.InstallDropIns(options => options.DropInRootDirectory = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "DropIns")); elsa.AddSwagger(); elsa.AddFastEndpointsAssembly(); ConfigureForTest?.Invoke(elsa); }); // Obfuscate HTTP request headers. services.AddActivityStateFilter(); // Optionally configure recurring tasks using alternative schedules. services.Configure(options => { options.Schedule.ConfigureTask(TimeSpan.FromSeconds(300)); options.Schedule.ConfigureTask(TimeSpan.FromSeconds(300)); options.Schedule.ConfigureTask(TimeSpan.FromHours(4)); }); services.Configure(options => options.Ttl = TimeSpan.FromSeconds(10)); services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1)); services.AddHealthChecks(); services.AddControllers(); services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*"))); // Build the web application. var app = builder.Build(); // Configure the pipeline. if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage(); // CORS. app.UseCors(); // Health checks. app.MapHealthChecks("/"); // Routing used for SignalR. app.UseRouting(); // Security. app.UseAuthentication(); app.UseAuthorization(); // Multitenancy. if (useMultitenancy) app.UseTenants(); // Elsa API endpoints for designer. var routePrefix = app.Services.GetRequiredService>().Value.RoutePrefix; app.UseWorkflowsApi(routePrefix); // Captures unhandled exceptions and returns a JSON response. app.UseJsonSerializationErrorHandler(); // Elsa HTTP Endpoint activities. app.UseWorkflows(); app.MapControllers(); // Swagger API documentation. if (app.Environment.IsDevelopment()) { app.UseSwaggerUI(); } // SignalR. if (useSignalR) { app.UseWorkflowsSignalRHubs(); } // Run. await app.RunAsync(); /// /// 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; } }