using System.Text.Encodings.Web; using System.Threading.RateLimiting; using Elsa.Caching.Options; using Elsa.Common.RecurringTasks; using Elsa.Expressions.Helpers; using Elsa.Extensions; using Elsa.Features.Services; using Elsa.Http.Options; using Elsa.Identity.Multitenancy; using Elsa.Persistence.EFCore.Extensions; using Elsa.Persistence.EFCore.Modules.Management; using Elsa.Persistence.EFCore.Modules.Runtime; using Elsa.Server.Web.Activities; using Elsa.Server.Web.ActivityHosts; using Elsa.Server.Web.Filters; using Elsa.Tenants; using Elsa.Tenants.AspNetCore; using Elsa.Tenants.Extensions; using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Extensions; using Elsa.Workflows; using Elsa.Workflows.Activities.Flowchart.Extensions; using Elsa.Workflows.Api; using Elsa.Workflows.CommitStates.Strategies; using Elsa.Workflows.IncidentStrategies; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Options; using Elsa.Workflows.Runtime.Distributed.Extensions; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; // ReSharper disable RedundantAssignment const bool useReadOnlyMode = false; const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests. const bool useStructuredLogs = false; // Enable to inspect backend logs from Elsa Studio. const bool useMultitenancy = true; const bool disableVariableWrappers = false; const string elsaApiRateLimitingPolicy = "elsa-api"; const string httpWorkflowRateLimitingPolicy = "elsa-http-workflows"; ObjectConverter.StrictMode = true; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; var configuration = builder.Configuration; var identitySection = configuration.GetSection("Identity"); var identityTokenSection = identitySection.GetSection("Tokens"); var ingressRateLimitingSection = configuration.GetSection("IngressRateLimiting"); var useIngressRateLimiting = ingressRateLimitingSection.GetValue("Enabled", false); var registerIngressRateLimitingPolicies = ingressRateLimitingSection.GetValue("RegisterReferencePolicies", useIngressRateLimiting); var configuredAllowLocalDistributedRuntimeLockProvider = configuration.GetValue("DistributedRuntime:AllowLocalLockProviderInDistributedRuntime"); var allowLocalDistributedRuntimeLockProvider = configuredAllowLocalDistributedRuntimeLockProvider ?? builder.Environment.IsDevelopment(); services .AddElsa(elsa => { elsa .AddActivitiesFrom() .AddActivityHost() .AddWorkflowsFrom() .UseIdentity(identity => { 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.UseCommitStrategies(strategies => { strategies.AddStandardStrategies(); strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10))); }); }) .UseFlowchart(flowchart => flowchart.UseTokenBasedExecution()) .UseWorkflowManagement(management => { management.UseEntityFrameworkCore(ef => ef.UseSqlite()); management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit); management.UseCache(); management.UseReadOnlyMode(useReadOnlyMode); }) .UseWorkflowRuntime(runtime => { runtime.UseEntityFrameworkCore(ef => ef.UseSqlite()); runtime.UseCache(); runtime.UseDistributedRuntime(); // This sample host acknowledges single-host file-system locks in development or explicit single-host opt-in. runtime.DistributedLockingOptions = options => options.AllowLocalLockProviderInDistributedRuntime = allowLocalDistributedRuntimeLockProvider; }) .UseWorkflowsApi() .UseDashboardApi() .UseFluentStorageProvider() .UseElsaScriptBlobStorage() .UseScheduling() .UseCSharp(options => { configuration.GetSection("Scripting:CSharp").Bind(options); options.DisableWrappers = disableVariableWrappers; options.AppendScript("string Greet(string name) => $\"Hello {name}!\";"); options.AppendScript("string SayHelloWorld() => Greet(\"World\");"); }) .UseJavaScript(options => { options.AllowClrAccess = true; 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); http.UseCache(); }); if(useMultitenancy) { elsa.UseTenants(tenants => { tenants.UseConfigurationBasedTenantsProvider(options => configuration.GetSection("Multitenancy").Bind(options)); tenants.ConfigureMultitenancy(options => options.TenantResolverPipelineBuilder = new TenantResolverPipelineBuilder() .Append()); }); } if(useStructuredLogs) elsa.UseStructuredLogs(); 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.FromSeconds(15)); }); services.Configure(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); }); services.Configure(options => options.Ttl = TimeSpan.FromSeconds(3600)); services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1)); services.Configure(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy)); if (useIngressRateLimiting) { services.PostConfigure(options => { if (options.RateLimitingPolicyName == null) options.RateLimitingPolicyName = elsaApiRateLimitingPolicy; }); services.PostConfigure(options => { if (options.RateLimitingPolicyName == null) options.RateLimitingPolicyName = httpWorkflowRateLimitingPolicy; }); } services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; if (registerIngressRateLimitingPolicies) { options.AddFixedWindowLimiter(elsaApiRateLimitingPolicy, limiterOptions => { limiterOptions.PermitLimit = ingressRateLimitingSection.GetValue("ApiPermitLimit", 120); limiterOptions.Window = TimeSpan.FromSeconds(ingressRateLimitingSection.GetValue("ApiWindowSeconds", 60)); limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; limiterOptions.QueueLimit = ingressRateLimitingSection.GetValue("ApiQueueLimit", 0); }); options.AddFixedWindowLimiter(httpWorkflowRateLimitingPolicy, limiterOptions => { limiterOptions.PermitLimit = ingressRateLimitingSection.GetValue("HttpWorkflowPermitLimit", 60); limiterOptions.Window = TimeSpan.FromSeconds(ingressRateLimitingSection.GetValue("HttpWorkflowWindowSeconds", 60)); limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; limiterOptions.QueueLimit = ingressRateLimitingSection.GetValue("HttpWorkflowQueueLimit", 0); }); } }); services .AddHealthChecks() .AddElsaReadinessChecks(includeDistributedLocks: true); 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("/health/live", new() { Predicate = _ => false }); app.MapHealthChecks("/health/ready", new() { Predicate = check => check.Tags.Contains(HealthCheckExtensions.ElsaTag) && check.Tags.Contains(HealthCheckExtensions.ReadinessTag), ResultStatusCodes = { [HealthStatus.Degraded] = StatusCodes.Status503ServiceUnavailable, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }); app.MapHealthChecks("/", new() { Predicate = _ => false }); // Elsa API endpoints for designer. var apiEndpointOptions = app.Services.GetRequiredService>().Value; var routePrefix = apiEndpointOptions.RoutePrefix; app.MapWorkflowsApi(routePrefix); // Routing used for SignalR. app.UseRouting(); app.UseWorkflowsApiRateLimiting(routePrefix, apiEndpointOptions.RateLimitingPolicyName); // Elsa HTTP Endpoint activities. var httpActivityOptions = app.Services.GetRequiredService>().Value; app.UseWorkflowsRateLimiting(httpActivityOptions.BasePath, httpActivityOptions.RateLimitingPolicyName); if (useIngressRateLimiting || !string.IsNullOrWhiteSpace(apiEndpointOptions.RateLimitingPolicyName) || !string.IsNullOrWhiteSpace(httpActivityOptions.RateLimitingPolicyName)) app.UseRateLimiter(); // Security. app.UseAuthentication(); app.UseAuthorization(); // Multitenancy. if (useMultitenancy) app.UseTenants(); // Captures unhandled exceptions and returns a JSON response. app.UseJsonSerializationErrorHandler(); app.UseWorkflows(); app.MapControllers(); // Swagger API documentation. if (app.Environment.IsDevelopment()) { app.UseSwaggerUI(); } // SignalR. if (useSignalR) { app.UseWorkflowsSignalRHubs(); } // Structured log streaming for Studio diagnostics. if (useStructuredLogs) { app.UseStructuredLogs(); } // 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; } }