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>
This commit is contained in:
parent
3340e14b74
commit
a1d4e541fc
61
.github/workflows/elsa-studio.yml
vendored
61
.github/workflows/elsa-studio.yml
vendored
|
|
@ -1,61 +0,0 @@
|
|||
name: Elsa Studio Docker image
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- develop/3.6.0
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elsaworkflows/elsa-studio-v3-6-preview
|
||||
flavor: |
|
||||
latest=true
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=sha
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/ElsaStudio.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
@ -133,6 +133,7 @@
|
|||
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.14.0-beta.1"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.14.0-beta.1"/>
|
||||
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.26000"/>
|
||||
<PackageVersion Include="Parlot" Version="1.5.6"/>
|
||||
<PackageVersion Include="Polly" Version="8.6.5"/>
|
||||
<PackageVersion Include="Polly.Extensions" Version="8.6.5"/>
|
||||
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0"/>
|
||||
|
|
|
|||
93
Elsa.sln
93
Elsa.sln
|
|
@ -302,6 +302,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Scheduling.UnitTests",
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Common.IntegrationTests", "test\integration\Elsa.Common.IntegrationTests\Elsa.Common.IntegrationTests.csproj", "{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Dsl.ElsaScript", "src\modules\Elsa.Dsl.ElsaScript\Elsa.Dsl.ElsaScript.csproj", "{874F5A44-DB06-47AB-A18C-2D13942E0147}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Dsl.ElsaScript.IntegrationTests", "test\integration\Elsa.Dsl.ElsaScript.IntegrationTests\Elsa.Dsl.ElsaScript.IntegrationTests.csproj", "{4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.WorkflowProviders.BlobStorage.ElsaScript", "src\modules\Elsa.WorkflowProviders.BlobStorage.ElsaScript\Elsa.WorkflowProviders.BlobStorage.ElsaScript.csproj", "{2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{06DC6553-AAEF-4FC8-B16F-039F292E86A3}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.github\copilot-instructions.md = .github\copilot-instructions.md
|
||||
|
|
@ -313,7 +319,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
|
|||
.github\workflows\stale.yml = .github\workflows\stale.yml
|
||||
.github\workflows\pr.yml = .github\workflows\pr.yml
|
||||
.github\workflows\packages.yml = .github\workflows\packages.yml
|
||||
.github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml
|
||||
.github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml
|
||||
.github\workflows\docker-ca.yml = .github\workflows\docker-ca.yml
|
||||
.github\workflows\bounty.yml = .github\workflows\bounty.yml
|
||||
|
|
@ -330,6 +335,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issue_templates", "issue_te
|
|||
.github\ISSUE_TEMPLATE\documentation.md = .github\ISSUE_TEMPLATE\documentation.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dsl", "dsl", "{477C2416-312D-46AE-BCD6-8FA1FAB43624}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -454,74 +461,6 @@ Global
|
|||
{D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{75E08B0E-A41A-425C-9DCA-155505CE41CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{75E08B0E-A41A-425C-9DCA-155505CE41CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{75E08B0E-A41A-425C-9DCA-155505CE41CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{75E08B0E-A41A-425C-9DCA-155505CE41CD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BD120D97-4862-469E-B2CF-4109F7F21C20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BD120D97-4862-469E-B2CF-4109F7F21C20}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BD120D97-4862-469E-B2CF-4109F7F21C20}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BD120D97-4862-469E-B2CF-4109F7F21C20}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DB20B168-CD90-41F2-BE08-3D8C6F142459}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DB20B168-CD90-41F2-BE08-3D8C6F142459}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EB24F9FE-D7BD-4FCC-907E-AE400288C2A5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9CA02818-F7EB-4A0B-B27B-BC74ACD499C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C583AF05-D517-4B7F-8955-6B61500ED3D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
{6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
|
@ -642,6 +581,18 @@ Global
|
|||
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{874F5A44-DB06-47AB-A18C-2D13942E0147}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{874F5A44-DB06-47AB-A18C-2D13942E0147}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{874F5A44-DB06-47AB-A18C-2D13942E0147}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{874F5A44-DB06-47AB-A18C-2D13942E0147}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -740,6 +691,10 @@ Global
|
|||
{6C451CC5-280E-475E-B95E-23ADCF0CCBFD} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||
{2158FC82-6253-47F2-9216-B266710D50F1} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
|
||||
{36556068-64BF-4D4B-827E-EC8CDC23D516} = {06DC6553-AAEF-4FC8-B16F-039F292E86A3}
|
||||
{4B5A9D32-58F2-47A9-8A1B-8A9E2D76C3F5} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||
{2B7FB49D-E4B6-4AD5-981B-3D85B94F6F48} = {B08B4E00-C2AB-48F3-8389-449F42AEF179}
|
||||
{477C2416-312D-46AE-BCD6-8FA1FAB43624} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{874F5A44-DB06-47AB-A18C-2D13942E0147} = {477C2416-312D-46AE-BCD6-8FA1FAB43624}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
|
|
|
|||
3
run-dsl-tests.sh
Normal file
3
run-dsl-tests.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
# Run from the repository root, or adjust the path below as needed.
|
||||
dotnet test test/integration/Elsa.Dsl.ElsaScript.IntegrationTests/Elsa.Dsl.ElsaScript.IntegrationTests.csproj --verbosity normal
|
||||
|
|
@ -3,9 +3,12 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\modules\Elsa.Caching\Elsa.Caching.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Common\Elsa.Common.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Dsl.ElsaScript\Elsa.Dsl.ElsaScript.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Expressions\Elsa.Expressions.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Persistence.EFCore.Sqlite\Elsa.Persistence.EFCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Scheduling\Elsa.Scheduling.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.WorkflowProviders.BlobStorage.ElsaScript\Elsa.WorkflowProviders.BlobStorage.ElsaScript.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.WorkflowProviders.BlobStorage\Elsa.WorkflowProviders.BlobStorage.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Workflows.Runtime.Distributed\Elsa.Workflows.Runtime.Distributed.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
|
||||
|
|
@ -24,4 +27,19 @@
|
|||
<Folder Include="App_Data\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Workflows\hello-world.elsa">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Workflows\hello-world-http.elsa">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Workflows\for-loop.elsa">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Workflows\flowchart-test.elsa">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ using Elsa.Persistence.EFCore.Extensions;
|
|||
using Elsa.Persistence.EFCore.Modules.Management;
|
||||
using Elsa.Persistence.EFCore.Modules.Runtime;
|
||||
using Elsa.Server.Web.Filters;
|
||||
using Elsa.Tenants.AspNetCore;
|
||||
using Elsa.Tenants.Extensions;
|
||||
using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Api;
|
||||
using Elsa.Workflows.CommitStates.Strategies;
|
||||
using Elsa.Workflows.IncidentStrategies;
|
||||
|
|
@ -70,6 +74,8 @@ services
|
|||
runtime.UseDistributedRuntime();
|
||||
})
|
||||
.UseWorkflowsApi()
|
||||
.UseFluentStorageProvider()
|
||||
.UseElsaScriptBlobStorage()
|
||||
.UseScheduling()
|
||||
.UseCSharp(options =>
|
||||
{
|
||||
|
|
|
|||
16
src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa
Normal file
16
src/apps/Elsa.Server.Web/Workflows/flowchart-test.elsa
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use Elsa.Activities.Console;
|
||||
|
||||
workflow FlowchartTest {
|
||||
flowchart {
|
||||
var counter = 0;
|
||||
|
||||
entry Start;
|
||||
|
||||
Start: WriteLine("Starting flowchart");
|
||||
CheckCondition: WriteLine("Checking condition");
|
||||
EndNode: WriteLine("Done!");
|
||||
|
||||
Start -> CheckCondition;
|
||||
CheckCondition.Done -> EndNode;
|
||||
}
|
||||
}
|
||||
18
src/apps/Elsa.Server.Web/Workflows/for-loop.elsa
Normal file
18
src/apps/Elsa.Server.Web/Workflows/for-loop.elsa
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use expressions js;
|
||||
|
||||
workflow ForLoopTest
|
||||
{
|
||||
WriteLine("First loop")
|
||||
|
||||
for (var i = 0 through 10 step 1)
|
||||
{
|
||||
WriteLine(js => `Step: ${variables.i}`)
|
||||
}
|
||||
|
||||
WriteLine("Second loop")
|
||||
|
||||
for (var i = 0 to 10 step 2)
|
||||
WriteLine(cs => return $"Step: {Variables.Get("i")}";)
|
||||
|
||||
WriteLine("Done!")
|
||||
}
|
||||
8
src/apps/Elsa.Server.Web/Workflows/hello-world-http.elsa
Normal file
8
src/apps/Elsa.Server.Web/Workflows/hello-world-http.elsa
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
use expressions js;
|
||||
|
||||
workflow HelloWorldHttpDsl
|
||||
{
|
||||
var message = "Hello World from DSL via Expressions!";
|
||||
listen HttpEndpoint("/hello-world-dsl");
|
||||
WriteHttpResponse(js => `Message: ${variables.message}`);
|
||||
}
|
||||
6
src/apps/Elsa.Server.Web/Workflows/hello-world.elsa
Normal file
6
src/apps/Elsa.Server.Web/Workflows/hello-world.elsa
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use expressions js;
|
||||
|
||||
workflow HelloWorldDsl
|
||||
{
|
||||
WriteLine("Hello World from Elsa DSL!");
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
use Elsa.Activities.Console;
|
||||
|
||||
workflow HelloWorldDsl(
|
||||
DisplayName: "Hello World DSL",
|
||||
Description: "Demonstrates a simple ElsaScript workflow with metadata.",
|
||||
DefinitionId: "hello-world-dsl",
|
||||
DefinitionVersionId: "hello-world-dsl-v1",
|
||||
Version: 1,
|
||||
UsableAsActivity: true
|
||||
)
|
||||
{
|
||||
use expressions js;
|
||||
|
||||
WriteLine("Hello World from Elsa DSL!");
|
||||
}
|
||||
|
||||
workflow HelloWorldAsActivity(
|
||||
DisplayName: "Hello World as Activity",
|
||||
Description: "Can be embedded as an activity in other workflows.",
|
||||
DefinitionId: "hello-world-activity",
|
||||
DefinitionVersionId: "hello-world-activity-v1",
|
||||
Version: 1,
|
||||
UsableAsActivity: true
|
||||
)
|
||||
{
|
||||
use expressions js;
|
||||
|
||||
WriteLine(`Inner hello at ${Date.now()}`);
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Elsa": "Debug"
|
||||
"Elsa": "Warning",
|
||||
"Elsa.Workflows.ActivityRegistry": "Error"
|
||||
}
|
||||
},
|
||||
"HostBuilder": {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ using Elsa.Api.Client.Resources.WorkflowDefinitions.Enums;
|
|||
using Elsa.Api.Client.Shared.Models;
|
||||
using Refit;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Requests
|
||||
;
|
||||
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to list workflow definitions.
|
||||
|
|
@ -63,4 +62,4 @@ public class ListWorkflowDefinitionsRequest
|
|||
/// The order direction.
|
||||
/// </summary>
|
||||
public OrderDirection? OrderDirection { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,7 +153,6 @@ public class ActivityTestFixture
|
|||
services.AddSingleton<IWellKnownTypeRegistry, WellKnownTypeRegistry>();
|
||||
services.AddSingleton<IActivityDescriber, ActivityDescriber>();
|
||||
services.AddSingleton<IPropertyDefaultValueResolver, PropertyDefaultValueResolver>();
|
||||
services.AddSingleton<IActivityFactory, ActivityFactory>();
|
||||
services.AddSingleton<IPropertyUIHandlerResolver, PropertyUIHandlerResolver>();
|
||||
services.AddSingleton<IActivityRegistry, ActivityRegistry>();
|
||||
services.AddScoped<IActivityRegistryLookupService, ActivityRegistryLookupService>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an activity invocation.
|
||||
/// </summary>
|
||||
public class ActivityInvocationNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The activity name (e.g., "WriteLine", "SendEmail").
|
||||
/// </summary>
|
||||
public string ActivityName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments passed to the activity.
|
||||
/// </summary>
|
||||
public List<ArgumentNode> Arguments { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Optional variable to capture the result.
|
||||
/// </summary>
|
||||
public string? ResultVariable { get; set; }
|
||||
}
|
||||
17
src/modules/Elsa.Dsl.ElsaScript/Ast/ArgumentNode.cs
Normal file
17
src/modules/Elsa.Dsl.ElsaScript/Ast/ArgumentNode.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an argument passed to an activity.
|
||||
/// </summary>
|
||||
public class ArgumentNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The parameter name (null for positional arguments).
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The argument value expression.
|
||||
/// </summary>
|
||||
public ExpressionNode Value { get; set; } = null!;
|
||||
}
|
||||
17
src/modules/Elsa.Dsl.ElsaScript/Ast/AstNode.cs
Normal file
17
src/modules/Elsa.Dsl.ElsaScript/Ast/AstNode.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all AST nodes in ElsaScript.
|
||||
/// </summary>
|
||||
public abstract class AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The line number in the source where this node starts.
|
||||
/// </summary>
|
||||
public int Line { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The column number in the source where this node starts.
|
||||
/// </summary>
|
||||
public int Column { get; set; }
|
||||
}
|
||||
12
src/modules/Elsa.Dsl.ElsaScript/Ast/BlockNode.cs
Normal file
12
src/modules/Elsa.Dsl.ElsaScript/Ast/BlockNode.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a block of statements (maps to Sequence).
|
||||
/// </summary>
|
||||
public class BlockNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The statements in the block.
|
||||
/// </summary>
|
||||
public List<StatementNode> Statements { get; set; } = [];
|
||||
}
|
||||
22
src/modules/Elsa.Dsl.ElsaScript/Ast/ConnectionNode.cs
Normal file
22
src/modules/Elsa.Dsl.ElsaScript/Ast/ConnectionNode.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a connection in a flowchart.
|
||||
/// </summary>
|
||||
public class ConnectionNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The source label.
|
||||
/// </summary>
|
||||
public string Source { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The optional outcome.
|
||||
/// </summary>
|
||||
public string? Outcome { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The target label.
|
||||
/// </summary>
|
||||
public string Target { get; set; } = string.Empty;
|
||||
}
|
||||
127
src/modules/Elsa.Dsl.ElsaScript/Ast/ExpressionNode.cs
Normal file
127
src/modules/Elsa.Dsl.ElsaScript/Ast/ExpressionNode.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for expression nodes.
|
||||
/// </summary>
|
||||
public abstract class ExpressionNode : AstNode
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a literal expression.
|
||||
/// </summary>
|
||||
public class LiteralNode : ExpressionNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The literal value.
|
||||
/// </summary>
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Elsa expression with a specific language.
|
||||
/// </summary>
|
||||
public class ElsaExpressionNode : ExpressionNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The expression language (e.g., "js", "cs", "py", "liquid").
|
||||
/// If null, uses the default language from use statements.
|
||||
/// </summary>
|
||||
public string? Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The expression code.
|
||||
/// </summary>
|
||||
public string Expression { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a template string expression.
|
||||
/// </summary>
|
||||
public class TemplateStringNode : ExpressionNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The parts of the template string (alternating between literals and expressions).
|
||||
/// </summary>
|
||||
public List<TemplatePartNode> Parts { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a part of a template string.
|
||||
/// </summary>
|
||||
public class TemplatePartNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this is a literal or expression part.
|
||||
/// </summary>
|
||||
public bool IsExpression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content (literal text or expression code).
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an identifier (variable reference).
|
||||
/// </summary>
|
||||
public class IdentifierNode : ExpressionNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an array literal.
|
||||
/// </summary>
|
||||
public class ArrayLiteralNode : ExpressionNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The elements of the array.
|
||||
/// </summary>
|
||||
public List<ExpressionNode> Elements { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a binary expression (e.g., a + b).
|
||||
/// </summary>
|
||||
public class BinaryExpressionNode : ExpressionNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The left operand.
|
||||
/// </summary>
|
||||
public ExpressionNode Left { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The operator.
|
||||
/// </summary>
|
||||
public string Operator { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The right operand.
|
||||
/// </summary>
|
||||
public ExpressionNode Right { get; set; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a unary expression (e.g., ++i, i++).
|
||||
/// </summary>
|
||||
public class UnaryExpressionNode : ExpressionNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The operator.
|
||||
/// </summary>
|
||||
public string Operator { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The operand.
|
||||
/// </summary>
|
||||
public ExpressionNode Operand { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the operator is prefix (true) or postfix (false).
|
||||
/// </summary>
|
||||
public bool IsPrefix { get; set; }
|
||||
}
|
||||
27
src/modules/Elsa.Dsl.ElsaScript/Ast/FlowchartNode.cs
Normal file
27
src/modules/Elsa.Dsl.ElsaScript/Ast/FlowchartNode.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a flowchart definition.
|
||||
/// </summary>
|
||||
public class FlowchartNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Variable declarations scoped to the flowchart.
|
||||
/// </summary>
|
||||
public List<VariableDeclarationNode> Variables { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The labeled activities (nodes) in the flowchart.
|
||||
/// </summary>
|
||||
public List<LabeledActivityNode> Activities { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The connections (edges) between activities.
|
||||
/// </summary>
|
||||
public List<ConnectionNode> Connections { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The optional entry point label.
|
||||
/// </summary>
|
||||
public string? EntryPoint { get; set; }
|
||||
}
|
||||
27
src/modules/Elsa.Dsl.ElsaScript/Ast/ForEachNode.cs
Normal file
27
src/modules/Elsa.Dsl.ElsaScript/Ast/ForEachNode.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a foreach statement (e.g., foreach (var item in collection)).
|
||||
/// </summary>
|
||||
public class ForEachNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this foreach loop declares a new variable (var present in header).
|
||||
/// </summary>
|
||||
public bool DeclaresVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The loop variable name.
|
||||
/// </summary>
|
||||
public string VariableName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The collection expression to iterate over.
|
||||
/// </summary>
|
||||
public ExpressionNode Collection { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The body statement.
|
||||
/// </summary>
|
||||
public StatementNode Body { get; set; } = null!;
|
||||
}
|
||||
42
src/modules/Elsa.Dsl.ElsaScript/Ast/ForNode.cs
Normal file
42
src/modules/Elsa.Dsl.ElsaScript/Ast/ForNode.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a range-based for statement (e.g., for (var i = 0 to 10 step 1)).
|
||||
/// </summary>
|
||||
public class ForNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this for loop declares a new variable (var present in header).
|
||||
/// </summary>
|
||||
public bool DeclaresVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The loop variable name.
|
||||
/// </summary>
|
||||
public string VariableName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The start value expression.
|
||||
/// </summary>
|
||||
public ExpressionNode Start { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The end value expression.
|
||||
/// </summary>
|
||||
public ExpressionNode End { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The step value expression.
|
||||
/// </summary>
|
||||
public ExpressionNode Step { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the end value is inclusive (through) or exclusive (to).
|
||||
/// </summary>
|
||||
public bool IsInclusive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The body statement.
|
||||
/// </summary>
|
||||
public StatementNode Body { get; set; } = null!;
|
||||
}
|
||||
22
src/modules/Elsa.Dsl.ElsaScript/Ast/IfNode.cs
Normal file
22
src/modules/Elsa.Dsl.ElsaScript/Ast/IfNode.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an if statement.
|
||||
/// </summary>
|
||||
public class IfNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The condition expression.
|
||||
/// </summary>
|
||||
public ExpressionNode Condition { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The then branch.
|
||||
/// </summary>
|
||||
public StatementNode Then { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The optional else branch.
|
||||
/// </summary>
|
||||
public StatementNode? Else { get; set; }
|
||||
}
|
||||
20
src/modules/Elsa.Dsl.ElsaScript/Ast/LabeledActivityNode.cs
Normal file
20
src/modules/Elsa.Dsl.ElsaScript/Ast/LabeledActivityNode.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a labeled activity in a flowchart (node declaration).
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class LabeledActivityNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The label (node name).
|
||||
/// </summary>
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The activity statement (can be ActivityInvocationNode, BlockNode, etc.).
|
||||
/// </summary>
|
||||
public StatementNode Activity { get; set; } = null!;
|
||||
}
|
||||
12
src/modules/Elsa.Dsl.ElsaScript/Ast/ListenNode.cs
Normal file
12
src/modules/Elsa.Dsl.ElsaScript/Ast/ListenNode.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a listen statement (for triggers).
|
||||
/// </summary>
|
||||
public class ListenNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The activity invocation for the trigger.
|
||||
/// </summary>
|
||||
public ActivityInvocationNode Activity { get; set; } = null!;
|
||||
}
|
||||
19
src/modules/Elsa.Dsl.ElsaScript/Ast/ProgramNode.cs
Normal file
19
src/modules/Elsa.Dsl.ElsaScript/Ast/ProgramNode.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the root of an ElsaScript program, which can contain:
|
||||
/// - Global use statements
|
||||
/// - One or more workflow declarations
|
||||
/// </summary>
|
||||
public class ProgramNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Global use statements (imports and expression language settings) that apply to all workflows.
|
||||
/// </summary>
|
||||
public List<UseNode> GlobalUseStatements { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Workflow declarations in this program.
|
||||
/// </summary>
|
||||
public List<WorkflowNode> Workflows { get; set; } = [];
|
||||
}
|
||||
8
src/modules/Elsa.Dsl.ElsaScript/Ast/StatementNode.cs
Normal file
8
src/modules/Elsa.Dsl.ElsaScript/Ast/StatementNode.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for statement nodes.
|
||||
/// </summary>
|
||||
public abstract class StatementNode : AstNode
|
||||
{
|
||||
}
|
||||
17
src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchCaseNode.cs
Normal file
17
src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchCaseNode.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a switch case.
|
||||
/// </summary>
|
||||
public class SwitchCaseNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The case value.
|
||||
/// </summary>
|
||||
public ExpressionNode Value { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The case body.
|
||||
/// </summary>
|
||||
public StatementNode Body { get; set; } = null!;
|
||||
}
|
||||
22
src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchNode.cs
Normal file
22
src/modules/Elsa.Dsl.ElsaScript/Ast/SwitchNode.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a switch statement.
|
||||
/// </summary>
|
||||
public class SwitchNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The switch expression.
|
||||
/// </summary>
|
||||
public ExpressionNode Expression { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The cases.
|
||||
/// </summary>
|
||||
public List<SwitchCaseNode> Cases { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The optional default case.
|
||||
/// </summary>
|
||||
public StatementNode? Default { get; set; }
|
||||
}
|
||||
33
src/modules/Elsa.Dsl.ElsaScript/Ast/UseNode.cs
Normal file
33
src/modules/Elsa.Dsl.ElsaScript/Ast/UseNode.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a use statement (import or expression language configuration).
|
||||
/// </summary>
|
||||
public class UseNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of use statement (e.g., "expressions", namespace import).
|
||||
/// </summary>
|
||||
public UseType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value (e.g., "js", "cs", or a namespace).
|
||||
/// </summary>
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of use statement.
|
||||
/// </summary>
|
||||
public enum UseType
|
||||
{
|
||||
/// <summary>
|
||||
/// Expression language configuration (e.g., "use expressions js;").
|
||||
/// </summary>
|
||||
Expressions,
|
||||
|
||||
/// <summary>
|
||||
/// Namespace import (e.g., "use Elsa.Activities.Console;").
|
||||
/// </summary>
|
||||
Namespace
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a variable declaration.
|
||||
/// </summary>
|
||||
public class VariableDeclarationNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The variable kind (var, let, const).
|
||||
/// </summary>
|
||||
public VariableKind Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The variable name.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The initial value expression.
|
||||
/// </summary>
|
||||
public ExpressionNode? Value { get; set; }
|
||||
}
|
||||
10
src/modules/Elsa.Dsl.ElsaScript/Ast/VariableKind.cs
Normal file
10
src/modules/Elsa.Dsl.ElsaScript/Ast/VariableKind.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of variable declaration.
|
||||
/// </summary>
|
||||
public enum VariableKind
|
||||
{
|
||||
Var,
|
||||
Const
|
||||
}
|
||||
17
src/modules/Elsa.Dsl.ElsaScript/Ast/WhileNode.cs
Normal file
17
src/modules/Elsa.Dsl.ElsaScript/Ast/WhileNode.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a while statement.
|
||||
/// </summary>
|
||||
public class WhileNode : StatementNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The condition expression.
|
||||
/// </summary>
|
||||
public ExpressionNode Condition { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The body statement.
|
||||
/// </summary>
|
||||
public StatementNode Body { get; set; } = null!;
|
||||
}
|
||||
29
src/modules/Elsa.Dsl.ElsaScript/Ast/WorkflowNode.cs
Normal file
29
src/modules/Elsa.Dsl.ElsaScript/Ast/WorkflowNode.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
namespace Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a workflow definition in ElsaScript.
|
||||
/// </summary>
|
||||
public class WorkflowNode : AstNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The workflow identifier used in the DSL (the string after "workflow" keyword).
|
||||
/// This also serves as the default DefinitionId if not explicitly specified in metadata.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Workflow metadata properties (DisplayName, Description, DefinitionId, Version, etc.)
|
||||
/// These are specified in the optional parenthesized argument list after the workflow id.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The use statements (imports and expression language settings) at workflow scope.
|
||||
/// </summary>
|
||||
public List<UseNode> UseStatements { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The body of the workflow (a sequence of statements).
|
||||
/// </summary>
|
||||
public List<StatementNode> Body { get; set; } = [];
|
||||
}
|
||||
600
src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs
Normal file
600
src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
using Elsa.Dsl.ElsaScript.Ast;
|
||||
using Elsa.Dsl.ElsaScript.Contracts;
|
||||
using Elsa.Dsl.ElsaScript.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Compiler;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles an ElsaScript AST into Elsa workflows.
|
||||
/// </summary>
|
||||
public class ElsaScriptCompiler(IActivityRegistryLookupService activityRegistryLookupService, IElsaScriptParser parser) : IElsaScriptCompiler
|
||||
{
|
||||
private static readonly Dictionary<string, string> LanguageMappings = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["js"] = "JavaScript",
|
||||
["cs"] = "CSharp",
|
||||
["py"] = "Python",
|
||||
["liquid"] = "Liquid"
|
||||
};
|
||||
|
||||
private string _defaultExpressionLanguage = "JavaScript";
|
||||
private readonly Dictionary<string, Variable> _variables = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Workflow> CompileAsync(string source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var programNode = parser.Parse(source);
|
||||
return await CompileAsync(programNode, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Workflow> CompileAsync(ProgramNode programNode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the single workflow (enforced by parser)
|
||||
var workflowNode = programNode.Workflows.First();
|
||||
|
||||
// Merge global use statements with workflow-level ones
|
||||
// Create a temporary workflow node with merged use statements
|
||||
var mergedWorkflowNode = new WorkflowNode
|
||||
{
|
||||
Id = workflowNode.Id,
|
||||
Metadata = workflowNode.Metadata,
|
||||
UseStatements = [..programNode.GlobalUseStatements, ..workflowNode.UseStatements],
|
||||
Body = workflowNode.Body
|
||||
};
|
||||
|
||||
return await CompileWorkflowNodeAsync(mergedWorkflowNode, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Workflow> CompileWorkflowNodeAsync(WorkflowNode workflowNode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_variables.Clear();
|
||||
_defaultExpressionLanguage = "JavaScript";
|
||||
|
||||
// Process use statements (workflow-level overrides global)
|
||||
foreach (var useNode in workflowNode.UseStatements)
|
||||
{
|
||||
if (useNode.Type == UseType.Expressions)
|
||||
{
|
||||
_defaultExpressionLanguage = MapLanguageName(useNode.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Compile body statements
|
||||
var activities = new List<IActivity>();
|
||||
foreach (var statement in workflowNode.Body)
|
||||
{
|
||||
var activity = await CompileStatementAsync(statement, cancellationToken);
|
||||
if (activity != null)
|
||||
{
|
||||
activities.Add(activity);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the root activity (Sequence containing all statements)
|
||||
var root = activities.Count == 1
|
||||
? activities[0]
|
||||
: new Sequence
|
||||
{
|
||||
Activities = activities
|
||||
};
|
||||
|
||||
// Extract metadata with defaults
|
||||
var definitionId = GetMetadataValue<string>(workflowNode.Metadata, "DefinitionId") ?? workflowNode.Id;
|
||||
var displayName = GetMetadataValue<string>(workflowNode.Metadata, "DisplayName") ?? workflowNode.Id;
|
||||
var description = GetMetadataValue<string>(workflowNode.Metadata, "Description") ?? string.Empty;
|
||||
var definitionVersionId = GetMetadataValue<string>(workflowNode.Metadata, "DefinitionVersionId") ?? $"{definitionId}-v1";
|
||||
var version = GetMetadataValueOrDefault(workflowNode.Metadata, "Version", 1);
|
||||
var usableAsActivity = GetMetadataValue<bool?>(workflowNode.Metadata, "UsableAsActivity");
|
||||
|
||||
// Create the workflow
|
||||
var workflow = new Workflow
|
||||
{
|
||||
Name = displayName,
|
||||
Identity = new WorkflowIdentity(definitionId, version, definitionVersionId, null),
|
||||
WorkflowMetadata = new(displayName, description, ToolVersion: new("3.6.0")),
|
||||
Root = root,
|
||||
Variables = _variables.Values.ToList(),
|
||||
Options = new()
|
||||
{
|
||||
UsableAsActivity = usableAsActivity
|
||||
}
|
||||
};
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private T? GetMetadataValue<T>(Dictionary<string, object> metadata, string key) =>
|
||||
metadata.TryGetValue(key, out var value) ? ConvertValue<T>(value, default) : default;
|
||||
|
||||
private T GetMetadataValueOrDefault<T>(Dictionary<string, object> metadata, string key, T defaultValue) =>
|
||||
metadata.TryGetValue(key, out var value) ? ConvertValue(value, defaultValue) : defaultValue;
|
||||
|
||||
private static T? ConvertValue<T>(object value, T? defaultValue)
|
||||
{
|
||||
// Handle direct type match
|
||||
if (value is T typedValue)
|
||||
return typedValue;
|
||||
|
||||
// Try to convert
|
||||
try
|
||||
{
|
||||
var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
return (T)Convert.ChangeType(value, targetType);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Return default value if conversion fails
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActivity?> CompileStatementAsync(StatementNode statement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return statement switch
|
||||
{
|
||||
VariableDeclarationNode varDecl => CompileVariableDeclaration(varDecl),
|
||||
ActivityInvocationNode actInv => await CompileActivityInvocationAsync(actInv, cancellationToken),
|
||||
BlockNode block => await CompileBlockAsync(block, cancellationToken),
|
||||
IfNode ifNode => await CompileIfAsync(ifNode, cancellationToken),
|
||||
ForEachNode forEach => await CompileForEachAsync(forEach, cancellationToken),
|
||||
ForNode forNode => await CompileForAsync(forNode, cancellationToken),
|
||||
WhileNode whileNode => await CompileWhileAsync(whileNode, cancellationToken),
|
||||
SwitchNode switchNode => await CompileSwitchAsync(switchNode, cancellationToken),
|
||||
FlowchartNode flowchart => await CompileFlowchartAsync(flowchart, cancellationToken),
|
||||
ListenNode listen => await CompileListenAsync(listen, cancellationToken),
|
||||
_ => throw new NotSupportedException($"Statement type {statement.GetType().Name} is not supported")
|
||||
};
|
||||
}
|
||||
|
||||
private IActivity? CompileVariableDeclaration(VariableDeclarationNode varDecl)
|
||||
{
|
||||
// Create and register the variable
|
||||
var initialValue = varDecl.Value != null ? EvaluateConstantExpression(varDecl.Value) : null;
|
||||
var variable = new Variable(varDecl.Name, initialValue);
|
||||
_variables[varDecl.Name] = variable;
|
||||
|
||||
// Variable declarations don't produce activities themselves
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileActivityInvocationAsync(ActivityInvocationNode actInv, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Try to find the activity type by name - try several strategies
|
||||
var activityDescriptor = await activityRegistryLookupService.FindAsync(actInv.ActivityName);
|
||||
|
||||
// If not found, try with "Elsa." prefix
|
||||
if (activityDescriptor == null)
|
||||
{
|
||||
activityDescriptor = await activityRegistryLookupService.FindAsync($"Elsa.{actInv.ActivityName}");
|
||||
}
|
||||
|
||||
// If still not found, search by descriptor name
|
||||
if (activityDescriptor == null)
|
||||
{
|
||||
activityDescriptor = await activityRegistryLookupService.FindAsync(d => d.Name == actInv.ActivityName);
|
||||
}
|
||||
|
||||
if (activityDescriptor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Activity '{actInv.ActivityName}' not found in registry");
|
||||
}
|
||||
|
||||
var activityType = activityDescriptor.ClrType;
|
||||
|
||||
// Separate named and positional arguments
|
||||
var namedArgs = actInv.Arguments.Where(a => a.Name != null).ToList();
|
||||
var positionalArgs = actInv.Arguments.Where(a => a.Name == null).ToList();
|
||||
|
||||
IActivity activity;
|
||||
|
||||
// If we have positional arguments, try to find a matching constructor
|
||||
if (positionalArgs.Any())
|
||||
{
|
||||
activity = InstantiateActivityUsingConstructor(activityType, positionalArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No positional arguments, use default constructor
|
||||
var activityConstructorContext = new ActivityConstructorContext(activityDescriptor, ActivityActivator.Create);
|
||||
activity = activityDescriptor.Constructor(activityConstructorContext);
|
||||
}
|
||||
|
||||
// Set named argument properties
|
||||
foreach (var arg in namedArgs)
|
||||
{
|
||||
var property = activityType.GetProperty(arg.Name!);
|
||||
|
||||
if (property != null)
|
||||
{
|
||||
var value = CompileExpression(arg.Value, property.PropertyType);
|
||||
property.SetValue(activity, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Property '{arg.Name}' not found on activity type '{activityType.Name}'");
|
||||
}
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileBlockAsync(BlockNode block, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var activities = new List<IActivity>();
|
||||
|
||||
foreach (var statement in block.Statements)
|
||||
{
|
||||
var activity = await CompileStatementAsync(statement, cancellationToken);
|
||||
if (activity != null)
|
||||
{
|
||||
activities.Add(activity);
|
||||
}
|
||||
}
|
||||
|
||||
return new Sequence
|
||||
{
|
||||
Activities = activities
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileIfAsync(IfNode ifNode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var condition = CompileExpressionAsInput<bool>(ifNode.Condition);
|
||||
var thenActivity = await CompileStatementAsync(ifNode.Then, cancellationToken);
|
||||
var elseActivity = ifNode.Else != null ? await CompileStatementAsync(ifNode.Else, cancellationToken) : null;
|
||||
|
||||
return new If(condition)
|
||||
{
|
||||
Then = thenActivity,
|
||||
Else = elseActivity
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileForEachAsync(ForEachNode forEach, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Variable loopVariable;
|
||||
|
||||
if (forEach.DeclaresVariable)
|
||||
{
|
||||
// Create a new loop variable
|
||||
loopVariable = new Variable<object>(forEach.VariableName, null!);
|
||||
_variables[forEach.VariableName] = loopVariable;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reuse existing variable
|
||||
if (!_variables.TryGetValue(forEach.VariableName, out loopVariable!))
|
||||
{
|
||||
throw new InvalidOperationException($"Variable '{forEach.VariableName}' is not declared. Use 'var {forEach.VariableName}' to declare a new variable in the foreach loop.");
|
||||
}
|
||||
}
|
||||
|
||||
var items = CompileExpressionAsInput<ICollection<object>>(forEach.Collection);
|
||||
var body = await CompileStatementAsync(forEach.Body, cancellationToken);
|
||||
|
||||
var forEachActivity = new ForEach<object>(items)
|
||||
{
|
||||
CurrentValue = new(loopVariable),
|
||||
Body = body
|
||||
};
|
||||
|
||||
return forEachActivity;
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileForAsync(ForNode forNode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Variable loopVariable;
|
||||
|
||||
if (forNode.DeclaresVariable)
|
||||
{
|
||||
// Create a new loop variable
|
||||
loopVariable = new Variable<int>(forNode.VariableName, 0);
|
||||
_variables[forNode.VariableName] = loopVariable;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reuse existing variable
|
||||
if (!_variables.TryGetValue(forNode.VariableName, out loopVariable!))
|
||||
{
|
||||
throw new InvalidOperationException($"Variable '{forNode.VariableName}' is not declared. Use 'var {forNode.VariableName}' to declare a new variable in the for loop.");
|
||||
}
|
||||
}
|
||||
|
||||
var start = CompileExpressionAsInput<int>(forNode.Start);
|
||||
var end = CompileExpressionAsInput<int>(forNode.End);
|
||||
var step = CompileExpressionAsInput<int>(forNode.Step);
|
||||
var body = await CompileStatementAsync(forNode.Body, cancellationToken);
|
||||
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = start,
|
||||
End = end,
|
||||
Step = step,
|
||||
OuterBoundInclusive = new Input<bool>(forNode.IsInclusive),
|
||||
CurrentValue = new Output<object?>(loopVariable),
|
||||
Body = body
|
||||
};
|
||||
|
||||
return forActivity;
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileWhileAsync(WhileNode whileNode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var condition = CompileExpressionAsInput<bool>(whileNode.Condition);
|
||||
var body = await CompileStatementAsync(whileNode.Body, cancellationToken);
|
||||
|
||||
return new While(condition)
|
||||
{
|
||||
Body = body
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileSwitchAsync(SwitchNode switchNode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var cases = new List<SwitchCase>();
|
||||
|
||||
foreach (var caseNode in switchNode.Cases)
|
||||
{
|
||||
var caseExpression = CompileExpressionAsExpression(caseNode.Value);
|
||||
var caseBody = await CompileStatementAsync(caseNode.Body, cancellationToken);
|
||||
cases.Add(new("Case", caseExpression, caseBody!));
|
||||
}
|
||||
|
||||
var defaultActivity = switchNode.Default != null ? await CompileStatementAsync(switchNode.Default, cancellationToken) : null;
|
||||
|
||||
return new Switch
|
||||
{
|
||||
Cases = cases,
|
||||
Default = defaultActivity
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileFlowchartAsync(FlowchartNode flowchart, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Register flowchart-scoped variables
|
||||
foreach (var varDecl in flowchart.Variables)
|
||||
{
|
||||
CompileVariableDeclaration(varDecl);
|
||||
}
|
||||
|
||||
// Compile all labeled activities and build a label-to-activity map
|
||||
var labelToActivity = new Dictionary<string, IActivity>();
|
||||
foreach (var labeledNode in flowchart.Activities)
|
||||
{
|
||||
var activity = await CompileStatementAsync(labeledNode.Activity, cancellationToken);
|
||||
if (activity != null)
|
||||
{
|
||||
labelToActivity[labeledNode.Label] = activity;
|
||||
}
|
||||
}
|
||||
|
||||
// Create connections
|
||||
var connections = new List<Connection>();
|
||||
foreach (var connNode in flowchart.Connections)
|
||||
{
|
||||
if (!labelToActivity.TryGetValue(connNode.Source, out var sourceActivity))
|
||||
throw new InvalidOperationException($"Source label '{connNode.Source}' not found in flowchart");
|
||||
|
||||
if (!labelToActivity.TryGetValue(connNode.Target, out var targetActivity))
|
||||
throw new InvalidOperationException($"Target label '{connNode.Target}' not found in flowchart");
|
||||
|
||||
var source = new Endpoint(sourceActivity, connNode.Outcome);
|
||||
var target = new Endpoint(targetActivity);
|
||||
connections.Add(new Connection(source, target));
|
||||
}
|
||||
|
||||
// Create flowchart activity
|
||||
var flowchartActivity = new Workflows.Activities.Flowchart.Activities.Flowchart
|
||||
{
|
||||
Activities = labelToActivity.Values.ToList(),
|
||||
Connections = connections
|
||||
};
|
||||
|
||||
// Set entry point if specified
|
||||
if (!string.IsNullOrEmpty(flowchart.EntryPoint))
|
||||
{
|
||||
if (!labelToActivity.TryGetValue(flowchart.EntryPoint, out var startActivity))
|
||||
throw new InvalidOperationException($"Entry point label '{flowchart.EntryPoint}' not found in flowchart");
|
||||
|
||||
flowchartActivity.Start = startActivity;
|
||||
}
|
||||
|
||||
return flowchartActivity;
|
||||
}
|
||||
|
||||
private async Task<IActivity> CompileListenAsync(ListenNode listen, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Listen is just a regular activity invocation that can start a workflow
|
||||
var activity = await CompileActivityInvocationAsync(listen.Activity, cancellationToken);
|
||||
|
||||
// Try to set CanStartWorkflow if the activity supports it
|
||||
var canStartWorkflowProp = activity.GetType().GetProperty("CanStartWorkflow");
|
||||
if (canStartWorkflowProp != null && canStartWorkflowProp.PropertyType == typeof(bool))
|
||||
{
|
||||
canStartWorkflowProp.SetValue(activity, true);
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
private Input<T> CompileExpressionAsInput<T>(ExpressionNode exprNode)
|
||||
{
|
||||
if (exprNode is LiteralNode literal)
|
||||
{
|
||||
return new(new Literal(literal.Value!));
|
||||
}
|
||||
|
||||
if (exprNode is IdentifierNode identifier)
|
||||
{
|
||||
// Reference to a variable
|
||||
if (_variables.TryGetValue(identifier.Name, out var variable))
|
||||
{
|
||||
return new(variable);
|
||||
}
|
||||
|
||||
// If not found, treat as a literal
|
||||
return new(new Literal<T>(default!));
|
||||
}
|
||||
|
||||
if (exprNode is ElsaExpressionNode elsaExpr)
|
||||
{
|
||||
var language = elsaExpr.Language != null ? MapLanguageName(elsaExpr.Language) : _defaultExpressionLanguage;
|
||||
var expression = new Expression(language, elsaExpr.Expression);
|
||||
return new(expression);
|
||||
}
|
||||
|
||||
if (exprNode is ArrayLiteralNode arrayLiteral)
|
||||
{
|
||||
// For array literals, evaluate to a constant array if all elements are literals
|
||||
var elements = arrayLiteral.Elements.Select(EvaluateConstantExpression).ToArray();
|
||||
return new((T)(object)elements);
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Expression type {exprNode.GetType().Name} is not supported");
|
||||
}
|
||||
|
||||
private Expression CompileExpressionAsExpression(ExpressionNode exprNode)
|
||||
{
|
||||
if (exprNode is LiteralNode literal)
|
||||
{
|
||||
return Expression.LiteralExpression(literal.Value);
|
||||
}
|
||||
|
||||
if (exprNode is ElsaExpressionNode elsaExpr)
|
||||
{
|
||||
var language = elsaExpr.Language != null ? MapLanguageName(elsaExpr.Language) : _defaultExpressionLanguage;
|
||||
return new(language, elsaExpr.Expression);
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Expression type {exprNode.GetType().Name} is not supported as Expression");
|
||||
}
|
||||
|
||||
private object CompileExpression(ExpressionNode exprNode, Type targetType)
|
||||
{
|
||||
// Check if targetType is already Input<T>
|
||||
Type innerType;
|
||||
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Input<>))
|
||||
{
|
||||
// Extract the T from Input<T>
|
||||
innerType = targetType.GetGenericArguments()[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
innerType = targetType;
|
||||
}
|
||||
|
||||
// Use reflection to call CompileExpressionAsInput<T>
|
||||
var method = GetType().GetMethod(nameof(CompileExpressionAsInput),
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
var genericMethod = method!.MakeGenericMethod(innerType);
|
||||
|
||||
return genericMethod.Invoke(this, [exprNode])!;
|
||||
}
|
||||
|
||||
private object? EvaluateConstantExpression(ExpressionNode exprNode)
|
||||
{
|
||||
if (exprNode is LiteralNode literal)
|
||||
{
|
||||
return literal.Value;
|
||||
}
|
||||
|
||||
if (exprNode is ArrayLiteralNode arrayLiteral)
|
||||
{
|
||||
return arrayLiteral.Elements.Select(EvaluateConstantExpression).ToArray();
|
||||
}
|
||||
|
||||
// For non-constant expressions, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
private IActivity InstantiateActivityUsingConstructor(Type activityType, List<ArgumentNode> positionalArgs)
|
||||
{
|
||||
// Get all public constructors
|
||||
var constructors = activityType.GetConstructors(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
// Filter constructors that:
|
||||
// 1. Have the same number of required Input<T> parameters as positional arguments (excluding optional params)
|
||||
// 2. All non-optional parameters are Input<T> types
|
||||
var matchingConstructors = new List<(System.Reflection.ConstructorInfo ctor, System.Reflection.ParameterInfo[] inputParams)>();
|
||||
|
||||
foreach (var ctor in constructors)
|
||||
{
|
||||
var parameters = ctor.GetParameters();
|
||||
|
||||
// Filter to only Input<T> parameters that are not optional (don't have default values or CallerMemberName attributes)
|
||||
var inputParams = parameters.Where(p =>
|
||||
p.ParameterType.IsGenericType &&
|
||||
p.ParameterType.GetGenericTypeDefinition() == typeof(Input<>) &&
|
||||
!p.IsOptional &&
|
||||
!p.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CallerFilePathAttribute), false).Any() &&
|
||||
!p.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CallerLineNumberAttribute), false).Any() &&
|
||||
!p.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CallerMemberNameAttribute), false).Any()
|
||||
).ToArray();
|
||||
|
||||
// Check if the number of required Input<T> params matches our positional args
|
||||
if (inputParams.Length == positionalArgs.Count)
|
||||
{
|
||||
matchingConstructors.Add((ctor, inputParams));
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingConstructors.Any())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No matching constructor found for activity type '{activityType.Name}' with {positionalArgs.Count} positional argument(s). " +
|
||||
$"Constructors must have Input<T> parameters matching the number of positional arguments.");
|
||||
}
|
||||
|
||||
if (matchingConstructors.Count > 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Multiple matching constructors found for activity type '{activityType.Name}' with {positionalArgs.Count} positional argument(s). " +
|
||||
$"Please use named arguments to disambiguate.");
|
||||
}
|
||||
|
||||
var (selectedCtor, selectedInputParams) = matchingConstructors[0];
|
||||
|
||||
// Build the constructor arguments
|
||||
var ctorArgs = new List<object?>();
|
||||
var allParams = selectedCtor.GetParameters();
|
||||
|
||||
foreach (var param in allParams)
|
||||
{
|
||||
// Check if this is one of our Input<T> parameters
|
||||
var inputParamIndex = Array.IndexOf(selectedInputParams, param);
|
||||
|
||||
if (inputParamIndex >= 0)
|
||||
{
|
||||
// This is an Input<T> parameter - compile the corresponding positional argument
|
||||
var arg = positionalArgs[inputParamIndex];
|
||||
var value = CompileExpression(arg.Value, param.ParameterType);
|
||||
ctorArgs.Add(value);
|
||||
}
|
||||
else if (param.IsOptional)
|
||||
{
|
||||
// This is an optional parameter (like CallerFilePath) - use its default value
|
||||
ctorArgs.Add(param.DefaultValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This shouldn't happen if our filtering is correct
|
||||
throw new InvalidOperationException(
|
||||
$"Unexpected non-optional, non-Input<T> parameter '{param.Name}' in constructor for '{activityType.Name}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the activity using the constructor
|
||||
var activity = (IActivity)selectedCtor.Invoke(ctorArgs.ToArray());
|
||||
return activity;
|
||||
}
|
||||
|
||||
private static string MapLanguageName(string dslLanguage) =>
|
||||
LanguageMappings.TryGetValue(dslLanguage, out var mapped) ? mapped : dslLanguage;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using Elsa.Dsl.ElsaScript.Ast;
|
||||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles an ElsaScript AST into Elsa workflows.
|
||||
/// </summary>
|
||||
public interface IElsaScriptCompiler
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses and compiles the given ElsaScript source code into a workflow.
|
||||
/// </summary>
|
||||
/// <param name="source">The ElsaScript source code.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The compiled workflow.</returns>
|
||||
Task<Workflow> CompileAsync(string source, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the given program AST into a workflow.
|
||||
/// </summary>
|
||||
/// <param name="programNode">The program AST.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The compiled workflow.</returns>
|
||||
Task<Workflow> CompileAsync(ProgramNode programNode, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using Elsa.Dsl.ElsaScript.Ast;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Parses ElsaScript source code into an AST.
|
||||
/// </summary>
|
||||
public interface IElsaScriptParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the given source code into a program AST that can contain multiple workflows.
|
||||
/// </summary>
|
||||
/// <param name="source">The ElsaScript source code.</param>
|
||||
/// <returns>The parsed program AST containing all workflows.</returns>
|
||||
ProgramNode Parse(string source);
|
||||
}
|
||||
26
src/modules/Elsa.Dsl.ElsaScript/Elsa.Dsl.ElsaScript.csproj
Normal file
26
src/modules/Elsa.Dsl.ElsaScript/Elsa.Dsl.ElsaScript.csproj
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Elsa.Dsl.ElsaScript</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides a JavaScript-inspired textual DSL (ElsaScript) for authoring Elsa 3 workflows.
|
||||
</Description>
|
||||
<PackageTags>elsa module dsl elsascript workflows</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Parlot" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Expressions\Elsa.Expressions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Elsa.Dsl.ElsaScript.Features;
|
||||
using Elsa.Features.Services;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IModule"/>.
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the ElsaScript DSL feature.
|
||||
/// </summary>
|
||||
public static IModule UseElsaScript(this IModule module, Action<ElsaScriptFeature>? configure = null)
|
||||
{
|
||||
return module.Use(configure);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using Elsa.Dsl.ElsaScript.Compiler;
|
||||
using Elsa.Dsl.ElsaScript.Contracts;
|
||||
using Elsa.Dsl.ElsaScript.Materializers;
|
||||
using Elsa.Dsl.ElsaScript.Parser;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Workflows.Management;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Feature for ElsaScript DSL support.
|
||||
/// </summary>
|
||||
public class ElsaScriptFeature : FeatureBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ElsaScriptFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
// No additional configuration needed
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddSingleton<IElsaScriptParser, ElsaScriptParser>();
|
||||
Services.AddScoped<IElsaScriptCompiler, ElsaScriptCompiler>();
|
||||
Services.AddScoped<IWorkflowMaterializer, ElsaScriptWorkflowMaterializer>();
|
||||
}
|
||||
}
|
||||
3
src/modules/Elsa.Dsl.ElsaScript/FodyWeavers.xml
Normal file
3
src/modules/Elsa.Dsl.ElsaScript/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
43
src/modules/Elsa.Dsl.ElsaScript/Helpers/ActivityActivator.cs
Normal file
43
src/modules/Elsa.Dsl.ElsaScript/Helpers/ActivityActivator.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Helpers;
|
||||
|
||||
internal static class ActivityActivator
|
||||
{
|
||||
public static IActivity Create(Type type)
|
||||
{
|
||||
// Try parameterless ctor first (fast path)
|
||||
var parameterless = type.GetConstructor(Type.EmptyTypes);
|
||||
if (parameterless != null)
|
||||
return (IActivity)parameterless.Invoke([]);
|
||||
|
||||
// Fall back: pick a public ctor and provide default values for args
|
||||
var ctor = type
|
||||
.GetConstructors()
|
||||
.OrderBy(c => c.GetParameters().Length) // shortest first
|
||||
.FirstOrDefault()
|
||||
?? throw new InvalidOperationException(
|
||||
$"Type {type} has no public constructors and cannot be instantiated.");
|
||||
|
||||
var parameters = ctor.GetParameters();
|
||||
var args = new object?[parameters.Length];
|
||||
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var p = parameters[i];
|
||||
|
||||
if (p.HasDefaultValue)
|
||||
{
|
||||
args[i] = p.DefaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
args[i] = p.ParameterType.IsValueType
|
||||
? Activator.CreateInstance(p.ParameterType)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
return (IActivity)ctor.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using Elsa.Dsl.ElsaScript.Contracts;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Materializers;
|
||||
|
||||
/// <summary>
|
||||
/// Materializes a <see cref="Workflow"/> from a <see cref="WorkflowDefinition"/>'s ElsaScript source code.
|
||||
/// </summary>
|
||||
public class ElsaScriptWorkflowMaterializer(IElsaScriptCompiler compiler) : IWorkflowMaterializer
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the materializer.
|
||||
/// </summary>
|
||||
public const string MaterializerName = "ElsaScript";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => MaterializerName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<Workflow> MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken)
|
||||
{
|
||||
var source = definition.OriginalSource ?? string.Empty;
|
||||
var workflow = await compiler.CompileAsync(source, cancellationToken);
|
||||
|
||||
// Preserve the workflow definition ID and version
|
||||
workflow.Identity = new(
|
||||
definition.DefinitionId,
|
||||
definition.Version,
|
||||
definition.Id,
|
||||
definition.TenantId
|
||||
);
|
||||
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
630
src/modules/Elsa.Dsl.ElsaScript/Parser/ElsaScriptParser.cs
Normal file
630
src/modules/Elsa.Dsl.ElsaScript/Parser/ElsaScriptParser.cs
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
using Elsa.Dsl.ElsaScript.Ast;
|
||||
using Elsa.Dsl.ElsaScript.Contracts;
|
||||
using Parlot;
|
||||
using Parlot.Fluent;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.Parser;
|
||||
|
||||
/// <summary>
|
||||
/// ElsaScript parser using Parlot for robust parsing.
|
||||
/// </summary>
|
||||
public class ElsaScriptParser : IElsaScriptParser
|
||||
{
|
||||
private static readonly Parser<ProgramNode> ProgramParser;
|
||||
|
||||
static ElsaScriptParser()
|
||||
{
|
||||
// Keywords
|
||||
var useKeyword = Terms.Text("use");
|
||||
var workflowKeyword = Terms.Text("workflow");
|
||||
var expressionsKeyword = Terms.Text("expressions");
|
||||
var listenKeyword = Terms.Text("listen");
|
||||
var varKeyword = Terms.Text("var");
|
||||
var constKeyword = Terms.Text("const");
|
||||
var forKeyword = Terms.Text("for");
|
||||
var foreachKeyword = Terms.Text("foreach");
|
||||
var inKeyword = Terms.Text("in");
|
||||
var toKeyword = Terms.Text("to");
|
||||
var throughKeyword = Terms.Text("through");
|
||||
var stepKeyword = Terms.Text("step");
|
||||
var flowchartKeyword = Terms.Text("flowchart");
|
||||
var entryKeyword = Terms.Text("entry");
|
||||
|
||||
// Basic tokens
|
||||
var identifier = Terms.Identifier();
|
||||
var stringLiteral = Terms.String();
|
||||
var integerLiteral = Terms.Integer();
|
||||
var decimalLiteral = Terms.Decimal();
|
||||
|
||||
// Punctuation
|
||||
var semicolon = Terms.Char(';');
|
||||
var comma = Terms.Char(',');
|
||||
var colon = Terms.Char(':');
|
||||
var leftParen = Terms.Char('(');
|
||||
var rightParen = Terms.Char(')');
|
||||
var leftBrace = Terms.Char('{');
|
||||
var rightBrace = Terms.Char('}');
|
||||
var leftBracket = Terms.Char('[');
|
||||
var rightBracket = Terms.Char(']');
|
||||
var dot = Terms.Char('.');
|
||||
var arrow = Terms.Text("=>");
|
||||
var rightArrow = Terms.Text("->");
|
||||
var equals = Terms.Char('=');
|
||||
|
||||
// Deferred parsers for recursive structures
|
||||
var expression = Deferred<ExpressionNode>();
|
||||
var statement = Deferred<StatementNode>();
|
||||
|
||||
// Expression parsers
|
||||
var booleanLiteral = Terms.Text("true").Or(Terms.Text("false"))
|
||||
.Then<ExpressionNode>(x => new LiteralNode { Value = x.ToString() == "true" });
|
||||
|
||||
var numberLiteral = decimalLiteral
|
||||
.Then<ExpressionNode>(x => new LiteralNode { Value = x });
|
||||
|
||||
var intLiteral = integerLiteral
|
||||
.Then<ExpressionNode>(x => new LiteralNode { Value = (long)x });
|
||||
|
||||
var stringExpr = stringLiteral
|
||||
.Then<ExpressionNode>(x => new LiteralNode { Value = x.ToString() });
|
||||
|
||||
var identifierExpr = identifier
|
||||
.Then<ExpressionNode>(x => new IdentifierNode { Name = x.ToString() });
|
||||
|
||||
// Array literal: [expr, expr, ...]
|
||||
var commaSeparatedExpression = expression.And(ZeroOrOne(comma)).Then(x => x.Item1);
|
||||
var arrayLiteral = Between(leftBracket, ZeroOrMany(commaSeparatedExpression), rightBracket)
|
||||
.Then<ExpressionNode>(elements => new ArrayLiteralNode { Elements = elements.ToList() });
|
||||
|
||||
// Elsa expression: lang => <raw text until matching )>
|
||||
// We need to capture raw text after => up to the closing parenthesis
|
||||
// This supports nested parentheses by counting depth
|
||||
// Use a custom scanner-based parser wrapped in RawExpressionParser
|
||||
var rawExpressionText = new RawExpressionParser();
|
||||
|
||||
var elsaExpressionWithLang = identifier
|
||||
.And(arrow)
|
||||
.And(rawExpressionText)
|
||||
.Then<ExpressionNode>(x => new ElsaExpressionNode
|
||||
{
|
||||
Language = x.Item1.ToString(),
|
||||
Expression = x.Item3.ToString().Trim()
|
||||
});
|
||||
|
||||
var elsaExpressionWithoutLang = arrow
|
||||
.And(rawExpressionText)
|
||||
.Then<ExpressionNode>(x => new ElsaExpressionNode
|
||||
{
|
||||
Language = null,
|
||||
Expression = x.Item2.ToString().Trim()
|
||||
});
|
||||
|
||||
var elsaExpression = elsaExpressionWithLang.Or(elsaExpressionWithoutLang);
|
||||
|
||||
// Expression priority: try most specific first
|
||||
expression.Parser = elsaExpression
|
||||
.Or(arrayLiteral)
|
||||
.Or(booleanLiteral)
|
||||
.Or(numberLiteral)
|
||||
.Or(intLiteral)
|
||||
.Or(stringExpr)
|
||||
.Or(identifierExpr);
|
||||
|
||||
// Argument parser: name: value or just value
|
||||
var namedArgument = identifier
|
||||
.And(colon)
|
||||
.And(expression)
|
||||
.Then(x => new ArgumentNode { Name = x.Item1.ToString(), Value = x.Item3 });
|
||||
|
||||
var positionalArgument = expression
|
||||
.Then(x => new ArgumentNode { Value = x });
|
||||
|
||||
var argument = namedArgument.Or(positionalArgument);
|
||||
|
||||
var commaSeparatedArgument = argument.And(ZeroOrOne(comma)).Then(x => x.Item1);
|
||||
var arguments = ZeroOrMany(commaSeparatedArgument);
|
||||
|
||||
// Activity invocation: ActivityName(args) - with or without arguments
|
||||
var activityInvocationWithArgs = identifier
|
||||
.And(leftParen)
|
||||
.And(arguments)
|
||||
.And(rightParen)
|
||||
.Then(x => new ActivityInvocationNode
|
||||
{
|
||||
ActivityName = x.Item1.ToString(),
|
||||
Arguments = x.Item3.ToList()
|
||||
});
|
||||
|
||||
var activityInvocationNoArgs = identifier
|
||||
.And(leftParen)
|
||||
.And(rightParen)
|
||||
.Then(x => new ActivityInvocationNode
|
||||
{
|
||||
ActivityName = x.Item1.ToString(),
|
||||
Arguments = []
|
||||
});
|
||||
|
||||
var activityInvocation = activityInvocationWithArgs.Or(activityInvocationNoArgs);
|
||||
|
||||
// Variable declaration: var/const name = expr
|
||||
var variableKindParser = varKeyword.Or(constKeyword);
|
||||
|
||||
var variableDeclaration = variableKindParser
|
||||
.And(identifier)
|
||||
.And(equals)
|
||||
.And(expression)
|
||||
.Then<StatementNode>(x =>
|
||||
{
|
||||
// x is a flat tuple (kind, identifier, equals, expression)
|
||||
var kind = x.Item1.ToString() switch
|
||||
{
|
||||
"var" => VariableKind.Var,
|
||||
"const" => VariableKind.Const,
|
||||
_ => VariableKind.Var
|
||||
};
|
||||
|
||||
return new VariableDeclarationNode
|
||||
{
|
||||
Kind = kind,
|
||||
Name = x.Item2.ToString(),
|
||||
Value = x.Item4
|
||||
};
|
||||
});
|
||||
|
||||
// Listen statement: listen ActivityName(args)
|
||||
var listenStatement = listenKeyword
|
||||
.And(activityInvocation)
|
||||
.Then<StatementNode>(x => new ListenNode { Activity = x.Item2 });
|
||||
|
||||
// Statement: variable declaration, listen, or activity invocation
|
||||
var activityStatement = activityInvocation
|
||||
.Then<StatementNode>(x => x);
|
||||
|
||||
// Declare deferred for loop, foreach, and flowchart parsers
|
||||
var forStatement = Deferred<StatementNode>();
|
||||
var foreachStatement = Deferred<StatementNode>();
|
||||
var flowchartStatement = Deferred<StatementNode>();
|
||||
|
||||
statement.Parser = variableDeclaration
|
||||
.Or(listenStatement)
|
||||
.Or(forStatement)
|
||||
.Or(foreachStatement)
|
||||
.Or(flowchartStatement)
|
||||
.Or(activityStatement);
|
||||
|
||||
// Statement with optional semicolon
|
||||
var statementWithSemicolon = statement.And(ZeroOrOne(semicolon)).Then(x => x.Item1);
|
||||
|
||||
// For loop statement: for (var i = 0 to 10 step 1) { body } or for (i = 0 to 10) statement
|
||||
// Must be defined after statementWithSemicolon
|
||||
var rangeOperator = toKeyword.Or(throughKeyword);
|
||||
|
||||
// For body can be either a block or a single statement
|
||||
var forBlockBody = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
|
||||
.Then(statements => (StatementNode)(statements.Count == 1
|
||||
? statements.First()
|
||||
: new BlockNode { Statements = statements.ToList() }));
|
||||
var forSingleStatementBody = statement;
|
||||
var forBody = forBlockBody.Or(forSingleStatementBody);
|
||||
|
||||
// For header with optional var: (var i = start to/through end step stepValue)
|
||||
// or (i = start to/through end step stepValue)
|
||||
// Step clause is optional
|
||||
var optionalVarKeyword = ZeroOrOne(varKeyword);
|
||||
var optionalStepClause = ZeroOrOne(stepKeyword.And(expression).Then(x => x.Item2));
|
||||
|
||||
var forHeader = Between(leftParen,
|
||||
optionalVarKeyword
|
||||
.And(identifier)
|
||||
.And(equals)
|
||||
.And(expression)
|
||||
.And(rangeOperator)
|
||||
.And(expression)
|
||||
.And(optionalStepClause)
|
||||
.Then(x => (
|
||||
HasVar: x.Item1 != null,
|
||||
VarName: x.Item2.ToString(),
|
||||
Start: x.Item4,
|
||||
RangeOp: x.Item5.ToString(),
|
||||
End: x.Item6,
|
||||
Step: x.Item7
|
||||
)),
|
||||
rightParen);
|
||||
|
||||
var forStatementParser = forKeyword
|
||||
.And(forHeader)
|
||||
.And(forBody)
|
||||
.Then<StatementNode>(result =>
|
||||
{
|
||||
var header = result.Item2;
|
||||
var body = result.Item3;
|
||||
|
||||
// Default step to 1 if not specified
|
||||
var stepExpr = header.Step ?? new LiteralNode { Value = 1 };
|
||||
|
||||
return new ForNode
|
||||
{
|
||||
DeclaresVariable = header.HasVar,
|
||||
VariableName = header.VarName,
|
||||
Start = header.Start,
|
||||
End = header.End,
|
||||
Step = stepExpr,
|
||||
IsInclusive = header.RangeOp == "through",
|
||||
Body = body
|
||||
};
|
||||
});
|
||||
|
||||
forStatement.Parser = forStatementParser;
|
||||
|
||||
// ForEach statement: foreach (var item in collection) { body } or foreach (item in collection) statement
|
||||
// Must be defined after statementWithSemicolon
|
||||
// ForEach body can be either a block or a single statement
|
||||
var foreachBlockBody = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
|
||||
.Then(statements => (StatementNode)(statements.Count == 1
|
||||
? statements.First()
|
||||
: new BlockNode { Statements = statements.ToList() }));
|
||||
var foreachSingleStatementBody = statement;
|
||||
var foreachBody = foreachBlockBody.Or(foreachSingleStatementBody);
|
||||
|
||||
// ForEach header with optional var: (var item in collection) or (item in collection)
|
||||
var foreachOptionalVarKeyword = ZeroOrOne(varKeyword);
|
||||
|
||||
var foreachHeader = Between(leftParen,
|
||||
foreachOptionalVarKeyword
|
||||
.And(identifier)
|
||||
.And(inKeyword)
|
||||
.And(expression)
|
||||
.Then(x => (
|
||||
HasVar: x.Item1 != null,
|
||||
VarName: x.Item2.ToString(),
|
||||
Collection: x.Item4
|
||||
)),
|
||||
rightParen);
|
||||
|
||||
var foreachStatementParser = foreachKeyword
|
||||
.And(foreachHeader)
|
||||
.And(foreachBody)
|
||||
.Then<StatementNode>(result =>
|
||||
{
|
||||
var header = result.Item2;
|
||||
var body = result.Item3;
|
||||
|
||||
return new ForEachNode
|
||||
{
|
||||
DeclaresVariable = header.HasVar,
|
||||
VariableName = header.VarName,
|
||||
Collection = header.Collection,
|
||||
Body = body
|
||||
};
|
||||
});
|
||||
|
||||
foreachStatement.Parser = foreachStatementParser;
|
||||
|
||||
// Flowchart statement: flowchart { [variables] [nodes] [connections] [entry] }
|
||||
// Node declaration: label: statement;
|
||||
// Entry declaration: entry label;
|
||||
// Connection declaration: source -> target; or source.Outcome -> target;
|
||||
|
||||
// Flowchart body element can be:
|
||||
// 1. Variable declaration
|
||||
// 2. Node declaration (label: statement)
|
||||
// 3. Entry declaration (entry label)
|
||||
// 4. Connection declaration (source -> target or source.Outcome -> target)
|
||||
|
||||
// Node declaration: label: activityInvocation; or label: { block }
|
||||
// Note: We use activityInvocation directly (not statement) to avoid circular dependency
|
||||
// since statement includes flowchart which would include node declarations
|
||||
var nodeBlock = Between(leftBrace, ZeroOrMany(statementWithSemicolon), rightBrace)
|
||||
.Then<StatementNode>(statements => statements.Count == 1
|
||||
? statements.First()
|
||||
: new BlockNode { Statements = statements.ToList() });
|
||||
|
||||
var nodeActivityStatement = activityInvocation.Then<StatementNode>(s => s);
|
||||
|
||||
var nodeDeclaration = identifier
|
||||
.And(colon)
|
||||
.And(nodeBlock.Or(nodeActivityStatement))
|
||||
.And(ZeroOrOne(semicolon))
|
||||
.Then(x => new LabeledActivityNode
|
||||
{
|
||||
Label = x.Item1.ToString(),
|
||||
Activity = x.Item3
|
||||
});
|
||||
|
||||
// Entry declaration: entry label;
|
||||
var entryDeclaration = entryKeyword
|
||||
.And(identifier)
|
||||
.And(ZeroOrOne(semicolon))
|
||||
.Then(x => x.Item2.ToString());
|
||||
|
||||
// Connection declaration: source -> target; or source.Outcome -> target;
|
||||
// Source can be: identifier or identifier.identifier (with outcome)
|
||||
var optionalOutcome = ZeroOrOne(dot.And(identifier).Then(x => x.Item2.ToString()));
|
||||
|
||||
var connectionSource = identifier
|
||||
.And(optionalOutcome)
|
||||
.Then(x => (
|
||||
SourceLabel: x.Item1.ToString(),
|
||||
Outcome: x.Item2
|
||||
));
|
||||
|
||||
var connectionTarget = identifier;
|
||||
|
||||
var connectionDeclaration = connectionSource
|
||||
.And(rightArrow)
|
||||
.And(connectionTarget)
|
||||
.And(ZeroOrOne(semicolon))
|
||||
.Then(x => new ConnectionNode
|
||||
{
|
||||
Source = x.Item1.SourceLabel,
|
||||
Outcome = x.Item1.Outcome,
|
||||
Target = x.Item3.ToString()
|
||||
});
|
||||
|
||||
// Flowchart body element type - try each parser in order
|
||||
var flowchartBodyElement = variableDeclaration.Then<object>(v => v)
|
||||
.Or(entryDeclaration.Then<object>(e => e))
|
||||
.Or(nodeDeclaration.Then<object>(n => n))
|
||||
.Or(connectionDeclaration.Then<object>(c => c));
|
||||
|
||||
var flowchartBody = Between(leftBrace, ZeroOrMany(flowchartBodyElement), rightBrace);
|
||||
|
||||
var flowchartStatementParser = flowchartKeyword
|
||||
.And(flowchartBody)
|
||||
.Then<StatementNode>(result =>
|
||||
{
|
||||
var bodyElements = result.Item2;
|
||||
|
||||
var variables = new List<VariableDeclarationNode>();
|
||||
var nodes = new List<LabeledActivityNode>();
|
||||
var connections = new List<ConnectionNode>();
|
||||
string? entryPoint = null;
|
||||
|
||||
foreach (var element in bodyElements)
|
||||
{
|
||||
if (element is VariableDeclarationNode varDecl)
|
||||
variables.Add(varDecl);
|
||||
else if (element is LabeledActivityNode node)
|
||||
nodes.Add(node);
|
||||
else if (element is ConnectionNode conn)
|
||||
connections.Add(conn);
|
||||
else if (element is string entry)
|
||||
entryPoint = entry;
|
||||
}
|
||||
|
||||
return new FlowchartNode
|
||||
{
|
||||
Variables = variables,
|
||||
Activities = nodes,
|
||||
Connections = connections,
|
||||
EntryPoint = entryPoint
|
||||
};
|
||||
});
|
||||
|
||||
flowchartStatement.Parser = flowchartStatementParser;
|
||||
|
||||
// Use statement: use Namespace; or use expressions lang;
|
||||
var namespaceUse = identifier
|
||||
.And(ZeroOrMany(dot.And(identifier)))
|
||||
.Then(x =>
|
||||
{
|
||||
var ns = x.Item1.ToString();
|
||||
foreach (var part in x.Item2)
|
||||
{
|
||||
ns += "." + part.Item2.ToString();
|
||||
}
|
||||
return new UseNode { Type = UseType.Namespace, Value = ns };
|
||||
});
|
||||
|
||||
var expressionUse = expressionsKeyword
|
||||
.And(identifier)
|
||||
.Then(x => new UseNode { Type = UseType.Expressions, Value = x.Item2.ToString() });
|
||||
|
||||
var useStatement = useKeyword
|
||||
.And(expressionUse.Or(namespaceUse))
|
||||
.And(ZeroOrOne(semicolon))
|
||||
.Then(x => x.Item2);
|
||||
|
||||
// Workflow metadata: name: value
|
||||
var metadataEntry = identifier
|
||||
.And(colon)
|
||||
.And(expression)
|
||||
.Then(x => (Name: x.Item1.ToString(), Value: EvaluateConstantExpressionStatic(x.Item3)));
|
||||
|
||||
var commaSeparatedMetadata = metadataEntry.And(ZeroOrOne(comma)).Then(x => x.Item1);
|
||||
var metadataList = ZeroOrMany(commaSeparatedMetadata);
|
||||
|
||||
// Workflow declaration: workflow Identifier [(metadata)] { [use statements] [statements] }
|
||||
var workflowMetadata = Between(leftParen, metadataList, rightParen);
|
||||
|
||||
// Workflow body can contain use statements and regular statements
|
||||
var workflowUseStatement = useStatement;
|
||||
|
||||
var workflowBodyElement = Deferred<object>();
|
||||
workflowBodyElement.Parser = workflowUseStatement
|
||||
.Then<object>(u => u)
|
||||
.Or(statementWithSemicolon.Then<object>(s => s));
|
||||
|
||||
var workflowBody = Between(leftBrace, ZeroOrMany(workflowBodyElement), rightBrace);
|
||||
|
||||
var workflowWithMetadata = workflowKeyword
|
||||
.And(identifier)
|
||||
.And(workflowMetadata)
|
||||
.Then(x => (WorkflowId: x.Item2.ToString(), Metadata: x.Item3));
|
||||
|
||||
var workflowWithoutMetadata = workflowKeyword
|
||||
.And(identifier)
|
||||
.Then(x => (WorkflowId: x.Item2.ToString(), Metadata: (IReadOnlyList<(string Name, object Value)>?)null));
|
||||
|
||||
var workflowHeader = workflowWithMetadata.Or(workflowWithoutMetadata);
|
||||
|
||||
var workflowDeclaration = workflowHeader
|
||||
.And(workflowBody)
|
||||
.Then(x =>
|
||||
{
|
||||
var header = x.Item1;
|
||||
var bodyElements = x.Item2;
|
||||
|
||||
var metadataDict = new Dictionary<string, object>();
|
||||
if (header.Metadata != null)
|
||||
{
|
||||
foreach (var entry in header.Metadata)
|
||||
{
|
||||
metadataDict[entry.Name] = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Separate use statements from regular statements in body
|
||||
var workflowUses = new List<UseNode>();
|
||||
var statements = new List<StatementNode>();
|
||||
|
||||
foreach (var element in bodyElements)
|
||||
{
|
||||
if (element is UseNode useNode)
|
||||
workflowUses.Add(useNode);
|
||||
else if (element is StatementNode stmt)
|
||||
statements.Add(stmt);
|
||||
}
|
||||
|
||||
return new WorkflowNode
|
||||
{
|
||||
Id = header.WorkflowId,
|
||||
Metadata = metadataDict,
|
||||
UseStatements = workflowUses,
|
||||
Body = statements
|
||||
};
|
||||
});
|
||||
|
||||
// Program with single workflow: [global use statements] [workflow declaration]
|
||||
var programWithWorkflow = ZeroOrMany(useStatement)
|
||||
.And(workflowDeclaration)
|
||||
.Then(x =>
|
||||
{
|
||||
var globalUses = x.Item1.Select(u => (UseNode)u).ToList();
|
||||
var workflow = x.Item2;
|
||||
|
||||
return new ProgramNode
|
||||
{
|
||||
GlobalUseStatements = globalUses,
|
||||
Workflows = new List<WorkflowNode> { workflow }
|
||||
};
|
||||
});
|
||||
|
||||
// Fallback: raw statements without workflow keyword (backward compatibility)
|
||||
// Only match if there are actual statements (OneOrMany)
|
||||
var programWithStatements = ZeroOrMany(useStatement)
|
||||
.And(OneOrMany(statementWithSemicolon))
|
||||
.Then(x =>
|
||||
{
|
||||
var globalUses = x.Item1.Select(u => (UseNode)u).ToList();
|
||||
var statements = x.Item2.ToList();
|
||||
|
||||
return new ProgramNode
|
||||
{
|
||||
GlobalUseStatements = globalUses,
|
||||
Workflows = new List<WorkflowNode>
|
||||
{
|
||||
new WorkflowNode
|
||||
{
|
||||
Id = "DefaultWorkflow",
|
||||
UseStatements = new List<UseNode>(),
|
||||
Body = statements
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
var programParser = programWithWorkflow.Or(programWithStatements);
|
||||
|
||||
ProgramParser = programParser;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProgramNode Parse(string source)
|
||||
{
|
||||
if (!ProgramParser.TryParse(source, out var result, out var error))
|
||||
{
|
||||
var errorMessage = error != null
|
||||
? $"{error.Message} at {error.Position}"
|
||||
: "Unknown parse error";
|
||||
throw new ParseException($"Failed to parse ElsaScript: {errorMessage}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static helper to evaluate constant expressions during parsing.
|
||||
/// </summary>
|
||||
private static object EvaluateConstantExpressionStatic(ExpressionNode exprNode)
|
||||
{
|
||||
return exprNode switch
|
||||
{
|
||||
LiteralNode literal => literal.Value ?? string.Empty,
|
||||
IdentifierNode identifier => identifier.Name,
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when parsing fails.
|
||||
/// </summary>
|
||||
public class ParseException : Exception
|
||||
{
|
||||
public ParseException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom parser that captures raw text after => until the matching closing parenthesis.
|
||||
/// Supports nested parentheses.
|
||||
/// </summary>
|
||||
internal sealed class RawExpressionParser : Parser<TextSpan>
|
||||
{
|
||||
public override bool Parse(ParseContext context, ref ParseResult<TextSpan> result)
|
||||
{
|
||||
context.EnterParser(this);
|
||||
|
||||
var scanner = context.Scanner;
|
||||
var start = scanner.Cursor.Offset;
|
||||
var depth = 0;
|
||||
|
||||
while (!scanner.Cursor.Eof)
|
||||
{
|
||||
var ch = scanner.Cursor.Current;
|
||||
|
||||
if (ch == '(')
|
||||
{
|
||||
depth++;
|
||||
scanner.Cursor.Advance();
|
||||
}
|
||||
else if (ch == ')')
|
||||
{
|
||||
if (depth == 0)
|
||||
{
|
||||
// This is the closing paren for the activity invocation
|
||||
break;
|
||||
}
|
||||
depth--;
|
||||
scanner.Cursor.Advance();
|
||||
}
|
||||
else
|
||||
{
|
||||
scanner.Cursor.Advance();
|
||||
}
|
||||
}
|
||||
|
||||
var length = scanner.Cursor.Offset - start;
|
||||
if (length == 0)
|
||||
{
|
||||
context.ExitParser(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
var text = new TextSpan(scanner.Buffer, start, length);
|
||||
result.Set(start, scanner.Cursor.Offset, text);
|
||||
context.ExitParser(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
185
src/modules/Elsa.Dsl.ElsaScript/README.md
Normal file
185
src/modules/Elsa.Dsl.ElsaScript/README.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# ElsaScript DSL Implementation
|
||||
|
||||
This document provides an overview of the ElsaScript DSL implementation for Elsa Workflows.
|
||||
|
||||
## Overview
|
||||
|
||||
ElsaScript is a JavaScript-inspired textual DSL for authoring Elsa 3 workflows. It provides a concise, code-centric alternative to C# WorkflowBuilder APIs and JSON workflow definitions.
|
||||
|
||||
## Features Implemented
|
||||
|
||||
### Parser (`ElsaScriptParser`)
|
||||
|
||||
The parser uses a simplified regex-based approach to parse ElsaScript source code into an Abstract Syntax Tree (AST).
|
||||
|
||||
**Supported Syntax:**
|
||||
- `use` statements for namespaces and expression language configuration
|
||||
- `workflow "Name" { ... }` declarations
|
||||
- Variable declarations: `var`, `let`, `const`
|
||||
- Activity invocations with positional and named arguments
|
||||
- Expression literals (strings, numbers, booleans, arrays)
|
||||
- Expression language prefixes (`=>`, `js =>`, `cs =>`, `py =>`, `liquid =>`)
|
||||
- `listen` keyword for workflow triggers (partial support)
|
||||
|
||||
### AST Nodes
|
||||
|
||||
Complete set of AST node types:
|
||||
- `WorkflowNode` - Root workflow definition
|
||||
- `StatementNode` base class with implementations:
|
||||
- `VariableDeclarationNode`
|
||||
- `ActivityInvocationNode`
|
||||
- `BlockNode` (for sequences)
|
||||
- `IfNode`, `ForEachNode`, `ForNode`, `WhileNode`, `SwitchNode`
|
||||
- `FlowchartNode`, `ListenNode`
|
||||
- `ExpressionNode` base class with implementations:
|
||||
- `LiteralNode`, `IdentifierNode`, `ArrayLiteralNode`
|
||||
- `ElsaExpressionNode` (with language support)
|
||||
- `TemplateStringNode`, `BinaryExpressionNode`, `UnaryExpressionNode`
|
||||
- `ArgumentNode` for activity parameters
|
||||
- `UseNode` for import/configuration statements
|
||||
|
||||
### Compiler (`ElsaScriptCompiler`)
|
||||
|
||||
Compiles AST to Elsa workflow activities:
|
||||
- Traverses AST and constructs Elsa activity graphs
|
||||
- Maps DSL constructs to Elsa activities:
|
||||
- Blocks → `Sequence`
|
||||
- Variables → `Variable` instances
|
||||
- Activity invocations → Elsa activity instances
|
||||
- Control flow nodes → corresponding Elsa activities
|
||||
- Resolves activities via `IActivityRegistry`
|
||||
- Binds expressions to Elsa expression system with language support
|
||||
- Maps expression languages (js, cs, py, liquid) to Elsa providers
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
Integration with Elsa's module system:
|
||||
- `ElsaScriptFeature` for service registration
|
||||
- `ModuleExtensions.UseElsaScript()` for easy setup
|
||||
- Registers `IElsaScriptParser` and `IElsaScriptCompiler` services
|
||||
|
||||
## Example Usage
|
||||
|
||||
```elsa
|
||||
use Elsa.Activities.Console;
|
||||
use expressions js;
|
||||
|
||||
workflow "HelloWorld" {
|
||||
var greeting = "Hello";
|
||||
WriteLine(=> greeting + " World");
|
||||
WriteLine("Great to meet you!");
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
Six integration tests demonstrate the implementation:
|
||||
|
||||
1. **Parser can parse a simple workflow definition** - Verifies basic workflow parsing
|
||||
2. **Parser can parse variable declarations** - Tests var/let/const parsing
|
||||
3. **Parser can parse activity invocations with named arguments** - Tests argument parsing
|
||||
4. **Compiler service is registered and available** - Verifies DI setup
|
||||
5. **Compiler can parse and analyze a workflow AST** - Tests AST analysis
|
||||
6. **Compiler recognizes variable declarations in AST** - Tests variable compilation
|
||||
|
||||
## Known Limitations
|
||||
|
||||
This is a foundation implementation with several areas for future enhancement:
|
||||
|
||||
### Parser Limitations
|
||||
1. Uses regex instead of Parlot for parsing (simpler but less robust)
|
||||
2. Control flow parsing (if/foreach/while/for/switch) needs completion
|
||||
3. Block/sequence parsing needs enhancement
|
||||
4. Flowchart syntax not yet implemented
|
||||
5. Template string parsing incomplete
|
||||
|
||||
### Compiler Limitations
|
||||
1. Dynamic activity instantiation needs refinement
|
||||
2. Control flow compilation stubs need full implementation
|
||||
3. For loop compilation not implemented
|
||||
4. Flowchart compilation not implemented
|
||||
5. Error handling and diagnostics minimal
|
||||
|
||||
### Runtime Limitations
|
||||
1. End-to-end workflow execution not fully working
|
||||
2. Activity property setting needs enhancement
|
||||
3. Complex expression evaluation untested
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Short Term
|
||||
1. Complete parser using Parlot for robust parsing
|
||||
2. Implement full control flow parsing and compilation
|
||||
3. Fix dynamic activity instantiation for runtime execution
|
||||
4. Add comprehensive error messages and diagnostics
|
||||
|
||||
### Medium Term
|
||||
1. Implement flowchart parsing and compilation
|
||||
2. Support template strings with interpolation
|
||||
3. Add output capture syntax (`let result = Activity()`)
|
||||
4. Implement switch fallthrough options
|
||||
5. Add try/catch/finally support
|
||||
|
||||
### Long Term
|
||||
1. IDE support (syntax highlighting, IntelliSense)
|
||||
2. Debugging support
|
||||
3. Performance optimizations
|
||||
4. Extended DSL features (custom operators, macros, etc.)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Current tests focus on:
|
||||
- Parser correctness for basic constructs
|
||||
- AST structure validation
|
||||
- Compiler service availability
|
||||
- Variable and activity node recognition
|
||||
|
||||
Additional testing needed for:
|
||||
- Control flow execution
|
||||
- Expression evaluation
|
||||
- Error handling
|
||||
- Edge cases and invalid syntax
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Why Regex-Based Parser?
|
||||
The simplified regex-based parser was chosen to deliver a working proof-of-concept quickly. While less robust than a full Parlot implementation, it demonstrates the DSL concept and can be replaced with a more sophisticated parser later.
|
||||
|
||||
### Why Separate AST and Compiler?
|
||||
The separation allows for:
|
||||
- Multiple compilation targets (if needed)
|
||||
- AST analysis and transformation
|
||||
- Better error reporting
|
||||
- Easier testing of each component
|
||||
|
||||
### Why Reuse Elsa's Expression System?
|
||||
Rather than creating a new expression language, ElsaScript wraps Elsa's existing expression providers (JavaScript, C#, Python, Liquid). This provides:
|
||||
- Consistency with existing Elsa workflows
|
||||
- Proven expression evaluation
|
||||
- No additional dependencies
|
||||
- Familiar syntax for Elsa users
|
||||
|
||||
## Files Added
|
||||
|
||||
### Source Files
|
||||
- `src/modules/Elsa.Dsl.ElsaScript/`
|
||||
- `Ast/` - AST node definitions
|
||||
- `Compiler/` - AST to Elsa workflow compiler
|
||||
- `Contracts/` - Service interfaces
|
||||
- `Extensions/` - DI extension methods
|
||||
- `Features/` - Elsa feature module
|
||||
- `Parser/` - ElsaScript parser
|
||||
- `Elsa.Dsl.ElsaScript.csproj` - Project file
|
||||
|
||||
### Test Files
|
||||
- `test/integration/Elsa.Dsl.ElsaScript.IntegrationTests/`
|
||||
- `ParserTests.cs` - Parser integration tests
|
||||
- `CompilerTests.cs` - Compiler integration tests
|
||||
- `Elsa.Dsl.ElsaScript.IntegrationTests.csproj` - Test project
|
||||
|
||||
### Configuration
|
||||
- `Directory.Packages.props` - Added Parlot package reference
|
||||
|
||||
## Summary
|
||||
|
||||
The ElsaScript DSL provides a solid foundation for text-based workflow authoring in Elsa 3. While this initial implementation has limitations, it demonstrates the core concepts and provides a framework for future enhancements. The modular architecture integrates cleanly with Elsa's existing systems and can be extended incrementally.
|
||||
|
|
@ -29,6 +29,19 @@ public class HttpEndpoint : Trigger<HttpRequest>
|
|||
public HttpEndpoint([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The path to associate with the workflow.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ public class WriteHttpResponse : Activity
|
|||
public WriteHttpResponse([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public WriteHttpResponse(Input<object?> content, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
Content = content;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public WriteHttpResponse(Input<object?> content, Input<string?> contentType, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
Content = content;
|
||||
ContentType = contentType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The status code to return.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.Persistence.EFCore.MySql.Migrations.Management
|
||||
{
|
||||
[DbContext(typeof(ManagementElsaDbContext))]
|
||||
[Migration("20250711172853_V3_6")]
|
||||
[Migration("20251116182606_V3_6")]
|
||||
partial class V3_6
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -21,7 +21,7 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
|
@ -69,6 +69,9 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
|
|
@ -18,6 +18,14 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Management
|
|||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "StringData",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -45,6 +53,11 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Management
|
|||
oldNullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
|
@ -66,6 +66,9 @@ namespace Elsa.Persistence.EFCore.MySql.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ internal class Management : IEntityTypeConfiguration<WorkflowDefinition>, IEntit
|
|||
builder.Property<string>("Data").HasColumnType("NCLOB");
|
||||
builder.Property(x => x.Description).HasColumnType("NCLOB");
|
||||
builder.Property(x => x.MaterializerContext).HasColumnType("NCLOB");
|
||||
builder.Property(x => x.OriginalSource).HasColumnType("NCLOB");
|
||||
builder.Property(x => x.BinaryData).HasColumnType("BLOB");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class V3_6 : Migration
|
||||
{
|
||||
private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema;
|
||||
|
||||
/// <inheritdoc />
|
||||
public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema)
|
||||
{
|
||||
_schema = schema;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Data",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowInstances",
|
||||
type: "NVARCHAR2(2000)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NCLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "StringData",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NVARCHAR2(2000)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NCLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MaterializerContext",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NVARCHAR2(2000)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NCLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NVARCHAR2(2000)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NCLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Data",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NVARCHAR2(2000)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NCLOB",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "BinaryData",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "RAW(2000)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "BLOB",
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Data",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowInstances",
|
||||
type: "NCLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NVARCHAR2(2000)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "StringData",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NCLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NVARCHAR2(2000)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MaterializerContext",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NCLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NVARCHAR2(2000)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Description",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NCLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NVARCHAR2(2000)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Data",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NCLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NVARCHAR2(2000)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<byte[]>(
|
||||
name: "BinaryData",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "BLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(byte[]),
|
||||
oldType: "RAW(2000)",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata;
|
|||
namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
||||
{
|
||||
[DbContext(typeof(ManagementElsaDbContext))]
|
||||
[Migration("20250711191819_V3_6")]
|
||||
[Migration("20251116182825_V3_6")]
|
||||
partial class V3_6
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -21,7 +21,7 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
|
@ -69,6 +69,9 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("NVARCHAR2(450)");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("NVARCHAR2(2000)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("NVARCHAR2(2000)");
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class V3_6 : Migration
|
||||
{
|
||||
private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema;
|
||||
|
||||
/// <inheritdoc />
|
||||
public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema)
|
||||
{
|
||||
_schema = schema;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NCLOB",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "StringData",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "JSON",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "NCLOB",
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "StringData",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "NCLOB",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "JSON",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
|
@ -29,20 +29,20 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
|||
.HasColumnType("NVARCHAR2(450)");
|
||||
|
||||
b.Property<byte[]>("BinaryData")
|
||||
.HasColumnType("RAW(2000)");
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TIMESTAMP(7) WITH TIME ZONE");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("NVARCHAR2(2000)");
|
||||
.HasColumnType("NCLOB");
|
||||
|
||||
b.Property<string>("DefinitionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("NVARCHAR2(450)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("NVARCHAR2(2000)");
|
||||
.HasColumnType("NCLOB");
|
||||
|
||||
b.Property<bool>("IsLatest")
|
||||
.HasColumnType("BOOLEAN");
|
||||
|
|
@ -57,7 +57,7 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
|||
.HasColumnType("BOOLEAN");
|
||||
|
||||
b.Property<string>("MaterializerContext")
|
||||
.HasColumnType("NVARCHAR2(2000)");
|
||||
.HasColumnType("NCLOB");
|
||||
|
||||
b.Property<string>("MaterializerName")
|
||||
.IsRequired()
|
||||
|
|
@ -66,11 +66,14 @@ namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("NVARCHAR2(450)");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("NCLOB");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("NVARCHAR2(2000)");
|
||||
|
||||
b.Property<string>("StringData")
|
||||
.HasColumnType("NVARCHAR2(2000)");
|
||||
.HasColumnType("JSON");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.HasColumnType("NVARCHAR2(450)");
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||
namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management
|
||||
{
|
||||
[DbContext(typeof(ManagementElsaDbContext))]
|
||||
[Migration("20250711092046_V3_6")]
|
||||
[Migration("20251116182753_V3_6")]
|
||||
partial class V3_6
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -21,7 +21,7 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
|
@ -69,6 +69,9 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
|
@ -18,6 +18,13 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management
|
|||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
var schema = _schema.Schema;
|
||||
migrationBuilder.Sql($@"
|
||||
ALTER TABLE ""{schema}"".""WorkflowDefinitions""
|
||||
|
|
@ -37,6 +44,11 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management
|
|||
TYPE text
|
||||
USING ""StringData""::text;
|
||||
");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
|
@ -66,6 +66,9 @@ namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
|
|||
235
src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Management/20251116182628_V3_6.Designer.cs
generated
Normal file
235
src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Management/20251116182628_V3_6.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Elsa.Persistence.EFCore.Modules.Management;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Management
|
||||
{
|
||||
[DbContext(typeof(ManagementElsaDbContext))]
|
||||
[Migration("20251116182628_V3_6")]
|
||||
partial class V3_6
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowDefinition", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<byte[]>("BinaryData")
|
||||
.HasColumnType("varbinary(max)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DefinitionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsLatest")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsPublished")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsReadonly")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("MaterializerContext")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("MaterializerName")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("StringData")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ToolVersion")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool?>("UsableAsActivity")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsLatest")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_IsLatest");
|
||||
|
||||
b.HasIndex("IsPublished")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_IsPublished");
|
||||
|
||||
b.HasIndex("IsSystem")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_IsSystem");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_Name");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_TenantId");
|
||||
|
||||
b.HasIndex("UsableAsActivity")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_UsableAsActivity");
|
||||
|
||||
b.HasIndex("Version")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_Version");
|
||||
|
||||
b.HasIndex("DefinitionId", "Version")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_WorkflowDefinition_DefinitionId_Version");
|
||||
|
||||
b.ToTable("WorkflowDefinitions", "Elsa");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowInstance", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DataCompressionAlgorithm")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DefinitionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("DefinitionVersionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset?>("FinishedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int>("IncidentCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsExecuting")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ParentWorkflowInstanceId")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("SubStatus")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CorrelationId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_CorrelationId");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_WorkflowInstance_CreatedAt");
|
||||
|
||||
b.HasIndex("DefinitionId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_DefinitionId");
|
||||
|
||||
b.HasIndex("FinishedAt")
|
||||
.HasDatabaseName("IX_WorkflowInstance_FinishedAt");
|
||||
|
||||
b.HasIndex("IsExecuting")
|
||||
.HasDatabaseName("IX_WorkflowInstance_IsExecuting");
|
||||
|
||||
b.HasIndex("IsSystem")
|
||||
.HasDatabaseName("IX_WorkflowInstance_IsSystem");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Name");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status");
|
||||
|
||||
b.HasIndex("SubStatus")
|
||||
.HasDatabaseName("IX_WorkflowInstance_SubStatus");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_TenantId");
|
||||
|
||||
b.HasIndex("UpdatedAt")
|
||||
.HasDatabaseName("IX_WorkflowInstance_UpdatedAt");
|
||||
|
||||
b.HasIndex("Status", "DefinitionId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status_DefinitionId");
|
||||
|
||||
b.HasIndex("Status", "SubStatus")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status_SubStatus");
|
||||
|
||||
b.HasIndex("SubStatus", "DefinitionId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_SubStatus_DefinitionId");
|
||||
|
||||
b.HasIndex("Status", "SubStatus", "DefinitionId", "Version")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status_SubStatus_DefinitionId_Version");
|
||||
|
||||
b.ToTable("WorkflowInstances", "Elsa");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Management
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class V3_6 : Migration
|
||||
{
|
||||
private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema;
|
||||
|
||||
/// <inheritdoc />
|
||||
public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema)
|
||||
{
|
||||
_schema = schema;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "8.0.12")
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
|
@ -66,6 +66,9 @@ namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
|
|
|||
231
src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Management/20251116182711_V3_6.Designer.cs
generated
Normal file
231
src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Management/20251116182711_V3_6.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Elsa.Persistence.EFCore.Modules.Management;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Management
|
||||
{
|
||||
[DbContext(typeof(ManagementElsaDbContext))]
|
||||
[Migration("20251116182711_V3_6")]
|
||||
partial class V3_6
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "9.0.10");
|
||||
|
||||
modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowDefinition", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("BinaryData")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DefinitionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsLatest")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsPublished")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsReadonly")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MaterializerContext")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MaterializerName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StringData")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToolVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("UsableAsActivity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsLatest")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_IsLatest");
|
||||
|
||||
b.HasIndex("IsPublished")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_IsPublished");
|
||||
|
||||
b.HasIndex("IsSystem")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_IsSystem");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_Name");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_TenantId");
|
||||
|
||||
b.HasIndex("UsableAsActivity")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_UsableAsActivity");
|
||||
|
||||
b.HasIndex("Version")
|
||||
.HasDatabaseName("IX_WorkflowDefinition_Version");
|
||||
|
||||
b.HasIndex("DefinitionId", "Version")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_WorkflowDefinition_DefinitionId_Version");
|
||||
|
||||
b.ToTable("WorkflowDefinitions", "Elsa");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowInstance", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DataCompressionAlgorithm")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DefinitionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DefinitionVersionId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("FinishedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("IncidentCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsExecuting")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentWorkflowInstanceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubStatus")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CorrelationId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_CorrelationId");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_WorkflowInstance_CreatedAt");
|
||||
|
||||
b.HasIndex("DefinitionId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_DefinitionId");
|
||||
|
||||
b.HasIndex("FinishedAt")
|
||||
.HasDatabaseName("IX_WorkflowInstance_FinishedAt");
|
||||
|
||||
b.HasIndex("IsExecuting")
|
||||
.HasDatabaseName("IX_WorkflowInstance_IsExecuting");
|
||||
|
||||
b.HasIndex("IsSystem")
|
||||
.HasDatabaseName("IX_WorkflowInstance_IsSystem");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Name");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status");
|
||||
|
||||
b.HasIndex("SubStatus")
|
||||
.HasDatabaseName("IX_WorkflowInstance_SubStatus");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_TenantId");
|
||||
|
||||
b.HasIndex("UpdatedAt")
|
||||
.HasDatabaseName("IX_WorkflowInstance_UpdatedAt");
|
||||
|
||||
b.HasIndex("Status", "DefinitionId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status_DefinitionId");
|
||||
|
||||
b.HasIndex("Status", "SubStatus")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status_SubStatus");
|
||||
|
||||
b.HasIndex("SubStatus", "DefinitionId")
|
||||
.HasDatabaseName("IX_WorkflowInstance_SubStatus_DefinitionId");
|
||||
|
||||
b.HasIndex("Status", "SubStatus", "DefinitionId", "Version")
|
||||
.HasDatabaseName("IX_WorkflowInstance_Status_SubStatus_DefinitionId_Version");
|
||||
|
||||
b.ToTable("WorkflowInstances", "Elsa");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Management
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class V3_6 : Migration
|
||||
{
|
||||
private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema;
|
||||
|
||||
/// <inheritdoc />
|
||||
public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema)
|
||||
{
|
||||
_schema = schema;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OriginalSource",
|
||||
schema: _schema.Schema,
|
||||
table: "WorkflowDefinitions");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Management
|
|||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("Elsa")
|
||||
.HasAnnotation("ProductVersion", "8.0.12");
|
||||
.HasAnnotation("ProductVersion", "9.0.10");
|
||||
|
||||
modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowDefinition", b =>
|
||||
{
|
||||
|
|
@ -62,6 +62,9 @@ namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Management
|
|||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OriginalSource")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
mods=("Management")
|
||||
|
||||
# Define the list of providers
|
||||
# providers=("MySql" "SqlServer" "Sqlite" "PostgreSql" "Oracle")
|
||||
providers=("Oracle")
|
||||
providers=("MySql" "SqlServer" "Sqlite" "PostgreSql" "Oracle")
|
||||
|
||||
# Loop through each module
|
||||
for module in "${mods[@]}"; do
|
||||
|
|
|
|||
|
|
@ -108,50 +108,92 @@ public class ScheduledCronTask : IScheduledTask, IDisposable
|
|||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
// Check if disposed before proceeding
|
||||
if (_disposed) return;
|
||||
// Early exit if disposed to prevent accessing disposed resources
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
IServiceScope? scope = null;
|
||||
try
|
||||
{
|
||||
var acquired = false;
|
||||
try
|
||||
{
|
||||
acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!acquired) return;
|
||||
|
||||
_executing = true;
|
||||
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
||||
if (_cancellationRequested)
|
||||
{
|
||||
_cancellationRequested = false;
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error executing scheduled task");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
if (acquired && !_disposed)
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
scope = _scopeFactory.CreateScope();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Service provider was disposed, exit gracefully
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested && !_disposed)
|
||||
Schedule();
|
||||
using (scope)
|
||||
{
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var acquired = false;
|
||||
try
|
||||
{
|
||||
acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!acquired) return;
|
||||
|
||||
_executing = true;
|
||||
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
||||
if (_cancellationRequested)
|
||||
{
|
||||
_cancellationRequested = false;
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Semaphore was disposed during execution, exit gracefully
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Only log if not disposed to avoid logging after test context is disposed
|
||||
if (!_disposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogError(e, "Error executing scheduled task");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore logging errors (e.g., when test output is no longer available)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
if (acquired)
|
||||
{
|
||||
try
|
||||
{
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Semaphore was disposed, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check again if disposed before scheduling next execution
|
||||
if (!cancellationToken.IsCancellationRequested && !_disposed)
|
||||
Schedule();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
_timer?.Dispose();
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Extensions;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Scheduling.Services;
|
||||
|
||||
|
|
@ -8,8 +9,9 @@ namespace Elsa.Scheduling.Services;
|
|||
public class LocalScheduler : IScheduler
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IDictionary<string, IScheduledTask> _scheduledTasks = new Dictionary<string, IScheduledTask>();
|
||||
private readonly IDictionary<IScheduledTask, ICollection<string>> _scheduledTaskKeys = new Dictionary<IScheduledTask, ICollection<string>>();
|
||||
private readonly ILogger<LocalScheduler> _logger;
|
||||
private readonly ConcurrentDictionary<string, IScheduledTask> _scheduledTasks = new();
|
||||
private readonly ConcurrentDictionary<IScheduledTask, ICollection<string>> _scheduledTaskKeys = new();
|
||||
|
||||
// Note: Using lock instead of SemaphoreSlim because:
|
||||
// 1. All critical sections are synchronous (dictionary operations only)
|
||||
|
|
@ -21,9 +23,10 @@ public class LocalScheduler : IScheduler
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalScheduler"/> class.
|
||||
/// </summary>
|
||||
public LocalScheduler(IServiceProvider serviceProvider)
|
||||
public LocalScheduler(IServiceProvider serviceProvider, ILogger<LocalScheduler> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -70,13 +73,17 @@ public class LocalScheduler : IScheduler
|
|||
|
||||
private void RegisterScheduledTask(string name, IScheduledTask scheduledTask, IEnumerable<string>? keys = null)
|
||||
{
|
||||
if (_scheduledTasks.TryGetValue(name, out var existingScheduledTask))
|
||||
{
|
||||
existingScheduledTask.Cancel();
|
||||
_scheduledTaskKeys.Remove(existingScheduledTask);
|
||||
}
|
||||
|
||||
_scheduledTasks[name] = scheduledTask;
|
||||
_scheduledTasks.AddOrUpdate(
|
||||
name,
|
||||
addValueFactory: _ => scheduledTask,
|
||||
updateValueFactory: (_, existingScheduledTask) =>
|
||||
{
|
||||
existingScheduledTask.Cancel();
|
||||
var removed = _scheduledTaskKeys.TryRemove(existingScheduledTask, out var _);
|
||||
if (!removed)
|
||||
_logger.LogWarning("Tried to remove scheduled task keys for an existing scheduled task, but it was not present in _scheduledTaskKeys");
|
||||
return scheduledTask;
|
||||
});
|
||||
|
||||
if (keys != null)
|
||||
_scheduledTaskKeys[scheduledTask] = keys.ToList();
|
||||
|
|
@ -86,8 +93,8 @@ public class LocalScheduler : IScheduler
|
|||
{
|
||||
if (_scheduledTasks.TryGetValue(name, out var existingScheduledTask))
|
||||
{
|
||||
_scheduledTaskKeys.Remove(existingScheduledTask);
|
||||
_scheduledTasks.Remove(name);
|
||||
_scheduledTaskKeys.Remove(existingScheduledTask, out _);
|
||||
_scheduledTasks.Remove(name, out _);
|
||||
existingScheduledTask.Cancel();
|
||||
}
|
||||
}
|
||||
|
|
@ -100,8 +107,16 @@ public class LocalScheduler : IScheduler
|
|||
|
||||
foreach (var scheduledTask in scheduledTasks)
|
||||
{
|
||||
_scheduledTasks.RemoveWhere(x => x.Value == scheduledTask);
|
||||
_scheduledTaskKeys.Remove(scheduledTask);
|
||||
// Collect all keys in _scheduledTasks that map to this scheduledTask
|
||||
var matchingTaskKeys = _scheduledTasks.Where(x => x.Value == scheduledTask).Select(x => x.Key).ToList();
|
||||
foreach (var taskKey in matchingTaskKeys)
|
||||
{
|
||||
var removed = _scheduledTasks.TryRemove(taskKey, out _);
|
||||
if (!removed)
|
||||
_logger.LogWarning("Failed to remove scheduled task with key '{TaskKey}' for '{Key}' from _scheduledTasks", taskKey, key);
|
||||
}
|
||||
|
||||
_scheduledTaskKeys.Remove(scheduledTask, out _);
|
||||
scheduledTask.Cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides ElsaScript support for BlobStorage workflows provider.
|
||||
</Description>
|
||||
<PackageTags>elsa extension module elsascript blob storage</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.WorkflowProviders.BlobStorage\Elsa.WorkflowProviders.BlobStorage.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Dsl.ElsaScript\Elsa.Dsl.ElsaScript.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Features;
|
||||
|
||||
namespace Elsa.WorkflowProviders.BlobStorage.ElsaScript.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="IModule"/>.
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables ElsaScript support for BlobStorage workflows.
|
||||
/// </summary>
|
||||
public static IModule UseElsaScriptBlobStorage(this IModule module, Action<ElsaScriptBlobStorageFeature>? configure = null)
|
||||
{
|
||||
module.Configure(configure);
|
||||
return module;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using Elsa.Dsl.ElsaScript.Compiler;
|
||||
using Elsa.Dsl.ElsaScript.Features;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Contracts;
|
||||
using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Handlers;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.WorkflowProviders.BlobStorage.ElsaScript.Features;
|
||||
|
||||
/// <summary>
|
||||
/// A feature that enables ElsaScript support for the BlobStorage workflow provider.
|
||||
/// </summary>
|
||||
[DependsOn(typeof(BlobStorageFeature))]
|
||||
[DependsOn(typeof(ElsaScriptFeature))]
|
||||
public class ElsaScriptBlobStorageFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
// Register the ElsaScript format handler
|
||||
Services.AddScoped<IBlobWorkflowFormatHandler, ElsaScriptBlobWorkflowFormatHandler>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using Elsa.Dsl.ElsaScript.Contracts;
|
||||
using Elsa.Dsl.ElsaScript.Materializers;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Contracts;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using FluentStorage.Blobs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.WorkflowProviders.BlobStorage.ElsaScript.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handles ElsaScript-formatted workflow definitions from blob storage.
|
||||
/// </summary>
|
||||
public class ElsaScriptBlobWorkflowFormatHandler(IElsaScriptCompiler compiler, ILogger<ElsaScriptBlobWorkflowFormatHandler> logger) : IBlobWorkflowFormatHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "ElsaScript";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> SupportedExtensions => ["elsa"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(Blob blob, string? contentType)
|
||||
{
|
||||
// Extension filtering is already handled by the provider via SupportedExtensions.
|
||||
// Here we can optionally check content type for additional validation.
|
||||
if (!string.IsNullOrEmpty(contentType) && contentType.Contains("elsa", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// If no content type is available, assume we can handle it (since extension was already validated)
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<MaterializedWorkflow?> TryParseAsync(Blob blob, string content, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var workflow = await compiler.CompileAsync(content, cancellationToken);
|
||||
|
||||
return new(
|
||||
workflow,
|
||||
ProviderName: "FluentStorage",
|
||||
MaterializerName: ElsaScriptWorkflowMaterializer.MaterializerName,
|
||||
MaterializerContext: null,
|
||||
OriginalSource: content // Preserve the original ElsaScript source
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Intentionally catching all exceptions here to gracefully handle any failure during workflow parsing.
|
||||
// This includes ParseException, IOException, compiler errors, or any unexpected exceptions.
|
||||
// Since this is a format handler for user-provided files, we want to log and skip invalid files
|
||||
// rather than crashing the workflow loading process.
|
||||
logger.LogWarning(ex, "Failed to parse ElsaScript workflow from blob '{BlobPath}'. The file will be skipped.", blob.FullPath);
|
||||
|
||||
// Return null to indicate this handler can't process the content
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using Elsa.Workflows.Runtime;
|
||||
using FluentStorage.Blobs;
|
||||
|
||||
namespace Elsa.WorkflowProviders.BlobStorage.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Handles parsing of workflow definitions from blob storage in a specific format.
|
||||
/// </summary>
|
||||
public interface IBlobWorkflowFormatHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// A short name or ID of the handler, for diagnostics/logging.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file extensions that this handler supports (without the leading dot, e.g., "json", "elsa").
|
||||
/// This is used to optimize blob storage browsing by filtering files before reading their contents.
|
||||
/// Return an empty collection if the handler can process any file type.
|
||||
/// </summary>
|
||||
IEnumerable<string> SupportedExtensions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this handler can interpret the given blob based on metadata.
|
||||
/// Note: The provider will already have filtered by SupportedExtensions, so this is only
|
||||
/// needed for additional validation based on content type or other blob properties.
|
||||
/// </summary>
|
||||
/// <param name="blob">The blob to check.</param>
|
||||
/// <param name="contentType">The content type from blob metadata, if available.</param>
|
||||
bool CanHandle(Blob blob, string? contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the blob content into a MaterializedWorkflow.
|
||||
/// Returns null if the content is not valid for this handler.
|
||||
/// </summary>
|
||||
/// <param name="blob">The blob being parsed.</param>
|
||||
/// <param name="content">The text content of the blob.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
ValueTask<MaterializedWorkflow?> TryParseAsync(
|
||||
Blob blob,
|
||||
string content,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using Elsa.Features.Abstractions;
|
|||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Contracts;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Handlers;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Providers;
|
||||
using Elsa.Workflows.Management.Features;
|
||||
using FluentStorage;
|
||||
|
|
@ -31,6 +32,10 @@ public class BlobStorageFeature : FeatureBase
|
|||
public override void Apply()
|
||||
{
|
||||
Services.AddScoped<IBlobStorageProvider>(sp => new BlobStorageProvider(BlobStorage(sp)));
|
||||
|
||||
// Register the JSON format handler (built-in support)
|
||||
Services.AddScoped<IBlobWorkflowFormatHandler, JsonBlobWorkflowFormatHandler>();
|
||||
|
||||
Services.AddWorkflowsProvider<BlobStorageWorkflowsProvider>();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
using Elsa.WorkflowProviders.BlobStorage.Contracts;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Management.Mappers;
|
||||
using Elsa.Workflows.Management.Materializers;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using FluentStorage.Blobs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.WorkflowProviders.BlobStorage.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Handles JSON-formatted workflow definitions from blob storage.
|
||||
/// </summary>
|
||||
public class JsonBlobWorkflowFormatHandler(
|
||||
IActivitySerializer activitySerializer,
|
||||
WorkflowDefinitionMapper workflowDefinitionMapper,
|
||||
ILogger<JsonBlobWorkflowFormatHandler> logger) : IBlobWorkflowFormatHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "Json";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> SupportedExtensions => ["json"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(Blob blob, string? contentType)
|
||||
{
|
||||
// Extension filtering is already handled by the provider via SupportedExtensions.
|
||||
// Here we can optionally check content type for additional validation.
|
||||
if (!string.IsNullOrEmpty(contentType) && contentType.Contains("json", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// If no content type is available, assume we can handle it (since extension was already validated)
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<MaterializedWorkflow?> TryParseAsync(Blob blob, string content, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var workflowDefinitionModel = activitySerializer.Deserialize<WorkflowDefinitionModel>(content);
|
||||
var workflow = workflowDefinitionMapper.Map(workflowDefinitionModel);
|
||||
|
||||
var materialized = new MaterializedWorkflow(
|
||||
workflow,
|
||||
ProviderName: "FluentStorage",
|
||||
MaterializerName: JsonWorkflowMaterializer.MaterializerName,
|
||||
MaterializerContext: null,
|
||||
OriginalSource: content // Preserve full JSON for symmetric round-trip
|
||||
);
|
||||
|
||||
return new(materialized);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Intentionally catching all exceptions here to gracefully handle any failure during workflow parsing.
|
||||
// This includes JsonException, IOException, deserialization errors, or any unexpected exceptions.
|
||||
// Since this is a format handler for user-provided files, we want to log and skip invalid files
|
||||
// rather than crashing the workflow loading process.
|
||||
logger.LogWarning(ex, "Failed to parse JSON workflow from blob '{BlobPath}'. The file will be skipped.", blob.FullPath);
|
||||
|
||||
// Return null to indicate this handler can't process the content
|
||||
return new((MaterializedWorkflow?)null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,4 @@
|
|||
using Elsa.Common;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Contracts;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Management.Mappers;
|
||||
using Elsa.Workflows.Management.Materializers;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.WorkflowProviders.BlobStorage.Contracts;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using FluentStorage.Blobs;
|
||||
using JetBrains.Annotations;
|
||||
|
|
@ -17,29 +12,17 @@ namespace Elsa.WorkflowProviders.BlobStorage.Providers;
|
|||
public class BlobStorageWorkflowsProvider : IWorkflowsProvider
|
||||
{
|
||||
private readonly IBlobStorageProvider _blobStorageProvider;
|
||||
private readonly IActivitySerializer _activitySerializer;
|
||||
private readonly ISystemClock _systemClock;
|
||||
private readonly IHasher _hasher;
|
||||
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
|
||||
private readonly VariableDefinitionMapper _variableDefinitionMapper;
|
||||
private readonly IEnumerable<IBlobWorkflowFormatHandler> _handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlobStorageWorkflowsProvider"/> class.
|
||||
/// </summary>
|
||||
public BlobStorageWorkflowsProvider(
|
||||
IBlobStorageProvider blobStorageProvider,
|
||||
IActivitySerializer activitySerializer,
|
||||
ISystemClock systemClock,
|
||||
IHasher hasher,
|
||||
WorkflowDefinitionMapper workflowDefinitionMapper,
|
||||
VariableDefinitionMapper variableDefinitionMapper)
|
||||
IEnumerable<IBlobWorkflowFormatHandler> handlers)
|
||||
{
|
||||
_blobStorageProvider = blobStorageProvider;
|
||||
_activitySerializer = activitySerializer;
|
||||
_systemClock = systemClock;
|
||||
_hasher = hasher;
|
||||
_workflowDefinitionMapper = workflowDefinitionMapper;
|
||||
_variableDefinitionMapper = variableDefinitionMapper;
|
||||
_handlers = handlers;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -48,10 +31,26 @@ public class BlobStorageWorkflowsProvider : IWorkflowsProvider
|
|||
/// <inheritdoc />
|
||||
public async ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Aggregate supported extensions from all handlers
|
||||
var supportedExtensions = _handlers
|
||||
.SelectMany(h => h.SupportedExtensions)
|
||||
.Where(ext => !string.IsNullOrEmpty(ext))
|
||||
.Select(ext => ext.ToLowerInvariant())
|
||||
.ToHashSet();
|
||||
|
||||
var options = new ListOptions
|
||||
{
|
||||
Recurse = true,
|
||||
BrowseFilter = x => x.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || x.Name.EndsWith(".elsa", StringComparison.OrdinalIgnoreCase)
|
||||
BrowseFilter = blob =>
|
||||
{
|
||||
// If no handlers declare extensions, accept all files
|
||||
if (supportedExtensions.Count == 0)
|
||||
return true;
|
||||
|
||||
// Only accept files with supported extensions
|
||||
var extension = Path.GetExtension(blob.Name).TrimStart('.').ToLowerInvariant();
|
||||
return supportedExtensions.Contains(extension);
|
||||
}
|
||||
};
|
||||
|
||||
var blobStorage = _blobStorageProvider.GetBlobStorage();
|
||||
|
|
@ -60,30 +59,32 @@ public class BlobStorageWorkflowsProvider : IWorkflowsProvider
|
|||
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
var result = await ReadWorkflowAsync(blob, cancellationToken);
|
||||
results.Add(result);
|
||||
var workflow = await TryReadWorkflowAsync(blob, cancellationToken);
|
||||
if (workflow != null)
|
||||
results.Add(workflow);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async Task<MaterializedWorkflow> ReadWorkflowAsync(Blob blob, CancellationToken cancellationToken)
|
||||
private async Task<MaterializedWorkflow?> TryReadWorkflowAsync(Blob blob, CancellationToken cancellationToken)
|
||||
{
|
||||
var blobStorage = _blobStorageProvider.GetBlobStorage();
|
||||
var fileExtension = blob.FullPath.Split('.').Last();
|
||||
var data = await blobStorage.ReadTextAsync(blob.FullPath, cancellationToken: cancellationToken);
|
||||
var content = await blobStorage.ReadTextAsync(blob.FullPath, cancellationToken: cancellationToken);
|
||||
|
||||
if (string.Equals("json", fileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
return ReadJsonWorkflowDefinition(data);
|
||||
var contentType = blob.Properties.TryGetValue("ContentType", out var ct) ? ct?.ToString() : null;
|
||||
|
||||
throw new NotSupportedException($"The file extension '{fileExtension}' is not supported.");
|
||||
}
|
||||
foreach (var handler in _handlers)
|
||||
{
|
||||
if (!handler.CanHandle(blob, contentType))
|
||||
continue;
|
||||
|
||||
private MaterializedWorkflow ReadJsonWorkflowDefinition(string json)
|
||||
{
|
||||
var workflowDefinitionModel = _activitySerializer.Deserialize<WorkflowDefinitionModel>(json);
|
||||
var workflow = _workflowDefinitionMapper.Map(workflowDefinitionModel);
|
||||
var result = await handler.TryParseAsync(blob, content, cancellationToken);
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
return new (workflow, Name, JsonWorkflowMaterializer.MaterializerName);
|
||||
// No handler accepted this blob; ignore it
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
public interface IActivityFactory
|
||||
{
|
||||
IActivity Create(Type type, ActivityConstructorContext context);
|
||||
}
|
||||
|
|
@ -36,8 +36,12 @@ public static class ItemSourceActivityExecutionContextExtensions
|
|||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
if (items is IEnumerable<T> enumerable)
|
||||
else if (items is IAsyncEnumerable<T> asyncEnumerable)
|
||||
{
|
||||
await foreach (T item in asyncEnumerable)
|
||||
yield return item;
|
||||
}
|
||||
else if (items is IEnumerable<T> enumerable)
|
||||
{
|
||||
foreach (T item in enumerable)
|
||||
yield return item;
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ public class WorkflowsFeature : FeatureBase
|
|||
.AddScoped<IActivityRegistryLookupService, ActivityRegistryLookupService>()
|
||||
.AddSingleton<IPropertyDefaultValueResolver, PropertyDefaultValueResolver>()
|
||||
.AddSingleton<IPropertyUIHandlerResolver, PropertyUIHandlerResolver>()
|
||||
.AddSingleton<IActivityFactory, ActivityFactory>()
|
||||
.AddTransient<WorkflowBuilder>()
|
||||
.AddScoped(typeof(Func<IWorkflowBuilder>), sp => () => sp.GetRequiredService<WorkflowBuilder>())
|
||||
.AddScoped<IWorkflowBuilderFactory, WorkflowBuilderFactory>()
|
||||
|
|
|
|||
|
|
@ -1,21 +1,35 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Attributes;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Humanizer;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
namespace Elsa.Workflows.Models;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ActivityFactory : IActivityFactory
|
||||
public record ActivityConstructorContext(ActivityDescriptor ActivityDescriptor, Func<Type, IActivity> ActivityFactory)
|
||||
{
|
||||
public IActivity Create(Type type, ActivityConstructorContext context)
|
||||
public T CreateActivity<T>() where T : IActivity => (T)ActivityFactory(typeof(T));
|
||||
public IActivity CreateActivity(Type type) => ActivityFactory(type);
|
||||
}
|
||||
|
||||
public static class JsonActivityConstructorContextHelper
|
||||
{
|
||||
public static ActivityConstructorContext Create(ActivityDescriptor activityDescriptor, JsonElement element, JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
return new ActivityConstructorContext(activityDescriptor, type => CreateActivity(activityDescriptor, type, element, serializerOptions));
|
||||
}
|
||||
|
||||
public static T CreateActivity<T>(ActivityDescriptor activityDescriptor, JsonElement element, JsonSerializerOptions serializerOptions) where T : IActivity
|
||||
{
|
||||
return (T)CreateActivity(activityDescriptor, typeof(T), element, serializerOptions);
|
||||
}
|
||||
|
||||
public static IActivity CreateActivity(ActivityDescriptor activityDescriptor, Type type, JsonElement element, JsonSerializerOptions serializerOptions)
|
||||
{
|
||||
// 1) Grab the raw text
|
||||
var raw = context.Element.GetRawText();
|
||||
var raw = element.GetRawText();
|
||||
|
||||
// 2) Parse into a JsonNode so we can mutate
|
||||
var node = JsonNode.Parse(raw)!;
|
||||
|
|
@ -24,21 +38,21 @@ public class ActivityFactory : IActivityFactory
|
|||
StripTypeMetadata(node);
|
||||
|
||||
// 4) Get a cleaned JSON string
|
||||
var cleanedJson = node.ToJsonString(new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
var cleanedJson = node.ToJsonString(new(JsonSerializerDefaults.Web));
|
||||
|
||||
// 5) Parse it back to a JsonDocument so we can get a clean JsonElement
|
||||
using var doc = JsonDocument.Parse(cleanedJson);
|
||||
var cleanedElement = doc.RootElement.Clone();
|
||||
|
||||
// 6) Deserialize into your IActivity
|
||||
var activity = (IActivity)JsonSerializer.Deserialize(cleanedJson, type, context.SerializerOptions)!;
|
||||
// 6) Deserialize into IActivity, or create a plain instance
|
||||
var activity = (IActivity)JsonSerializer.Deserialize(cleanedJson, type, serializerOptions)!;
|
||||
|
||||
// 7) Pull out your boolean flags from the cleaned element
|
||||
var canStartWorkflow = GetBoolean(cleanedElement, "canStartWorkflow");
|
||||
var runAsynchronously = GetNullableBoolean(cleanedElement, "runAsynchronously");
|
||||
if (runAsynchronously is null)
|
||||
{
|
||||
if (context.ActivityDescriptor.Attributes.OfType<TaskActivityAttribute>().FirstOrDefault() is { } taskActivityAttribute)
|
||||
if (activityDescriptor.Attributes.OfType<TaskActivityAttribute>().FirstOrDefault() is { } taskActivityAttribute)
|
||||
{
|
||||
runAsynchronously = taskActivityAttribute.RunAsynchronously;
|
||||
}
|
||||
|
|
@ -53,8 +67,8 @@ public class ActivityFactory : IActivityFactory
|
|||
composite.Setup();
|
||||
|
||||
// 9) Your existing synthetic inputs/outputs routines, using the cleanedElement
|
||||
ReadSyntheticInputs(context.ActivityDescriptor, activity, cleanedElement, context.SerializerOptions);
|
||||
ReadSyntheticOutputs(context.ActivityDescriptor, activity, cleanedElement);
|
||||
ReadSyntheticInputs(activityDescriptor, activity, cleanedElement, serializerOptions);
|
||||
ReadSyntheticOutputs(activityDescriptor, activity, cleanedElement);
|
||||
|
||||
// 10) Finally re‑apply those flags
|
||||
activity.SetCanStartWorkflow(canStartWorkflow);
|
||||
|
|
@ -67,7 +81,7 @@ public class ActivityFactory : IActivityFactory
|
|||
/// Recursively remove any "_type" properties and unwrap any
|
||||
/// { "_type": "...", "items": [ ... ] } or "values": [ ... ] wrappers
|
||||
/// </summary>
|
||||
private void StripTypeMetadata(JsonNode node)
|
||||
private static void StripTypeMetadata(JsonNode node)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
|
|
@ -137,7 +151,7 @@ public class ActivityFactory : IActivityFactory
|
|||
}
|
||||
}
|
||||
|
||||
private void ReadSyntheticInputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activityRoot, JsonSerializerOptions options)
|
||||
private static void ReadSyntheticInputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activityRoot, JsonSerializerOptions options)
|
||||
{
|
||||
foreach (var inputDescriptor in activityDescriptor.Inputs.Where(x => x.IsSynthetic))
|
||||
{
|
||||
|
|
@ -165,7 +179,7 @@ public class ActivityFactory : IActivityFactory
|
|||
}
|
||||
}
|
||||
|
||||
private void ReadSyntheticOutputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activityRoot)
|
||||
private static void ReadSyntheticOutputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activityRoot)
|
||||
{
|
||||
foreach (var outputDescriptor in activityDescriptor.Outputs.Where(x => x.IsSynthetic))
|
||||
{
|
||||
|
|
@ -182,7 +196,10 @@ public class ActivityFactory : IActivityFactory
|
|||
if (!memoryReferenceElement.TryGetProperty("id", out var memoryReferenceIdElement))
|
||||
continue;
|
||||
|
||||
var variable = new Variable { Id = memoryReferenceIdElement.GetString()! };
|
||||
var variable = new Variable
|
||||
{
|
||||
Id = memoryReferenceIdElement.GetString()!
|
||||
};
|
||||
variable.Name = variable.Id;
|
||||
|
||||
var output = Activator.CreateInstance(wrappedType, variable)!;
|
||||
|
|
@ -193,12 +210,15 @@ public class ActivityFactory : IActivityFactory
|
|||
|
||||
private static bool GetBoolean(JsonElement element, string propertyName)
|
||||
{
|
||||
return GetNullableBoolean(element, propertyName) ?? false;
|
||||
return GetNullableBoolean(element, propertyName) ?? false;
|
||||
}
|
||||
|
||||
|
||||
private static bool? GetNullableBoolean(JsonElement element, string propertyName)
|
||||
{
|
||||
var propertyNames = new[] { propertyName.Camelize(), propertyName.Pascalize() };
|
||||
var propertyNames = new[]
|
||||
{
|
||||
propertyName.Camelize(), propertyName.Pascalize()
|
||||
};
|
||||
|
||||
foreach (var name in propertyNames)
|
||||
{
|
||||
|
|
@ -208,11 +228,11 @@ public class ActivityFactory : IActivityFactory
|
|||
return (bool?)canStartWorkflowElement.GetValue();
|
||||
}
|
||||
|
||||
if (element.TryGetProperty(propertyName.Camelize(), out var property)
|
||||
if (element.TryGetProperty(propertyName.Camelize(), out var property)
|
||||
&& (bool?)property.GetValue() is { } propValue)
|
||||
return propValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,11 @@ public class ActivityDescriptor
|
|||
/// </summary>
|
||||
public string TypeName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The .NET type of the activity type.
|
||||
/// </summary>
|
||||
public Type ClrType { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The namespace of the activity type.
|
||||
/// </summary>
|
||||
|
|
@ -123,8 +128,4 @@ public class ActivityDescriptor
|
|||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<JsonSerializerOptions, JsonSerializerOptions>? ConfigureSerializerOptions { get; set; }
|
||||
}
|
||||
|
||||
// TODO: Refactor this to remove the dependency on JsonElement and JsonSerializerOptions.
|
||||
// This limits the ability to use this class in other contexts, such as constructing activities from the DSL.
|
||||
public record ActivityConstructorContext(ActivityDescriptor ActivityDescriptor, JsonElement Element, JsonSerializerOptions SerializerOptions);
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ namespace Elsa.Workflows.Serialization.Converters;
|
|||
/// </summary>
|
||||
public class ActivityJsonConverter(
|
||||
IActivityRegistry activityRegistry,
|
||||
IActivityFactory activityFactory,
|
||||
IExpressionDescriptorRegistry expressionDescriptorRegistry,
|
||||
ActivityWriter activityWriter,
|
||||
IServiceProvider serviceProvider)
|
||||
|
|
@ -44,8 +43,7 @@ public class ActivityJsonConverter(
|
|||
if (activityDescriptor == null)
|
||||
{
|
||||
var notFoundActivityDescriptor = activityRegistry.Find<NotFoundActivity>()!;
|
||||
var notFoundContext = new ActivityConstructorContext(notFoundActivityDescriptor, activityRoot, clonedOptions);
|
||||
var notFoundActivity = (NotFoundActivity)activityFactory.Create(typeof(NotFoundActivity), notFoundContext);
|
||||
var notFoundActivity = JsonActivityConstructorContextHelper.CreateActivity<NotFoundActivity>(notFoundActivityDescriptor, activityRoot, clonedOptions);
|
||||
|
||||
notFoundActivity.Type = notFoundActivityTypeName;
|
||||
notFoundActivity.Version = 1;
|
||||
|
|
@ -57,7 +55,7 @@ public class ActivityJsonConverter(
|
|||
return notFoundActivity;
|
||||
}
|
||||
|
||||
var context = new ActivityConstructorContext(activityDescriptor, activityRoot, clonedOptions);
|
||||
var context = JsonActivityConstructorContextHelper.Create(activityDescriptor, activityRoot, clonedOptions);
|
||||
var activity = activityDescriptor.Constructor(context);
|
||||
return activity;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ using Container = Elsa.Workflows.Activities.Container;
|
|||
namespace Elsa.Workflows;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolver, IActivityFactory activityFactory, IPropertyUIHandlerResolver propertyUIHandlerResolver) : IActivityDescriber
|
||||
public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolver, IPropertyUIHandlerResolver propertyUIHandlerResolver) : IActivityDescriber
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ActivityDescriptor> DescribeActivityAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken = default)
|
||||
|
|
@ -70,6 +70,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
|
|||
var descriptor = new ActivityDescriptor
|
||||
{
|
||||
TypeName = fullTypeName,
|
||||
ClrType = activityType,
|
||||
Namespace = ns,
|
||||
Name = typeName,
|
||||
Category = category,
|
||||
|
|
@ -91,7 +92,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
|
|||
},
|
||||
Constructor = context =>
|
||||
{
|
||||
var activity = activityFactory.Create(activityType, context);
|
||||
var activity = context.CreateActivity(activityType);
|
||||
activity.Type = fullTypeName;
|
||||
return activity;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using Humanizer;
|
|||
|
||||
namespace Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
|
||||
|
||||
public class WorkflowDefinitionActivityDescriptorFactory(IActivityFactory activityFactory)
|
||||
public class WorkflowDefinitionActivityDescriptorFactory
|
||||
{
|
||||
public ActivityDescriptor CreateDescriptor(WorkflowDefinition definition, WorkflowDefinition? latestPublishedDefinition = null)
|
||||
{
|
||||
|
|
@ -32,6 +32,7 @@ public class WorkflowDefinitionActivityDescriptorFactory(IActivityFactory activi
|
|||
return new()
|
||||
{
|
||||
TypeName = typeName,
|
||||
ClrType = typeof(WorkflowDefinitionActivity),
|
||||
Name = typeName,
|
||||
Version = definition.Version,
|
||||
DisplayName = definition.Name,
|
||||
|
|
@ -56,7 +57,7 @@ public class WorkflowDefinitionActivityDescriptorFactory(IActivityFactory activi
|
|||
},
|
||||
Constructor = context =>
|
||||
{
|
||||
var activity = (WorkflowDefinitionActivity)activityFactory.Create(typeof(WorkflowDefinitionActivity), context);
|
||||
var activity = context.CreateActivity<WorkflowDefinitionActivity>();
|
||||
activity.Type = typeName;
|
||||
activity.WorkflowDefinitionId = definition.DefinitionId;
|
||||
activity.WorkflowDefinitionVersionId = definition.Id;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,14 @@ public class WorkflowDefinition : VersionedEntity
|
|||
/// A textual representation of the workflow. The data is to be interpreted by the configured materializer.
|
||||
/// </summary>
|
||||
public string? StringData { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The original source representation of the workflow (JSON, ElsaScript, YAML, etc.).
|
||||
/// When present, materializers should prefer this over StringData for full round-trip fidelity.
|
||||
/// This field enables symmetric materialization without requiring serialization round-trips.
|
||||
/// </summary>
|
||||
public string? OriginalSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A binary representation of the workflow. The data is to be interpreted by the configured materializer.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -11,33 +11,35 @@ namespace Elsa.Workflows.Management;
|
|||
[PublicAPI]
|
||||
public static class ActivityDescriberExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes an output property.
|
||||
/// </summary>
|
||||
/// <param name="activityDescriber">The activity describer.</param>
|
||||
/// <param name="expression">The property expression.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="TActivity">The type of the activity.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property.</typeparam>
|
||||
/// <returns>The output descriptor.</returns>
|
||||
public static async Task<OutputDescriptor> DescribeOutputProperty<TActivity, TProperty>(this IActivityDescriber activityDescriber, Expression<Func<TActivity, TProperty>> expression, CancellationToken cancellationToken = default)
|
||||
extension(IActivityDescriber activityDescriber)
|
||||
{
|
||||
var propertyInfo = expression.GetProperty()!;
|
||||
return await activityDescriber.DescribeOutputPropertyAsync(propertyInfo, cancellationToken);
|
||||
}
|
||||
/// <summary>
|
||||
/// Describes an output property.
|
||||
/// </summary>
|
||||
/// <param name="expression">The property expression.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="TActivity">The type of the activity.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property.</typeparam>
|
||||
/// <returns>The output descriptor.</returns>
|
||||
public async Task<OutputDescriptor> DescribeOutputProperty<TActivity, TProperty>(Expression<Func<TActivity, TProperty>> expression, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var propertyInfo = expression.GetProperty()!;
|
||||
return await activityDescriber.DescribeOutputPropertyAsync(propertyInfo, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes an input property.
|
||||
/// </summary>
|
||||
/// <param name="activityDescriber">The activity describer.</param>
|
||||
/// <param name="expression">The property expression.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="TActivity">The type of the activity.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property.</typeparam>
|
||||
/// <returns>The input descriptor.</returns>
|
||||
public static async Task<InputDescriptor> DescribeInputPropertyAsync<TActivity, TProperty>(this IActivityDescriber activityDescriber, Expression<Func<TActivity, TProperty>> expression, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var propertyInfo = expression.GetProperty()!;
|
||||
return await activityDescriber.DescribeInputPropertyAsync(propertyInfo, cancellationToken);
|
||||
/// <summary>
|
||||
/// Describes an input property.
|
||||
/// </summary>
|
||||
/// <param name="expression">The property expression.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <typeparam name="TActivity">The type of the activity.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property.</typeparam>
|
||||
/// <returns>The input descriptor.</returns>
|
||||
public async Task<InputDescriptor> DescribeInputPropertyAsync<TActivity, TProperty>(Expression<Func<TActivity, TProperty>> expression, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var propertyInfo = expression.GetProperty()!;
|
||||
return await activityDescriber.DescribeInputPropertyAsync(propertyInfo, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a set of extension methods for <see cref="IActivityFactory"/>.
|
||||
/// </summary>
|
||||
public static class ActivityFactoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an activity of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="activityFactory">The activity factory.</param>
|
||||
/// <param name="context">The activity constructor context.</param>
|
||||
/// <typeparam name="T">The type of the activity to create.</typeparam>
|
||||
/// <returns>The created activity.</returns>
|
||||
public static T Create<T>(this IActivityFactory activityFactory, ActivityConstructorContext context) => (T)activityFactory.Create(typeof(T), context);
|
||||
}
|
||||
|
|
@ -32,6 +32,15 @@ public class WorkflowDefinitionMapper
|
|||
/// <returns>The mapped <see cref="Workflow"/>.</returns>
|
||||
public Workflow Map(WorkflowDefinition source)
|
||||
{
|
||||
// NEW WAY: If OriginalSource is present, deserialize the full WorkflowDefinitionModel
|
||||
// This enables symmetric round-trip without re-serialization
|
||||
if (!string.IsNullOrEmpty(source.OriginalSource))
|
||||
{
|
||||
var model = _activitySerializer.Deserialize<WorkflowDefinitionModel>(source.OriginalSource);
|
||||
return MapFromModelWithIdentity(model, source);
|
||||
}
|
||||
|
||||
// OLD WAY: Deserialize from StringData (backwards compatibility)
|
||||
var root = _activitySerializer.Deserialize(source.StringData!);
|
||||
|
||||
return new(
|
||||
|
|
@ -49,6 +58,30 @@ public class WorkflowDefinitionMapper
|
|||
source.IsSystem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="WorkflowDefinitionModel"/> to a <see cref="Workflow"/>, using identity from stored definition.
|
||||
/// </summary>
|
||||
private Workflow MapFromModelWithIdentity(WorkflowDefinitionModel model, WorkflowDefinition storedDefinition)
|
||||
{
|
||||
var root = model.Root!;
|
||||
var variables = _variableDefinitionMapper.Map(model.Variables).ToList();
|
||||
var options = model.Options ?? new WorkflowOptions();
|
||||
|
||||
return new(
|
||||
new(storedDefinition.DefinitionId, storedDefinition.Version, storedDefinition.Id, storedDefinition.TenantId),
|
||||
new(storedDefinition.IsLatest, storedDefinition.IsPublished),
|
||||
new(model.Name, model.Description, model.CreatedAt, model.ToolVersion),
|
||||
options,
|
||||
root,
|
||||
variables,
|
||||
model.Inputs ?? new List<InputDefinition>(),
|
||||
model.Outputs ?? new List<OutputDefinition>(),
|
||||
model.Outcomes ?? new List<string>(),
|
||||
model.CustomProperties ?? new Dictionary<string, object>(),
|
||||
storedDefinition.IsReadonly,
|
||||
storedDefinition.IsSystem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="WorkflowDefinitionModel"/> to a <see cref="Workflow"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Workflows.Management.Options;
|
||||
using Elsa.Workflows.Models;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Workflows.Management.Providers;
|
||||
|
|
@ -8,6 +9,7 @@ namespace Elsa.Workflows.Management.Providers;
|
|||
/// <summary>
|
||||
/// Provides activity descriptors based on a list of activity types registered in the <see cref="ManagementOptions"/>.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class TypedActivityProvider : IActivityProvider
|
||||
{
|
||||
private readonly IActivityDescriber _activityDescriber;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ public static class WorkflowDictionaryExtensions
|
|||
/// </summary>
|
||||
public static void Add<TWorkflow>(this IDictionary<string, Func<IServiceProvider, ValueTask<IWorkflow>>> dictionary) where TWorkflow : IWorkflow
|
||||
{
|
||||
dictionary.Add(typeof(TWorkflow).Name, sp =>
|
||||
// FullName should never be null here, as we filter out generic types
|
||||
dictionary.Add(typeof(TWorkflow).FullName!, sp =>
|
||||
{
|
||||
var workflow = ActivatorUtilities.GetServiceOrCreateInstance<TWorkflow>(sp);
|
||||
return new ValueTask<IWorkflow>(workflow);
|
||||
|
|
@ -26,7 +27,8 @@ public static class WorkflowDictionaryExtensions
|
|||
/// </summary>
|
||||
public static void Add(this IDictionary<string, Func<IServiceProvider, ValueTask<IWorkflow>>> dictionary, Type workflowType)
|
||||
{
|
||||
dictionary.Add(workflowType.Name, sp =>
|
||||
// FullName should never be null here, as we filter out generic types
|
||||
dictionary.Add(workflowType.FullName!, sp =>
|
||||
{
|
||||
var workflow = (IWorkflow)ActivatorUtilities.GetServiceOrCreateInstance(sp, workflowType);
|
||||
return new ValueTask<IWorkflow>(workflow);
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ namespace Elsa.Workflows.Runtime;
|
|||
/// <param name="ProviderName">The name of the provider that provided the workflow definition.</param>
|
||||
/// <param name="MaterializerName">The name of the materializer that materialized the workflow.</param>
|
||||
/// <param name="MaterializerContext">The context of the materializer that materialized the workflow.</param>
|
||||
public record MaterializedWorkflow(Workflow Workflow, string ProviderName, string MaterializerName, object? MaterializerContext = null);
|
||||
/// <param name="OriginalSource">The original source representation (JSON, ElsaScript, YAML, etc.). When present, enables symmetric round-trip preservation without re-serialization.</param>
|
||||
public record MaterializedWorkflow(Workflow Workflow, string ProviderName, string MaterializerName, object? MaterializerContext = null, string? OriginalSource = null);
|
||||
|
|
@ -22,7 +22,8 @@ public class BackgroundWorkflowDispatcher(ICommandSender commandSender, ITenantA
|
|||
Properties = request.Properties,
|
||||
CorrelationId = request.CorrelationId,
|
||||
InstanceId = request.InstanceId,
|
||||
TriggerActivityId = request.TriggerActivityId
|
||||
TriggerActivityId = request.TriggerActivityId,
|
||||
ParentWorkflowInstanceId = request.ParentWorkflowInstanceId,
|
||||
};
|
||||
|
||||
await commandSender.SendAsync(command, CommandStrategy.Background, CreateHeaders(), cancellationToken);
|
||||
|
|
|
|||
|
|
@ -120,8 +120,25 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
|
|||
var materializerContext = materializedWorkflow.MaterializerContext;
|
||||
var materializerContextJson = materializerContext != null ? _payloadSerializer.Serialize(materializerContext) : null;
|
||||
|
||||
// Serialize the workflow root.
|
||||
var workflowJson = _activitySerializer.Serialize(workflow.Root);
|
||||
// Determine StringData and OriginalSource based on what's provided
|
||||
string? stringData;
|
||||
var originalSource = materializedWorkflow.OriginalSource;
|
||||
|
||||
if (originalSource != null)
|
||||
{
|
||||
// NEW WAY: OriginalSource is provided
|
||||
// For JSON workflows, we still need to populate StringData with the serialized root for backwards compatibility
|
||||
stringData = materializedWorkflow.MaterializerName == "Json" ? _activitySerializer.Serialize(workflow.Root) :
|
||||
// For new formats (ElsaScript, YAML, etc.), only OriginalSource is needed
|
||||
// StringData can be null as these materializers only use OriginalSource
|
||||
null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// OLD WAY: No OriginalSource provided (backwards compatibility for existing workflows)
|
||||
// Serialize the workflow root as before
|
||||
stringData = _activitySerializer.Serialize(workflow.Root);
|
||||
}
|
||||
|
||||
// Check if there's already a workflow definition stored with this definition ID and version.
|
||||
var specificVersionFilter = new WorkflowDefinitionFilter
|
||||
|
|
@ -171,7 +188,8 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
|
|||
workflowDefinition.Inputs = workflow.Inputs;
|
||||
workflowDefinition.Outputs = workflow.Outputs;
|
||||
workflowDefinition.Outcomes = workflow.Outcomes;
|
||||
workflowDefinition.StringData = workflowJson;
|
||||
workflowDefinition.StringData = stringData;
|
||||
workflowDefinition.OriginalSource = originalSource;
|
||||
workflowDefinition.CreatedAt = workflow.WorkflowMetadata.CreatedAt == default ? _systemClock.UtcNow : workflow.WorkflowMetadata.CreatedAt;
|
||||
workflowDefinition.Options = workflow.Options;
|
||||
workflowDefinition.ProviderName = materializedWorkflow.ProviderName;
|
||||
|
|
|
|||
16
test-flowchart.txt
Normal file
16
test-flowchart.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use Elsa.Activities.Console;
|
||||
|
||||
workflow FlowchartTest {
|
||||
flowchart {
|
||||
var counter = 0;
|
||||
|
||||
entry Start;
|
||||
|
||||
Start: WriteLine("Starting flowchart");
|
||||
CheckCondition: WriteLine("Checking condition");
|
||||
EndNode: WriteLine("Done!");
|
||||
|
||||
Start -> CheckCondition;
|
||||
CheckCondition.Done -> EndNode;
|
||||
}
|
||||
}
|
||||
10
test-simple-flowchart.elsa
Normal file
10
test-simple-flowchart.elsa
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use Elsa.Activities.Console;
|
||||
|
||||
workflow SimpleFlowchart {
|
||||
flowchart {
|
||||
entry A;
|
||||
A: WriteLine("Test");
|
||||
A -> B;
|
||||
B: WriteLine("Done");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
using Elsa.Dsl.ElsaScript.Contracts;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the ElsaScript compiler.
|
||||
/// </summary>
|
||||
public class CompilerTests
|
||||
{
|
||||
private readonly IElsaScriptCompiler _compiler;
|
||||
|
||||
public CompilerTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
var services = new TestApplicationBuilder(testOutputHelper)
|
||||
.ConfigureElsa(elsa =>
|
||||
{
|
||||
elsa.UseElsaScript();
|
||||
elsa.AddActivitiesFrom<HttpEndpoint>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
_compiler = services.GetRequiredService<IElsaScriptCompiler>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile a simple workflow from source")]
|
||||
public async Task CompileAsync_WithSimpleWorkflowSource_ShouldCreateWorkflowWithCorrectName()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use Elsa.Activities.Console;
|
||||
use expressions js;
|
||||
|
||||
workflow HelloWorld {
|
||||
WriteLine(""Hello World"");
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert - verify the workflow is correctly compiled
|
||||
Assert.NotNull(workflow);
|
||||
Assert.Equal("HelloWorld", workflow.Name);
|
||||
Assert.NotNull(workflow.Root);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile workflow with variable declarations")]
|
||||
public async Task CompileAsync_WithVariableDeclarations_ShouldCreateWorkflowWithAllVariables()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow VariableTest {
|
||||
var message = ""Hello from variable"";
|
||||
var count = 42;
|
||||
const pi = 3.14;
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert - verify variables are compiled correctly
|
||||
Assert.NotNull(workflow);
|
||||
Assert.Equal("VariableTest", workflow.Name);
|
||||
Assert.Equal(3, workflow.Variables.Count);
|
||||
|
||||
Assert.Contains(workflow.Variables, v => v.Name == "message");
|
||||
Assert.Contains(workflow.Variables, v => v.Name == "count");
|
||||
Assert.Contains(workflow.Variables, v => v.Name == "pi");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile workflow without workflow keyword")]
|
||||
public async Task CompileAsync_WithoutWorkflowKeyword_ShouldCreateWorkflow()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"WriteLine(""Hello World"");";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
Assert.NotNull(workflow.Root);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile complex workflow with variables, listen statements, and expressions")]
|
||||
public async Task CompileAsync_WithComplexWorkflow_ShouldCreateWorkflowWithCorrectStructure()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow HelloWorldHttpDsl {
|
||||
var message = ""Hello World from DSL via Expressions!"";
|
||||
listen HttpEndpoint(""/hello-world-dsl"");
|
||||
WriteLine(js => `Message: ${message}`);
|
||||
WriteHttpResponse(message);
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
Assert.Equal("HelloWorldHttpDsl", workflow.Name);
|
||||
|
||||
// Verify workflow has the message variable
|
||||
Assert.Single(workflow.Variables);
|
||||
var messageVar = workflow.Variables.First();
|
||||
Assert.Equal("message", messageVar.Name);
|
||||
Assert.Equal("Hello World from DSL via Expressions!", messageVar.Value);
|
||||
|
||||
// Verify root is a Sequence with 3 activities (HttpEndpoint, WriteLine, WriteHttpResponse)
|
||||
var sequence = Assert.IsType<Sequence>(workflow.Root);
|
||||
Assert.Equal(3, sequence.Activities.Count);
|
||||
|
||||
// Verify HttpEndpoint activity (from listen statement)
|
||||
var httpEndpoint = sequence.Activities.ElementAt(0);
|
||||
Assert.Equal("Elsa.HttpEndpoint", httpEndpoint.Type);
|
||||
|
||||
// Verify HttpEndpoint can start workflow (CanStartWorkflow property should be true)
|
||||
var canStartWorkflowProp = httpEndpoint.GetType().GetProperty("CanStartWorkflow");
|
||||
Assert.NotNull(canStartWorkflowProp);
|
||||
var canStartWorkflow = (bool)canStartWorkflowProp.GetValue(httpEndpoint)!;
|
||||
Assert.True(canStartWorkflow);
|
||||
|
||||
// Verify WriteLine activity with JavaScript expression
|
||||
var writeLine = sequence.Activities.ElementAt(1);
|
||||
Assert.Equal("Elsa.WriteLine", writeLine.Type);
|
||||
|
||||
var textProp = writeLine.GetType().GetProperty("Text");
|
||||
Assert.NotNull(textProp);
|
||||
var textInput = Assert.IsType<Input<string>>(textProp.GetValue(writeLine));
|
||||
|
||||
// Verify it's a JavaScript expression
|
||||
var expression = textInput.Expression;
|
||||
Assert.NotNull(expression);
|
||||
Assert.Equal("JavaScript", expression.Type);
|
||||
Assert.Contains("Message:", expression.Value?.ToString());
|
||||
Assert.Contains("message", expression.Value?.ToString());
|
||||
|
||||
// Verify WriteHttpResponse activity with variable reference
|
||||
var writeHttpResponse = sequence.Activities.ElementAt(2);
|
||||
Assert.Equal("Elsa.WriteHttpResponse", writeHttpResponse.Type);
|
||||
|
||||
var contentProp = writeHttpResponse.GetType().GetProperty("Content");
|
||||
Assert.NotNull(contentProp);
|
||||
var contentInput = Assert.IsType<Input<object>>(contentProp.GetValue(writeHttpResponse));
|
||||
|
||||
// Verify it references the message variable
|
||||
var memoryBlockReference = contentInput.MemoryBlockReference();
|
||||
Assert.NotNull(memoryBlockReference);
|
||||
Assert.Equal(messageVar.Id, memoryBlockReference.Id);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile for loop with 'to' keyword (exclusive)")]
|
||||
public async Task CompileAsync_WithForLoopExclusive_ShouldCreateForActivity()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow ForLoopTest {
|
||||
for (var i = 0 to 10 step 1)
|
||||
{
|
||||
WriteLine(js => `Step: ${i}`)
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
Assert.Equal("ForLoopTest", workflow.Name);
|
||||
|
||||
// Verify the For activity is created
|
||||
var forActivity = Assert.IsType<For>(workflow.Root);
|
||||
Assert.NotNull(forActivity);
|
||||
|
||||
// Verify Start, End, Step values
|
||||
Assert.NotNull(forActivity.Start);
|
||||
Assert.NotNull(forActivity.End);
|
||||
Assert.NotNull(forActivity.Step);
|
||||
|
||||
// Verify OuterBoundInclusive is false (exclusive 'to')
|
||||
Assert.NotNull(forActivity.OuterBoundInclusive);
|
||||
var outerBoundInput = forActivity.OuterBoundInclusive;
|
||||
var outerBoundExpr = outerBoundInput.Expression;
|
||||
Assert.NotNull(outerBoundExpr);
|
||||
Assert.False((bool?)outerBoundExpr.Value);
|
||||
|
||||
// Verify loop variable exists
|
||||
Assert.Single(workflow.Variables);
|
||||
var loopVar = workflow.Variables.First();
|
||||
Assert.Equal("i", loopVar.Name);
|
||||
|
||||
// Verify body exists
|
||||
Assert.NotNull(forActivity.Body);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile for loop with 'through' keyword (inclusive)")]
|
||||
public async Task CompileAsync_WithForLoopInclusive_ShouldCreateForActivity()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow ForLoopInclusiveTest {
|
||||
for (var i = 0 through 10 step 1)
|
||||
{
|
||||
WriteLine(js => `Step: ${i}`)
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
Assert.Equal("ForLoopInclusiveTest", workflow.Name);
|
||||
|
||||
// Verify the For activity is created
|
||||
var forActivity = Assert.IsType<For>(workflow.Root);
|
||||
Assert.NotNull(forActivity);
|
||||
|
||||
// Verify OuterBoundInclusive is true (inclusive 'through')
|
||||
Assert.NotNull(forActivity.OuterBoundInclusive);
|
||||
var outerBoundInput = forActivity.OuterBoundInclusive;
|
||||
var outerBoundExpr = outerBoundInput.Expression;
|
||||
Assert.NotNull(outerBoundExpr);
|
||||
Assert.True((bool?)outerBoundExpr.Value);
|
||||
|
||||
// Verify loop variable exists
|
||||
Assert.Single(workflow.Variables);
|
||||
var loopVar = workflow.Variables.First();
|
||||
Assert.Equal("i", loopVar.Name);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile workflow with metadata")]
|
||||
public async Task CompileAsync_WithWorkflowMetadata_ShouldCreateWorkflowWithCorrectMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use Elsa.Activities.Console;
|
||||
|
||||
workflow HelloWorldDsl(
|
||||
DisplayName: ""Hello World DSL"",
|
||||
Description: ""Demonstrates ElsaScript with metadata"",
|
||||
DefinitionId: ""hello-world-dsl"",
|
||||
DefinitionVersionId: ""hello-world-dsl-v1"",
|
||||
Version: 2,
|
||||
UsableAsActivity: true
|
||||
) {
|
||||
use expressions js;
|
||||
|
||||
WriteLine(""Hello World from Elsa DSL!"");
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
|
||||
// Check identity
|
||||
Assert.Equal("hello-world-dsl", workflow.Identity.DefinitionId);
|
||||
Assert.Equal(2, workflow.Identity.Version);
|
||||
Assert.Equal("hello-world-dsl-v1", workflow.Identity.Id);
|
||||
|
||||
// Check metadata
|
||||
Assert.Equal("Hello World DSL", workflow.WorkflowMetadata.Name);
|
||||
Assert.Equal("Demonstrates ElsaScript with metadata", workflow.WorkflowMetadata.Description);
|
||||
|
||||
// Check options
|
||||
Assert.True(workflow.Options.UsableAsActivity);
|
||||
|
||||
// Check root activity
|
||||
Assert.NotNull(workflow.Root);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile empty flowchart")]
|
||||
public async Task CompileAsync_WithEmptyFlowchart_ShouldCreateFlowchartActivity()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
workflow FlowchartTest {
|
||||
flowchart {
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
Assert.NotNull(workflow.Root);
|
||||
|
||||
Assert.IsType<Workflows.Activities.Flowchart.Activities.Flowchart>(workflow.Root);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile flowchart with node and connection")]
|
||||
public async Task CompileAsync_WithFlowchartNodeAndConnection_ShouldCreateFlowchartWithCorrectStructure()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
workflow FlowchartTest {
|
||||
flowchart {
|
||||
entry Start;
|
||||
Start: WriteLine();
|
||||
End: WriteLine();
|
||||
Start -> End;
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
Assert.NotNull(workflow.Root);
|
||||
|
||||
var flowchart = Assert.IsType<Workflows.Activities.Flowchart.Activities.Flowchart>(workflow.Root);
|
||||
|
||||
// Check that flowchart has 2 activities
|
||||
Assert.Equal(2, flowchart.Activities.Count);
|
||||
|
||||
// Check connections
|
||||
Assert.Single(flowchart.Connections);
|
||||
|
||||
// Check entry point is set
|
||||
Assert.NotNull(flowchart.Start);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler can compile flowchart with block node")]
|
||||
public async Task CompileAsync_WithFlowchartBlockNode_ShouldCreateSequenceActivity()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
workflow FlowchartWithBlock {
|
||||
flowchart {
|
||||
entry Start;
|
||||
|
||||
Start: {
|
||||
WriteLine(""First"");
|
||||
WriteLine(""Second"");
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var workflow = await _compiler.CompileAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(workflow);
|
||||
Assert.NotNull(workflow.Root);
|
||||
|
||||
var flowchart = Assert.IsType<Workflows.Activities.Flowchart.Activities.Flowchart>(workflow.Root);
|
||||
|
||||
// Should have one activity
|
||||
Assert.Single(flowchart.Activities);
|
||||
|
||||
// The activity should be a Sequence (from the block)
|
||||
var sequenceActivity = Assert.IsType<Sequence>(flowchart.Activities.First());
|
||||
|
||||
// Sequence should have 2 activities
|
||||
Assert.Equal(2, sequenceActivity.Activities.Count);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Compiler resets default expression language between compilations")]
|
||||
public async Task CompileAsync_WithSecondWorkflowAfterLiquid_ShouldResetToJavaScript()
|
||||
{
|
||||
// Arrange - First workflow sets Liquid as default
|
||||
var firstSource = @"
|
||||
use expressions liquid;
|
||||
|
||||
workflow FirstWorkflow {
|
||||
WriteLine(liquid => ""{{ 'test' }}"");
|
||||
}";
|
||||
|
||||
// Second workflow with no 'use expressions' directive
|
||||
var secondSource = @"
|
||||
workflow SecondWorkflow {
|
||||
WriteLine(js => ""test"");
|
||||
}";
|
||||
|
||||
// Act - Compile first workflow (sets default to Liquid)
|
||||
var firstWorkflow = await _compiler.CompileAsync(firstSource);
|
||||
|
||||
// Act - Compile second workflow (should reset to JavaScript)
|
||||
var secondWorkflow = await _compiler.CompileAsync(secondSource);
|
||||
|
||||
// Assert - Verify first workflow used Liquid
|
||||
Assert.NotNull(firstWorkflow);
|
||||
var firstWriteLine = firstWorkflow.Root;
|
||||
var firstTextProp = firstWriteLine.GetType().GetProperty("Text");
|
||||
var firstTextInput = Assert.IsType<Input<string>>(firstTextProp!.GetValue(firstWriteLine));
|
||||
Assert.Equal("Liquid", firstTextInput.Expression?.Type);
|
||||
|
||||
// Assert - Verify second workflow uses JavaScript (not leaked Liquid)
|
||||
Assert.NotNull(secondWorkflow);
|
||||
var secondWriteLine = secondWorkflow.Root;
|
||||
var secondTextProp = secondWriteLine.GetType().GetProperty("Text");
|
||||
var secondTextInput = Assert.IsType<Input<string>>(secondTextProp!.GetValue(secondWriteLine));
|
||||
Assert.Equal("JavaScript", secondTextInput.Expression?.Type);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Include>[Elsa.Dsl.ElsaScript]*</Include>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Dsl.ElsaScript\Elsa.Dsl.ElsaScript.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Expressions.JavaScript\Elsa.Expressions.JavaScript.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
using Elsa.Dsl.ElsaScript.Contracts;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Dsl.ElsaScript.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the ElsaScript parser.
|
||||
/// </summary>
|
||||
public class ParserTests
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IElsaScriptParser _parser;
|
||||
|
||||
public ParserTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_services = new TestApplicationBuilder(testOutputHelper)
|
||||
.ConfigureElsa(elsa => elsa.UseElsaScript())
|
||||
.Build();
|
||||
|
||||
_parser = _services.GetRequiredService<IElsaScriptParser>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse a simple workflow definition")]
|
||||
public void Parse_WithSimpleWorkflowDefinition_ShouldReturnWorkflowWithCorrectStructure()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use Elsa.Activities.Console;
|
||||
use expressions js;
|
||||
|
||||
workflow HelloWorld {
|
||||
WriteLine(""Hello World"");
|
||||
WriteLine(""Great to meet you!"");
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal("HelloWorld", workflow.Id);
|
||||
|
||||
// Global use statements
|
||||
Assert.Equal(2, program.GlobalUseStatements.Count);
|
||||
|
||||
// No workflow-level use statements (all are global)
|
||||
Assert.Empty(workflow.UseStatements);
|
||||
|
||||
Assert.Equal(2, workflow.Body.Count);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse variable declarations")]
|
||||
public void Parse_WithVariableDeclarations_ShouldReturnWorkflowWithAllVariableNodes()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow VariableTest {
|
||||
var greeting = ""Hello"";
|
||||
var count = 42;
|
||||
const pi = 3.14;
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal("VariableTest", workflow.Id);
|
||||
Assert.Equal(3, workflow.Body.Count);
|
||||
|
||||
var varDecl = Assert.IsType<Ast.VariableDeclarationNode>(workflow.Body[0]);
|
||||
Assert.Equal(Ast.VariableKind.Var, varDecl.Kind);
|
||||
Assert.Equal("greeting", varDecl.Name);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse activity invocations with named arguments")]
|
||||
public void Parse_WithActivityInvocationWithNamedArguments_ShouldReturnActivityNodeWithCorrectArguments()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow ActivityTest {
|
||||
WriteLine(Text: ""Hello World"");
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Single(workflow.Body);
|
||||
|
||||
var activity = Assert.IsType<Ast.ActivityInvocationNode>(workflow.Body[0]);
|
||||
Assert.Equal("WriteLine", activity.ActivityName);
|
||||
Assert.Single(activity.Arguments);
|
||||
Assert.Equal("Text", activity.Arguments[0].Name);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse listen statements")]
|
||||
public void Parse_WithListenStatement_ShouldReturnWorkflowWithListenNode()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow ListenTest {
|
||||
listen HttpEndpoint(""/test"");
|
||||
WriteLine(""Triggered!"");
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal(2, workflow.Body.Count);
|
||||
|
||||
var listen = Assert.IsType<Ast.ListenNode>(workflow.Body[0]);
|
||||
Assert.Equal("HttpEndpoint", listen.Activity.ActivityName);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse workflow without workflow keyword")]
|
||||
public void Parse_WithoutWorkflowKeyword_ShouldReturnWorkflowWithCorrectStructure()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"WriteLine(""Hello World""); WriteLine(""Great to meet you!"");";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal(2, workflow.Body.Count);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse complex workflow with variables, listen statements, and ElsaScript expressions")]
|
||||
public void Parse_WithComplexWorkflow_ShouldReturnWorkflowWithCorrectStructure()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow HelloWorldHttpDsl {
|
||||
var message = ""Hello World from DSL via Expressions!"";
|
||||
listen HttpEndpoint(""/hello-world-dsl"");
|
||||
WriteLine(js => `Message: ${message}`);
|
||||
WriteHttpResponse(message);
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal("HelloWorldHttpDsl", workflow.Id);
|
||||
|
||||
// Verify global use statement
|
||||
Assert.Single(program.GlobalUseStatements);
|
||||
var useStatement = program.GlobalUseStatements[0];
|
||||
Assert.Equal(Ast.UseType.Expressions, useStatement.Type);
|
||||
Assert.Equal("js", useStatement.Value);
|
||||
|
||||
// No workflow-level use statements
|
||||
Assert.Empty(workflow.UseStatements);
|
||||
|
||||
// Verify body contains: var declaration, listen statement, WriteLine, WriteHttpResponse
|
||||
Assert.Equal(4, workflow.Body.Count);
|
||||
|
||||
// Verify variable declaration
|
||||
var varDecl = Assert.IsType<Ast.VariableDeclarationNode>(workflow.Body[0]);
|
||||
Assert.Equal("message", varDecl.Name);
|
||||
Assert.Equal(Ast.VariableKind.Var, varDecl.Kind);
|
||||
|
||||
// Verify listen statement
|
||||
var listenNode = Assert.IsType<Ast.ListenNode>(workflow.Body[1]);
|
||||
Assert.Equal("HttpEndpoint", listenNode.Activity.ActivityName);
|
||||
Assert.Single(listenNode.Activity.Arguments);
|
||||
|
||||
// Verify WriteLine with ElsaScript expression
|
||||
var writeLineNode = Assert.IsType<Ast.ActivityInvocationNode>(workflow.Body[2]);
|
||||
Assert.Equal("WriteLine", writeLineNode.ActivityName);
|
||||
Assert.Single(writeLineNode.Arguments);
|
||||
var writeLineExpr = Assert.IsType<Ast.ElsaExpressionNode>(writeLineNode.Arguments[0].Value);
|
||||
Assert.Equal("js", writeLineExpr.Language);
|
||||
|
||||
// Verify WriteHttpResponse with variable reference
|
||||
var writeHttpNode = Assert.IsType<Ast.ActivityInvocationNode>(workflow.Body[3]);
|
||||
Assert.Equal("WriteHttpResponse", writeHttpNode.ActivityName);
|
||||
Assert.Single(writeHttpNode.Arguments);
|
||||
var writeHttpArg = Assert.IsType<Ast.IdentifierNode>(writeHttpNode.Arguments[0].Value);
|
||||
Assert.Equal("message", writeHttpArg.Name);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse for loop with 'to' keyword (exclusive)")]
|
||||
public void Parse_WithForLoopExclusive_ShouldReturnWorkflowWithForNode()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow ForLoopTest {
|
||||
for (var i = 0 to 10 step 1)
|
||||
{
|
||||
WriteLine(js => `Step: ${i}`)
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal("ForLoopTest", workflow.Id);
|
||||
Assert.Single(workflow.Body);
|
||||
|
||||
var forNode = Assert.IsType<Ast.ForNode>(workflow.Body[0]);
|
||||
Assert.True(forNode.DeclaresVariable); // var i
|
||||
Assert.Equal("i", forNode.VariableName);
|
||||
Assert.False(forNode.IsInclusive);
|
||||
|
||||
var startLiteral = Assert.IsType<Ast.LiteralNode>(forNode.Start);
|
||||
// Numbers are parsed as decimals by the parser
|
||||
Assert.Equal(0m, Convert.ToDecimal(startLiteral.Value!));
|
||||
|
||||
var endLiteral = Assert.IsType<Ast.LiteralNode>(forNode.End);
|
||||
Assert.Equal(10m, Convert.ToDecimal(endLiteral.Value!));
|
||||
|
||||
var stepLiteral = Assert.IsType<Ast.LiteralNode>(forNode.Step);
|
||||
Assert.Equal(1m, Convert.ToDecimal(stepLiteral.Value!));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse for loop with 'through' keyword (inclusive)")]
|
||||
public void Parse_WithForLoopInclusive_ShouldReturnWorkflowWithForNode()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use expressions js;
|
||||
|
||||
workflow ForLoopInclusiveTest {
|
||||
for (var i = 0 through 10 step 1)
|
||||
{
|
||||
WriteLine(js => `Step: ${i}`)
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal("ForLoopInclusiveTest", workflow.Id);
|
||||
Assert.Single(workflow.Body);
|
||||
|
||||
var forNode = Assert.IsType<Ast.ForNode>(workflow.Body[0]);
|
||||
Assert.True(forNode.DeclaresVariable); // var i
|
||||
Assert.Equal("i", forNode.VariableName);
|
||||
Assert.True(forNode.IsInclusive);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse workflow with metadata")]
|
||||
public void Parse_WithWorkflowMetadata_ShouldReturnWorkflowWithCorrectMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
use Elsa.Activities.Console;
|
||||
|
||||
workflow HelloWorldDsl(
|
||||
DisplayName: ""Hello World DSL"",
|
||||
Description: ""Demonstrates ElsaScript with metadata"",
|
||||
DefinitionId: ""hello-world-dsl"",
|
||||
DefinitionVersionId: ""hello-world-dsl-v1"",
|
||||
Version: 1,
|
||||
UsableAsActivity: true
|
||||
) {
|
||||
use expressions js;
|
||||
|
||||
WriteLine(""Hello World from Elsa DSL!"");
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal("HelloWorldDsl", workflow.Id);
|
||||
|
||||
// Check metadata
|
||||
Assert.Equal("Hello World DSL", (string)workflow.Metadata["DisplayName"]);
|
||||
Assert.Equal("Demonstrates ElsaScript with metadata", (string)workflow.Metadata["Description"]);
|
||||
Assert.Equal("hello-world-dsl", (string)workflow.Metadata["DefinitionId"]);
|
||||
Assert.Equal("hello-world-dsl-v1", (string)workflow.Metadata["DefinitionVersionId"]);
|
||||
Assert.Equal(1L, Convert.ToInt64(workflow.Metadata["Version"]));
|
||||
Assert.True(Convert.ToBoolean(workflow.Metadata["UsableAsActivity"]));
|
||||
|
||||
// Check use statements (workflow-level only, global handled separately)
|
||||
Assert.Single(workflow.UseStatements);
|
||||
Assert.Equal(Ast.UseType.Expressions, workflow.UseStatements[0].Type);
|
||||
Assert.Equal("js", workflow.UseStatements[0].Value);
|
||||
|
||||
// Check body
|
||||
Assert.Single(workflow.Body);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse empty flowchart")]
|
||||
public void Parse_WithEmptyFlowchart_ShouldReturnFlowchartNode()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
workflow FlowchartTest {
|
||||
flowchart {
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
Assert.Equal("FlowchartTest", workflow.Id);
|
||||
Assert.Single(workflow.Body);
|
||||
|
||||
Assert.IsType<Ast.FlowchartNode>(workflow.Body[0]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse flowchart with node and connection")]
|
||||
public void Parse_WithFlowchartNodeAndConnection_ShouldReturnCorrectStructure()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
workflow FlowchartTest {
|
||||
flowchart {
|
||||
entry Start;
|
||||
Start: WriteLine();
|
||||
End: WriteLine();
|
||||
Start -> End;
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
Assert.Single(program.Workflows);
|
||||
|
||||
var workflow = program.Workflows[0];
|
||||
var flowchart = Assert.IsType<Ast.FlowchartNode>(workflow.Body[0]);
|
||||
|
||||
// Check entry point
|
||||
Assert.Equal("Start", flowchart.EntryPoint);
|
||||
|
||||
// Check nodes
|
||||
Assert.Equal(2, flowchart.Activities.Count);
|
||||
|
||||
// Check connections
|
||||
Assert.Single(flowchart.Connections);
|
||||
Assert.Equal("Start", flowchart.Connections[0].Source);
|
||||
Assert.Equal("End", flowchart.Connections[0].Target);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parser can parse flowchart with block node")]
|
||||
public void Parse_WithFlowchartBlockNode_ShouldReturnBlockStatement()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
workflow FlowchartWithBlock {
|
||||
flowchart {
|
||||
entry Start;
|
||||
|
||||
Start: {
|
||||
WriteLine(""First"");
|
||||
WriteLine(""Second"");
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var program = _parser.Parse(source);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(program);
|
||||
var workflow = program.Workflows[0];
|
||||
var flowchart = Assert.IsType<Ast.FlowchartNode>(workflow.Body[0]);
|
||||
|
||||
Assert.Single(flowchart.Activities);
|
||||
Assert.Equal("Start", flowchart.Activities[0].Label);
|
||||
|
||||
// The block should be a BlockNode with 2 statements
|
||||
var blockNode = Assert.IsType<Ast.BlockNode>(flowchart.Activities[0].Activity);
|
||||
Assert.Equal(2, blockNode.Statements.Count);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue