diff --git a/.specify/feature.json b/.specify/feature.json
index d418d5e78..1c74f069f 100644
--- a/.specify/feature.json
+++ b/.specify/feature.json
@@ -1,3 +1,3 @@
{
- "feature_directory": "specs/008-diagnostics-otel"
+ "feature_directory": "specs/010-workflow-json-hardening"
}
diff --git a/AGENTS.md b/AGENTS.md
index 62610a8e8..3390d955c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -83,7 +83,7 @@ Before handing off changes, verify the following when applicable:
For additional context about technologies to be used, project structure,
-shell commands, and other important information, read `specs/008-diagnostics-otel/plan.md`.
+shell commands, and other important information, read `specs/010-workflow-json-hardening/plan.md`.
## Active Technologies
diff --git a/Elsa.sln b/Elsa.sln
index 46a604192..2fc20c983 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34003.232
@@ -347,11 +347,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.UnitTests", "t
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Workflows.Api.UnitTests", "test\unit\Elsa.Workflows.Api.UnitTests\Elsa.Workflows.Api.UnitTests.csproj", "{81CFD2E0-2E5E-4810-ADB8-A08301199166}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry", "src\modules\Elsa.Diagnostics.OpenTelemetry\Elsa.Diagnostics.OpenTelemetry.csproj", "{007E3396-34EE-40A8-B697-23D694DC9EBE}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry", "src\modules\Elsa.Diagnostics.OpenTelemetry\Elsa.Diagnostics.OpenTelemetry.csproj", "{8F4AD54E-8586-4D8C-82E6-69218DD4280F}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry.UnitTests", "test\unit\Elsa.Diagnostics.OpenTelemetry.UnitTests\Elsa.Diagnostics.OpenTelemetry.UnitTests.csproj", "{D9D6C168-36CB-46EB-838A-B26C22FE9FDB}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry.UnitTests", "test\unit\Elsa.Diagnostics.OpenTelemetry.UnitTests\Elsa.Diagnostics.OpenTelemetry.UnitTests.csproj", "{60F844CC-6DA6-45EB-BA5E-B480B4E77624}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry.IntegrationTests", "test\integration\Elsa.Diagnostics.OpenTelemetry.IntegrationTests\Elsa.Diagnostics.OpenTelemetry.IntegrationTests.csproj", "{B5075CE7-7332-4F78-AB3D-492D670789FB}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Diagnostics.OpenTelemetry.IntegrationTests", "test\integration\Elsa.Diagnostics.OpenTelemetry.IntegrationTests\Elsa.Diagnostics.OpenTelemetry.IntegrationTests.csproj", "{B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -1393,42 +1393,42 @@ Global
{81CFD2E0-2E5E-4810-ADB8-A08301199166}.Release|x64.Build.0 = Release|Any CPU
{81CFD2E0-2E5E-4810-ADB8-A08301199166}.Release|x86.ActiveCfg = Release|Any CPU
{81CFD2E0-2E5E-4810-ADB8-A08301199166}.Release|x86.Build.0 = Release|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Debug|x64.ActiveCfg = Debug|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Debug|x64.Build.0 = Debug|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Debug|x86.ActiveCfg = Debug|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Debug|x86.Build.0 = Debug|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Release|Any CPU.Build.0 = Release|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Release|x64.ActiveCfg = Release|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Release|x64.Build.0 = Release|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Release|x86.ActiveCfg = Release|Any CPU
- {007E3396-34EE-40A8-B697-23D694DC9EBE}.Release|x86.Build.0 = Release|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Debug|x64.ActiveCfg = Debug|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Debug|x64.Build.0 = Debug|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Debug|x86.ActiveCfg = Debug|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Debug|x86.Build.0 = Debug|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Release|Any CPU.Build.0 = Release|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Release|x64.ActiveCfg = Release|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Release|x64.Build.0 = Release|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Release|x86.ActiveCfg = Release|Any CPU
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB}.Release|x86.Build.0 = Release|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Debug|x64.ActiveCfg = Debug|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Debug|x64.Build.0 = Debug|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Debug|x86.Build.0 = Debug|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Release|Any CPU.Build.0 = Release|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Release|x64.ActiveCfg = Release|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Release|x64.Build.0 = Release|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Release|x86.ActiveCfg = Release|Any CPU
- {B5075CE7-7332-4F78-AB3D-492D670789FB}.Release|x86.Build.0 = Release|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x64.Build.0 = Debug|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Debug|x86.Build.0 = Debug|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x64.ActiveCfg = Release|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x64.Build.0 = Release|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x86.ActiveCfg = Release|Any CPU
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F}.Release|x86.Build.0 = Release|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x64.Build.0 = Debug|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Debug|x86.Build.0 = Debug|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|Any CPU.Build.0 = Release|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x64.ActiveCfg = Release|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x64.Build.0 = Release|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x86.ActiveCfg = Release|Any CPU
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624}.Release|x86.Build.0 = Release|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x64.Build.0 = Debug|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Debug|x86.Build.0 = Debug|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x64.ActiveCfg = Release|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x64.Build.0 = Release|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.ActiveCfg = Release|Any CPU
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1552,9 +1552,9 @@ Global
{09B4B78B-FE02-44E2-8667-E182AF921C54} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{7D905CEC-B30B-4C99-B5F7-3052D33EC8E9} = {18453B51-25EB-4317-A4B3-B10518252E92}
{81CFD2E0-2E5E-4810-ADB8-A08301199166} = {18453B51-25EB-4317-A4B3-B10518252E92}
- {D9D6C168-36CB-46EB-838A-B26C22FE9FDB} = {18453B51-25EB-4317-A4B3-B10518252E92}
- {B5075CE7-7332-4F78-AB3D-492D670789FB} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
- {007E3396-34EE-40A8-B697-23D694DC9EBE} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D}
+ {8F4AD54E-8586-4D8C-82E6-69218DD4280F} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {60F844CC-6DA6-45EB-BA5E-B480B4E77624} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {B6A62D42-D9AA-47BE-BCBC-FCB866D346F0} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/doc/wiki/workflow-core.md b/doc/wiki/workflow-core.md
index 83707c568..c82a48106 100644
--- a/doc/wiki/workflow-core.md
+++ b/doc/wiki/workflow-core.md
@@ -148,6 +148,14 @@ Core serializers live under [Serialization](../../src/modules/Elsa.Workflows.Cor
Custom constructor and additional converter configurators are registered by `WorkflowsFeature`.
+### Workflow JSON Type Identifiers
+
+Workflow JSON type resolution uses the shared `ISerializationTypeRegistry` from `Elsa.Common.Serialization`, not expression type aliases. Register workflow-serializable payload types through `SerializationTypeOptions`; keep `ExpressionOptions` for expression/type metadata only.
+
+New workflow JSON writes preferred aliases when a type is registered. Compatibility reads also accept explicitly registered legacy names, including selected CLR names from older persisted workflow JSON. Unknown CLR names are rejected rather than loaded dynamically. Polymorphic object reads also reject abstract, interface, open generic, and unsupported collection targets unless the resolver can map a known collection interface to a concrete collection type.
+
+Public API payloads that expose workflow JSON type identifiers should emit values from `ISerializationTypeRegistry`. For example, incident strategy descriptors return the alias that workflow JSON accepts, while registered legacy CLR names remain readable during the compatibility window.
+
## When To Change This Layer
Change workflow core only when you are changing engine semantics, activity contracts, execution state, serialization, core activity behavior, or flowchart behavior. If the change is about persisted definitions, API DTOs, background dispatch, or a module-specific transport, start in management, API, runtime, or the extension module instead.
diff --git a/specs/010-workflow-json-hardening/checklists/requirements.md b/specs/010-workflow-json-hardening/checklists/requirements.md
new file mode 100644
index 000000000..9a990910a
--- /dev/null
+++ b/specs/010-workflow-json-hardening/checklists/requirements.md
@@ -0,0 +1,34 @@
+# Specification Quality Checklist: Workflow JSON Type Hardening
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-05-29
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- Validation passed. The issue explicitly requires a dedicated workflow serialization type registry or resolver; the specification names this capability as a required product behavior while deferring implementation details to planning.
diff --git a/specs/010-workflow-json-hardening/contracts/workflow-json-type-identifiers.md b/specs/010-workflow-json-hardening/contracts/workflow-json-type-identifiers.md
new file mode 100644
index 000000000..2e39ff815
--- /dev/null
+++ b/specs/010-workflow-json-hardening/contracts/workflow-json-type-identifiers.md
@@ -0,0 +1,15 @@
+# Contract: Workflow JSON Type Identifiers
+
+## Identifier Rules
+
+- New workflow JSON writes aliases when a type is registered with a workflow JSON alias.
+- Compatibility reads accept registered aliases and registered legacy names.
+- Unknown CLR names are rejected.
+- Abstract, interface, open generic, and inappropriate collection targets are rejected for polymorphic object materialization.
+- Supported collection aliases are limited to known collection wrappers closed over registered element types.
+
+## Incident Strategy Descriptor
+
+`GET /descriptors/incident-strategies` returns `typeName` values from the shared serialization type registry.
+
+Clients should persist or submit returned `typeName` values unchanged. During the compatibility window, existing CLR names registered as legacy identifiers remain accepted when workflow JSON is read.
diff --git a/specs/010-workflow-json-hardening/data-model.md b/specs/010-workflow-json-hardening/data-model.md
new file mode 100644
index 000000000..1782d97ec
--- /dev/null
+++ b/specs/010-workflow-json-hardening/data-model.md
@@ -0,0 +1,20 @@
+# Data Model: Workflow JSON Type Hardening
+
+## SerializationTypeOptions
+
+- Stores workflow JSON aliases mapped to concrete types.
+- Stores optional legacy names mapped to the same concrete types.
+- Provides default primitive and JSON island aliases needed by workflow payloads.
+- Lives in `Elsa.Common` so non-workflow serialization layers can share the same trust boundary.
+
+## SerializationTypeRegistry
+
+- Runtime registry built from `SerializationTypeOptions`.
+- Resolves aliases and registered legacy names to types.
+- Lists registered types for compatibility resolution.
+- Returns preferred aliases for writing new workflow JSON and public descriptor values.
+
+## Workflow Type Identifier
+
+- Alias: preferred stable identifier for new workflow JSON.
+- Legacy name: supported compatibility identifier for previously persisted workflow JSON or older clients.
diff --git a/specs/010-workflow-json-hardening/plan.md b/specs/010-workflow-json-hardening/plan.md
new file mode 100644
index 000000000..d68f63df0
--- /dev/null
+++ b/specs/010-workflow-json-hardening/plan.md
@@ -0,0 +1,88 @@
+# Implementation Plan: Workflow JSON Type Hardening
+
+**Branch**: `codex/7541-workflow-json-hardening` | **Date**: 2026-05-29 | **Spec**: [spec.md](spec.md)
+**Input**: Feature specification from `/specs/010-workflow-json-hardening/spec.md`
+
+**Note**: This template is filled in by the `/speckit-plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
+
+## Summary
+
+Reintroduce workflow JSON hardening through a workflow-specific type registry and resolver. Existing registered legacy CLR names remain readable, new workflow JSON emits aliases when available, and public descriptor APIs use the same identifier contract they accept.
+
+## Technical Context
+
+
+
+**Language/Version**: C# latest with nullable reference types, multi-targeting through existing project settings
+**Primary Dependencies**: System.Text.Json, Elsa feature/module infrastructure, existing expression registry for expression-only behavior
+**Storage**: Existing workflow JSON payloads only; no schema changes
+**Testing**: Targeted `dotnet test` for workflow core/runtime/api test projects
+**Target Platform**: Elsa server/library consumers on supported .NET target frameworks
+**Project Type**: Modular .NET library and API modules
+**Performance Goals**: Type lookup remains dictionary-based and does not add reflection scans to normal serialization
+**Constraints**: No arbitrary CLR type loading; compatibility only for explicitly registered workflow JSON legacy names
+**Scale/Scope**: Workflow definitions, workflow state, trigger/bookmark payloads, incident strategy descriptor contract, and module/extension registrations
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+PASS. The change stays inside existing workflow modules, adds one explicit extensibility point for serialization trust, preserves public compatibility, and includes focused tests. No new persistence provider or unrelated module is introduced.
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/[###-feature]/
+├── plan.md # This file (/speckit-plan command output)
+├── research.md # Phase 0 output (/speckit-plan command)
+├── data-model.md # Phase 1 output (/speckit-plan command)
+├── quickstart.md # Phase 1 output (/speckit-plan command)
+├── contracts/ # Phase 1 output (/speckit-plan command)
+└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
+```
+
+### Source Code (repository root)
+
+
+```text
+src/modules/Elsa.Workflows.Core/
+├── Contracts/
+├── Extensions/
+├── Options/
+├── Serialization/
+└── Features/
+
+src/modules/Elsa.Workflows.Runtime/
+├── Features/
+├── ShellFeatures/
+└── Services/
+
+src/modules/Elsa.Workflows.Api/
+└── Endpoints/IncidentStrategies/
+
+test/unit/Elsa.Workflows.Core.UnitTests/
+test/unit/Elsa.Workflows.Runtime.UnitTests/
+test/integration/Elsa.Workflows.IntegrationTests/
+```
+
+**Structure Decision**: Extend the existing workflow core serialization surface and update runtime/API registrations in place. Tests stay in the nearest existing workflow test projects.
+
+## Complexity Tracking
+
+> **Fill ONLY if Constitution Check has violations that must be justified**
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
+| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
diff --git a/specs/010-workflow-json-hardening/quickstart.md b/specs/010-workflow-json-hardening/quickstart.md
new file mode 100644
index 000000000..9ea443ee7
--- /dev/null
+++ b/specs/010-workflow-json-hardening/quickstart.md
@@ -0,0 +1,11 @@
+# Quickstart: Workflow JSON Type Hardening
+
+1. Register built-in workflow JSON aliases through workflow features.
+2. Register module payload aliases through each module that serializes workflow payloads.
+3. Verify `TypeJsonConverter` and `PolymorphicObjectConverter` use the shared serialization registry, not expression options.
+4. Run targeted tests:
+
+```sh
+dotnet test test/unit/Elsa.Workflows.Core.UnitTests/Elsa.Workflows.Core.UnitTests.csproj --filter SerializationTypeResolverTests
+dotnet test test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj --filter WorkflowRuntimeFeatureTests
+```
diff --git a/specs/010-workflow-json-hardening/research.md b/specs/010-workflow-json-hardening/research.md
new file mode 100644
index 000000000..03c83d02f
--- /dev/null
+++ b/specs/010-workflow-json-hardening/research.md
@@ -0,0 +1,25 @@
+# Research: Workflow JSON Type Hardening
+
+## Decision: Dedicated Workflow JSON Registry
+
+Use a workflow-specific registry and options object for `TypeJsonConverter`, `PolymorphicObjectConverter`, workflow state serialization, bookmark payload serialization, trigger comparison, hashing, and descriptor contracts.
+
+**Rationale**: The issue explicitly rejects using `ExpressionOptions` as the workflow JSON trust boundary. A dedicated registry makes the allowed workflow JSON surface explicit while leaving expression aliases available for expression evaluation and designer variable metadata.
+
+**Alternatives considered**: Reusing `IWellKnownTypeRegistry` was rejected because it is fed by `ExpressionOptions`. Direct `Type.GetType` fallback was rejected because it can load arbitrary CLR names.
+
+## Decision: Explicit Legacy Compatibility Names
+
+Compatibility reads accept aliases and registered legacy names, including simple assembly-qualified names, full assembly-qualified names for registered types, and supported collection wrappers over registered element types.
+
+**Rationale**: Existing persisted workflows may contain CLR names, but the trust boundary must be the registration list, not assembly probing.
+
+**Alternatives considered**: A broad assembly allow-list was rejected for this slice because it is harder to reason about and can accidentally expose unrelated types from trusted assemblies.
+
+## Decision: Alias-First Public Descriptor Contract
+
+Incident strategy descriptors should emit the workflow JSON alias for registered strategies while keeping legacy CLR names readable on input during the compatibility window.
+
+**Rationale**: The reported dropdown failure came from descriptors returning CLR names while hardened reads expected aliases. Emitting aliases aligns new clients with hardened serialization without breaking old payloads.
+
+**Alternatives considered**: Keeping CLR names in descriptors was rejected because it perpetuates the inconsistent contract.
diff --git a/specs/010-workflow-json-hardening/spec.md b/specs/010-workflow-json-hardening/spec.md
new file mode 100644
index 000000000..01ecff726
--- /dev/null
+++ b/specs/010-workflow-json-hardening/spec.md
@@ -0,0 +1,103 @@
+# Feature Specification: Workflow JSON Type Hardening
+
+**Feature Branch**: `codex/7541-workflow-json-hardening`
+**Created**: 2026-05-29
+**Status**: Draft
+**Input**: User description: "GitHub issue #7541: Redo workflow JSON type hardening with dedicated alias design"
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Load Existing Workflows Safely (Priority: P1)
+
+Operators can upgrade Elsa without losing access to persisted workflows whose JSON contains existing type identifiers, including CLR type names emitted by earlier versions.
+
+**Why this priority**: Backward compatibility is required before hardening can be reintroduced without breaking production workflows.
+
+**Independent Test**: Load representative persisted workflow JSON that contains legacy type names and verify the workflow can be read while unknown or unsafe type identifiers are rejected.
+
+**Acceptance Scenarios**:
+
+1. **Given** a persisted workflow that references a known legacy type name, **When** the workflow is deserialized after upgrade, **Then** the workflow loads successfully.
+2. **Given** a persisted workflow that references an unknown or untrusted type name, **When** the workflow is deserialized, **Then** the workflow is rejected with a clear failure instead of resolving arbitrary types.
+3. **Given** a persisted workflow that contains abstract, interface, open generic, or collection type identifiers where a concrete workflow type is required, **When** the workflow is deserialized, **Then** the identifier is rejected.
+
+---
+
+### User Story 2 - Use Consistent Type Identifiers in APIs (Priority: P2)
+
+API consumers receive consistent type identifiers for workflow-facing options such as incident handling strategies and can submit those identifiers back without relying on implementation-specific names.
+
+**Why this priority**: The reported incident strategy dropdown failure came from inconsistent contracts between API descriptors and hardened workflow JSON reads.
+
+**Independent Test**: Request descriptor data for incident strategies, select a returned option, and verify a workflow or workflow definition using that option can be accepted and read.
+
+**Acceptance Scenarios**:
+
+1. **Given** API descriptor data for incident handling strategies, **When** the options are returned, **Then** every option uses the documented workflow type identifier contract.
+2. **Given** a client submits a supported incident strategy identifier from the descriptor payload, **When** the workflow payload is processed, **Then** the identifier resolves to the intended strategy.
+3. **Given** older clients submit legacy CLR identifiers during the compatibility window, **When** the workflow payload is processed, **Then** supported legacy identifiers continue to resolve.
+
+---
+
+### User Story 3 - Register Workflow-Serializable Types Explicitly (Priority: P3)
+
+Module and extension authors can explicitly register the types and legacy names that are valid in workflow serialization payloads without using expression configuration as the trust boundary.
+
+**Why this priority**: Hardening must be extensible for built-in modules, custom workflow types, runtime payloads, and third-party extensions.
+
+**Independent Test**: Register a custom workflow-facing type and legacy identifier, then verify alias-based payloads and supported legacy payloads resolve while unrelated types do not.
+
+**Acceptance Scenarios**:
+
+1. **Given** a module registers a workflow-serializable type with an alias, **When** workflow JSON references the alias, **Then** the type resolves successfully.
+2. **Given** a module registers a supported legacy name for a workflow-serializable type, **When** compatibility JSON references the legacy name, **Then** the type resolves successfully.
+3. **Given** a type is registered only for expression use, **When** workflow JSON references it, **Then** workflow JSON resolution does not accept it unless it is also registered for workflow serialization.
+
+### Edge Cases
+
+- Legacy JSON references a CLR type moved between assemblies or renamed after earlier persistence.
+- Payloads reference unknown, untrusted, abstract, interface, open generic, array, dictionary, or collection types.
+- JSON islands contain values that resemble type metadata but should remain normal JSON data.
+- Runtime trigger and bookmark payloads include polymorphic values that need the same trust model as workflow definitions.
+- Custom workflow types are registered by host applications or extensions after core services are configured.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: The system MUST keep workflow JSON type resolution separate from expression type alias configuration.
+- **FR-002**: The system MUST provide a dedicated registry or resolver for workflow-serializable type identifiers.
+- **FR-003**: The system MUST allow built-in modules, extensions, and host applications to register workflow-serializable aliases.
+- **FR-004**: The system MUST allow supported legacy type names to be registered for backward-compatible reads.
+- **FR-005**: The system MUST document which legacy identifiers remain readable during the compatibility window and how unsupported identifiers fail.
+- **FR-006**: The system MUST reject unknown, untrusted, abstract, open generic, interface, and inappropriate collection type identifiers when resolving workflow JSON types.
+- **FR-007**: The system MUST continue loading existing workflows that reference supported CLR type names during the compatibility window.
+- **FR-008**: The system MUST serialize new workflow-facing payloads with the documented type identifier contract where the payload belongs to that contract.
+- **FR-009**: The system MUST keep public API payloads that expose workflow type identifiers internally consistent for request and response flows.
+- **FR-010**: The system MUST cover the incident handling strategy option flow with a regression test.
+- **FR-011**: The system MUST cover persisted workflow JSON, alias JSON, runtime trigger or bookmark payloads, JSON islands, and custom workflow types with tests.
+- **FR-012**: The system MUST document the security model, trust boundaries, and migration behavior before hardened resolution is reintroduced.
+
+### Key Entities *(include if feature involves data)*
+
+- **Workflow Type Identifier**: A stable value used in workflow-facing JSON or API payloads to identify an allowed type.
+- **Workflow-Serializable Type Registration**: A trusted registration that maps a workflow type identifier and optional legacy names to a concrete allowed type.
+- **Compatibility Window**: The supported period or mode in which selected legacy CLR type names remain readable.
+- **Public Type Identifier Contract**: The documented request and response behavior for API payloads that expose workflow type identifiers.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: All representative legacy workflow JSON fixtures covered by tests load successfully when they use supported legacy identifiers.
+- **SC-002**: Tests demonstrate rejection for unknown, untrusted, abstract, interface, open generic, and inappropriate collection type identifiers.
+- **SC-003**: Incident strategy descriptor and submit flows pass a regression test that uses the same identifier contract in both directions.
+- **SC-004**: New workflow JSON hardening tests run without depending on expression type alias configuration.
+- **SC-005**: Documentation explains the trust model and compatibility behavior clearly enough for module authors to register workflow-serializable types without reading implementation code.
+
+## Assumptions
+
+- The compatibility window accepts known Elsa workflow-related CLR type names and explicitly registered host or extension legacy names, not arbitrary CLR resolution.
+- Public API type identifier changes can be transitional: aliases are preferred for new payloads while supported legacy values remain readable.
+- Existing workflow JSON fixtures are sufficient to represent persisted workflow compatibility risks, with new fixtures added where gaps are found.
+- Cloud vault, external migration tooling, and data store schema changes are outside this feature unless existing tests prove they are required.
diff --git a/specs/010-workflow-json-hardening/tasks.md b/specs/010-workflow-json-hardening/tasks.md
new file mode 100644
index 000000000..00ec64244
--- /dev/null
+++ b/specs/010-workflow-json-hardening/tasks.md
@@ -0,0 +1,125 @@
+# Tasks: Workflow JSON Type Hardening
+
+**Input**: Design documents from `/specs/010-workflow-json-hardening/`
+**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/
+
+**Tests**: Required by the feature specification for compatibility, rejection behavior, incident strategies, runtime payloads, JSON islands, and custom workflow types.
+
+## Phase 1: Setup (Shared Infrastructure)
+
+**Purpose**: Confirm the clean worktree and documentation context.
+
+- [x] T001 Verify the dedicated worktree status in `/Users/sipke/Projects/Elsa/elsa-core-7541`
+- [x] T002 Update Spec Kit agent context in `/Users/sipke/Projects/Elsa/elsa-core-7541/AGENTS.md`
+
+---
+
+## Phase 2: Foundational (Blocking Prerequisites)
+
+**Purpose**: Add the dedicated workflow JSON trust boundary before changing user-facing behavior.
+
+- [x] T003 Add serialization type registry contracts in `src/modules/Elsa.Common/Serialization/ISerializationTypeRegistry.cs`
+- [x] T004 Add serialization type options in `src/modules/Elsa.Common/Serialization/SerializationTypeOptions.cs`
+- [x] T005 Add serialization type registry implementation in `src/modules/Elsa.Common/Serialization/SerializationTypeRegistry.cs`
+- [x] T006 Add workflow JSON registration extensions in `src/modules/Elsa.Common/Extensions/SerializationTypeOptionsExtensions.cs`
+- [x] T007 Update workflow JSON resolver to use the dedicated registry in `src/modules/Elsa.Common/Serialization/SerializationTypeResolver.cs`
+- [x] T008 Update workflow type converters to use the dedicated registry in `src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverterFactory.cs`, and `src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicDictionaryConverter.cs`
+
+**Checkpoint**: Workflow JSON converters no longer depend on `ExpressionOptions`.
+
+---
+
+## Phase 3: User Story 1 - Load Existing Workflows Safely (Priority: P1) MVP
+
+**Goal**: Existing registered CLR type names remain readable while unsafe names fail.
+
+**Independent Test**: Deserialize legacy workflow JSON/type payloads with registered names and verify unsafe identifiers are rejected.
+
+### Tests for User Story 1
+
+- [x] T009 [US1] Update resolver tests for dedicated registry compatibility in `test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs`
+- [x] T010 [US1] Add regression coverage for expression-only aliases not being accepted by workflow JSON in `test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/SerializationTypeResolverTests.cs`
+
+### Implementation for User Story 1
+
+- [x] T011 [US1] Register core workflow JSON aliases and legacy names in `src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs` and `src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs`
+- [x] T012 [US1] Update workflow serializers and hashing to consume `ISerializationTypeRegistry` in `src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs`, `src/modules/Elsa.Workflows.Core/Serialization/Serializers/BookmarkPayloadSerializer.cs`, and `src/modules/Elsa.Workflows.Core/Services/Hasher.cs`
+
+**Checkpoint**: User Story 1 can be validated independently.
+
+---
+
+## Phase 4: User Story 2 - Use Consistent Type Identifiers in APIs (Priority: P2)
+
+**Goal**: Incident strategy descriptors emit the same workflow type identifiers that workflow JSON accepts.
+
+**Independent Test**: Descriptor output for incident strategies returns aliases and supported legacy identifiers remain readable.
+
+### Tests for User Story 2
+
+- [x] T013 [US2] Add incident strategy descriptor regression tests in `test/unit/Elsa.Workflows.Api.UnitTests/Endpoints/IncidentStrategies/ListTests.cs`
+
+### Implementation for User Story 2
+
+- [x] T014 [US2] Register incident strategy aliases and legacy names in `src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs` and `src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs`
+- [x] T015 [US2] Emit workflow JSON aliases from incident strategy descriptors in `src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs`
+- [x] T016 [US2] Update client model documentation for identifier semantics in `src/clients/Elsa.Api.Client/Resources/IncidentStrategies/Models/IncidentStrategyDescriptor.cs`
+
+**Checkpoint**: User Story 2 can be validated independently.
+
+---
+
+## Phase 5: User Story 3 - Register Workflow-Serializable Types Explicitly (Priority: P3)
+
+**Goal**: Modules and host applications register workflow JSON types without using expression options.
+
+**Independent Test**: Runtime and extension payloads resolve from workflow JSON registrations while expression-only aliases do not.
+
+### Tests for User Story 3
+
+- [x] T017 [US3] Update runtime feature tests for workflow JSON registrations in `test/unit/Elsa.Workflows.Runtime.UnitTests/Features/WorkflowRuntimeFeatureTests.cs`
+- [x] T018 [US3] Update runtime trigger comparer tests for the dedicated registry in `test/unit/Elsa.Workflows.Runtime.UnitTests/Comparers/WorkflowTriggerEqualityComparerTests.cs`
+
+### Implementation for User Story 3
+
+- [x] T019 [US3] Move runtime workflow payload registration to workflow JSON options in `src/modules/Elsa.Workflows.Runtime/WorkflowRuntimeTypeAliasRegistrar.cs`, `src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs`, and `src/modules/Elsa.Workflows.Runtime/ShellFeatures/WorkflowRuntimeFeature.cs`
+- [x] T020 [US3] Move module payload registrations to workflow JSON options in `src/modules/Elsa.Http/Features/HttpFeature.cs`, `src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs`, `src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs`, `src/modules/Elsa.Resilience/Features/ResilienceFeature.cs`, `src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs`, `src/modules/Elsa.Alterations/Features/AlterationsFeature.cs`, `src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs`, and `src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs`
+- [x] T021 [US3] Update runtime trigger comparison to use `ISerializationTypeRegistry` in `src/modules/Elsa.Workflows.Runtime/Comparers/WorkflowTriggerEqualityComparer.cs` and `src/modules/Elsa.Workflows.Runtime/Services/TriggerIndexer.cs`
+
+**Checkpoint**: User Story 3 can be validated independently.
+
+---
+
+## Phase 6: Polish & Cross-Cutting Concerns
+
+**Purpose**: Documentation and validation.
+
+- [x] T022 Add workflow JSON hardening documentation in `doc/wiki/workflow-core.md`
+- [x] T023 Run targeted workflow core unit tests with `dotnet test test/unit/Elsa.Workflows.Core.UnitTests/Elsa.Workflows.Core.UnitTests.csproj --filter SerializationTypeResolverTests`
+- [x] T024 Run targeted workflow runtime unit tests with `dotnet test test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj --filter WorkflowRuntimeFeatureTests`
+- [x] T025 Review changed files and ensure `.specify/feature.json` and Spec Kit artifacts are correct
+
+---
+
+## Dependencies & Execution Order
+
+- **Setup**: T001-T002 first.
+- **Foundational**: T003-T008 block all user stories.
+- **US1**: T009-T012 produces the MVP compatibility slice.
+- **US2**: T013-T016 depends on the registry from Foundational and may run after US1.
+- **US3**: T017-T021 depends on the registry from Foundational and may run after US1.
+- **Polish**: T022-T025 after implementation.
+
+## Parallel Opportunities
+
+- Test updates in T009, T013, T017, and T018 can be developed in parallel after T003-T008.
+- Module registration moves in T020 can be parallelized by module after the extension API exists.
+- Documentation T022 can run after the contract behavior is finalized.
+
+## Implementation Strategy
+
+1. Establish the dedicated registry and switch converters.
+2. Preserve legacy compatibility for existing workflow JSON.
+3. Align incident strategy API descriptors with the registry.
+4. Move runtime/module payload registrations off expression options.
+5. Validate with targeted tests and documentation.
diff --git a/src/apps/Elsa.ModularServer.Web/Program.cs b/src/apps/Elsa.ModularServer.Web/Program.cs
index 3a5624e39..f27fae972 100644
--- a/src/apps/Elsa.ModularServer.Web/Program.cs
+++ b/src/apps/Elsa.ModularServer.Web/Program.cs
@@ -48,7 +48,6 @@ services.AddOpenTelemetry()
.AddHttpClientInstrumentation()
.AddMeter(WorkflowInstrumentation.MeterName)
.AddOtlpExporter(options => ConfigureDiagnosticsOtlpExporter(options, configuration, "metrics")));
-
var nuplaneConfiguration = configuration.GetSection("Nuplane");
services.AddNuplane(nuplaneConfiguration, nuplane =>
diff --git a/src/clients/Elsa.Api.Client/Resources/IncidentStrategies/Models/IncidentStrategyDescriptor.cs b/src/clients/Elsa.Api.Client/Resources/IncidentStrategies/Models/IncidentStrategyDescriptor.cs
index fc401f354..657396a88 100644
--- a/src/clients/Elsa.Api.Client/Resources/IncidentStrategies/Models/IncidentStrategyDescriptor.cs
+++ b/src/clients/Elsa.Api.Client/Resources/IncidentStrategies/Models/IncidentStrategyDescriptor.cs
@@ -3,7 +3,7 @@ namespace Elsa.Api.Client.Resources.IncidentStrategies.Models;
///
/// Represents an incident strategy.
///
-/// The .NET type name of the strategy.
+/// The workflow JSON type identifier of the strategy.
/// The display name of the strategy.
/// The description of the strategy.
-public record IncidentStrategyDescriptor(string TypeName, string DisplayName, string? Description);
\ No newline at end of file
+public record IncidentStrategyDescriptor(string TypeName, string DisplayName, string? Description);
diff --git a/src/modules/Elsa.Alterations/Features/AlterationsFeature.cs b/src/modules/Elsa.Alterations/Features/AlterationsFeature.cs
index 2b608d392..6f9173636 100644
--- a/src/modules/Elsa.Alterations/Features/AlterationsFeature.cs
+++ b/src/modules/Elsa.Alterations/Features/AlterationsFeature.cs
@@ -6,11 +6,12 @@ using Elsa.Alterations.Core.Stores;
using Elsa.Alterations.Extensions;
using Elsa.Alterations.Services;
using Elsa.Alterations.Workflows;
-using Elsa.Workflows.Serialization.Options;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
+using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Common.Serialization;
namespace Elsa.Alterations.Features;
@@ -60,10 +61,10 @@ public class AlterationsFeature : FeatureBase
///
public override void Apply()
{
- Services.Configure(options =>
+ Services.Configure(options =>
{
- options.RegisterTypeAlias(typeof(AlterationPlanParams), typeof(AlterationPlanParams).GetSimpleAssemblyQualifiedName());
options.RegisterTypeAlias(typeof(AlterationPlanParams), nameof(AlterationPlanParams));
+ options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AlterationPlanParams));
});
Services.AddScoped();
diff --git a/src/modules/Elsa.Alterations/ShellFeatures/AlterationsFeature.cs b/src/modules/Elsa.Alterations/ShellFeatures/AlterationsFeature.cs
index aa6962a91..8721c3a9c 100644
--- a/src/modules/Elsa.Alterations/ShellFeatures/AlterationsFeature.cs
+++ b/src/modules/Elsa.Alterations/ShellFeatures/AlterationsFeature.cs
@@ -8,10 +8,11 @@ using Elsa.Alterations.Core.Stores;
using Elsa.Alterations.Extensions;
using Elsa.Alterations.Services;
using Elsa.Alterations.Workflows;
-using Elsa.Workflows.Serialization.Options;
using Elsa.Extensions;
+using Elsa.Workflows.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Common.Serialization;
namespace Elsa.Alterations.ShellFeatures;
@@ -42,10 +43,10 @@ public class AlterationsFeature : IFastEndpointsShellFeature
public void ConfigureServices(IServiceCollection services)
{
- services.Configure(options =>
+ services.Configure(options =>
{
- options.RegisterTypeAlias(typeof(AlterationPlanParams), typeof(AlterationPlanParams).GetSimpleAssemblyQualifiedName());
options.RegisterTypeAlias(typeof(AlterationPlanParams), nameof(AlterationPlanParams));
+ options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AlterationPlanParams));
});
services.AddScoped();
diff --git a/src/modules/Elsa.Common/Extensions/SerializationTypeOptionsExtensions.cs b/src/modules/Elsa.Common/Extensions/SerializationTypeOptionsExtensions.cs
new file mode 100644
index 000000000..fe257dbe3
--- /dev/null
+++ b/src/modules/Elsa.Common/Extensions/SerializationTypeOptionsExtensions.cs
@@ -0,0 +1,53 @@
+using Elsa.Common.Serialization;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+///
+/// Extends .
+///
+public static class SerializationTypeOptionsExtensions
+{
+ ///
+ /// Registers a preferred serialization alias.
+ ///
+ public static SerializationTypeOptions AddTypeAlias(this SerializationTypeOptions options, string alias) => options.RegisterTypeAlias(typeof(T), alias);
+
+ ///
+ /// Registers the type name as the preferred serialization alias.
+ ///
+ public static SerializationTypeOptions AddTypeAlias(this SerializationTypeOptions options) => options.RegisterTypeAlias(typeof(T), typeof(T).Name);
+
+ ///
+ /// Registers a preferred serialization alias and the current simple assembly-qualified name as a legacy identifier.
+ ///
+ public static SerializationTypeOptions AddTypeAliasWithLegacyName(this SerializationTypeOptions options, Type type, string alias)
+ {
+ options.RegisterTypeAlias(type, alias);
+ options.RegisterLegacySimpleAssemblyQualifiedName(type);
+ return options;
+ }
+
+ ///
+ /// Registers a preferred serialization alias and the current simple assembly-qualified name as a legacy identifier.
+ ///
+ public static SerializationTypeOptions AddTypeAliasWithLegacyName(this SerializationTypeOptions options, string alias) => options.AddTypeAliasWithLegacyName(typeof(T), alias);
+
+ ///
+ /// Registers the current simple assembly-qualified name as a compatibility identifier.
+ ///
+ public static SerializationTypeOptions AddLegacySimpleAssemblyQualifiedName(this SerializationTypeOptions options) => options.RegisterLegacySimpleAssemblyQualifiedName(typeof(T));
+
+ ///
+ /// Registers the current simple assembly-qualified name as a compatibility identifier.
+ ///
+ public static SerializationTypeOptions AddLegacySimpleAssemblyQualifiedName(this SerializationTypeOptions options, Type type) => options.RegisterLegacySimpleAssemblyQualifiedName(type);
+
+ ///
+ /// Registers the current simple assembly-qualified name as the preferred alias for compatibility-only types.
+ ///
+ public static SerializationTypeOptions AddSimpleAssemblyQualifiedTypeAlias(this SerializationTypeOptions options, Type type)
+ {
+ return options.RegisterTypeAlias(type, type.GetSimpleAssemblyQualifiedName());
+ }
+}
diff --git a/src/modules/Elsa.Common/Extensions/TypeExtensions.cs b/src/modules/Elsa.Common/Extensions/TypeExtensions.cs
index 863a2057b..588eda811 100644
--- a/src/modules/Elsa.Common/Extensions/TypeExtensions.cs
+++ b/src/modules/Elsa.Common/Extensions/TypeExtensions.cs
@@ -1,3 +1,5 @@
+using System.Collections.Concurrent;
+
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
@@ -6,6 +8,17 @@ namespace Elsa.Extensions;
///
public static class TypeExtensions
{
+ private static readonly ConcurrentDictionary SimpleAssemblyQualifiedTypeNameCache = new();
+
+ ///
+ /// Gets the assembly-qualified name of the type, without version, culture, and public key token information.
+ ///
+ public static string GetSimpleAssemblyQualifiedName(this Type type)
+ {
+ if (type is null) throw new ArgumentNullException(nameof(type));
+ return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, BuildSimplifiedName);
+ }
+
///
/// Returns true of the type is generic, false otherwise.
///
@@ -54,4 +67,22 @@ public static class TypeExtensions
{
return type.IsPrimitive || type == typeof(decimal) || type == typeof(float) || type == typeof(double) || type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte) || type == typeof(uint) || type == typeof(ulong) || type == typeof(ushort) || type == typeof(sbyte);
}
-}
\ No newline at end of file
+
+ private static string BuildSimplifiedName(Type type)
+ {
+ var assemblyName = type.Assembly.GetName().Name;
+
+ if (type.IsGenericType)
+ {
+ var genericTypeName = type.GetGenericTypeDefinition().FullName!;
+ var backtickIndex = genericTypeName.IndexOf('`');
+ var typeNameWithoutArity = genericTypeName[..backtickIndex];
+ var arity = genericTypeName[backtickIndex..];
+ var simplifiedGenericArguments = type.GetGenericArguments().Select(BuildSimplifiedName);
+
+ return $"{typeNameWithoutArity}{arity}[[{string.Join("],[", simplifiedGenericArguments)}]], {assemblyName}";
+ }
+
+ return $"{type.FullName}, {assemblyName}";
+ }
+}
diff --git a/src/modules/Elsa.Common/Serialization/ISerializationTypeRegistry.cs b/src/modules/Elsa.Common/Serialization/ISerializationTypeRegistry.cs
new file mode 100644
index 000000000..123963a1f
--- /dev/null
+++ b/src/modules/Elsa.Common/Serialization/ISerializationTypeRegistry.cs
@@ -0,0 +1,27 @@
+namespace Elsa.Common.Serialization;
+
+///
+/// A registry of types that may be resolved from serialization type identifiers.
+///
+public interface ISerializationTypeRegistry
+{
+ ///
+ /// Registers a type with an alias.
+ ///
+ void RegisterType(Type type, string alias);
+
+ ///
+ /// Attempts to get the preferred alias for the specified type.
+ ///
+ bool TryGetAlias(Type type, out string alias);
+
+ ///
+ /// Attempts to get the type associated with the specified alias or legacy name.
+ ///
+ bool TryGetType(string alias, out Type type);
+
+ ///
+ /// Returns all registered types.
+ ///
+ IEnumerable ListTypes();
+}
diff --git a/src/modules/Elsa.Common/Serialization/SerializationTypeOptions.cs b/src/modules/Elsa.Common/Serialization/SerializationTypeOptions.cs
new file mode 100644
index 000000000..d252fd068
--- /dev/null
+++ b/src/modules/Elsa.Common/Serialization/SerializationTypeOptions.cs
@@ -0,0 +1,85 @@
+using System.Collections.ObjectModel;
+using System.Dynamic;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Elsa.Extensions;
+
+namespace Elsa.Common.Serialization;
+
+///
+/// Options for serialization type identifiers.
+///
+public class SerializationTypeOptions
+{
+ private readonly IDictionary _aliasTypeDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly IDictionary _typeAliasDictionary = new Dictionary();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SerializationTypeOptions()
+ {
+ AliasTypeDictionary = new ReadOnlyDictionary(_aliasTypeDictionary);
+ TypeAliasDictionary = new ReadOnlyDictionary(_typeAliasDictionary);
+
+ RegisterTypeAlias(typeof(short), "Int16");
+ RegisterTypeAlias(typeof(int), "Int32");
+ RegisterTypeAlias(typeof(long), "Int64");
+ RegisterLegacyTypeName(typeof(long), "Long");
+ RegisterTypeAlias(typeof(float), "Single");
+ RegisterTypeAlias(typeof(object), "Object");
+ RegisterTypeAlias(typeof(string), "String");
+ RegisterTypeAlias(typeof(bool), "Boolean");
+ RegisterTypeAlias(typeof(decimal), "Decimal");
+ RegisterTypeAlias(typeof(double), "Double");
+ RegisterTypeAlias(typeof(byte[]), "ByteArray");
+ RegisterTypeAlias(typeof(Guid), nameof(Guid));
+ RegisterTypeAlias(typeof(DateTime), nameof(DateTime));
+ RegisterTypeAlias(typeof(DateTimeOffset), nameof(DateTimeOffset));
+ RegisterTypeAlias(typeof(TimeSpan), nameof(TimeSpan));
+ RegisterTypeAlias(typeof(Stream), nameof(Stream));
+ RegisterTypeAlias(typeof(ExpandoObject), "JSON");
+ RegisterTypeAlias(typeof(JsonElement), nameof(JsonElement));
+ RegisterTypeAlias(typeof(JsonNode), nameof(JsonNode));
+ RegisterTypeAlias(typeof(JsonObject), nameof(JsonObject));
+ RegisterTypeAlias(typeof(JsonArray), nameof(JsonArray));
+ RegisterTypeAlias(typeof(IDictionary), "StringDictionary");
+ RegisterTypeAlias(typeof(IDictionary), "ObjectDictionary");
+ RegisterTypeAlias(typeof(Dictionary), "StringMap");
+ RegisterTypeAlias(typeof(Dictionary), "ObjectMap");
+ }
+
+ ///
+ /// Gets aliases and legacy names keyed by identifier.
+ ///
+ public IDictionary AliasTypeDictionary { get; }
+
+ ///
+ /// Gets preferred aliases keyed by type.
+ ///
+ public IDictionary TypeAliasDictionary { get; }
+
+ ///
+ /// Registers a preferred serialization alias.
+ ///
+ public SerializationTypeOptions RegisterTypeAlias(Type type, string alias)
+ {
+ _aliasTypeDictionary[alias] = type;
+ _typeAliasDictionary[type] = alias;
+ return this;
+ }
+
+ ///
+ /// Registers a legacy serialization identifier for compatibility reads.
+ ///
+ public SerializationTypeOptions RegisterLegacyTypeName(Type type, string typeName)
+ {
+ _aliasTypeDictionary[typeName] = type;
+ return this;
+ }
+
+ ///
+ /// Registers the type's simple assembly-qualified name as a legacy serialization identifier.
+ ///
+ public SerializationTypeOptions RegisterLegacySimpleAssemblyQualifiedName(Type type) => RegisterLegacyTypeName(type, type.GetSimpleAssemblyQualifiedName());
+}
diff --git a/src/modules/Elsa.Common/Serialization/SerializationTypeRegistry.cs b/src/modules/Elsa.Common/Serialization/SerializationTypeRegistry.cs
new file mode 100644
index 000000000..9b02f2d61
--- /dev/null
+++ b/src/modules/Elsa.Common/Serialization/SerializationTypeRegistry.cs
@@ -0,0 +1,60 @@
+using Microsoft.Extensions.Options;
+
+namespace Elsa.Common.Serialization;
+
+///
+public class SerializationTypeRegistry : ISerializationTypeRegistry
+{
+ private readonly Dictionary _aliasTypeDictionary = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _typeAliasDictionary = new();
+
+ ///
+ /// Creates a default registry.
+ ///
+ public static ISerializationTypeRegistry CreateDefault()
+ {
+ return new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(new SerializationTypeOptions()));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SerializationTypeRegistry(IOptions options)
+ {
+ foreach (var entry in options.Value.AliasTypeDictionary)
+ RegisterTypeName(entry.Value, entry.Key);
+
+ foreach (var entry in options.Value.TypeAliasDictionary)
+ RegisterType(entry.Key, entry.Value);
+ }
+
+ ///
+ public void RegisterType(Type type, string alias)
+ {
+ _typeAliasDictionary[type] = alias;
+ RegisterTypeName(type, alias);
+ }
+
+ ///
+ public bool TryGetAlias(Type type, out string alias) => _typeAliasDictionary.TryGetValue(type, out alias!);
+
+ ///
+ public bool TryGetType(string alias, out Type type) => _aliasTypeDictionary.TryGetValue(alias, out type!);
+
+ ///
+ public IEnumerable ListTypes() => _aliasTypeDictionary.Values.Distinct();
+
+ private void RegisterTypeName(Type type, string alias)
+ {
+ _aliasTypeDictionary[alias] = type;
+
+ if (type.IsPrimitive || type.IsValueType && Nullable.GetUnderlyingType(type) == null)
+ {
+ var nullableType = typeof(Nullable<>).MakeGenericType(type);
+ _aliasTypeDictionary[$"{alias}?"] = nullableType;
+
+ if (_typeAliasDictionary.ContainsKey(type))
+ _typeAliasDictionary[nullableType] = $"{alias}?";
+ }
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Helpers/WorkflowJsonTypeResolver.cs b/src/modules/Elsa.Common/Serialization/SerializationTypeResolver.cs
similarity index 60%
rename from src/modules/Elsa.Workflows.Core/Serialization/Helpers/WorkflowJsonTypeResolver.cs
rename to src/modules/Elsa.Common/Serialization/SerializationTypeResolver.cs
index d5258b9ae..250a25326 100644
--- a/src/modules/Elsa.Workflows.Core/Serialization/Helpers/WorkflowJsonTypeResolver.cs
+++ b/src/modules/Elsa.Common/Serialization/SerializationTypeResolver.cs
@@ -3,14 +3,13 @@ using System.Collections.ObjectModel;
using System.Reflection;
using System.Text.Json;
using Elsa.Extensions;
-using Elsa.Workflows.Serialization.Options;
-namespace Elsa.Workflows.Serialization.Helpers;
+namespace Elsa.Common.Serialization;
///
-/// Resolves workflow JSON type aliases and CLR type names.
+/// Resolves serialization type aliases without loading arbitrary CLR type names.
///
-public static class WorkflowJsonTypeResolver
+public static class SerializationTypeResolver
{
private static readonly IDictionary GenericCollectionTypes = new Dictionary(StringComparer.Ordinal)
{
@@ -52,47 +51,85 @@ public static class WorkflowJsonTypeResolver
[typeof(IDictionary)] = typeof(Dictionary)
};
- public static Type ResolveType(WorkflowJsonOptions workflowJsonOptions, string? typeAlias, bool allowLegacyClrTypeNames = true)
+ ///
+ /// Resolves the specified serialization type alias.
+ ///
+ public static Type ResolveType(ISerializationTypeRegistry serializationTypeRegistry, string? typeAlias)
{
if (string.IsNullOrWhiteSpace(typeAlias))
- throw new JsonException("The workflow JSON type alias is missing.");
+ throw new JsonException("The serialization type alias is missing.");
- if (TryResolveType(workflowJsonOptions, typeAlias, allowLegacyClrTypeNames, out var type))
+ if (TryResolveType(serializationTypeRegistry, typeAlias, out var type))
return type;
- var supportedTypeNames = allowLegacyClrTypeNames
- ? "Registered aliases, supported compound aliases, and CLR type names"
- : "Registered aliases, supported compound aliases, and registered legacy CLR type names";
- throw new JsonException($"Unknown workflow JSON type alias '{typeAlias}'. {supportedTypeNames} can be deserialized.");
+ throw new JsonException(
+ $"Unknown serialization type alias '{typeAlias}'. Only registered aliases and supported compound aliases can be deserialized.");
}
- public static bool TryResolveType(WorkflowJsonOptions workflowJsonOptions, string typeAlias, out Type type)
- {
- return TryResolveType(workflowJsonOptions, typeAlias, true, out type);
- }
-
- public static bool TryResolveType(WorkflowJsonOptions workflowJsonOptions, string typeAlias, bool allowLegacyClrTypeNames, out Type type)
+ ///
+ /// Attempts to resolve the specified serialization type alias.
+ ///
+ public static bool TryResolveType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, out Type type)
{
IReadOnlyList? registeredTypes = null;
- return TryResolveType(workflowJsonOptions, typeAlias, allowLegacyClrTypeNames, ref registeredTypes, out type);
+ return TryResolveType(serializationTypeRegistry, typeAlias, ref registeredTypes, out type);
}
- public static bool TryGetAlias(WorkflowJsonOptions workflowJsonOptions, Type type, out string alias)
+ private static bool TryResolveType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList? registeredTypes, out Type type)
{
- if (workflowJsonOptions.TryGetAlias(type, out alias!))
+ if (serializationTypeRegistry.TryGetType(typeAlias, out var registeredType))
+ {
+ type = registeredType;
+ return true;
+ }
+
+ if (TryResolveArrayType(serializationTypeRegistry, typeAlias, ref registeredTypes, out var arrayType))
+ {
+ type = arrayType;
+ return true;
+ }
+
+ if (TryResolveGenericCollectionType(serializationTypeRegistry, typeAlias, ref registeredTypes, out var genericCollectionType))
+ {
+ type = genericCollectionType;
+ return true;
+ }
+
+ if (TryResolveRegisteredLegacyTypeName(serializationTypeRegistry, typeAlias, ref registeredTypes, out var legacyType))
+ {
+ type = legacyType;
+ return true;
+ }
+
+ type = null!;
+ return false;
+ }
+
+ ///
+ /// Attempts to return a serialization type alias that this resolver can read back.
+ ///
+ public static bool TryGetAlias(ISerializationTypeRegistry serializationTypeRegistry, Type type, out string alias)
+ {
+ if (serializationTypeRegistry.TryGetAlias(type, out alias!))
return true;
- if (type.IsArray && TryGetAlias(workflowJsonOptions, type.GetElementType()!, out var elementTypeAlias))
+ if (type.IsArray)
{
- alias = $"{elementTypeAlias}[]";
- return true;
+ var elementType = type.GetElementType()!;
+
+ if (TryGetAlias(serializationTypeRegistry, elementType, out var elementTypeAlias))
+ {
+ alias = $"{elementTypeAlias}[]";
+ return true;
+ }
}
if (type is { IsGenericType: true, GenericTypeArguments.Length: 1 })
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
+
if (TryGetWritableGenericCollectionAlias(genericTypeDefinition, out var genericTypeAlias) &&
- TryGetAlias(workflowJsonOptions, type.GenericTypeArguments[0], out elementTypeAlias))
+ TryGetAlias(serializationTypeRegistry, type.GenericTypeArguments[0], out var elementTypeAlias))
{
alias = $"{genericTypeAlias}<{elementTypeAlias}>";
return true;
@@ -103,17 +140,9 @@ public static class WorkflowJsonTypeResolver
return false;
}
- public static string GetAliasOrLegacyClrTypeName(WorkflowJsonOptions workflowJsonOptions, Type type)
- {
- if (TryGetAlias(workflowJsonOptions, type, out var alias))
- return alias;
-
- if (!workflowJsonOptions.AllowLegacyClrTypeNames)
- throw new JsonException($"Type '{type}' is not registered as a workflow JSON type alias.");
-
- return type.GetSimpleAssemblyQualifiedName();
- }
-
+ ///
+ /// Attempts to map a supported collection interface type to an instantiable concrete type.
+ ///
public static bool TryGetInstantiableCollectionType(Type type, out Type instantiableType)
{
if (type.IsGenericType)
@@ -133,41 +162,22 @@ public static class WorkflowJsonTypeResolver
return false;
}
- private static bool TryResolveType(WorkflowJsonOptions workflowJsonOptions, string typeAlias, bool allowLegacyClrTypeNames, ref IReadOnlyList? registeredTypes, out Type type)
- {
- if (workflowJsonOptions.TryGetType(typeAlias, out type!))
- return true;
-
- if (TryResolveArrayType(workflowJsonOptions, typeAlias, allowLegacyClrTypeNames, ref registeredTypes, out type))
- return true;
-
- if (TryResolveGenericCollectionType(workflowJsonOptions, typeAlias, allowLegacyClrTypeNames, ref registeredTypes, out type))
- return true;
-
- if (TryResolveRegisteredLegacyTypeName(workflowJsonOptions, typeAlias, allowLegacyClrTypeNames, ref registeredTypes, out type))
- return true;
-
- if (allowLegacyClrTypeNames && TryResolveClrTypeName(typeAlias, out type))
- return true;
-
- type = null!;
- return false;
- }
-
- private static bool TryResolveArrayType(WorkflowJsonOptions workflowJsonOptions, string typeAlias, bool allowLegacyClrTypeNames, ref IReadOnlyList? registeredTypes, out Type type)
+ private static bool TryResolveArrayType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList? registeredTypes, out Type type)
{
type = null!;
+
if (!typeAlias.EndsWith("[]", StringComparison.Ordinal))
return false;
- if (!TryResolveType(workflowJsonOptions, typeAlias[..^2], allowLegacyClrTypeNames, ref registeredTypes, out var elementType))
+ var elementTypeAlias = typeAlias[..^2];
+ if (!TryResolveType(serializationTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
return false;
type = elementType.MakeArrayType();
return true;
}
- private static bool TryResolveGenericCollectionType(WorkflowJsonOptions workflowJsonOptions, string typeAlias, bool allowLegacyClrTypeNames, ref IReadOnlyList? registeredTypes, out Type type)
+ private static bool TryResolveGenericCollectionType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList? registeredTypes, out Type type)
{
type = null!;
var genericStart = typeAlias.IndexOf('<', StringComparison.Ordinal);
@@ -175,10 +185,12 @@ public static class WorkflowJsonTypeResolver
if (genericStart <= 0 || !typeAlias.EndsWith(">", StringComparison.Ordinal))
return false;
- if (!GenericCollectionTypes.TryGetValue(typeAlias[..genericStart], out var genericTypeDefinition))
+ var genericTypeAlias = typeAlias[..genericStart];
+ if (!GenericCollectionTypes.TryGetValue(genericTypeAlias, out var genericTypeDefinition))
return false;
- if (!TryResolveType(workflowJsonOptions, typeAlias[(genericStart + 1)..^1], allowLegacyClrTypeNames, ref registeredTypes, out var elementType))
+ var elementTypeAlias = typeAlias[(genericStart + 1)..^1];
+ if (!TryResolveType(serializationTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
return false;
type = genericTypeDefinition.MakeGenericType(elementType);
@@ -192,60 +204,59 @@ public static class WorkflowJsonTypeResolver
if (GenericCollectionInterfaceMappings.TryGetValue(genericTypeDefinition, out var instantiableGenericTypeDefinition) &&
GenericCollectionAliases.TryGetValue(instantiableGenericTypeDefinition, out alias!))
+ {
return true;
+ }
alias = null!;
return false;
}
- private static bool TryResolveClrTypeName(string typeAlias, out Type type)
+ private static bool TryResolveRegisteredLegacyTypeName(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList? registeredTypes, out Type type)
{
- try
- {
- type = Type.GetType(typeAlias, false)!;
- return type != null;
- }
- catch (Exception e) when (e is ArgumentException or FileLoadException or FileNotFoundException or TypeLoadException or BadImageFormatException)
- {
- type = null!;
- return false;
- }
- }
-
- private static bool TryResolveRegisteredLegacyTypeName(WorkflowJsonOptions workflowJsonOptions, string typeAlias, bool allowLegacyClrTypeNames, ref IReadOnlyList? registeredTypes, out Type type)
- {
- registeredTypes ??= workflowJsonOptions.ListTypes().ToArray();
+ registeredTypes ??= GetRegisteredTypes(serializationTypeRegistry);
var registeredTypeSnapshot = registeredTypes;
- type = registeredTypeSnapshot.FirstOrDefault(x =>
- string.Equals(x.GetSimpleAssemblyQualifiedName(), typeAlias, StringComparison.Ordinal) ||
- string.Equals(x.AssemblyQualifiedName, typeAlias, StringComparison.Ordinal))!;
-
- if (type != null)
+ if (TryResolveRegisteredSimpleAssemblyQualifiedName(registeredTypeSnapshot, typeAlias, out type))
return true;
- if (TryResolveLegacyGenericCollectionTypeName(workflowJsonOptions, typeAlias, allowLegacyClrTypeNames, ref registeredTypes, out type))
+ if (TryResolveLegacyGenericCollectionTypeName(serializationTypeRegistry, typeAlias, ref registeredTypes, out type))
return true;
+ Type? resolvedType;
+
try
{
- var resolvedType = Type.GetType(
+ resolvedType = Type.GetType(
typeAlias,
assemblyName => ResolveAssembly(registeredTypeSnapshot, assemblyName),
(assembly, typeName, ignoreCase) => ResolveType(registeredTypeSnapshot, assembly, typeName, ignoreCase),
false);
-
- type = resolvedType!;
- return resolvedType != null;
}
catch (Exception e) when (e is ArgumentException or FileLoadException)
{
- type = null!;
- return false;
+ resolvedType = null;
}
+
+ type = resolvedType!;
+ return resolvedType != null;
}
- private static bool TryResolveLegacyGenericCollectionTypeName(WorkflowJsonOptions workflowJsonOptions, string typeAlias, bool allowLegacyClrTypeNames, ref IReadOnlyList? registeredTypes, out Type type)
+ private static IReadOnlyList GetRegisteredTypes(ISerializationTypeRegistry serializationTypeRegistry)
+ {
+ return serializationTypeRegistry.ListTypes().ToArray();
+ }
+
+ private static bool TryResolveRegisteredSimpleAssemblyQualifiedName(IEnumerable registeredTypes, string typeAlias, out Type type)
+ {
+ type = registeredTypes.FirstOrDefault(x =>
+ string.Equals(x.GetSimpleAssemblyQualifiedName(), typeAlias, StringComparison.Ordinal) ||
+ string.Equals(x.AssemblyQualifiedName, typeAlias, StringComparison.Ordinal))!;
+
+ return type != null;
+ }
+
+ private static bool TryResolveLegacyGenericCollectionTypeName(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList? registeredTypes, out Type type)
{
type = null!;
@@ -261,9 +272,11 @@ public static class WorkflowJsonTypeResolver
if (!string.Equals(assemblyName, genericTypeDefinition.Assembly.GetName().Name, StringComparison.Ordinal))
continue;
- if (!TryResolveType(workflowJsonOptions, typeAlias[prefix.Length..separatorIndex], allowLegacyClrTypeNames, ref registeredTypes, out var elementType))
+ var elementTypeAlias = typeAlias[prefix.Length..separatorIndex];
+ if (!TryResolveType(serializationTypeRegistry, elementTypeAlias, ref registeredTypes, out var elementType))
return false;
+ // The resolver only closes known collection definitions over registered element types.
#pragma warning disable IL2055
type = genericTypeDefinition.MakeGenericType(elementType);
#pragma warning restore IL2055
diff --git a/src/modules/Elsa.Diagnostics.ConsoleLogs/Contracts/ElsaConsoleLogFilter.cs b/src/modules/Elsa.Diagnostics.ConsoleLogs/Contracts/ElsaConsoleLogFilter.cs
index b4b0e42fb..3579ac646 100644
--- a/src/modules/Elsa.Diagnostics.ConsoleLogs/Contracts/ElsaConsoleLogFilter.cs
+++ b/src/modules/Elsa.Diagnostics.ConsoleLogs/Contracts/ElsaConsoleLogFilter.cs
@@ -5,21 +5,21 @@ namespace Elsa.Diagnostics.ConsoleLogs.Contracts;
///
/// Console log filter accepted by Elsa REST and SignalR endpoints.
///
-public sealed class ElsaConsoleLogFilter
+public sealed record ElsaConsoleLogFilter
{
- public string? SourceId { get; set; }
+ public string? SourceId { get; init; }
[JsonConverter(typeof(ConsoleStreamJsonConverter))]
- public global::ConsoleLogStreaming.Core.Models.ConsoleStream? Stream { get; set; }
- public string? Query { get; set; }
- public string? WorkflowInstanceId { get; set; }
- public string? WorkflowDefinitionId { get; set; }
- public string? WorkflowDefinitionVersionId { get; set; }
- public string? ActivityInstanceId { get; set; }
- public string? ActivityId { get; set; }
- public string? ActivityNodeId { get; set; }
- public IDictionary? Metadata { get; set; }
- public DateTimeOffset? From { get; set; }
- public DateTimeOffset? To { get; set; }
- public int? Limit { get; set; }
-}
+ public global::ConsoleLogStreaming.Core.Models.ConsoleStream? Stream { get; init; }
+ public string? Query { get; init; }
+ public string? WorkflowInstanceId { get; init; }
+ public string? WorkflowDefinitionId { get; init; }
+ public string? WorkflowDefinitionVersionId { get; init; }
+ public string? ActivityInstanceId { get; init; }
+ public string? ActivityId { get; init; }
+ public string? ActivityNodeId { get; init; }
+ public IReadOnlyDictionary? Metadata { get; init; }
+ public DateTimeOffset? From { get; init; }
+ public DateTimeOffset? To { get; init; }
+ public int? Limit { get; init; }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Diagnostics.ConsoleLogs/Endpoints/ConsoleLogs/Sources/Endpoint.cs b/src/modules/Elsa.Diagnostics.ConsoleLogs/Endpoints/ConsoleLogs/Sources/Endpoint.cs
index 81858b72b..39246d9b7 100644
--- a/src/modules/Elsa.Diagnostics.ConsoleLogs/Endpoints/ConsoleLogs/Sources/Endpoint.cs
+++ b/src/modules/Elsa.Diagnostics.ConsoleLogs/Endpoints/ConsoleLogs/Sources/Endpoint.cs
@@ -2,6 +2,7 @@ using ConsoleLogStreaming.Core;
using Elsa.Abstractions;
using Elsa.Diagnostics.ConsoleLogs.Permissions;
using JetBrains.Annotations;
+using ConsoleLogSource = ConsoleLogStreaming.Core.Models.ConsoleLogSource;
namespace Elsa.Diagnostics.ConsoleLogs.Endpoints.ConsoleLogs.Sources;
@@ -16,6 +17,7 @@ internal class Endpoint(IConsoleLogProvider provider) : ElsaEndpointWithoutReque
public override async Task> ExecuteAsync(CancellationToken cancellationToken)
{
- return await provider.ListSourcesAsync(cancellationToken);
+ var sources = await provider.ListSourcesAsync(cancellationToken);
+ return sources.ToList();
}
}
diff --git a/src/modules/Elsa.Expressions.JavaScript/Features/JavaScriptFeature.cs b/src/modules/Elsa.Expressions.JavaScript/Features/JavaScriptFeature.cs
index 043008a60..23c0a8281 100644
--- a/src/modules/Elsa.Expressions.JavaScript/Features/JavaScriptFeature.cs
+++ b/src/modules/Elsa.Expressions.JavaScript/Features/JavaScriptFeature.cs
@@ -1,4 +1,5 @@
using Elsa.Caching.Features;
+using Elsa.Expressions.Options;
using Elsa.Common.Features;
using Elsa.Expressions.Features;
using Elsa.Extensions;
@@ -15,7 +16,9 @@ using Elsa.Expressions.JavaScript.Services;
using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts;
using Elsa.Expressions.JavaScript.TypeDefinitions.Services;
using Elsa.Workflows;
+using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Common.Serialization;
namespace Elsa.Expressions.JavaScript.Features;
@@ -59,6 +62,8 @@ public class JavaScriptFeature : FeatureBase
public override void Apply()
{
Services.Configure(JintOptions);
+ Services.Configure(JavaScriptExceptionTypeAliasRegistrar.Register);
+ Services.Configure(JavaScriptExceptionTypeAliasRegistrar.Register);
// JavaScript services.
Services
@@ -72,7 +77,7 @@ public class JavaScriptFeature : FeatureBase
.AddScoped()
.AddScoped()
.AddScoped()
- .AddSingleton()
+ .AddSingleton()
.AddFunctionDefinitionProvider()
.AddFunctionDefinitionProvider()
.AddFunctionDefinitionProvider()
@@ -94,4 +99,4 @@ public class JavaScriptFeature : FeatureBase
// UI property handlers.
Services.AddScoped();
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Expressions.JavaScript/JavaScriptExceptionTypeAliasRegistrar.cs b/src/modules/Elsa.Expressions.JavaScript/JavaScriptExceptionTypeAliasRegistrar.cs
new file mode 100644
index 000000000..7ad876ba6
--- /dev/null
+++ b/src/modules/Elsa.Expressions.JavaScript/JavaScriptExceptionTypeAliasRegistrar.cs
@@ -0,0 +1,34 @@
+using System.Reflection;
+using Acornima;
+using Elsa.Expressions.Options;
+using Elsa.Workflows.Options;
+using Jint;
+using Jint.Runtime;
+using Elsa.Common.Serialization;
+
+namespace Elsa.Expressions.JavaScript;
+
+internal static class JavaScriptExceptionTypeAliasRegistrar
+{
+ public static void Register(ExpressionOptions options)
+ {
+ options.RegisterTypeAlias(typeof(ScriptPreparationException), nameof(ScriptPreparationException));
+ options.RegisterTypeAlias(typeof(JavaScriptException), nameof(JavaScriptException));
+ options.RegisterTypeAlias(typeof(SyntaxErrorException), nameof(SyntaxErrorException));
+
+ var wrapperExceptionType = typeof(JavaScriptException).GetNestedType("JavaScriptErrorWrapperException", BindingFlags.Public | BindingFlags.NonPublic);
+ if (wrapperExceptionType != null)
+ options.RegisterTypeAlias(wrapperExceptionType, "Jint.JavaScriptErrorWrapperException");
+ }
+
+ public static void Register(SerializationTypeOptions options)
+ {
+ options.RegisterTypeAlias(typeof(ScriptPreparationException), nameof(ScriptPreparationException));
+ options.RegisterTypeAlias(typeof(JavaScriptException), nameof(JavaScriptException));
+ options.RegisterTypeAlias(typeof(SyntaxErrorException), nameof(SyntaxErrorException));
+
+ var wrapperExceptionType = typeof(JavaScriptException).GetNestedType("JavaScriptErrorWrapperException", BindingFlags.Public | BindingFlags.NonPublic);
+ if (wrapperExceptionType != null)
+ options.RegisterTypeAlias(wrapperExceptionType, "Jint.JavaScriptErrorWrapperException");
+ }
+}
diff --git a/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs b/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs
index 8111b335b..27edd192c 100644
--- a/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs
+++ b/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs
@@ -8,11 +8,14 @@ using Elsa.Expressions.JavaScript.Providers;
using Elsa.Expressions.JavaScript.Services;
using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts;
using Elsa.Expressions.JavaScript.TypeDefinitions.Services;
+using Elsa.Expressions.Options;
using Elsa.Extensions;
using Elsa.PackageManifest.Generator.Hints;
using Elsa.Workflows;
+using Elsa.Workflows.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Common.Serialization;
namespace Elsa.Expressions.JavaScript.ShellFeatures;
@@ -104,6 +107,8 @@ public class JavaScriptFeature : IShellFeature
options.DisableWrappers = DisableWrappers;
options.DisableVariableCopying = DisableVariableCopying;
});
+ services.Configure(JavaScriptExceptionTypeAliasRegistrar.Register);
+ services.Configure(JavaScriptExceptionTypeAliasRegistrar.Register);
// JavaScript services.
services
@@ -115,7 +120,7 @@ public class JavaScriptFeature : IShellFeature
.AddScoped()
.AddScoped()
.AddScoped()
- .AddSingleton()
+ .AddSingleton()
.AddFunctionDefinitionProvider()
.AddFunctionDefinitionProvider()
.AddFunctionDefinitionProvider()
diff --git a/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs b/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs
index 09b5990a4..b88e4e1ad 100644
--- a/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs
+++ b/src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs
@@ -1,4 +1,3 @@
-using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Elsa.Expressions.Models;
@@ -11,18 +10,6 @@ namespace Elsa.Extensions;
///
public static class TypeExtensions
{
- private static readonly ConcurrentDictionary SimpleAssemblyQualifiedTypeNameCache = new();
-
- ///
- /// Gets the assembly-qualified name of the type, without any version info etc.
- /// E.g. "System.String, System.Private.CoreLib"
- ///
- public static string GetSimpleAssemblyQualifiedName(this Type type)
- {
- if (type is null) throw new ArgumentNullException(nameof(type));
- return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, GetSimplifiedName);
- }
-
///
/// Returns the default value for the specified type.
///
@@ -113,24 +100,4 @@ public static class TypeExtensions
return sb.ToString();
}
- private static string GetSimplifiedName(Type type)
- {
- var assemblyName = type.Assembly.GetName().Name;
-
- if (type.IsGenericType)
- {
- var genericTypeName = type.GetGenericTypeDefinition().FullName!;
- var backtickIndex = genericTypeName.IndexOf('`');
- var typeNameWithoutArity = genericTypeName[..backtickIndex];
- var arity = genericTypeName[backtickIndex..];
-
- var genericArguments = type.GetGenericArguments();
- var simplifiedGenericArguments = genericArguments.Select(GetSimplifiedName);
-
- return $"{typeNameWithoutArity}{arity}[[{string.Join("],[", simplifiedGenericArguments)}]], {assemblyName}";
- }
-
- var typeName = type.FullName;
- return $"{typeName}, {assemblyName}";
- }
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs
index 6df56020f..6e406665a 100644
--- a/src/modules/Elsa.Http/Features/HttpFeature.cs
+++ b/src/modules/Elsa.Http/Features/HttpFeature.cs
@@ -1,5 +1,4 @@
using System.Net;
-using Elsa.Workflows.Serialization.Options;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
@@ -21,12 +20,14 @@ using Elsa.Http.UIHints;
using Elsa.Resilience.Extensions;
using Elsa.Resilience.Features;
using Elsa.Workflows;
+using Elsa.Workflows.Options;
using FluentStorage;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
+using Elsa.Common.Serialization;
namespace Elsa.Http.Features;
@@ -247,22 +248,22 @@ public class HttpFeature(IModule module) : FeatureBase(module)
foreach (var httpWorkflowInstanceIdSelectorType in HttpWorkflowInstanceIdSelectorTypes)
Services.AddScoped(typeof(IHttpWorkflowInstanceIdSelector), httpWorkflowInstanceIdSelectorType);
- Services.Configure(options =>
+ Services.Configure(options =>
{
- options.AddTypeAlias("HttpRequest");
- options.AddTypeAlias("HttpResponse");
- options.AddTypeAlias("HttpResponseMessage");
- options.AddTypeAlias("HttpHeaders");
- options.AddTypeAlias("RouteData");
- options.AddTypeAlias("FormFile");
- options.AddTypeAlias("FormFile[]");
- options.AddTypeAlias("HttpFile");
- options.AddTypeAlias("HttpFile[]");
- options.AddTypeAlias("Downloadable");
- options.AddTypeAlias("Downloadable[]");
- options.AddTypeAlias();
- options.AddTypeAlias();
- options.AddTypeAlias();
+ options.RegisterTypeAlias(typeof(HttpRequest), "HttpRequest");
+ options.RegisterTypeAlias(typeof(HttpResponse), "HttpResponse");
+ options.RegisterTypeAlias(typeof(HttpResponseMessage), "HttpResponseMessage");
+ options.RegisterTypeAlias(typeof(HttpHeaders), "HttpHeaders");
+ options.RegisterTypeAlias(typeof(HttpRouteData), "RouteData");
+ options.RegisterTypeAlias(typeof(IFormFile), "FormFile");
+ options.RegisterTypeAlias(typeof(IFormFile[]), "FormFile[]");
+ options.RegisterTypeAlias(typeof(HttpFile), "HttpFile");
+ options.RegisterTypeAlias(typeof(HttpFile[]), "HttpFile[]");
+ options.RegisterTypeAlias(typeof(Downloadable), "Downloadable");
+ options.RegisterTypeAlias(typeof(Downloadable[]), "Downloadable[]");
+ options.RegisterTypeAlias(typeof(HttpStatusCode), nameof(HttpStatusCode));
+ options.RegisterTypeAlias(typeof(HttpRequestException), nameof(HttpRequestException));
+ options.RegisterTypeAlias(typeof(HttpEndpointBookmarkPayload), nameof(HttpEndpointBookmarkPayload));
});
}
}
diff --git a/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs b/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
index b59c0874b..7f333e191 100644
--- a/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
+++ b/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
@@ -1,7 +1,6 @@
using System.Net;
using CShells.AspNetCore.Features;
using CShells.Features;
-using Elsa.Workflows.Serialization.Options;
using Elsa.Extensions;
using Elsa.Http.Bookmarks;
using Elsa.Http.ContentWriters;
@@ -20,6 +19,7 @@ using Elsa.Http.UIHints;
using Elsa.Resilience.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Management.Extensions;
+using Elsa.Workflows.Options;
using FluentStorage;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
@@ -28,6 +28,7 @@ using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
+using Elsa.Common.Serialization;
namespace Elsa.Http.ShellFeatures;
@@ -218,22 +219,22 @@ public class HttpFeature : IMiddlewareShellFeature
foreach (var httpWorkflowInstanceIdSelectorType in HttpWorkflowInstanceIdSelectorTypes)
services.AddScoped(typeof(IHttpWorkflowInstanceIdSelector), httpWorkflowInstanceIdSelectorType);
- services.Configure(options =>
+ services.Configure(options =>
{
- options.AddTypeAlias("HttpRequest");
- options.AddTypeAlias("HttpResponse");
- options.AddTypeAlias("HttpResponseMessage");
- options.AddTypeAlias("HttpHeaders");
- options.AddTypeAlias("RouteData");
- options.AddTypeAlias("FormFile");
- options.AddTypeAlias("FormFile[]");
- options.AddTypeAlias("HttpFile");
- options.AddTypeAlias("HttpFile[]");
- options.AddTypeAlias("Downloadable");
- options.AddTypeAlias("Downloadable[]");
- options.AddTypeAlias();
- options.AddTypeAlias();
- options.AddTypeAlias();
+ options.RegisterTypeAlias(typeof(HttpRequest), "HttpRequest");
+ options.RegisterTypeAlias(typeof(HttpResponse), "HttpResponse");
+ options.RegisterTypeAlias(typeof(HttpResponseMessage), "HttpResponseMessage");
+ options.RegisterTypeAlias(typeof(HttpHeaders), "HttpHeaders");
+ options.RegisterTypeAlias(typeof(HttpRouteData), "RouteData");
+ options.RegisterTypeAlias(typeof(IFormFile), "FormFile");
+ options.RegisterTypeAlias(typeof(IFormFile[]), "FormFile[]");
+ options.RegisterTypeAlias(typeof(HttpFile), "HttpFile");
+ options.RegisterTypeAlias(typeof(HttpFile[]), "HttpFile[]");
+ options.RegisterTypeAlias(typeof(Downloadable), "Downloadable");
+ options.RegisterTypeAlias(typeof(Downloadable[]), "Downloadable[]");
+ options.RegisterTypeAlias(typeof(HttpStatusCode), nameof(HttpStatusCode));
+ options.RegisterTypeAlias(typeof(HttpRequestException), nameof(HttpRequestException));
+ options.RegisterTypeAlias(typeof(HttpEndpointBookmarkPayload), nameof(HttpEndpointBookmarkPayload));
});
}
diff --git a/src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs b/src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs
index 0c8e27392..b13b41a41 100644
--- a/src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs
+++ b/src/modules/Elsa.Persistence.EFCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs
@@ -1,7 +1,9 @@
+using Elsa.Expressions.Options;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Features;
+using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Persistence.EFCore.Modules.Management;
@@ -28,4 +30,4 @@ public class EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : Persis
base.Apply();
AddEntityStore();
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Resilience/Features/ResilienceFeature.cs b/src/modules/Elsa.Resilience/Features/ResilienceFeature.cs
index 5cc8f6d3e..887ff7de5 100644
--- a/src/modules/Elsa.Resilience/Features/ResilienceFeature.cs
+++ b/src/modules/Elsa.Resilience/Features/ResilienceFeature.cs
@@ -10,8 +10,10 @@ using Elsa.Resilience.Recorders;
using Elsa.Resilience.Serialization;
using Elsa.Resilience.StrategySources;
using Elsa.Workflows;
+using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Elsa.Common.Serialization;
namespace Elsa.Resilience.Features;
@@ -66,6 +68,11 @@ public class ResilienceFeature(IModule module) : FeatureBase(module)
{
options.AddTypeAlias>("RetryAttemptRecordList");
});
+
+ Services.Configure(options =>
+ {
+ options.RegisterTypeAlias(typeof(List), "RetryAttemptRecordList");
+ });
}
public override void Apply()
diff --git a/src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs b/src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs
index 4e57ce778..dedacd3c5 100644
--- a/src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs
+++ b/src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs
@@ -10,8 +10,10 @@ using Elsa.Resilience.Recorders;
using Elsa.Resilience.Serialization;
using Elsa.Resilience.StrategySources;
using Elsa.Workflows;
+using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Elsa.Common.Serialization;
namespace Elsa.Resilience.ShellFeatures;
@@ -26,6 +28,11 @@ public class ResilienceFeature : IFastEndpointsShellFeature
{
options.AddTypeAlias>("RetryAttemptRecordList");
});
+
+ services.Configure(options =>
+ {
+ options.RegisterTypeAlias(typeof(List), "RetryAttemptRecordList");
+ });
services.AddOptions();
services.AddOptions();
diff --git a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs
index df06db78e..20efd0a61 100644
--- a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs
+++ b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs
@@ -1,6 +1,5 @@
using Elsa.Common.Features;
using Elsa.Common.Multitenancy;
-using Elsa.Workflows.Serialization.Options;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
@@ -11,7 +10,9 @@ using Elsa.Scheduling.Services;
using Elsa.Scheduling.StartupTasks;
using Elsa.Scheduling.TriggerPayloadValidators;
using Elsa.Workflows.Management.Features;
+using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Common.Serialization;
namespace Elsa.Scheduling.Features;
@@ -58,14 +59,14 @@ public class SchedulingFeature : FeatureBase
// Graceful shutdown: register scheduled-trigger ingress for diagnostic visibility (FR-006).
.AddSingleton();
- Services.Configure(options =>
+ Services.Configure(options =>
{
- options.AddTypeAlias();
- options.AddTypeAlias();
- options.AddTypeAlias();
- options.AddTypeAlias();
- options.AddTypeAlias();
- options.AddTypeAlias();
+ options.RegisterTypeAlias(typeof(CronBookmarkPayload), nameof(CronBookmarkPayload));
+ options.RegisterTypeAlias(typeof(CronTriggerPayload), nameof(CronTriggerPayload));
+ options.RegisterTypeAlias(typeof(DelayPayload), nameof(DelayPayload));
+ options.RegisterTypeAlias(typeof(StartAtPayload), nameof(StartAtPayload));
+ options.RegisterTypeAlias(typeof(TimerBookmarkPayload), nameof(TimerBookmarkPayload));
+ options.RegisterTypeAlias(typeof(TimerTriggerPayload), nameof(TimerTriggerPayload));
});
Module.Configure(management => management.AddActivitiesFrom());
diff --git a/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs b/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs
index 1459795d7..79eb86831 100644
--- a/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs
+++ b/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs
@@ -1,8 +1,8 @@
using CShells.Features;
using Elsa.Common.Multitenancy;
using Elsa.Common.ShellFeatures;
-using Elsa.Workflows.Serialization.Options;
using Elsa.Extensions;
+using Elsa.Workflows.Options;
using Elsa.Scheduling.Bookmarks;
using Elsa.Scheduling.Handlers;
using Elsa.Scheduling.Services;
@@ -11,6 +11,7 @@ using Elsa.Scheduling.TriggerPayloadValidators;
using Elsa.Workflows.Management.Extensions;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Common.Serialization;
namespace Elsa.Scheduling.ShellFeatures;
@@ -51,7 +52,7 @@ public class SchedulingFeature : IShellFeature
.AddTriggerPayloadValidator()
.AddActivitiesFrom();
- services.Configure(options =>
+ services.Configure(options =>
{
options.AddTypeAlias();
options.AddTypeAlias();
diff --git a/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs b/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs
index 2c63c1354..e6bd2a3d1 100644
--- a/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs
+++ b/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
-[assembly: InternalsVisibleTo("Elsa.Workflows.IntegrationTests")]
\ No newline at end of file
+[assembly: InternalsVisibleTo("Elsa.Workflows.IntegrationTests")]
+[assembly: InternalsVisibleTo("Elsa.Workflows.Api.UnitTests")]
diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs
index cafa9453d..95c87902b 100644
--- a/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs
+++ b/src/modules/Elsa.Workflows.Api/Endpoints/IncidentStrategies/List/Endpoint.cs
@@ -2,16 +2,17 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Elsa.Abstractions;
-using Elsa.Extensions;
using Elsa.Models;
+using Elsa.Workflows;
using Humanizer;
+using Elsa.Common.Serialization;
namespace Elsa.Workflows.Api.Endpoints.IncidentStrategies.List;
///
/// Returns list of available implementations.
///
-internal class List(IEnumerable strategies) : ElsaEndpointWithoutRequest>
+internal class List(IEnumerable strategies, ISerializationTypeRegistry workflowJsonTypeRegistry) : ElsaEndpointWithoutRequest>
{
public override void Configure()
{
@@ -21,15 +22,15 @@ internal class List(IEnumerable strategies) : ElsaEndpointWit
public override Task> ExecuteAsync(CancellationToken cancellationToken)
{
- var descriptors = strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
- var response =new ListResponse(descriptors);
+ var descriptors = strategies.Select(x => IncidentStrategyDescriptor.FromStrategy(x, workflowJsonTypeRegistry)).OrderBy(x => x.DisplayName).ToList();
+ var response = new ListResponse(descriptors);
return Task.FromResult(response);
}
}
internal record IncidentStrategyDescriptor(string DisplayName, string Description, string TypeName)
{
- public static IncidentStrategyDescriptor FromStrategy(IIncidentStrategy strategy)
+ public static IncidentStrategyDescriptor FromStrategy(IIncidentStrategy strategy, ISerializationTypeRegistry workflowJsonTypeRegistry)
{
var type = strategy.GetType();
var displayNameAttribute = type.GetCustomAttribute();
@@ -38,6 +39,7 @@ internal record IncidentStrategyDescriptor(string DisplayName, string Descriptio
var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize();
var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? "";
- return new IncidentStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName());
+ var typeName = workflowJsonTypeRegistry.TryGetAlias(type, out var alias) ? alias : type.FullName!;
+ return new IncidentStrategyDescriptor(displayName, description, typeName);
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs
index 71bfbb4a6..afa42aaf6 100644
--- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs
+++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs
@@ -4,10 +4,9 @@ using Elsa.Extensions;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Serialization.Converters;
-using Elsa.Workflows.Serialization.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
+using Elsa.Common.Serialization;
namespace Elsa.Workflows.Activities.Flowchart.Serialization;
@@ -15,7 +14,7 @@ namespace Elsa.Workflows.Activities.Flowchart.Serialization;
/// A JSON converter for .
///
[UsedImplicitly]
-public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IOptions workflowJsonOptions, ILoggerFactory loggerFactory) : JsonConverter
+public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, ISerializationTypeRegistry workflowJsonTypeRegistry, ILoggerFactory loggerFactory) : JsonConverter
{
private const string AllActivitiesKey = "allActivities";
private const string AllConnectionsKey = "allConnections";
@@ -48,7 +47,7 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IOptio
var variables = variablesElement.ValueKind != JsonValueKind.Undefined ? variablesElement.Deserialize>(options) ?? new List() : new List();
var polymorphicOptions = options.Clone();
- polymorphicOptions.Converters.Add(new PolymorphicDictionaryConverter(options, workflowJsonOptions.Value));
+ polymorphicOptions.Converters.Add(new PolymorphicDictionaryConverter(options, workflowJsonTypeRegistry));
var metadataElement = doc.RootElement.TryGetProperty("metadata", out var metadataEl) ? metadataEl : default;
var metadata = metadataElement.ValueKind != JsonValueKind.Undefined ? metadataElement.Deserialize>(polymorphicOptions) ?? new Dictionary() : new Dictionary();
@@ -106,7 +105,7 @@ public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IOptio
var flowchartSerializerOptions = new JsonSerializerOptions(options);
flowchartSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary, loggerFactory));
- flowchartSerializerOptions.Converters.Add(new PolymorphicDictionaryConverter(options, workflowJsonOptions.Value));
+ flowchartSerializerOptions.Converters.Add(new PolymorphicDictionaryConverter(options, workflowJsonTypeRegistry));
JsonSerializer.Serialize(writer, model, flowchartSerializerOptions);
}
diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowJsonOptionsExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowJsonOptionsExtensions.cs
deleted file mode 100644
index 851cc8214..000000000
--- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowJsonOptionsExtensions.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using Elsa.Common.Models;
-using Elsa.Extensions;
-using Elsa.Workflows;
-using Elsa.Workflows.Activities.Flowchart.Models;
-using Elsa.Workflows.Exceptions;
-using Elsa.Workflows.IncidentStrategies;
-using Elsa.Workflows.Memory;
-using Elsa.Workflows.Models;
-using Elsa.Workflows.Serialization.Options;
-using Elsa.Workflows.Services;
-using Elsa.Workflows.State;
-using Newtonsoft.Json.Linq;
-
-// ReSharper disable once CheckNamespace
-namespace Elsa.Extensions;
-
-///
-/// Extends .
-///
-public static class WorkflowJsonOptionsExtensions
-{
- ///
- /// Registers using its CLR type name.
- ///
- public static void AddTypeAlias(this WorkflowJsonOptions options) => options.RegisterTypeAlias(typeof(T), typeof(T).Name);
-
- ///
- /// Registers using the specified alias.
- ///
- public static void AddTypeAlias(this WorkflowJsonOptions options, string alias) => options.RegisterTypeAlias(typeof(T), alias);
-
- ///
- /// Registers the built-in workflow JSON aliases.
- ///
- public static void RegisterWorkflowTypeAliases(this WorkflowJsonOptions options)
- {
- options.RegisterTypeAlias(typeof(ExceptionState), nameof(ExceptionState));
- options.RegisterTypeAlias(typeof(FaultException), nameof(FaultException));
- options.RegisterTypeAlias(typeof(VariablesDictionary), nameof(VariablesDictionary));
- options.RegisterTypeAlias(typeof(Token), nameof(Token));
- options.RegisterTypeAlias(typeof(FlowJoinMode), "Elsa.Workflows.Core.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core");
- options.RegisterTypeAlias(typeof(FlowJoinMode), typeof(FlowJoinMode).GetSimpleAssemblyQualifiedName());
- options.RegisterTypeAlias(typeof(FlowJoinMode), nameof(FlowJoinMode));
- options.RegisterTypeAlias(typeof(WorkflowStorageDriver), typeof(WorkflowStorageDriver).GetSimpleAssemblyQualifiedName());
- options.RegisterTypeAlias(typeof(WorkflowStorageDriver), nameof(WorkflowStorageDriver));
- options.RegisterTypeAlias(typeof(WorkflowInstanceStorageDriver), typeof(WorkflowInstanceStorageDriver).GetSimpleAssemblyQualifiedName());
- options.RegisterTypeAlias(typeof(WorkflowInstanceStorageDriver), nameof(WorkflowInstanceStorageDriver));
- options.RegisterTypeAlias(typeof(MemoryStorageDriver), typeof(MemoryStorageDriver).GetSimpleAssemblyQualifiedName());
- options.RegisterTypeAlias(typeof(MemoryStorageDriver), nameof(MemoryStorageDriver));
- options.RegisterTypeAlias(typeof(FaultStrategy), typeof(FaultStrategy).GetSimpleAssemblyQualifiedName());
- options.RegisterTypeAlias(typeof(FaultStrategy), nameof(FaultStrategy));
- options.RegisterTypeAlias(typeof(ContinueWithIncidentsStrategy), typeof(ContinueWithIncidentsStrategy).GetSimpleAssemblyQualifiedName());
- options.RegisterTypeAlias(typeof(ContinueWithIncidentsStrategy), nameof(ContinueWithIncidentsStrategy));
- options.RegisterTypeAlias(typeof(Exception), nameof(Exception));
- options.RegisterTypeAlias(typeof(ArgumentException), nameof(ArgumentException));
- options.RegisterTypeAlias(typeof(ArgumentNullException), nameof(ArgumentNullException));
- options.RegisterTypeAlias(typeof(InvalidOperationException), nameof(InvalidOperationException));
- options.RegisterTypeAlias(typeof(NullReferenceException), nameof(NullReferenceException));
- options.RegisterTypeAlias(typeof(OperationCanceledException), nameof(OperationCanceledException));
- options.RegisterTypeAlias(typeof(TaskCanceledException), nameof(TaskCanceledException));
- options.RegisterTypeAlias(typeof(TimeoutException), nameof(TimeoutException));
- options.RegisterTypeAlias(typeof(NotSupportedException), nameof(NotSupportedException));
- options.RegisterTypeAlias(typeof(JObject), nameof(JObject));
- options.RegisterTypeAlias(typeof(JArray), nameof(JArray));
- }
-}
diff --git a/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs b/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
index 5414125df..8fb59634d 100644
--- a/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
@@ -4,8 +4,9 @@ using Elsa.Features.Services;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Activities.Flowchart.Options;
using Elsa.Workflows.Activities.Flowchart.Serialization;
-using Elsa.Workflows.Serialization.Options;
+using Elsa.Workflows.Options;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Common.Serialization;
namespace Elsa.Workflows.Features;
@@ -35,6 +36,6 @@ public class FlowchartFeature : FeatureBase
if (FlowchartOptionsConfigurator != null)
Services.Configure(FlowchartOptionsConfigurator);
- Services.Configure(options => options.AddTypeAlias("FlowScope"));
+ Services.Configure(options => options.AddTypeAlias("FlowScope"));
}
}
diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
index 005b87d09..89c17f04c 100644
--- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs
@@ -2,19 +2,21 @@ using Elsa.Common;
using Elsa.Common.Features;
using Elsa.Common.Serialization;
using Elsa.Expressions.Features;
-using Elsa.Workflows.Serialization.Options;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.ActivationValidators;
+using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Builders;
using Elsa.Workflows.CommitStates;
+using Elsa.Workflows.Exceptions;
using Elsa.Workflows.IncidentStrategies;
using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.LogPersistence.Strategies;
using Elsa.Workflows.Middleware.Activities;
using Elsa.Workflows.Middleware.Workflows;
+using Elsa.Workflows.Options;
using Elsa.Workflows.Pipelines.ActivityExecution;
using Elsa.Workflows.Pipelines.WorkflowExecution;
using Elsa.Workflows.PortResolvers;
@@ -22,12 +24,14 @@ using Elsa.Workflows.Serialization.Configurators;
using Elsa.Workflows.Serialization.Helpers;
using Elsa.Workflows.Serialization.Serializers;
using Elsa.Workflows.Services;
+using Elsa.Workflows.State;
using Elsa.Workflows.UIHints.CheckList;
using Elsa.Workflows.UIHints.Dictionary;
using Elsa.Workflows.UIHints.Dropdown;
using Elsa.Workflows.UIHints.JsonEditor;
using Elsa.Workflows.UIHints.RadioList;
using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json.Linq;
namespace Elsa.Workflows.Features;
@@ -165,7 +169,31 @@ public class WorkflowsFeature : FeatureBase
private void AddElsaCore(IServiceCollection services)
{
- services.Configure(options => options.RegisterWorkflowTypeAliases());
+ services.Configure(options =>
+ {
+ options.AddTypeAlias(nameof(ExceptionState));
+ options.AddTypeAlias(nameof(FaultException));
+ options.AddTypeAlias(nameof(VariablesDictionary));
+ options.AddTypeAlias(nameof(Token));
+ options.RegisterLegacyTypeName(typeof(FlowJoinMode), "Elsa.Workflows.Core.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core");
+ options.AddTypeAliasWithLegacyName(nameof(FlowJoinMode));
+ options.AddTypeAliasWithLegacyName(nameof(WorkflowStorageDriver));
+ options.AddTypeAliasWithLegacyName(nameof(WorkflowInstanceStorageDriver));
+ options.AddTypeAliasWithLegacyName(nameof(MemoryStorageDriver));
+ options.AddTypeAliasWithLegacyName(nameof(FaultStrategy));
+ options.AddTypeAliasWithLegacyName(nameof(ContinueWithIncidentsStrategy));
+ options.AddTypeAlias(nameof(Exception));
+ options.AddTypeAlias(nameof(ArgumentException));
+ options.AddTypeAlias(nameof(ArgumentNullException));
+ options.AddTypeAlias(nameof(InvalidOperationException));
+ options.AddTypeAlias(nameof(NullReferenceException));
+ options.AddTypeAlias(nameof(OperationCanceledException));
+ options.AddTypeAlias(nameof(TaskCanceledException));
+ options.AddTypeAlias(nameof(TimeoutException));
+ options.AddTypeAlias(nameof(NotSupportedException));
+ options.AddTypeAlias(nameof(JObject));
+ options.AddTypeAlias(nameof(JArray));
+ });
services
@@ -237,6 +265,7 @@ public class WorkflowsFeature : FeatureBase
.AddStorageDriver()
// Serialization.
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/OutputJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/OutputJsonConverter.cs
index 888c732e9..af9f90393 100644
--- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/OutputJsonConverter.cs
+++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/OutputJsonConverter.cs
@@ -1,10 +1,9 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using Elsa.Extensions;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
-using Elsa.Workflows.Serialization.Helpers;
-using Elsa.Workflows.Serialization.Options;
-using Microsoft.Extensions.Options;
+using Elsa.Common.Serialization;
namespace Elsa.Workflows.Serialization.Converters;
@@ -13,12 +12,12 @@ namespace Elsa.Workflows.Serialization.Converters;
///
public class OutputJsonConverter : JsonConverter