elsa-core/src/apps/Elsa.Server.Web/Program.cs
Sipke Schoorstra f9055a1041
feat(apps): enable BPMN interchange in the Elsa.Server.Web sample host (#8068)
* feat(apps): enable BPMN interchange in the Elsa.Server.Web sample host

Turns on .UseBpmnInterchange() unconditionally alongside the host's other
showcase features so the analyze endpoint (and BPMN execution) can be
exercised end-to-end against a live server, per #8055 (part of #7909).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(bpmn): fix sample host method names to UseBpmn/UseBpmnInterchange

The enabling-BPMN sample referenced AddBpmn()/AddBpmnInterchange(), which
do not exist; the real extension methods are UseBpmn() and
UseBpmnInterchange().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(bpmn): note dev-certs trust prerequisite for analyze example

The login curl in the "Trying it" walkthrough fails TLS verification
when the local ASP.NET Core development certificate isn't trusted,
leaving TOKEN empty. Document the one-time dotnet dev-certs --trust
step instead of suggesting curl -k.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 00:39:29 -07:00

314 lines
12 KiB
C#

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<bool?>("DistributedRuntime:AllowLocalLockProviderInDistributedRuntime");
var allowLocalDistributedRuntimeLockProvider =
configuredAllowLocalDistributedRuntimeLockProvider ?? builder.Environment.IsDevelopment();
services
.AddElsa(elsa =>
{
elsa
.AddActivitiesFrom<Program>()
.AddActivityHost<Penguin>()
.AddWorkflowsFrom<Program>()
.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()
.UseWorkflowRuntimeDashboard()
.UseFluentStorageProvider()
.UseElsaScriptBlobStorage()
.UseScheduling()
.UseBpmnInterchange()
.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<CurrentUserTenantResolver>());
});
}
if(useStructuredLogs)
{
elsa
.UseStructuredLogs()
.UseStructuredLogsDashboard();
}
ConfigureForTest?.Invoke(elsa);
});
// Obfuscate HTTP request headers.
services.AddActivityStateFilter<HttpRequestAuthenticationHeaderFilter>();
// Optionally configure recurring tasks using alternative schedules.
services.Configure<RecurringTaskOptions>(options =>
{
options.Schedule.ConfigureTask<TriggerBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
options.Schedule.ConfigureTask<PurgeBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
options.Schedule.ConfigureTask<RestartInterruptedWorkflowsTask>(TimeSpan.FromSeconds(15));
});
services.Configure<RuntimeOptions>(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); });
services.Configure<BookmarkQueuePurgeOptions>(options => options.Ttl = TimeSpan.FromSeconds(3600));
services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));
services.Configure<IncidentOptions>(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy));
if (useIngressRateLimiting)
{
services.PostConfigure<ApiEndpointOptions>(options =>
{
if (options.RateLimitingPolicyName == null)
options.RateLimitingPolicyName = elsaApiRateLimitingPolicy;
});
services.PostConfigure<HttpActivityOptions>(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<IOptions<ApiEndpointOptions>>().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<IOptions<HttpActivityOptions>>().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();
/// <summary>
/// The main entry point for the application made public for end to end testing.
/// </summary>
[UsedImplicitly]
public partial class Program
{
/// <summary>
/// Set by the test runner to configure the module for testing.
/// </summary>
public static Action<IModule>? ConfigureForTest { get; set; }
}