2025-12-10 19:48:50 +00:00
using System.Runtime.CompilerServices ;
2025-05-20 11:38:24 +00:00
using System.Text.Json ;
using Elsa.Expressions.Models ;
using Elsa.Extensions ;
using Elsa.Http.Bookmarks ;
using Elsa.Http.Extensions ;
using Elsa.Http.UIHints ;
using Elsa.Workflows ;
using Elsa.Workflows.Attributes ;
using Elsa.Workflows.UIHints ;
using Elsa.Workflows.Models ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Routing ;
using Microsoft.Extensions.DependencyInjection ;
namespace Elsa.Http ;
/// <summary>
/// Wait for an inbound HTTP request that matches the specified path and methods.
/// </summary>
[Activity("Elsa", "HTTP", "Wait for an inbound HTTP request that matches the specified path and methods.", DisplayName = "HTTP Endpoint")]
[Output(IsSerializable = false)]
public class HttpEndpoint : Trigger < HttpRequest >
{
internal const string HttpContextInputKey = "HttpContext" ;
internal const string PathInputKey = "Path" ;
/// <inheritdoc />
public HttpEndpoint ( [ CallerFilePath ] string? source = null , [ CallerLineNumber ] int? line = null ) : base ( source , line )
{
}
Add Elsa Script DSL (#7076)
* Update packages.yml
* Update elsa-server-and-studio.yml
* Update elsa-server.yml
* Update elsa-studio.yml (#6715)
* Update ListWorkflowDefinitionsRequest.cs (#6761)
Remove unnecessary line breaks
* Correct namespace and import for `ConfigureEngineWithVariableTypes`.
* Resolves build issues, update package versions and restructure project references
- Updated multiple package versions in `Directory.Packages.props` for better dependency management, including `BenchmarkDotNet`, `FastEndpoints`, and `Microsoft.Extensions.Http.Resilience`.
- Minor version upgrade for `System.Formats.Asn1` in `_build.csproj`.
- Replaced project reference to `Elsa.csproj` with `Elsa.IO.Http.csproj` in `Elsa.ServerAndStudio.Web.csproj`, enhancing modularity.
- Added new using directive for `Elsa.IO.Http.Features` in `Program.cs` to support new HTTP functionalities.
* Remove unused project references from Elsa.sln
These changes indicate that the associated projects or dependencies are no longer needed or have been replaced by other components in the solution.
* Rename copilot-setup-steps.yml.yml to copilot-setup-steps.yml
* Update RawStringContent encoding in JsonContentFactory (#6786)
* Update RawStringContent encoding in JsonContentFactory
Modified the instantiation of `RawStringContent` to use a
new `UTF8Encoding` instance with `encoderShouldEmitUTF8Identifier`
set to `false`, affecting the handling of the UTF-8 byte order
mark (BOM) in serialized JSON content. Fixes a bug with content length being different than expected.
* Refactor JsonContentFactory to reuse UTF8Encoding
Introduced a private static readonly field `_utf8Encoding` in the `JsonContentFactory` class to improve code readability and performance. This change replaces the instantiation of `UTF8Encoding` in the `CreateHttpContent` method, allowing for the reuse of the same encoding instance.
---------
Co-authored-by: Max Brooks <Max@compyl.com>
* Enhance thread safety with ConcurrentDictionary usage (#6760)
* Enhance thread safety with ConcurrentDictionary usage
Replaced `IDictionary` with `ConcurrentDictionary` for
both `_scheduledTasks` and `_scheduledTaskKeys` to
improve thread safety in a multi-threaded environment.
Updated methods `RegisterScheduledTask`,
`RemoveScheduledTask`, and `RemoveScheduledTasks` to
utilize the `Remove` method of `ConcurrentDictionary`,
ensuring safe and efficient removal of scheduled tasks.
* Refactor task registration and removal logic
Updated `RegisterScheduledTask` to use `AddOrUpdate` for streamlined task management. This change simplifies the addition and updating of scheduled tasks by consolidating logic into a single operation. Introduced `RemoveScheduledTask` method to handle task removal by name, improving code organization and clarity.
* Improve task removal handling in LocalScheduler
Modified the `LocalScheduler` class to enhance the removal process of scheduled tasks from the `_scheduledTaskKeys` collection. The removal operation now captures the result in a variable and includes a conditional check to log a warning if the task was not found, improving error handling and debugging capabilities.
* Refactor task removal in LocalScheduler
Updated the removal process for scheduled tasks in `_scheduledTasks`.
The new implementation collects all corresponding keys and attempts to remove them individually, logging warnings for any failures. This enhances error handling and provides better debugging information.
---------
Co-authored-by: Max Brooks <Max@compyl.com>
* Add IAsyncEnumerable check to ItemSourceActivityExecutionContextExtensions.GetItemSource (#6897)
* Use FullName in WorkflowDictionary (#6923)
* Fixed ParentWorkflowInstanceId not being set (#7029)
Co-authored-by: Peter Klooster <peter.klooster@autotaalglas.nl>
* Remove unused solution projects and update package references
- Deleted several project references from `Elsa.sln` to clean up the solution.
- Updated `Directory.Packages.props` for consistency and alignment with the latest package versions.
* Simplify CI pipeline by removing `Test` step from `Compile+Test+Pack` process.
* Initial plan
* Add ElsaScript DSL module with parser and compiler
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
* Add integration tests for ElsaScript DSL
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
* Add comprehensive documentation for ElsaScript DSL
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
* Refactor workflow activity instantiation logic
- Removed `ActivityFactory` and its related interfaces and extensions.
- Introduced `ActivityActivator` for handling activity creation.
- Extended AST with support for comprehensive workflow structures:
- Added nodes for flowcharts, if/else, loops, and variable declarations.
- Updated `IElsaScriptCompiler` to use asynchronous methods.
- Expanded `ElsaScriptParser` to simplify syntax for `UseNode` and argument parsing.
- Adjusted compiler and parser for compatibility with new workflow AST model.
* Refactor test method names for clarity and add new compiler and parser tests
- Updated method names in `CompilerTests` and `ParserTests` for better readability and description of test intent.
- Added tests for compiler and parser:
- Support for workflows without the `workflow` keyword.
* Refactor `ElsaScriptParser` to improve statement parsing and introduce a tokenizer
- Added `TokenizeStatements` method to split source into statements for enhanced parsing accuracy.
- Updated logic to process statements instead of raw lines, reducing parsing complexity and improving reliability.
- Improved handling of workflow and statement parsing, including edge cases with braces, parentheses, and string literals.
* Introduce ElsaScript support for BlobStorage workflow provider
- Added the `Elsa.WorkflowProviders.BlobStorage.ElsaScript` module to enable ElsaScript-based workflow definitions for BlobStorage.
- Implemented `ElsaScriptBlobWorkflowFormatHandler` for parsing ElsaScript workflows stored in BlobStorage.
- Extended `ElsaScriptParser` to leverage Parlot for improved DSL parsing.
- Introduced `IBlobWorkflowFormatHandler` to centralize workflow format handling and parsing.
- Updated `Elsa.Server.Web` to reference the new module and include an ElsaScript "Hello World" example workflow.
* Refactor ElsaScript services, update logging, and improve workflow handling
- Changed `ElsaScriptCompiler` service registration from `Singleton` to `Scoped` for better dependency management.
- Enhanced the "Hello World" example workflow and added `CopyToOutputDirectory` configuration.
- Removed unused namespaces and adjusted references in multiple projects to improve maintainability.
- Updated logging levels in `appsettings.json` to reduce unnecessary debug output.
- Improved `PolymorphicObjectConverter` by removing redundant dependencies.
- Added missing references to enhance feature support and ensure compatibility.
* Refactor activity instantiation and improve argument handling in `ElsaScriptCompiler`
- Added support for positional arguments with constructor matching logic.
- Refactored `InstantiateActivityUsingConstructor` to enhance activity creation.
- Updated `ActivityDescriptor` and related types to include `ClrType` for streamlined activity resolution.
- Simplified `TypedActivityProvider` by annotating it with `[UsedImplicitly]`.
- Adjusted `ElsaScriptParser` to remove unnecessary options from string literal definitions.
* Add HTTP-enabled "Hello World" workflow and support for additional HTTP activity constructors
- Introduced a new ElsaScript example workflow `hello-world-http.elsa` with an HTTP endpoint and response.
- Enhanced `HttpEndpoint` and `WriteHttpResponse` activities with additional constructors for improved flexibility.
- Updated project to include the new workflow in the output directory.
* Enhance `ElsaScriptParser` with a custom parser to handle nested raw expressions for ElsaScript workflows
- Introduced `RawExpressionParser` to parse raw text after `=>` up to a matching closing parenthesis.
- Updated `elsaExpressionWithLang` and `elsaExpressionWithoutLang` to use `RawExpressionParser`.
- Trimmed whitespace in parsed expressions.
- Added integration and parser tests for complex workflows with variables and expressions.
- Updated example workflow `hello-world-http.elsa` to demonstrate expression usage.
- Added `Elsa.Http` module reference to enable HTTP-based activities.
* Update "Hello World" workflow to simplify naming and enhance response logic
- Renamed workflow from `HelloWorldHttpDsl2` to `HelloWorldHttpDsl`.
- Updated HTTP endpoint path to `/hello-world-dsl` for consistency.
- Improved response logic by utilizing `getMessage()` JavaScript function.
* Add support for `OriginalSource` in workflow materialization and enhance ElsaScript materializer
- Introduced `OriginalSource` property in `WorkflowDefinition` and `MaterializedWorkflow` for preserving original source representation (e.g., ElsaScript, JSON, YAML).
- Added `ElsaScriptWorkflowMaterializer` implementation to materialize workflows directly from ElsaScript source.
- Updated `DefaultWorkflowDefinitionStorePopulator` to determine `StringData` or `OriginalSource` based on materialized workflow format.
- Enhanced `WorkflowDefinitionMapper` to support symmetric round-tripping with `OriginalSource`.
- Registered `ElsaScriptWorkflowMaterializer` in `ElsaScriptFeature` for dependency injection.
- Updated `JsonBlobWorkflowFormatHandler` and added `OriginalSource` support for round-trip preservation.
- Simplified `ElsaScriptParser` by aligning variable and parser naming.
* Update V3_6 migrations for PostgreSQL, MySQL, and Oracle databases and associated designer files.
* Handle disposal and race conditions in `ScheduledCronTask`
- Added `_disposed` flag to prevent accessing disposed resources.
- Updated `_executionSemaphore` and `_scopeFactory` logic to safely handle `ObjectDisposedException`.
- Enhanced task scheduling and timer disposal with additional safeguards against race conditions.
- Modified tests to ensure proper disposal and logging behavior when handling edge cases.
* Add support for metadata in ElsaScript workflows and enhance parser and compiler functionality
- Introduced metadata syntax in ElsaScript workflows (e.g., `DisplayName`, `Description`, `Version`) to enable metadata-driven behavior.
- Enhanced `ElsaScriptCompiler` to process metadata and properly integrate it into `Workflow` objects.
- Updated `ElsaScriptParser` to parse program-level AST with support for multiple workflows and global use statements.
- Refactored tests to validate metadata parsing and ensure backward compatibility with existing workflows.
- Added new test cases to cover scenarios like metadata parsing, compilation, and multi-workflow programs.
* Add support for `foreach` loops in ElsaScript and remove `let` keyword
- Introduced `foreach` loop syntax in `ElsaScriptParser` and `ElsaScriptCompiler`, enabling iteration over collections with optional variable declaration.
- Updated `ForNode` and `ForEachNode` to include a `DeclaresVariable` flag for improved variable handling.
- Removed support for the `let` keyword in variable declarations, streamlining syntax to use `var` and `const` only.
- Enhanced `for` loop syntax to support optional `var` declaration and block or single-statement bodies.
- Refactored test cases to validate `foreach` and `for` loop enhancements and ensure backward compatibility.
* Simplify ElsaScript workflow syntax by removing redundant quotes in workflow identifiers and updating `for` loop syntax for clarity and consistency.
* Remove redundant quotes from workflow identifiers in integration tests
* Simplify Elsa scripts and improve error handling
- Removed redundant braces in workflow declarations for streamlined syntax.
- Enhanced logging in `JsonBlobWorkflowFormatHandler` and `ElsaScriptBlobWorkflowFormatHandler` to warn on parsing errors and provide context.
- Updated configuration to log errors for `Elsa.Workflows.ActivityRegistry`.
- Refined "Hello World" and "For Loop" workflows for clarity and added improved loop handling.
* Refine Elsa workflows and update compiler logic
- Simplified "Hello World" workflow by adding braces and improving consistency.
- Adjusted "For Loop" workflow to rename and clarify logic, including expression updates and variable handling.
- Fixed compiler mapping of `"cs"` to `"CSharp"` for better clarity.
- Enhanced "Hello World HTTP" workflow to correctly reference `variables.message` in expressions.
* Add flowchart support in ElsaScript parser, compiler, and integration tests
- Introduced `flowchart` syntax in `ElsaScriptParser` to support flowchart-based workflows.
- Updated `ElsaScriptCompiler` to compile `flowchart` nodes with labeled activities, connections, entry points, and variables.
- Added integration tests for parsing and compiling empty and simple flowcharts.
- Enhanced `FlowchartNode` and `LabeledActivityNode` for better representation of flowchart structures.
- Improved error handling and logging for invalid flowchart configurations.
* Add tests for compiling and parsing flowcharts with nodes, connections, and block nodes in ElsaScript
- Added integration tests for compiling and validating flowchart structures, including activities, connections, and entry points.
- Implemented parser tests for parsing flowcharts with node connections and block nodes.
- Updated project files to include new workflow examples for testing.
* Add Parlot package and update project file in integration tests
- Added `Parlot` package version `0.0.27` to `Directory.Packages.props`.
- Updated integration test project file to include a new `Include` directive for better targeting.
* Update Parlot package to version 1.5.2 in Directory.Packages.props
* Remove `elsa-server-and-studio.yml` workflow and update solution file
- Deleted `elsa-server-and-studio.yml` workflow as it's no longer needed.
- Updated `Elsa.sln` to remove reference to the deleted workflow.
* Remove `elsa-studio.yml` workflow and update solution and packages
- Deleted `elsa-studio.yml` workflow as it's no longer used.
- Updated `Elsa.sln` to remove reference to the deleted workflow.
- Changed `base_version` in `packages.yml` from `3.7.0` to `3.6.0`.
* Downgrade Docker image in `elsa-server.yml` workflow from `v3.7.0-preview` to `v3.6.0-preview`
* Update Docker image tag in `elsa-server.yml` workflow from `v3.6.0-preview` to `v3.6-preview`
* Add logging support to `LocalScheduler` and replace `Debug.WriteLine` with `ILogger`
* Remove unused `System.Collections.Generic` and `Elsa.Extensions` imports in `LocalScheduler`
- Cleaned up unnecessary using directives to improve code readability and maintainability.
- Minor whitespace adjustment for consistent formatting.
* Remove unnecessary whitespace in `LocalScheduler` for consistent formatting
* Improve exception handling in blob workflow format handlers
- Updated exception handling in `ElsaScriptBlobWorkflowFormatHandler` and `JsonBlobWorkflowFormatHandler` to gracefully catch and log all exceptions during workflow parsing.
- Adjusted comments to clarify behavior for invalid user-provided files, ensuring the workflow loading process is not disrupted.
* Refactor blob workflow format handlers to use `SupportedExtensions` for improved file filtering
- Added `SupportedExtensions` property to all blob format handlers to optimize blob storage browsing.
- Simplified `CanHandle` logic by removing extension checks, leveraging `SupportedExtensions` for initial filtering.
- Updated comments for clarity and consistency across handlers.
* Refactor `DefaultWorkflowDefinitionStorePopulator` to simplify `stringData` assignment logic and improve readability
* Remove outdated comment in `CompilerTests` about skipped tests
* Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Refactor `ElsaScriptCompiler` to streamline type conversion logic, improve language mapping, and enhance asynchronous flowchart compilation
* [WIP] Update ParseError printing based on feedback (#7082)
* Initial plan
* Fix ParseError formatting to use Message and Position properties
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
* Replace `as` casts with direct casts in ParserTests for null safety (#7083)
* Initial plan
* Replace 'as' casts with direct casts in ParserTests for better null safety
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
* Fix Oracle column types for OriginalSource and other large text fields (#7079)
* Initial plan
* Fix Oracle OriginalSource and StringData column types to handle large data
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
* Refactor tests to replace type checks with `Assert.IsType` for improved clarity and type safety
* Initial plan (#7080)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* Add `Parlot` package reference and update solution structure by removing and reorganizing projects and workflows.
* Set default expression language to "JavaScript" in `ElsaScriptCompiler`.
* Add integration test to verify default expression language resets between ElsaScript compilations
* Simplify UTF-8 encoding in JsonContentFactory (#7081)
* Initial plan
* Remove explicit UTF8Encoding in JsonContentFactory and use Encoding.UTF8
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
* Fix test to use Encoding.UTF8.GetByteCount for multi-byte character support
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
---------
Co-authored-by: Ender <37611092+zengande@users.noreply.github.com>
Co-authored-by: Matt <knibbsy10@live.com>
Co-authored-by: Max Brooks <45081361+MaxBrooks114@users.noreply.github.com>
Co-authored-by: Max Brooks <Max@compyl.com>
Co-authored-by: FuJa0815 <30809803+FuJa0815@users.noreply.github.com>
Co-authored-by: Peter Klooster <crashkonijn@gmail.com>
Co-authored-by: Peter Klooster <peter.klooster@autotaalglas.nl>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 18:57:50 +00:00
/// <inheritdoc />
public HttpEndpoint ( Input < string > path , Input < string > method , [ CallerFilePath ] string? source = null , [ CallerLineNumber ] int? line = null ) : base ( source , line )
{
Path = path ;
SupportedMethods = new ( ObjectLiteral . From ( new [ ] { method } ) ) ;
}
/// <inheritdoc />
public HttpEndpoint ( Input < string > path , [ CallerFilePath ] string? source = null , [ CallerLineNumber ] int? line = null ) : base ( source , line )
{
Path = path ;
}
2025-05-20 11:38:24 +00:00
/// <summary>
/// The path to associate with the workflow.
/// </summary>
[ Input (
Description = "The path to associate with the workflow." ,
UIHint = InputUIHints . SingleLine ,
UIHandler = typeof ( HttpEndpointPathUIHandler )
) ]
public Input < string > Path { get ; set ; } = null ! ;
/// <summary>
/// The HTTP methods to accept.
/// </summary>
[ Input (
Description = "The HTTP methods to accept." ,
Options = new [ ] { "GET" , "POST" , "PUT" , "HEAD" , "DELETE" } ,
UIHint = InputUIHints . CheckList ) ]
public Input < ICollection < string > > SupportedMethods { get ; set ; } = new ( ObjectLiteral . From ( new [ ] { HttpMethods . Get } ) ) ;
/// <summary>
/// Allow authenticated requests only.
/// </summary>
[Input(Description = "Allow authenticated requests only.", Category = "Security")]
public Input < bool > Authorize { get ; set ; } = new ( false ) ;
/// <summary>
/// Provide a policy to evaluate. If the policy fails, the request is forbidden.
/// </summary>
[Input(Description = "Provide a policy to evaluate. If the policy fails, the request is forbidden.", Category = "Security")]
public Input < string? > Policy { get ; set ; } = new ( default ( string? ) ) ;
/// <summary>
/// The maximum time allowed to process the request.
/// </summary>
[Input(Description = "The maximum time allowed to process the request.", Category = "Upload")]
public Input < TimeSpan ? > RequestTimeout { get ; set ; } = null ! ;
/// <summary>
/// The maximum request size allowed in bytes.
/// </summary>
[Input(Description = "The maximum request size allowed in bytes.", Category = "Upload")]
public Input < long? > RequestSizeLimit { get ; set ; } = null ! ;
/// <summary>
/// The maximum request size allowed in bytes.
/// </summary>
[Input(Description = "The maximum file size allowed in bytes for an individual file.", Category = "Upload")]
public Input < long? > FileSizeLimit { get ; set ; } = null ! ;
/// <summary>
/// The allowed file extensions,
/// </summary>
[Input(Description = "Only file extensions in this list are allowed. Leave empty to allow all extensions", Category = "Upload", UIHint = InputUIHints.MultiText)]
public Input < ICollection < string > > AllowedFileExtensions { get ; set ; } = null ! ;
/// <summary>
/// The allowed file extensions,
/// </summary>
[Input(Description = "File extensions in this list are forbidden. Leave empty to not block any extension.", Category = "Upload", UIHint = InputUIHints.MultiText)]
public Input < ICollection < string > > BlockedFileExtensions { get ; set ; } = null ! ;
/// <summary>
/// The allowed file extensions,
/// </summary>
[Input(Description = "Only MIME types in this list are allowed. Leave empty to allow all types", Category = "Upload", UIHint = InputUIHints.MultiText)]
public Input < ICollection < string > > AllowedMimeTypes { get ; set ; } = null ! ;
/// <summary>
/// A value indicating whether to expose the "Request too large" outcome.
/// </summary>
[Input(Description = "A value indicating whether to expose the \"Request too large\" outcome.", Category = "Outcomes")]
public bool ExposeRequestTooLargeOutcome { get ; set ; }
/// <summary>
/// A value indicating whether to expose the "File too large" outcome.
/// </summary>
[Input(Description = "A value indicating whether to expose the \"File too large\" outcome.", Category = "Outcomes")]
public bool ExposeFileTooLargeOutcome { get ; set ; }
/// <summary>
/// A value indicating whether to expose the "Invalid file extension" outcome.
/// </summary>
[Input(Description = "A value indicating whether to expose the \"Invalid file extension\" outcome.", Category = "Outcomes")]
public bool ExposeInvalidFileExtensionOutcome { get ; set ; }
/// <summary>
/// A value indicating whether to expose the "Invalid file MIME type" outcome.
/// </summary>
[Input(Description = "A value indicating whether to expose the \"Invalid file MIME type\" outcome.", Category = "Outcomes")]
public bool ExposeInvalidFileMimeTypeOutcome { get ; set ; }
/// <summary>
/// The parsed request content, if any.
/// </summary>
[Output(Description = "The parsed request content, if any.")]
public Output < object? > ParsedContent { get ; set ; } = null ! ;
/// <summary>
/// The uploaded files, if any.
/// </summary>
[Output(Description = "The uploaded files, if any.", IsSerializable = false)]
public Output < IFormFile [ ] > Files { get ; set ; } = null ! ;
2025-10-25 09:58:34 +00:00
/// <summary>
/// The first uploaded file, if any.
/// </summary>
[Output(Description = "The first uploaded file, if any.", IsSerializable = false)]
public Output < IFormFile ? > File { get ; set ; } = null ! ;
2025-05-20 11:38:24 +00:00
/// <summary>
/// The parsed route data, if any.
/// </summary>
[Output(Description = "The parsed route data, if any.")]
public Output < IDictionary < string , object > > RouteData { get ; set ; } = null ! ;
/// <summary>
/// The querystring data, if any.
/// </summary>
[Output(Description = "The querystring data, if any.")]
public Output < IDictionary < string , object > > QueryStringData { get ; set ; } = null ! ;
/// <summary>
/// The headers, if any.
/// </summary>
[Output(Description = "The headers, if any.")]
public Output < IDictionary < string , object > > Headers { get ; set ; } = null ! ;
/// <inheritdoc />
protected override IEnumerable < object > GetTriggerPayloads ( TriggerIndexingContext context )
{
context . TriggerName = HttpStimulusNames . HttpEndpoint ;
return GetBookmarkPayloads ( context . ExpressionExecutionContext ) ;
}
/// <inheritdoc />
protected override async ValueTask ExecuteAsync ( ActivityExecutionContext context )
{
var path = Path . Get ( context ) ;
var methods = SupportedMethods . GetOrDefault ( context ) ? ? new List < string > { HttpMethods . Get } ;
2025-12-10 19:48:50 +00:00
await context . WaitForHttpRequestAsync ( path , methods , OnResumeAsync , Elsa . Http . HttpStimulusNames . HttpEndpoint ) ;
2025-05-20 11:38:24 +00:00
}
private async ValueTask OnResumeAsync ( ActivityExecutionContext context )
{
var httpContextAccessor = context . GetRequiredService < IHttpContextAccessor > ( ) ;
var httpContext = httpContextAccessor . HttpContext ;
if ( httpContext = = null )
{
// We're executing in a non-HTTP context (e.g. in a virtual actor).
// Create a bookmark to allow the invoker to export the state and resume execution from there.
context . CreateCrossBoundaryBookmark ( ) ;
return ;
}
await HandleRequestAsync ( context ) ;
}
private async Task HandleRequestAsync ( ActivityExecutionContext context )
{
var httpContextAccessor = context . GetRequiredService < IHttpContextAccessor > ( ) ;
var httpContext = httpContextAccessor . HttpContext ! ;
// Provide the received HTTP request as output.
var request = httpContext . Request ;
context . Set ( Result , request ) ;
// Read route data, if any.
var path = context . GetWorkflowInput < PathString > ( PathInputKey ) ;
var routeData = GetRouteData ( httpContext , path ) ;
var routeDictionary = routeData . Values . ToDictionary ( route = > route . Key , route = > route . Value ! ) ;
var queryStringDictionary = httpContext . Request . Query . ToObjectDictionary ( ) ;
var headersDictionary = httpContext . Request . Headers . ToObjectDictionary ( ) ;
context . Set ( RouteData , routeDictionary ) ;
context . Set ( QueryStringData , queryStringDictionary ) ;
context . Set ( Headers , headersDictionary ) ;
// Validate request size.
if ( ! ValidateRequestSize ( context , httpContext ) )
{
await HandleRequestTooLargeAsync ( context , httpContext ) ;
return ;
}
// Handle Form Fields
if ( request . HasFormContentType )
{
var formFields = request . Form . ToObjectDictionary ( ) ;
ParsedContent . Set ( context , formFields ) ;
// Read files, if any.
var files = ReadFilesAsync ( context , request ) ;
if ( files . Any ( ) )
{
if ( ! ValidateFileSizes ( context , httpContext , files ) )
{
await HandleFileSizeTooLargeAsync ( context , httpContext ) ;
return ;
}
if ( ! ValidateFileExtensionWhitelist ( context , httpContext , files ) )
{
await HandleInvalidFileExtensionWhitelistAsync ( context , httpContext ) ;
return ;
}
if ( ! ValidateFileExtensionBlacklist ( context , httpContext , files ) )
{
await HandleInvalidFileExtensionBlacklistAsync ( context , httpContext ) ;
return ;
}
if ( ! ValidateFileMimeTypes ( context , httpContext , files ) )
{
await HandleInvalidFileMimeTypesAsync ( context , httpContext ) ;
return ;
}
Files . Set ( context , files . ToArray ( ) ) ;
2025-10-25 09:58:34 +00:00
File . Set ( context , files . FirstOrDefault ( ) ) ;
2025-05-20 11:38:24 +00:00
}
}
else
{
// Parse Non-Form content.
try
{
var content = await ParseContentAsync ( context , request ) ;
ParsedContent . Set ( context , content ) ;
}
catch ( JsonException e )
{
await HandleInvalidJsonPayloadAsync ( context , httpContext , e ) ;
throw ;
}
}
// Complete.
await context . CompleteActivityAsync ( ) ;
}
private IFormFileCollection ReadFilesAsync ( ActivityExecutionContext context , HttpRequest request )
{
return request . HasFormContentType ? request . Form . Files : new FormFileCollection ( ) ;
}
private bool ValidateRequestSize ( ActivityExecutionContext context , HttpContext httpContext )
{
var requestSizeLimit = RequestSizeLimit . GetOrDefault ( context ) ;
if ( ! requestSizeLimit . HasValue )
return true ;
var requestSize = httpContext . Request . ContentLength ? ? 0 ;
return requestSize < = requestSizeLimit ;
}
private async Task HandleRequestTooLargeAsync ( ActivityExecutionContext context , HttpContext httpContext )
{
var exposeRequestTooLargeOutcome = ExposeRequestTooLargeOutcome ;
if ( exposeRequestTooLargeOutcome )
{
await context . CompleteActivityWithOutcomesAsync ( "Request too large" ) ;
}
else
{
var response = httpContext . Response ;
response . StatusCode = StatusCodes . Status413PayloadTooLarge ;
await response . WriteAsJsonAsync ( new
{
Message = $"The maximum request size allowed is {RequestSizeLimit.Get(context)} bytes."
} ) ;
await response . Body . FlushAsync ( ) ;
}
}
private bool ValidateFileSizes ( ActivityExecutionContext context , HttpContext httpContext , IFormFileCollection files )
{
var fileSizeLimit = FileSizeLimit . GetOrDefault ( context ) ;
if ( ! fileSizeLimit . HasValue )
return true ;
if ( ! files . Any ( file = > file . Length > fileSizeLimit . Value ) )
return true ;
return false ;
}
private async Task HandleFileSizeTooLargeAsync ( ActivityExecutionContext context , HttpContext httpContext )
{
var exposeFileTooLargeOutcome = ExposeFileTooLargeOutcome ;
if ( exposeFileTooLargeOutcome )
{
await context . CompleteActivityWithOutcomesAsync ( "File too large" ) ;
}
else
{
var response = httpContext . Response ;
response . StatusCode = StatusCodes . Status413PayloadTooLarge ;
await response . WriteAsJsonAsync ( new
{
Message = $"The maximum file size allowed is {FileSizeLimit.Get(context)} bytes."
} ) ;
await response . Body . FlushAsync ( ) ;
}
}
private bool ValidateFileExtensionWhitelist ( ActivityExecutionContext context , HttpContext httpContext , IFormFileCollection files )
{
var allowedFileExtensions = AllowedFileExtensions . GetOrDefault ( context ) ;
if ( allowedFileExtensions = = null | | ! allowedFileExtensions . Any ( ) )
return true ;
if ( files . All ( file = > allowedFileExtensions . Contains ( System . IO . Path . GetExtension ( file . FileName ) , StringComparer . OrdinalIgnoreCase ) ) )
return true ;
return false ;
}
private async Task HandleInvalidFileExtensionWhitelistAsync ( ActivityExecutionContext context , HttpContext httpContext )
{
if ( ExposeInvalidFileExtensionOutcome )
{
await context . CompleteActivityWithOutcomesAsync ( "Invalid file extension" ) ;
return ;
}
var response = httpContext . Response ;
var allowedFileExtensions = AllowedFileExtensions . GetOrDefault ( context ) ! ;
response . StatusCode = StatusCodes . Status415UnsupportedMediaType ;
await response . WriteAsJsonAsync ( new
{
Message = $"Only the following file extensions are allowed: {string.Join(" , ", allowedFileExtensions)}"
} ) ;
await response . Body . FlushAsync ( ) ;
}
private bool ValidateFileExtensionBlacklist ( ActivityExecutionContext context , HttpContext httpContext , IFormFileCollection files )
{
var blockedFileExtensions = BlockedFileExtensions . GetOrDefault ( context ) ;
if ( blockedFileExtensions = = null | | ! blockedFileExtensions . Any ( ) )
return true ;
if ( ! files . Any ( file = > blockedFileExtensions . Contains ( System . IO . Path . GetExtension ( file . FileName ) , StringComparer . OrdinalIgnoreCase ) ) )
return true ;
return false ;
}
private async Task HandleInvalidFileExtensionBlacklistAsync ( ActivityExecutionContext context , HttpContext httpContext )
{
if ( ExposeInvalidFileExtensionOutcome )
{
await context . CompleteActivityWithOutcomesAsync ( "Invalid file extension" ) ;
return ;
}
var blockedFileExtensions = BlockedFileExtensions . GetOrDefault ( context ) ! ;
var response = httpContext . Response ;
response . StatusCode = StatusCodes . Status415UnsupportedMediaType ;
await response . WriteAsJsonAsync ( new
{
Message = $"The following file extensions are not allowed: {string.Join(" , ", blockedFileExtensions)}"
} ) ;
await response . Body . FlushAsync ( ) ;
}
private bool ValidateFileMimeTypes ( ActivityExecutionContext context , HttpContext httpContext , IFormFileCollection files )
{
var allowedMimeTypes = AllowedMimeTypes . GetOrDefault ( context ) ;
if ( allowedMimeTypes = = null | | ! allowedMimeTypes . Any ( ) )
return true ;
if ( files . All ( file = > allowedMimeTypes . Contains ( file . ContentType , StringComparer . OrdinalIgnoreCase ) ) )
return true ;
return false ;
}
private async Task HandleInvalidFileMimeTypesAsync ( ActivityExecutionContext context , HttpContext httpContext )
{
if ( ExposeInvalidFileMimeTypeOutcome )
{
await context . CompleteActivityWithOutcomesAsync ( "Invalid file MIME type" ) ;
return ;
}
var allowedMimeTypes = AllowedMimeTypes . GetOrDefault ( context ) ! ;
var response = httpContext . Response ;
response . StatusCode = StatusCodes . Status415UnsupportedMediaType ;
await response . WriteAsJsonAsync ( new
{
Message = $"Only the following MIME types are allowed: {string.Join(" , ", allowedMimeTypes)}"
} ) ;
await response . Body . FlushAsync ( ) ;
}
private async Task HandleInvalidJsonPayloadAsync ( ActivityExecutionContext context , HttpContext httpContext , JsonException exception )
{
var response = httpContext . Response ;
response . StatusCode = StatusCodes . Status400BadRequest ;
await response . WriteAsJsonAsync ( new
{
exception . Message ,
exception . Path ,
exception . LineNumber ,
} ) ;
await response . Body . FlushAsync ( ) ;
}
private async Task < object? > ParseContentAsync ( ActivityExecutionContext context , HttpRequest httpRequest )
{
if ( ! HasContent ( httpRequest ) )
return null ;
var cancellationToken = context . CancellationToken ;
var targetType = ParsedContent . GetTargetType ( context ) ;
var contentStream = httpRequest . Body ;
var contentType = httpRequest . ContentType ! ;
var headers = httpRequest . Headers . ToDictionary ( x = > x . Key , x = > x . Value . ToArray ( ) ) ;
2025-11-12 15:56:31 +00:00
return await context . ParseContentAsync ( contentStream , contentType , targetType , headers ! , cancellationToken ) ;
2025-05-20 11:38:24 +00:00
}
private static bool HasContent ( HttpRequest httpRequest ) = > httpRequest . Headers . ContentLength > 0 ;
private IEnumerable < object > GetBookmarkPayloads ( ExpressionExecutionContext context )
{
// Generate bookmark data for path and selected methods.
var normalizedRoute = context . Get ( Path ) ! . NormalizeRoute ( ) ;
var methods = SupportedMethods . GetOrDefault ( context ) ? ? new List < string > { HttpMethods . Get } ;
var authorize = Authorize . GetOrDefault ( context ) ;
var policy = Policy . GetOrDefault ( context ) ;
var requestTimeout = RequestTimeout . GetOrDefault ( context ) ;
var requestSizeLimit = RequestSizeLimit . GetOrDefault ( context ) ;
return methods
. Select ( x = > new HttpEndpointBookmarkPayload ( normalizedRoute , x . ToLowerInvariant ( ) , authorize , policy , requestTimeout , requestSizeLimit ) )
. Cast < object > ( )
. ToArray ( ) ;
}
private static RouteData GetRouteData ( HttpContext httpContext , string path )
{
var routeData = httpContext . GetRouteData ( ) ;
var routeTable = httpContext . RequestServices . GetRequiredService < IRouteTable > ( ) ;
var routeMatcher = httpContext . RequestServices . GetRequiredService < IRouteMatcher > ( ) ;
var matchingRouteQuery =
from route in routeTable
let routeValues = routeMatcher . Match ( route . Route , path )
where routeValues ! = null
select new { route , routeValues } ;
var matchingRoute = matchingRouteQuery . FirstOrDefault ( ) ;
if ( matchingRoute = = null )
return routeData ;
foreach ( var ( key , value ) in matchingRoute . routeValues ! )
routeData . Values [ key ] = value ;
return routeData ;
}
2025-10-25 09:58:34 +00:00
}