Refactor: Overhauls workflow JSON type serialization (#7549)
* Avoid null endpoint DTO metadata in tests * Enforce console logs hub read permission * Remove unused console logs hub import * Support mapped endpoint metadata in auth tests * Reduce console log capture throughput impact * Address Copilot console logs review * Refactor task scheduling to support tenant-level background work and enhance logging functionality. * Introduce ConsoleStreamHook for stdout/stderr tee and enhance logging validation. Adjust test cases and startup warnings for distributed lock provider usage. * Refactor console logging pipeline with capture optimization and new ConsoleLogsHost; update tests accordingly. * Add Ansi SGR parser for console logs and associated unit tests * Remove ANSI color renderings and parsers; integrate ConsoleLogScopeAccessor for improved logging context with workflow instance ID support. * Address console logs code quality feedback * Address PR review feedback * Preserve console logs extension points * Stabilize console logs host lifecycle * Address final automated review comments * Tighten console log capture shutdown * Address console log review feedback * Address follow-up review feedback * Cover final review feedback * Avoid recursive console provider initialization * Guard console host lease shutdown * Preserve console log scope and provider lifetime * Correlate console log scope fallback * Tighten console scope correlation * Expose host services during provider construction * Redact ANSI-normalized console lines * Remove `ConsoleCaptureTee` and related services and tests * Use pipeline contributors for console log context * Update CShells package versions to 0.0.24-preview.132 * Filter live console logs by workflow instance * Enhance console logging with activity execution metadata and extend test coverage. * Address console logs stream consumption comment * Add diagnostics OpenTelemetry backend * Introduce dedicated workflow JSON type registry and hardening This change addresses GitHub issue #7541 by establishing a separate type registry (`IWorkflowJsonTypeRegistry`) for workflow JSON serialization. This decouples workflow type resolution from expression type aliases, enforcing a strict trust boundary. Key aspects: - New workflow JSON emits preferred aliases for registered types. - Existing persisted workflows can be loaded via registered legacy names. - Unknown, abstract, interface, open generic, or inappropriate collection types are rejected during deserialization, enhancing security. - Public APIs (e.g., incident strategies) now expose consistent workflow JSON type identifiers. This ensures secure, predictable, and backward-compatible handling of types within workflow definitions and payloads. * Remove unused project references and streamline console log endpoint * Move serialization type aliases to Elsa.Common * Update serialization integration fixtures for aliases * Stabilize missing rate limiter policy test
This commit is contained in:
parent
b280a711b2
commit
c2fb027c41
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"feature_directory": "specs/008-diagnostics-otel"
|
||||
"feature_directory": "specs/010-workflow-json-hardening"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ Before handing off changes, verify the following when applicable:
|
|||
|
||||
<!-- SPECKIT START -->
|
||||
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`.
|
||||
<!-- SPECKIT END -->
|
||||
|
||||
## Active Technologies
|
||||
|
|
|
|||
86
Elsa.sln
86
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}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
34
specs/010-workflow-json-hardening/checklists/requirements.md
Normal file
34
specs/010-workflow-json-hardening/checklists/requirements.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
20
specs/010-workflow-json-hardening/data-model.md
Normal file
20
specs/010-workflow-json-hardening/data-model.md
Normal file
|
|
@ -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.
|
||||
88
specs/010-workflow-json-hardening/plan.md
Normal file
88
specs/010-workflow-json-hardening/plan.md
Normal file
|
|
@ -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
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the content in this section with the technical details
|
||||
for the project. The structure here is presented in advisory capacity to guide
|
||||
the iteration process.
|
||||
-->
|
||||
|
||||
**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)
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
|
||||
for this feature. Delete unused options and expand the chosen structure with
|
||||
real paths (e.g., apps/admin, packages/something). The delivered plan must
|
||||
not include Option labels.
|
||||
-->
|
||||
|
||||
```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] |
|
||||
11
specs/010-workflow-json-hardening/quickstart.md
Normal file
11
specs/010-workflow-json-hardening/quickstart.md
Normal file
|
|
@ -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
|
||||
```
|
||||
25
specs/010-workflow-json-hardening/research.md
Normal file
25
specs/010-workflow-json-hardening/research.md
Normal file
|
|
@ -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.
|
||||
103
specs/010-workflow-json-hardening/spec.md
Normal file
103
specs/010-workflow-json-hardening/spec.md
Normal file
|
|
@ -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.
|
||||
125
specs/010-workflow-json-hardening/tasks.md
Normal file
125
specs/010-workflow-json-hardening/tasks.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ namespace Elsa.Api.Client.Resources.IncidentStrategies.Models;
|
|||
/// <summary>
|
||||
/// Represents an incident strategy.
|
||||
/// </summary>
|
||||
/// <param name="TypeName">The .NET type name of the strategy.</param>
|
||||
/// <param name="TypeName">The workflow JSON type identifier of the strategy.</param>
|
||||
/// <param name="DisplayName">The display name of the strategy.</param>
|
||||
/// <param name="Description">The description of the strategy.</param>
|
||||
public record IncidentStrategyDescriptor(string TypeName, string DisplayName, string? Description);
|
||||
public record IncidentStrategyDescriptor(string TypeName, string DisplayName, string? Description);
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.Configure<WorkflowJsonOptions>(options =>
|
||||
Services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.RegisterTypeAlias(typeof(AlterationPlanParams), typeof(AlterationPlanParams).GetSimpleAssemblyQualifiedName());
|
||||
options.RegisterTypeAlias(typeof(AlterationPlanParams), nameof(AlterationPlanParams));
|
||||
options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AlterationPlanParams));
|
||||
});
|
||||
|
||||
Services.AddScoped<IAlterationPlanManager, AlterationPlanManager>();
|
||||
|
|
|
|||
|
|
@ -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<WorkflowJsonOptions>(options =>
|
||||
services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.RegisterTypeAlias(typeof(AlterationPlanParams), typeof(AlterationPlanParams).GetSimpleAssemblyQualifiedName());
|
||||
options.RegisterTypeAlias(typeof(AlterationPlanParams), nameof(AlterationPlanParams));
|
||||
options.RegisterLegacySimpleAssemblyQualifiedName(typeof(AlterationPlanParams));
|
||||
});
|
||||
|
||||
services.AddScoped<IAlterationPlanManager, AlterationPlanManager>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
using Elsa.Common.Serialization;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extends <see cref="SerializationTypeOptions"/>.
|
||||
/// </summary>
|
||||
public static class SerializationTypeOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAlias<T>(this SerializationTypeOptions options, string alias) => options.RegisterTypeAlias(typeof(T), alias);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the type name as the preferred serialization alias.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAlias<T>(this SerializationTypeOptions options) => options.RegisterTypeAlias(typeof(T), typeof(T).Name);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias and the current simple assembly-qualified name as a legacy identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAliasWithLegacyName(this SerializationTypeOptions options, Type type, string alias)
|
||||
{
|
||||
options.RegisterTypeAlias(type, alias);
|
||||
options.RegisterLegacySimpleAssemblyQualifiedName(type);
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias and the current simple assembly-qualified name as a legacy identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddTypeAliasWithLegacyName<T>(this SerializationTypeOptions options, string alias) => options.AddTypeAliasWithLegacyName(typeof(T), alias);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the current simple assembly-qualified name as a compatibility identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddLegacySimpleAssemblyQualifiedName<T>(this SerializationTypeOptions options) => options.RegisterLegacySimpleAssemblyQualifiedName(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Registers the current simple assembly-qualified name as a compatibility identifier.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddLegacySimpleAssemblyQualifiedName(this SerializationTypeOptions options, Type type) => options.RegisterLegacySimpleAssemblyQualifiedName(type);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the current simple assembly-qualified name as the preferred alias for compatibility-only types.
|
||||
/// </summary>
|
||||
public static SerializationTypeOptions AddSimpleAssemblyQualifiedTypeAlias(this SerializationTypeOptions options, Type type)
|
||||
{
|
||||
return options.RegisterTypeAlias(type, type.GetSimpleAssemblyQualifiedName());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
|
|
@ -6,6 +8,17 @@ namespace Elsa.Extensions;
|
|||
/// </summary>
|
||||
public static class TypeExtensions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, string> SimpleAssemblyQualifiedTypeNameCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly-qualified name of the type, without version, culture, and public key token information.
|
||||
/// </summary>
|
||||
public static string GetSimpleAssemblyQualifiedName(this Type type)
|
||||
{
|
||||
if (type is null) throw new ArgumentNullException(nameof(type));
|
||||
return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, BuildSimplifiedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true of the type is generic, false otherwise.
|
||||
/// </summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
namespace Elsa.Common.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// A registry of types that may be resolved from serialization type identifiers.
|
||||
/// </summary>
|
||||
public interface ISerializationTypeRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a type with an alias.
|
||||
/// </summary>
|
||||
void RegisterType(Type type, string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the preferred alias for the specified type.
|
||||
/// </summary>
|
||||
bool TryGetAlias(Type type, out string alias);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the type associated with the specified alias or legacy name.
|
||||
/// </summary>
|
||||
bool TryGetType(string alias, out Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all registered types.
|
||||
/// </summary>
|
||||
IEnumerable<Type> ListTypes();
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Options for serialization type identifiers.
|
||||
/// </summary>
|
||||
public class SerializationTypeOptions
|
||||
{
|
||||
private readonly IDictionary<string, Type> _aliasTypeDictionary = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly IDictionary<Type, string> _typeAliasDictionary = new Dictionary<Type, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SerializationTypeOptions"/> class.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions()
|
||||
{
|
||||
AliasTypeDictionary = new ReadOnlyDictionary<string, Type>(_aliasTypeDictionary);
|
||||
TypeAliasDictionary = new ReadOnlyDictionary<Type, string>(_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<string, string>), "StringDictionary");
|
||||
RegisterTypeAlias(typeof(IDictionary<string, object>), "ObjectDictionary");
|
||||
RegisterTypeAlias(typeof(Dictionary<string, string>), "StringMap");
|
||||
RegisterTypeAlias(typeof(Dictionary<string, object>), "ObjectMap");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets aliases and legacy names keyed by identifier.
|
||||
/// </summary>
|
||||
public IDictionary<string, Type> AliasTypeDictionary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets preferred aliases keyed by type.
|
||||
/// </summary>
|
||||
public IDictionary<Type, string> TypeAliasDictionary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a preferred serialization alias.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions RegisterTypeAlias(Type type, string alias)
|
||||
{
|
||||
_aliasTypeDictionary[alias] = type;
|
||||
_typeAliasDictionary[type] = alias;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a legacy serialization identifier for compatibility reads.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions RegisterLegacyTypeName(Type type, string typeName)
|
||||
{
|
||||
_aliasTypeDictionary[typeName] = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the type's simple assembly-qualified name as a legacy serialization identifier.
|
||||
/// </summary>
|
||||
public SerializationTypeOptions RegisterLegacySimpleAssemblyQualifiedName(Type type) => RegisterLegacyTypeName(type, type.GetSimpleAssemblyQualifiedName());
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Common.Serialization;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SerializationTypeRegistry : ISerializationTypeRegistry
|
||||
{
|
||||
private readonly Dictionary<string, Type> _aliasTypeDictionary = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<Type, string> _typeAliasDictionary = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default registry.
|
||||
/// </summary>
|
||||
public static ISerializationTypeRegistry CreateDefault()
|
||||
{
|
||||
return new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(new SerializationTypeOptions()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SerializationTypeRegistry"/> class.
|
||||
/// </summary>
|
||||
public SerializationTypeRegistry(IOptions<SerializationTypeOptions> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterType(Type type, string alias)
|
||||
{
|
||||
_typeAliasDictionary[type] = alias;
|
||||
RegisterTypeName(type, alias);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetAlias(Type type, out string alias) => _typeAliasDictionary.TryGetValue(type, out alias!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetType(string alias, out Type type) => _aliasTypeDictionary.TryGetValue(alias, out type!);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Type> 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}?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves workflow JSON type aliases and CLR type names.
|
||||
/// Resolves serialization type aliases without loading arbitrary CLR type names.
|
||||
/// </summary>
|
||||
public static class WorkflowJsonTypeResolver
|
||||
public static class SerializationTypeResolver
|
||||
{
|
||||
private static readonly IDictionary<string, Type> GenericCollectionTypes = new Dictionary<string, Type>(StringComparer.Ordinal)
|
||||
{
|
||||
|
|
@ -52,47 +51,85 @@ public static class WorkflowJsonTypeResolver
|
|||
[typeof(IDictionary)] = typeof(Dictionary<string, object>)
|
||||
};
|
||||
|
||||
public static Type ResolveType(WorkflowJsonOptions workflowJsonOptions, string? typeAlias, bool allowLegacyClrTypeNames = true)
|
||||
/// <summary>
|
||||
/// Resolves the specified serialization type alias.
|
||||
/// </summary>
|
||||
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)
|
||||
/// <summary>
|
||||
/// Attempts to resolve the specified serialization type alias.
|
||||
/// </summary>
|
||||
public static bool TryResolveType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, out Type type)
|
||||
{
|
||||
IReadOnlyList<Type>? 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<Type>? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to return a serialization type alias that this resolver can read back.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to map a supported collection interface type to an instantiable concrete type.
|
||||
/// </summary>
|
||||
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<Type>? 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<Type>? registeredTypes, out Type type)
|
||||
private static bool TryResolveArrayType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? 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<Type>? registeredTypes, out Type type)
|
||||
private static bool TryResolveGenericCollectionType(ISerializationTypeRegistry serializationTypeRegistry, string typeAlias, ref IReadOnlyList<Type>? 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<Type>? 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<Type>? 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<Type>? registeredTypes, out Type type)
|
||||
private static IReadOnlyList<Type> GetRegisteredTypes(ISerializationTypeRegistry serializationTypeRegistry)
|
||||
{
|
||||
return serializationTypeRegistry.ListTypes().ToArray();
|
||||
}
|
||||
|
||||
private static bool TryResolveRegisteredSimpleAssemblyQualifiedName(IEnumerable<Type> 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<Type>? 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
|
||||
|
|
@ -5,21 +5,21 @@ namespace Elsa.Diagnostics.ConsoleLogs.Contracts;
|
|||
/// <summary>
|
||||
/// Console log filter accepted by Elsa REST and SignalR endpoints.
|
||||
/// </summary>
|
||||
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<string, string>? 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<string, string>? Metadata { get; init; }
|
||||
public DateTimeOffset? From { get; init; }
|
||||
public DateTimeOffset? To { get; init; }
|
||||
public int? Limit { get; init; }
|
||||
}
|
||||
|
|
@ -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<IReadOnlyCollection<ConsoleLogSource>> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await provider.ListSourcesAsync(cancellationToken);
|
||||
var sources = await provider.ListSourcesAsync(cancellationToken);
|
||||
return sources.ToList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ExpressionOptions>(JavaScriptExceptionTypeAliasRegistrar.Register);
|
||||
Services.Configure<SerializationTypeOptions>(JavaScriptExceptionTypeAliasRegistrar.Register);
|
||||
|
||||
// JavaScript services.
|
||||
Services
|
||||
|
|
@ -72,7 +77,7 @@ public class JavaScriptFeature : FeatureBase
|
|||
.AddScoped<ITypeDefinitionService, TypeDefinitionService>()
|
||||
.AddScoped<ITypeDescriber, TypeDescriber>()
|
||||
.AddScoped<ITypeDefinitionDocumentRenderer, TypeDefinitionDocumentRenderer>()
|
||||
.AddSingleton<ITypeAliasRegistry, TypeAliasRegistry>()
|
||||
.AddSingleton<ITypeAliasRegistry, Services.TypeAliasRegistry>()
|
||||
.AddFunctionDefinitionProvider<CommonFunctionsDefinitionProvider>()
|
||||
.AddFunctionDefinitionProvider<ActivityOutputFunctionsDefinitionProvider>()
|
||||
.AddFunctionDefinitionProvider<RunJavaScriptFunctionsDefinitionProvider>()
|
||||
|
|
@ -94,4 +99,4 @@ public class JavaScriptFeature : FeatureBase
|
|||
// UI property handlers.
|
||||
Services.AddScoped<IPropertyUIHandler, RunJavaScriptOptionsProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ExpressionOptions>(JavaScriptExceptionTypeAliasRegistrar.Register);
|
||||
services.Configure<SerializationTypeOptions>(JavaScriptExceptionTypeAliasRegistrar.Register);
|
||||
|
||||
// JavaScript services.
|
||||
services
|
||||
|
|
@ -115,7 +120,7 @@ public class JavaScriptFeature : IShellFeature
|
|||
.AddScoped<ITypeDefinitionService, TypeDefinitionService>()
|
||||
.AddScoped<ITypeDescriber, TypeDescriber>()
|
||||
.AddScoped<ITypeDefinitionDocumentRenderer, TypeDefinitionDocumentRenderer>()
|
||||
.AddSingleton<ITypeAliasRegistry, TypeAliasRegistry>()
|
||||
.AddSingleton<ITypeAliasRegistry, Services.TypeAliasRegistry>()
|
||||
.AddFunctionDefinitionProvider<CommonFunctionsDefinitionProvider>()
|
||||
.AddFunctionDefinitionProvider<ActivityOutputFunctionsDefinitionProvider>()
|
||||
.AddFunctionDefinitionProvider<RunJavaScriptFunctionsDefinitionProvider>()
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
/// </summary>
|
||||
public static class TypeExtensions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, string> SimpleAssemblyQualifiedTypeNameCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly-qualified name of the type, without any version info etc.
|
||||
/// E.g. "System.String, System.Private.CoreLib"
|
||||
/// </summary>
|
||||
public static string GetSimpleAssemblyQualifiedName(this Type type)
|
||||
{
|
||||
if (type is null) throw new ArgumentNullException(nameof(type));
|
||||
return SimpleAssemblyQualifiedTypeNameCache.GetOrAdd(type, GetSimplifiedName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the default value for the specified type.
|
||||
/// </summary>
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkflowJsonOptions>(options =>
|
||||
Services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.AddTypeAlias<HttpRequest>("HttpRequest");
|
||||
options.AddTypeAlias<HttpResponse>("HttpResponse");
|
||||
options.AddTypeAlias<HttpResponseMessage>("HttpResponseMessage");
|
||||
options.AddTypeAlias<HttpHeaders>("HttpHeaders");
|
||||
options.AddTypeAlias<HttpRouteData>("RouteData");
|
||||
options.AddTypeAlias<IFormFile>("FormFile");
|
||||
options.AddTypeAlias<IFormFile[]>("FormFile[]");
|
||||
options.AddTypeAlias<HttpFile>("HttpFile");
|
||||
options.AddTypeAlias<HttpFile[]>("HttpFile[]");
|
||||
options.AddTypeAlias<Downloadable>("Downloadable");
|
||||
options.AddTypeAlias<Downloadable[]>("Downloadable[]");
|
||||
options.AddTypeAlias<HttpStatusCode>();
|
||||
options.AddTypeAlias<HttpRequestException>();
|
||||
options.AddTypeAlias<HttpEndpointBookmarkPayload>();
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkflowJsonOptions>(options =>
|
||||
services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.AddTypeAlias<HttpRequest>("HttpRequest");
|
||||
options.AddTypeAlias<HttpResponse>("HttpResponse");
|
||||
options.AddTypeAlias<HttpResponseMessage>("HttpResponseMessage");
|
||||
options.AddTypeAlias<HttpHeaders>("HttpHeaders");
|
||||
options.AddTypeAlias<HttpRouteData>("RouteData");
|
||||
options.AddTypeAlias<IFormFile>("FormFile");
|
||||
options.AddTypeAlias<IFormFile[]>("FormFile[]");
|
||||
options.AddTypeAlias<HttpFile>("HttpFile");
|
||||
options.AddTypeAlias<HttpFile[]>("HttpFile[]");
|
||||
options.AddTypeAlias<Downloadable>("Downloadable");
|
||||
options.AddTypeAlias<Downloadable[]>("Downloadable[]");
|
||||
options.AddTypeAlias<HttpStatusCode>();
|
||||
options.AddTypeAlias<HttpRequestException>();
|
||||
options.AddTypeAlias<HttpEndpointBookmarkPayload>();
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<WorkflowDefinition, EFCoreWorkflowDefinitionStore>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<RetryAttemptRecord>>("RetryAttemptRecordList");
|
||||
});
|
||||
|
||||
Services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.RegisterTypeAlias(typeof(List<RetryAttemptRecord>), "RetryAttemptRecordList");
|
||||
});
|
||||
}
|
||||
|
||||
public override void Apply()
|
||||
|
|
|
|||
|
|
@ -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<List<RetryAttemptRecord>>("RetryAttemptRecordList");
|
||||
});
|
||||
|
||||
services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.RegisterTypeAlias(typeof(List<RetryAttemptRecord>), "RetryAttemptRecordList");
|
||||
});
|
||||
|
||||
services.AddOptions<ResilienceOptions>();
|
||||
services.AddOptions<SimulateResponseOptions>();
|
||||
|
|
|
|||
|
|
@ -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<Elsa.Workflows.Runtime.IIngressSource, Elsa.Scheduling.IngressSources.ScheduledTriggerIngressSource>();
|
||||
|
||||
Services.Configure<WorkflowJsonOptions>(options =>
|
||||
Services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.AddTypeAlias<CronBookmarkPayload>();
|
||||
options.AddTypeAlias<CronTriggerPayload>();
|
||||
options.AddTypeAlias<DelayPayload>();
|
||||
options.AddTypeAlias<StartAtPayload>();
|
||||
options.AddTypeAlias<TimerBookmarkPayload>();
|
||||
options.AddTypeAlias<TimerTriggerPayload>();
|
||||
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<WorkflowManagementFeature>(management => management.AddActivitiesFrom<SchedulingFeature>());
|
||||
|
|
|
|||
|
|
@ -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<CronTriggerPayloadValidator, CronTriggerPayload>()
|
||||
.AddActivitiesFrom<SchedulingFeature>();
|
||||
|
||||
services.Configure<WorkflowJsonOptions>(options =>
|
||||
services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.AddTypeAlias<CronBookmarkPayload>();
|
||||
options.AddTypeAlias<CronTriggerPayload>();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Elsa.Workflows.IntegrationTests")]
|
||||
[assembly: InternalsVisibleTo("Elsa.Workflows.IntegrationTests")]
|
||||
[assembly: InternalsVisibleTo("Elsa.Workflows.Api.UnitTests")]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Returns list of available <see cref="IIncidentStrategy" /> implementations.
|
||||
/// </summary>
|
||||
internal class List(IEnumerable<IIncidentStrategy> strategies) : ElsaEndpointWithoutRequest<ListResponse<IncidentStrategyDescriptor>>
|
||||
internal class List(IEnumerable<IIncidentStrategy> strategies, ISerializationTypeRegistry workflowJsonTypeRegistry) : ElsaEndpointWithoutRequest<ListResponse<IncidentStrategyDescriptor>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
|
|
@ -21,15 +22,15 @@ internal class List(IEnumerable<IIncidentStrategy> strategies) : ElsaEndpointWit
|
|||
|
||||
public override Task<ListResponse<IncidentStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var descriptors = strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
|
||||
var response =new ListResponse<IncidentStrategyDescriptor>(descriptors);
|
||||
var descriptors = strategies.Select(x => IncidentStrategyDescriptor.FromStrategy(x, workflowJsonTypeRegistry)).OrderBy(x => x.DisplayName).ToList();
|
||||
var response = new ListResponse<IncidentStrategyDescriptor>(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<DisplayNameAttribute>();
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <see cref="Activities.Flowchart"/>.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, IOptions<WorkflowJsonOptions> workflowJsonOptions, ILoggerFactory loggerFactory) : JsonConverter<Activities.Flowchart>
|
||||
public class FlowchartJsonConverter(IIdentityGenerator identityGenerator, ISerializationTypeRegistry workflowJsonTypeRegistry, ILoggerFactory loggerFactory) : JsonConverter<Activities.Flowchart>
|
||||
{
|
||||
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<ICollection<Variable>>(options) ?? new List<Variable>() : new List<Variable>();
|
||||
|
||||
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<IDictionary<string, object>>(polymorphicOptions) ?? new Dictionary<string, object>() : new Dictionary<string, object>();
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Extends <see cref="WorkflowJsonOptions"/>.
|
||||
/// </summary>
|
||||
public static class WorkflowJsonOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers <typeparamref name="T"/> using its CLR type name.
|
||||
/// </summary>
|
||||
public static void AddTypeAlias<T>(this WorkflowJsonOptions options) => options.RegisterTypeAlias(typeof(T), typeof(T).Name);
|
||||
|
||||
/// <summary>
|
||||
/// Registers <typeparamref name="T"/> using the specified alias.
|
||||
/// </summary>
|
||||
public static void AddTypeAlias<T>(this WorkflowJsonOptions options, string alias) => options.RegisterTypeAlias(typeof(T), alias);
|
||||
|
||||
/// <summary>
|
||||
/// Registers the built-in workflow JSON aliases.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WorkflowJsonOptions>(options => options.AddTypeAlias<FlowScope>("FlowScope"));
|
||||
Services.Configure<SerializationTypeOptions>(options => options.AddTypeAlias<FlowScope>("FlowScope"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkflowJsonOptions>(options => options.RegisterWorkflowTypeAliases());
|
||||
services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.AddTypeAlias<ExceptionState>(nameof(ExceptionState));
|
||||
options.AddTypeAlias<FaultException>(nameof(FaultException));
|
||||
options.AddTypeAlias<VariablesDictionary>(nameof(VariablesDictionary));
|
||||
options.AddTypeAlias<Token>(nameof(Token));
|
||||
options.RegisterLegacyTypeName(typeof(FlowJoinMode), "Elsa.Workflows.Core.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core");
|
||||
options.AddTypeAliasWithLegacyName<FlowJoinMode>(nameof(FlowJoinMode));
|
||||
options.AddTypeAliasWithLegacyName<WorkflowStorageDriver>(nameof(WorkflowStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<WorkflowInstanceStorageDriver>(nameof(WorkflowInstanceStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<MemoryStorageDriver>(nameof(MemoryStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<FaultStrategy>(nameof(FaultStrategy));
|
||||
options.AddTypeAliasWithLegacyName<ContinueWithIncidentsStrategy>(nameof(ContinueWithIncidentsStrategy));
|
||||
options.AddTypeAlias<Exception>(nameof(Exception));
|
||||
options.AddTypeAlias<ArgumentException>(nameof(ArgumentException));
|
||||
options.AddTypeAlias<ArgumentNullException>(nameof(ArgumentNullException));
|
||||
options.AddTypeAlias<InvalidOperationException>(nameof(InvalidOperationException));
|
||||
options.AddTypeAlias<NullReferenceException>(nameof(NullReferenceException));
|
||||
options.AddTypeAlias<OperationCanceledException>(nameof(OperationCanceledException));
|
||||
options.AddTypeAlias<TaskCanceledException>(nameof(TaskCanceledException));
|
||||
options.AddTypeAlias<TimeoutException>(nameof(TimeoutException));
|
||||
options.AddTypeAlias<NotSupportedException>(nameof(NotSupportedException));
|
||||
options.AddTypeAlias<JObject>(nameof(JObject));
|
||||
options.AddTypeAlias<JArray>(nameof(JArray));
|
||||
});
|
||||
|
||||
services
|
||||
|
||||
|
|
@ -237,6 +265,7 @@ public class WorkflowsFeature : FeatureBase
|
|||
.AddStorageDriver<MemoryStorageDriver>()
|
||||
|
||||
// Serialization.
|
||||
.AddSingleton<ISerializationTypeRegistry, SerializationTypeRegistry>()
|
||||
.AddSingleton<IWorkflowStateSerializer, JsonWorkflowStateSerializer>()
|
||||
.AddSingleton<IPayloadSerializer, JsonPayloadSerializer>()
|
||||
.AddSingleton<IActivitySerializer, JsonActivitySerializer>()
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
/// </summary>
|
||||
public class OutputJsonConverter<T> : JsonConverter<Output<T>?>
|
||||
{
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions;
|
||||
private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
|
||||
|
||||
/// <inheritdoc />
|
||||
public OutputJsonConverter(IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
public OutputJsonConverter(ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
_workflowJsonOptions = workflowJsonOptions.Value;
|
||||
_workflowJsonTypeRegistry = workflowJsonTypeRegistry;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -51,7 +50,9 @@ public class OutputJsonConverter<T> : JsonConverter<Output<T>?>
|
|||
public override void Write(Utf8JsonWriter writer, Output<T>? value, JsonSerializerOptions options)
|
||||
{
|
||||
var valueType = typeof(T);
|
||||
var valueTypeAlias = WorkflowJsonTypeResolver.GetAliasOrLegacyClrTypeName(_workflowJsonOptions, valueType);
|
||||
var valueTypeAlias = SerializationTypeResolver.TryGetAlias(_workflowJsonTypeRegistry, valueType, out var alias)
|
||||
? alias
|
||||
: valueType.GetSimpleAssemblyQualifiedName();
|
||||
|
||||
var model = new
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Converters;
|
||||
|
||||
|
|
@ -12,9 +12,9 @@ public class PolymorphicDictionaryConverter : JsonConverter<IDictionary<string,
|
|||
private readonly JsonConverter<object> _objectConverter;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PolymorphicDictionaryConverter(JsonSerializerOptions options, WorkflowJsonOptions workflowJsonOptions)
|
||||
public PolymorphicDictionaryConverter(JsonSerializerOptions options, ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
var factory = (JsonConverterFactory)(options.Converters.FirstOrDefault(x => x is PolymorphicObjectConverterFactory) ?? new PolymorphicObjectConverterFactory(workflowJsonOptions));
|
||||
var factory = (JsonConverterFactory)(options.Converters.FirstOrDefault(x => x is PolymorphicObjectConverterFactory) ?? new PolymorphicObjectConverterFactory(workflowJsonTypeRegistry));
|
||||
_objectConverter = (JsonConverter<object>)factory.CreateConverter(typeof(object), options)!;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ using System.Text.Json;
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Serialization.Helpers;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Serialization.ReferenceHandlers;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Converters;
|
||||
|
||||
|
|
@ -24,30 +22,22 @@ public class PolymorphicObjectConverter : JsonConverter<object>
|
|||
private const string IdPropertyName = "$id";
|
||||
private const string RefPropertyName = "$ref";
|
||||
private const string ValuesPropertyName = "$values";
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions;
|
||||
private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolymorphicObjectConverter"/> class.
|
||||
/// </summary>
|
||||
public PolymorphicObjectConverter(IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
: this(workflowJsonOptions.Value)
|
||||
public PolymorphicObjectConverter(ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolymorphicObjectConverter"/> class.
|
||||
/// </summary>
|
||||
public PolymorphicObjectConverter(WorkflowJsonOptions workflowJsonOptions)
|
||||
{
|
||||
_workflowJsonOptions = workflowJsonOptions;
|
||||
_workflowJsonTypeRegistry = workflowJsonTypeRegistry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolymorphicObjectConverter"/> class.
|
||||
/// </summary>
|
||||
public PolymorphicObjectConverter()
|
||||
: this(new WorkflowJsonOptions())
|
||||
{
|
||||
_workflowJsonTypeRegistry = SerializationTypeRegistry.CreateDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -358,12 +348,14 @@ public class PolymorphicObjectConverter : JsonConverter<object>
|
|||
}
|
||||
|
||||
// If we found the _type property, attempt to resolve the type.
|
||||
return typeName != null ? WorkflowJsonTypeResolver.ResolveType(_workflowJsonOptions, typeName, _workflowJsonOptions.AllowLegacyClrTypeNames) : default;
|
||||
return typeName != null ? SerializationTypeResolver.ResolveType(_workflowJsonTypeRegistry, typeName) : default;
|
||||
}
|
||||
|
||||
private void WriteTypeMetadata(Utf8JsonWriter writer, Type type)
|
||||
{
|
||||
var typeAlias = WorkflowJsonTypeResolver.GetAliasOrLegacyClrTypeName(_workflowJsonOptions, type);
|
||||
if (!SerializationTypeResolver.TryGetAlias(_workflowJsonTypeRegistry, type, out var typeAlias))
|
||||
return;
|
||||
|
||||
writer.WritePropertyName(TypePropertyName);
|
||||
writer.WriteStringValue(typeAlias);
|
||||
}
|
||||
|
|
@ -376,7 +368,7 @@ public class PolymorphicObjectConverter : JsonConverter<object>
|
|||
if (!targetType.IsInterface && !targetType.IsAbstract)
|
||||
return targetType;
|
||||
|
||||
if (WorkflowJsonTypeResolver.TryGetInstantiableCollectionType(targetType, out var instantiableCollectionType))
|
||||
if (SerializationTypeResolver.TryGetInstantiableCollectionType(targetType, out var instantiableCollectionType))
|
||||
return instantiableCollectionType;
|
||||
|
||||
throw new JsonException($"Workflow JSON type alias resolved to non-instantiable type '{targetType}'.");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Converters;
|
||||
|
||||
|
|
@ -11,30 +10,22 @@ namespace Elsa.Workflows.Serialization.Converters;
|
|||
/// </summary>
|
||||
public class PolymorphicObjectConverterFactory : JsonConverterFactory
|
||||
{
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions;
|
||||
private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
|
||||
|
||||
/// <summary>
|
||||
/// A JSON converter factory that creates <see cref="PolymorphicObjectConverter"/> instances.
|
||||
/// </summary>
|
||||
public PolymorphicObjectConverterFactory(IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
: this(workflowJsonOptions.Value)
|
||||
public PolymorphicObjectConverterFactory(ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A JSON converter factory that creates <see cref="PolymorphicObjectConverter"/> instances.
|
||||
/// </summary>
|
||||
public PolymorphicObjectConverterFactory(WorkflowJsonOptions workflowJsonOptions)
|
||||
{
|
||||
_workflowJsonOptions = workflowJsonOptions;
|
||||
_workflowJsonTypeRegistry = workflowJsonTypeRegistry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor for use with attributes.
|
||||
/// </summary>
|
||||
public PolymorphicObjectConverterFactory()
|
||||
: this(new WorkflowJsonOptions())
|
||||
{
|
||||
_workflowJsonTypeRegistry = SerializationTypeRegistry.CreateDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -57,8 +48,8 @@ public class PolymorphicObjectConverterFactory : JsonConverterFactory
|
|||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (typeof(IDictionary<string, object>).IsAssignableFrom(typeToConvert))
|
||||
return new PolymorphicDictionaryConverter(options, _workflowJsonOptions);
|
||||
return new PolymorphicDictionaryConverter(options, _workflowJsonTypeRegistry);
|
||||
|
||||
return new PolymorphicObjectConverter(_workflowJsonOptions);
|
||||
return new PolymorphicObjectConverter(_workflowJsonTypeRegistry);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,34 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Serialization.Helpers;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Extensions;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes <see cref="Type"/> objects to a simple alias representing the type.
|
||||
/// Unregistered types are written as metadata-only aliases and intentionally deserialize to <see cref="Exception"/> instead of loading the original CLR type.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class TypeJsonConverter : JsonConverter<Type>
|
||||
{
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions;
|
||||
/// <summary>
|
||||
/// Prefix for unregistered type metadata that is not used for CLR type loading during deserialization.
|
||||
/// </summary>
|
||||
private const string UnregisteredTypeAliasPrefix = "UnregisteredClrType:";
|
||||
private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TypeJsonConverter(ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
_workflowJsonTypeRegistry = workflowJsonTypeRegistry;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TypeJsonConverter()
|
||||
: this(new WorkflowJsonOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TypeJsonConverter(IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
: this(workflowJsonOptions.Value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TypeJsonConverter(WorkflowJsonOptions workflowJsonOptions)
|
||||
{
|
||||
_workflowJsonOptions = workflowJsonOptions;
|
||||
_workflowJsonTypeRegistry = SerializationTypeRegistry.CreateDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -43,14 +41,18 @@ public class TypeJsonConverter : JsonConverter<Type>
|
|||
public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var typeAlias = reader.GetString();
|
||||
if (typeAlias?.StartsWith(UnregisteredTypeAliasPrefix, StringComparison.Ordinal) == true)
|
||||
return typeof(Exception);
|
||||
|
||||
return WorkflowJsonTypeResolver.ResolveType(_workflowJsonOptions, typeAlias, _workflowJsonOptions.AllowLegacyClrTypeNames);
|
||||
return SerializationTypeResolver.ResolveType(_workflowJsonTypeRegistry, typeAlias);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options)
|
||||
{
|
||||
var typeAlias = WorkflowJsonTypeResolver.GetAliasOrLegacyClrTypeName(_workflowJsonOptions, value);
|
||||
if (!SerializationTypeResolver.TryGetAlias(_workflowJsonTypeRegistry, value, out var typeAlias))
|
||||
typeAlias = $"{UnregisteredTypeAliasPrefix}{value.GetSimpleAssemblyQualifiedName()}";
|
||||
|
||||
writer.WriteStringValue(typeAlias);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ using System.Text.Json;
|
|||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Converters;
|
||||
|
||||
|
|
@ -18,16 +17,9 @@ public class VariableConverter : JsonConverter<Variable>
|
|||
|
||||
/// <inheritdoc />
|
||||
// ReSharper disable once ContextualLoggerProblem
|
||||
public VariableConverter(ILogger<VariableMapper> logger)
|
||||
public VariableConverter(ISerializationTypeRegistry workflowJsonTypeRegistry, ILogger<VariableMapper> logger)
|
||||
{
|
||||
_mapper = new VariableMapper(logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
// ReSharper disable once ContextualLoggerProblem
|
||||
public VariableConverter(ILogger<VariableMapper> logger, IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
{
|
||||
_mapper = new VariableMapper(logger, workflowJsonOptions);
|
||||
_mapper = new VariableMapper(workflowJsonTypeRegistry, logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Converters;
|
||||
|
||||
|
|
@ -12,31 +11,20 @@ namespace Elsa.Workflows.Serialization.Converters;
|
|||
/// </summary>
|
||||
public class VariableConverterFactory : JsonConverterFactory
|
||||
{
|
||||
private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IOptions<WorkflowJsonOptions>? _workflowJsonOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
// ReSharper disable once ContextualLoggerProblem
|
||||
public VariableConverterFactory(ILoggerFactory loggerFactory)
|
||||
public VariableConverterFactory(ISerializationTypeRegistry workflowJsonTypeRegistry, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_workflowJsonTypeRegistry = workflowJsonTypeRegistry;
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
// ReSharper disable once ContextualLoggerProblem
|
||||
public VariableConverterFactory(ILoggerFactory loggerFactory, IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_workflowJsonOptions = workflowJsonOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert) => typeof(Variable).IsAssignableFrom(typeToConvert);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var logger = _loggerFactory.CreateLogger<VariableMapper>();
|
||||
return _workflowJsonOptions != null ? new VariableConverter(logger, _workflowJsonOptions) : new VariableConverter(logger);
|
||||
}
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new VariableConverter(_workflowJsonTypeRegistry, _loggerFactory.CreateLogger<VariableMapper>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Extensions;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Options used by workflow JSON serializers to map stable type aliases to CLR types.
|
||||
/// </summary>
|
||||
public class WorkflowJsonOptions
|
||||
{
|
||||
private readonly Dictionary<string, Type> _typesByAlias = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<Type, string> _aliasesByType = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkflowJsonOptions"/> class.
|
||||
/// </summary>
|
||||
public WorkflowJsonOptions()
|
||||
{
|
||||
RegisterDefaultTypeAliases();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether unrestricted CLR type names can be resolved for backwards compatibility.
|
||||
/// </summary>
|
||||
public bool AllowLegacyClrTypeNames { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a type alias.
|
||||
/// </summary>
|
||||
public WorkflowJsonOptions RegisterTypeAlias(Type type, string alias)
|
||||
{
|
||||
_typesByAlias[alias] = type;
|
||||
_aliasesByType[type] = alias;
|
||||
|
||||
if (type.IsPrimitive || type.IsValueType && Nullable.GetUnderlyingType(type) == null)
|
||||
{
|
||||
var nullableType = typeof(Nullable<>).MakeGenericType(type);
|
||||
var nullableAlias = alias + "?";
|
||||
_typesByAlias[nullableAlias] = nullableType;
|
||||
_aliasesByType[nullableType] = nullableAlias;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve the specified alias to a type.
|
||||
/// </summary>
|
||||
public bool TryGetType(string alias, out Type type) => _typesByAlias.TryGetValue(alias, out type!);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the primary alias registered for the specified type.
|
||||
/// </summary>
|
||||
public bool TryGetAlias(Type type, out string alias) => _aliasesByType.TryGetValue(type, out alias!);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all registered types.
|
||||
/// </summary>
|
||||
public IEnumerable<Type> ListTypes() => _typesByAlias.Values.Distinct();
|
||||
|
||||
private void RegisterDefaultTypeAliases()
|
||||
{
|
||||
this.AddTypeAlias<short>("Int16");
|
||||
this.AddTypeAlias<int>("Int32");
|
||||
this.AddTypeAlias<long>("Int64");
|
||||
this.AddTypeAlias<long>("Long");
|
||||
this.AddTypeAlias<float>("Single");
|
||||
this.AddTypeAlias<object>("Object");
|
||||
this.AddTypeAlias<string>("String");
|
||||
this.AddTypeAlias<bool>("Boolean");
|
||||
this.AddTypeAlias<decimal>("Decimal");
|
||||
this.AddTypeAlias<double>("Double");
|
||||
this.AddTypeAlias<byte[]>("ByteArray");
|
||||
this.AddTypeAlias<Guid>();
|
||||
this.AddTypeAlias<DateTime>();
|
||||
this.AddTypeAlias<DateTimeOffset>();
|
||||
this.AddTypeAlias<TimeSpan>();
|
||||
this.AddTypeAlias<Stream>();
|
||||
this.AddTypeAlias<ExpandoObject>("JSON");
|
||||
this.AddTypeAlias<JsonElement>();
|
||||
this.AddTypeAlias<JsonNode>();
|
||||
this.AddTypeAlias<JsonObject>();
|
||||
this.AddTypeAlias<JsonArray>();
|
||||
this.AddTypeAlias<IDictionary<string, string>>("StringDictionary");
|
||||
this.AddTypeAlias<IDictionary<string, object>>("ObjectDictionary");
|
||||
this.AddTypeAlias<Dictionary<string, string>>("StringMap");
|
||||
this.AddTypeAlias<Dictionary<string, object>>("ObjectMap");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Serializers;
|
||||
|
||||
|
|
@ -13,15 +12,7 @@ public class BookmarkPayloadSerializer : IBookmarkPayloadSerializer
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BookmarkPayloadSerializer"/> class.
|
||||
/// </summary>
|
||||
public BookmarkPayloadSerializer()
|
||||
: this(Microsoft.Extensions.Options.Options.Create(new WorkflowJsonOptions()))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BookmarkPayloadSerializer"/> class.
|
||||
/// </summary>
|
||||
public BookmarkPayloadSerializer(IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
public BookmarkPayloadSerializer(ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
_settings = new JsonSerializerOptions
|
||||
{
|
||||
|
|
@ -30,7 +21,7 @@ public class BookmarkPayloadSerializer : IBookmarkPayloadSerializer
|
|||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
_settings.Converters.Add(new TypeJsonConverter(workflowJsonOptions));
|
||||
_settings.Converters.Add(new TypeJsonConverter(workflowJsonTypeRegistry));
|
||||
_settings.Converters.Add(new FuncExpressionValueConverter());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Text.Json;
|
||||
using Elsa.Common.Serialization;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Serialization.ReferenceHandlers;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Workflows.Serialization.Serializers;
|
||||
|
||||
|
|
@ -14,26 +12,18 @@ namespace Elsa.Workflows.Serialization.Serializers;
|
|||
/// Serializes and deserializes workflow states from and to JSON.
|
||||
/// </summary>
|
||||
public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStateSerializer
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IOptions<WorkflowJsonOptions> _workflowJsonOptions;
|
||||
{
|
||||
private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonWorkflowStateSerializer"/> class.
|
||||
/// </summary>
|
||||
public JsonWorkflowStateSerializer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
|
||||
: this(serviceProvider, loggerFactory, Microsoft.Extensions.Options.Options.Create(new WorkflowJsonOptions()))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonWorkflowStateSerializer"/> class.
|
||||
/// </summary>
|
||||
public JsonWorkflowStateSerializer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
public JsonWorkflowStateSerializer(IServiceProvider serviceProvider, ISerializationTypeRegistry workflowJsonTypeRegistry, ILoggerFactory loggerFactory)
|
||||
: base(serviceProvider)
|
||||
{
|
||||
_workflowJsonTypeRegistry = workflowJsonTypeRegistry;
|
||||
_loggerFactory = loggerFactory;
|
||||
_workflowJsonOptions = workflowJsonOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -144,9 +134,9 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat
|
|||
/// <inheritdoc />
|
||||
protected override void AddConverters(JsonSerializerOptions options)
|
||||
{
|
||||
options.Converters.Add(new TypeJsonConverter(_workflowJsonOptions));
|
||||
options.Converters.Add(new PolymorphicObjectConverterFactory(_workflowJsonOptions));
|
||||
options.Converters.Add(new VariableConverterFactory(_loggerFactory, _workflowJsonOptions));
|
||||
options.Converters.Add(new TypeJsonConverter(_workflowJsonTypeRegistry));
|
||||
options.Converters.Add(new PolymorphicObjectConverterFactory(_workflowJsonTypeRegistry));
|
||||
options.Converters.Add(new VariableConverterFactory(_workflowJsonTypeRegistry, _loggerFactory));
|
||||
options.Converters.Add(new FuncExpressionValueConverter());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,9 +71,10 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer
|
|||
protected override void AddConverters(JsonSerializerOptions options)
|
||||
{
|
||||
var expressionDescriptorRegistry = ServiceProvider.GetRequiredService<IExpressionDescriptorRegistry>();
|
||||
var workflowJsonTypeRegistry = ServiceProvider.GetRequiredService<ISerializationTypeRegistry>();
|
||||
|
||||
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
|
||||
options.Converters.Add(CreateInstance<TypeJsonConverter>());
|
||||
options.Converters.Add(new TypeJsonConverter(workflowJsonTypeRegistry));
|
||||
options.Converters.Add(new SafeValueConverterFactory());
|
||||
options.Converters.Add(new ExpressionJsonConverterFactory(expressionDescriptorRegistry));
|
||||
options.Converters.Add(new FuncExpressionValueConverter());
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ using System.Text;
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
|
|
@ -13,12 +12,7 @@ public class Hasher : IHasher
|
|||
{
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
public Hasher()
|
||||
: this(Microsoft.Extensions.Options.Options.Create(new WorkflowJsonOptions()))
|
||||
{
|
||||
}
|
||||
|
||||
public Hasher(IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
public Hasher(ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
_serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
|
|
@ -27,7 +21,7 @@ public class Hasher : IHasher
|
|||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
_serializerOptions.Converters.Add(new TypeJsonConverter(workflowJsonOptions));
|
||||
_serializerOptions.Converters.Add(new TypeJsonConverter(workflowJsonTypeRegistry));
|
||||
_serializerOptions.Converters.Add(new ExcludeFromHashConverterFactory());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ using Elsa.Expressions.Helpers;
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Serialization.Helpers;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
|
|
@ -16,40 +15,21 @@ namespace Elsa.Workflows;
|
|||
public class VariableMapper
|
||||
{
|
||||
private readonly ILogger<VariableMapper> _logger;
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions;
|
||||
private readonly ISerializationTypeRegistry _workflowJsonTypeRegistry;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VariableMapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="workflowJsonTypeRegistry">The serialization type registry.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public VariableMapper(ILogger<VariableMapper> logger)
|
||||
: this(logger, Microsoft.Extensions.Options.Options.Create(new WorkflowJsonOptions()))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VariableMapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="workflowJsonOptions">The workflow JSON options.</param>
|
||||
public VariableMapper(ILogger<VariableMapper> logger, IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
: this(logger, workflowJsonOptions.Value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VariableMapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="workflowJsonOptions">The workflow JSON options.</param>
|
||||
public VariableMapper(ILogger<VariableMapper> logger, WorkflowJsonOptions workflowJsonOptions)
|
||||
public VariableMapper(ISerializationTypeRegistry workflowJsonTypeRegistry, ILogger<VariableMapper> logger)
|
||||
{
|
||||
_workflowJsonTypeRegistry = workflowJsonTypeRegistry;
|
||||
_logger = logger;
|
||||
_workflowJsonOptions = workflowJsonOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public VariableMapper() : this(NullLogger<VariableMapper>.Instance)
|
||||
public VariableMapper() : this(SerializationTypeRegistry.CreateDefault(), NullLogger<VariableMapper>.Instance)
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -60,6 +40,7 @@ public class VariableMapper
|
|||
public Variable Map(VariableModel source)
|
||||
{
|
||||
var type = ResolveVariableType(source.TypeName);
|
||||
|
||||
var variableGenericType = typeof(Variable<>).MakeGenericType(type);
|
||||
var variable = (Variable)Activator.CreateInstance(variableGenericType)!;
|
||||
|
||||
|
|
@ -84,19 +65,27 @@ public class VariableMapper
|
|||
var variableType = source.GetType();
|
||||
var value = source.Value;
|
||||
var valueType = variableType.IsConstructedGenericType ? variableType.GetGenericArguments().FirstOrDefault() ?? typeof(object) : typeof(object);
|
||||
var valueTypeAlias = WorkflowJsonTypeResolver.GetAliasOrLegacyClrTypeName(_workflowJsonOptions, valueType);
|
||||
var storageDriverTypeName = source.StorageDriverType != null ? WorkflowJsonTypeResolver.GetAliasOrLegacyClrTypeName(_workflowJsonOptions, source.StorageDriverType) : null;
|
||||
var valueTypeAlias = SerializationTypeResolver.TryGetAlias(_workflowJsonTypeRegistry, valueType, out var alias) ? alias : valueType.GetSimpleAssemblyQualifiedName();
|
||||
var storageDriverTypeName = GetTypeName(source.StorageDriverType);
|
||||
var serializedValue = value.Format();
|
||||
|
||||
return new(source.Id, source.Name, valueTypeAlias, serializedValue, storageDriverTypeName);
|
||||
}
|
||||
|
||||
private string? GetTypeName(Type? type)
|
||||
{
|
||||
if (type == null)
|
||||
return null;
|
||||
|
||||
return SerializationTypeResolver.TryGetAlias(_workflowJsonTypeRegistry, type, out var alias) ? alias : type.GetSimpleAssemblyQualifiedName();
|
||||
}
|
||||
|
||||
private Type ResolveVariableType(string? typeAlias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeAlias))
|
||||
return typeof(object);
|
||||
|
||||
if (WorkflowJsonTypeResolver.TryResolveType(_workflowJsonOptions, typeAlias, _workflowJsonOptions.AllowLegacyClrTypeNames, out var type))
|
||||
if (SerializationTypeResolver.TryResolveType(_workflowJsonTypeRegistry, typeAlias, out var type))
|
||||
return type;
|
||||
|
||||
_logger.LogWarning("Failed to resolve variable type alias {VariableTypeName}", typeAlias);
|
||||
|
|
@ -108,7 +97,7 @@ public class VariableMapper
|
|||
if (string.IsNullOrWhiteSpace(typeAlias))
|
||||
return null;
|
||||
|
||||
if (WorkflowJsonTypeResolver.TryResolveType(_workflowJsonOptions, typeAlias, _workflowJsonOptions.AllowLegacyClrTypeNames, out var type) && IsStorageDriverType(type))
|
||||
if (SerializationTypeResolver.TryResolveType(_workflowJsonTypeRegistry, typeAlias, out var type) && IsStorageDriverType(type))
|
||||
return type;
|
||||
|
||||
_logger.LogWarning("Failed to resolve storage driver type alias {StorageDriverTypeName}", typeAlias);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ using Elsa.Extensions;
|
|||
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 JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.ShellFeatures;
|
||||
|
||||
|
|
@ -33,6 +34,6 @@ public class FlowchartFeature : IShellFeature
|
|||
if (FlowchartOptionsConfigurator != null)
|
||||
services.Configure(FlowchartOptionsConfigurator);
|
||||
|
||||
services.Configure<WorkflowJsonOptions>(options => options.AddTypeAlias<FlowScope>("FlowScope"));
|
||||
services.Configure<SerializationTypeOptions>(options => options.AddTypeAlias<FlowScope>("FlowScope"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,29 +5,33 @@ using Elsa.Common.ShellFeatures;
|
|||
using Elsa.Expressions.ShellFeatures;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.ActivationValidators;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Activities.Flowchart.Options;
|
||||
using Elsa.Workflows.Activities.Flowchart.Serialization;
|
||||
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;
|
||||
using Elsa.Workflows.Serialization.Configurators;
|
||||
using Elsa.Workflows.Serialization.Helpers;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
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.ShellFeatures;
|
||||
|
||||
|
|
@ -84,7 +88,31 @@ public class WorkflowsFeature : IShellFeature
|
|||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.Configure<WorkflowJsonOptions>(options => options.RegisterWorkflowTypeAliases());
|
||||
services.Configure<SerializationTypeOptions>(options =>
|
||||
{
|
||||
options.AddTypeAlias<ExceptionState>(nameof(ExceptionState));
|
||||
options.AddTypeAlias<FaultException>(nameof(FaultException));
|
||||
options.AddTypeAlias<VariablesDictionary>(nameof(VariablesDictionary));
|
||||
options.AddTypeAlias<Token>(nameof(Token));
|
||||
options.RegisterLegacyTypeName(typeof(FlowJoinMode), "Elsa.Workflows.Core.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core");
|
||||
options.AddTypeAliasWithLegacyName<FlowJoinMode>(nameof(FlowJoinMode));
|
||||
options.AddTypeAliasWithLegacyName<WorkflowStorageDriver>(nameof(WorkflowStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<WorkflowInstanceStorageDriver>(nameof(WorkflowInstanceStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<MemoryStorageDriver>(nameof(MemoryStorageDriver));
|
||||
options.AddTypeAliasWithLegacyName<FaultStrategy>(nameof(FaultStrategy));
|
||||
options.AddTypeAliasWithLegacyName<ContinueWithIncidentsStrategy>(nameof(ContinueWithIncidentsStrategy));
|
||||
options.AddTypeAlias<Exception>(nameof(Exception));
|
||||
options.AddTypeAlias<ArgumentException>(nameof(ArgumentException));
|
||||
options.AddTypeAlias<ArgumentNullException>(nameof(ArgumentNullException));
|
||||
options.AddTypeAlias<InvalidOperationException>(nameof(InvalidOperationException));
|
||||
options.AddTypeAlias<NullReferenceException>(nameof(NullReferenceException));
|
||||
options.AddTypeAlias<OperationCanceledException>(nameof(OperationCanceledException));
|
||||
options.AddTypeAlias<TaskCanceledException>(nameof(TaskCanceledException));
|
||||
options.AddTypeAlias<TimeoutException>(nameof(TimeoutException));
|
||||
options.AddTypeAlias<NotSupportedException>(nameof(NotSupportedException));
|
||||
options.AddTypeAlias<JObject>(nameof(JObject));
|
||||
options.AddTypeAlias<JArray>(nameof(JArray));
|
||||
});
|
||||
|
||||
services
|
||||
// Core.
|
||||
|
|
@ -155,6 +183,7 @@ public class WorkflowsFeature : IShellFeature
|
|||
.AddStorageDriver<MemoryStorageDriver>()
|
||||
|
||||
// Serialization.
|
||||
.AddSingleton<ISerializationTypeRegistry, SerializationTypeRegistry>()
|
||||
.AddSingleton<IWorkflowStateSerializer, JsonWorkflowStateSerializer>()
|
||||
.AddSingleton<IPayloadSerializer, JsonPayloadSerializer>()
|
||||
.AddSingleton<IActivitySerializer, JsonActivitySerializer>()
|
||||
|
|
@ -192,7 +221,7 @@ public class WorkflowsFeature : IShellFeature
|
|||
|
||||
// Logging
|
||||
.AddLogging();
|
||||
|
||||
|
||||
// Overridable services
|
||||
services.AddScoped<ICommitStateHandler, NoopCommitStateHandler>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
using System.Reflection;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Expressions.Extensions;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using Elsa.Workflows.Management.Options;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Workflows.Management.Extensions;
|
||||
|
|
@ -99,8 +100,9 @@ public static class ManagementServiceCollectionExtensions
|
|||
/// </summary>
|
||||
public static IServiceCollection AddVariableTypeAndAlias<T>(this IServiceCollection services, string alias, string category)
|
||||
{
|
||||
services.Configure<SerializationTypeOptions>(options => options.RegisterTypeAlias(typeof(T), alias));
|
||||
return services
|
||||
.AddVariableDescriptor<T>(category)
|
||||
.Configure<WorkflowJsonOptions>(options => options.AddTypeAlias<T>(alias));
|
||||
.AddTypeAlias<T>(alias);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ using Elsa.Features.Services;
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Management.Features;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
|
@ -74,9 +75,11 @@ public static class ModuleExtensions
|
|||
/// </summary>
|
||||
public static IModule AddVariableTypeAndAlias<T>(this IModule module, string alias, string category)
|
||||
{
|
||||
module.UseWorkflowManagement(management => management.AddVariableType<T>(category));
|
||||
module.Services.Configure<WorkflowJsonOptions>(options => options.AddTypeAlias<T>(alias));
|
||||
return module;
|
||||
module.Services.Configure<SerializationTypeOptions>(options => options.RegisterTypeAlias(typeof(T), alias));
|
||||
|
||||
return module
|
||||
.UseWorkflowManagement(management => management.AddVariableType<T>(category))
|
||||
.AddTypeAlias<T>(alias);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Management.Features;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Options;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Management;
|
||||
|
||||
|
|
@ -17,7 +18,8 @@ public static class WorkflowManagementFeatureExtensions
|
|||
public static WorkflowManagementFeature AddVariableTypeAndAlias<T>(this WorkflowManagementFeature management, string alias, string category)
|
||||
{
|
||||
management.AddVariableType<T>(category);
|
||||
management.Module.Services.Configure<WorkflowJsonOptions>(options => options.AddTypeAlias<T>(alias));
|
||||
management.Module.AddTypeAlias<T>(alias);
|
||||
management.Module.Services.Configure<SerializationTypeOptions>(options => options.RegisterTypeAlias(typeof(T), alias));
|
||||
return management;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Elsa.Caching.Features;
|
|||
using Elsa.Common.Codecs;
|
||||
using Elsa.Common.Features;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Expressions.Options;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
|
|
@ -27,10 +27,12 @@ using Elsa.Workflows.Management.Options;
|
|||
using Elsa.Workflows.Management.Providers;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Management.Stores;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Serialization.Serializers;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Management.Features;
|
||||
|
||||
|
|
@ -313,8 +315,9 @@ public class WorkflowManagementFeature(IModule module) : FeatureBase(module)
|
|||
options.IsReadOnlyMode = IsReadOnlyMode;
|
||||
});
|
||||
|
||||
Services.Configure<ExpressionOptions>(options => options.RegisterTypeAlias(typeof(ClrWorkflowMaterializerContext), nameof(ClrWorkflowMaterializerContext)));
|
||||
Services.Configure<SerializationTypeOptions>(options => options.RegisterTypeAlias(typeof(ClrWorkflowMaterializerContext), nameof(ClrWorkflowMaterializerContext)));
|
||||
Services.Configure<HostMethodActivitiesOptions>(_ => { });
|
||||
Services.Configure<WorkflowReferenceGraphOptions>(_ => { });
|
||||
Services.Configure<WorkflowJsonOptions>(options => options.RegisterTypeAlias(typeof(ClrWorkflowMaterializerContext), nameof(ClrWorkflowMaterializerContext)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,24 @@ using Elsa.Expressions.Helpers;
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Serialization.Helpers;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Management.Mappers;
|
||||
|
||||
/// <summary>
|
||||
/// Maps <see cref="Variable"/>s to <see cref="VariableDefinition"/>s and vice versa.
|
||||
/// </summary>
|
||||
public class VariableDefinitionMapper(IOptions<WorkflowJsonOptions> workflowJsonOptions, IServiceScopeFactory scopeFactory, ILogger<VariableDefinitionMapper> logger)
|
||||
public class VariableDefinitionMapper(ISerializationTypeRegistry workflowJsonTypeRegistry, IServiceScopeFactory scopeFactory, ILogger<VariableDefinitionMapper> logger)
|
||||
{
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions = workflowJsonOptions.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="VariableDefinition"/> to a <see cref="Variable"/>.
|
||||
/// </summary>
|
||||
public Variable? Map(VariableDefinition source)
|
||||
{
|
||||
var type = WorkflowJsonTypeResolver.TryResolveType(_workflowJsonOptions, source.TypeName, _workflowJsonOptions.AllowLegacyClrTypeNames, out var resolvedType) ? resolvedType : null;
|
||||
var type = SerializationTypeResolver.TryResolveType(workflowJsonTypeRegistry, source.TypeName, out var resolvedType) ? resolvedType : null;
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
|
|
@ -72,10 +69,14 @@ public class VariableDefinitionMapper(IOptions<WorkflowJsonOptions> workflowJson
|
|||
{
|
||||
var variableType = source.GetType();
|
||||
var valueType = variableType.IsConstructedGenericType ? variableType.GetGenericArguments().FirstOrDefault() ?? typeof(object) : typeof(object);
|
||||
var valueTypeAlias = WorkflowJsonTypeResolver.TryGetAlias(_workflowJsonOptions, valueType, out var alias) ? alias : null;
|
||||
var valueTypeAlias = SerializationTypeResolver.TryGetAlias(workflowJsonTypeRegistry, valueType, out var alias) ? alias : null;
|
||||
var value = source.Value;
|
||||
var serializedValue = value.Format();
|
||||
var storageDriverTypeName = source.StorageDriverType != null ? WorkflowJsonTypeResolver.GetAliasOrLegacyClrTypeName(_workflowJsonOptions, source.StorageDriverType) : null;
|
||||
var storageDriverTypeName = source.StorageDriverType != null
|
||||
? SerializationTypeResolver.TryGetAlias(workflowJsonTypeRegistry, source.StorageDriverType, out var storageDriverAlias)
|
||||
? storageDriverAlias
|
||||
: source.StorageDriverType.GetSimpleAssemblyQualifiedName()
|
||||
: null;
|
||||
|
||||
// Handles the case where an alias exists for an array or collection type. E.g. byte[] -> ByteArray.
|
||||
if (valueTypeAlias != null && (valueType.IsArray || valueType.IsCollectionType()))
|
||||
|
|
@ -84,7 +85,9 @@ public class VariableDefinitionMapper(IOptions<WorkflowJsonOptions> workflowJson
|
|||
var isArray = valueType.IsArray;
|
||||
var isCollection = valueType.IsCollectionType();
|
||||
var elementValueType = isArray ? valueType.GetElementType()! : isCollection ? valueType.GenericTypeArguments[0] : valueType;
|
||||
var elementTypeAlias = WorkflowJsonTypeResolver.GetAliasOrLegacyClrTypeName(_workflowJsonOptions, elementValueType);
|
||||
var elementTypeAlias = SerializationTypeResolver.TryGetAlias(workflowJsonTypeRegistry, elementValueType, out var elementAlias)
|
||||
? elementAlias
|
||||
: elementValueType.GetSimpleAssemblyQualifiedName();
|
||||
|
||||
return new(source.Id, source.Name, elementTypeAlias, isArray, serializedValue, storageDriverTypeName);
|
||||
}
|
||||
|
|
@ -99,7 +102,7 @@ public class VariableDefinitionMapper(IOptions<WorkflowJsonOptions> workflowJson
|
|||
if (string.IsNullOrEmpty(storageDriverTypeName))
|
||||
return null;
|
||||
|
||||
if (WorkflowJsonTypeResolver.TryResolveType(_workflowJsonOptions, storageDriverTypeName, _workflowJsonOptions.AllowLegacyClrTypeNames, out var type) && typeof(IStorageDriver).IsAssignableFrom(type))
|
||||
if (SerializationTypeResolver.TryResolveType(workflowJsonTypeRegistry, storageDriverTypeName, out var type) && typeof(IStorageDriver).IsAssignableFrom(type))
|
||||
return type;
|
||||
|
||||
// TODO: The following code handles backward compatibility with variable definitions referencing older .NET type namespaces.
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ using CShells.Features;
|
|||
using Elsa.Caching.Features;
|
||||
using Elsa.Common.Features;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Features;
|
||||
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
|
||||
using Elsa.Workflows.Management.Contracts;
|
||||
|
|
@ -25,6 +25,7 @@ using Elsa.Workflows.Management.Stores;
|
|||
using Elsa.Workflows.Serialization.Serializers;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Management.ShellFeatures;
|
||||
|
||||
|
|
@ -133,6 +134,6 @@ public class WorkflowManagementFeature : IShellFeature
|
|||
// Register the default variable descriptors declared on this feature.
|
||||
services.AddVariableDescriptors(VariableDescriptors);
|
||||
|
||||
services.Configure<WorkflowJsonOptions>(options => options.RegisterTypeAlias(typeof(ClrWorkflowMaterializerContext), nameof(ClrWorkflowMaterializerContext)));
|
||||
services.Configure<SerializationTypeOptions>(options => options.RegisterTypeAlias(typeof(ClrWorkflowMaterializerContext), nameof(ClrWorkflowMaterializerContext)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ using System.Text.Json.Serialization;
|
|||
using System.Text.Json.Serialization.Metadata;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.Comparers;
|
||||
|
||||
|
|
@ -18,23 +18,14 @@ public class WorkflowTriggerEqualityComparer : IEqualityComparer<StoredTrigger>
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkflowTriggerEqualityComparer"/> class.
|
||||
/// </summary>
|
||||
public WorkflowTriggerEqualityComparer()
|
||||
: this(new WorkflowJsonOptions())
|
||||
public WorkflowTriggerEqualityComparer() : this(SerializationTypeRegistry.CreateDefault())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkflowTriggerEqualityComparer"/> class.
|
||||
/// </summary>
|
||||
public WorkflowTriggerEqualityComparer(IOptions<WorkflowJsonOptions> workflowJsonOptions)
|
||||
: this(workflowJsonOptions.Value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkflowTriggerEqualityComparer"/> class.
|
||||
/// </summary>
|
||||
public WorkflowTriggerEqualityComparer(WorkflowJsonOptions workflowJsonOptions)
|
||||
public WorkflowTriggerEqualityComparer(ISerializationTypeRegistry workflowJsonTypeRegistry)
|
||||
{
|
||||
_settings = new()
|
||||
{
|
||||
|
|
@ -53,8 +44,8 @@ public class WorkflowTriggerEqualityComparer : IEqualityComparer<StoredTrigger>
|
|||
// polymorphic object properties serialize identically to their stored representation.
|
||||
_settings.Converters.Add(new JsonStringEnumConverter());
|
||||
_settings.Converters.Add(JsonMetadataServices.TimeSpanConverter);
|
||||
_settings.Converters.Add(new PolymorphicObjectConverterFactory(workflowJsonOptions));
|
||||
_settings.Converters.Add(new TypeJsonConverter(workflowJsonOptions));
|
||||
_settings.Converters.Add(new PolymorphicObjectConverterFactory(workflowJsonTypeRegistry));
|
||||
_settings.Converters.Add(new TypeJsonConverter(workflowJsonTypeRegistry));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Discovery;
|
||||
|
|
@ -10,6 +10,7 @@ using Elsa.Workflows.Runtime.Options;
|
|||
using Elsa.Workflows.Runtime.Providers;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -93,6 +94,6 @@ public static class DependencyInjectionExtensions
|
|||
private static void AddWorkflowRegistration(IServiceCollection services, Type workflowType)
|
||||
{
|
||||
services.PostConfigure<RuntimeOptions>(options => options.Workflows.Add(workflowType));
|
||||
services.Configure<WorkflowJsonOptions>(options => options.RegisterTypeAlias(workflowType, workflowType.GetSimpleAssemblyQualifiedName()));
|
||||
services.Configure<SerializationTypeOptions>(options => options.AddSimpleAssemblyQualifiedTypeAlias(workflowType));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using Elsa.Common;
|
|||
using Elsa.Common.DistributedHosting;
|
||||
using Elsa.Common.Features;
|
||||
using Elsa.Common.RecurringTasks;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
|
|
@ -14,13 +13,16 @@ using Elsa.Workflows.Features;
|
|||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Contracts;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Runtime.ActivationValidators;
|
||||
using Elsa.Workflows.Runtime.Comparers;
|
||||
using Elsa.Workflows.Runtime.Bookmarks;
|
||||
using Elsa.Workflows.Runtime.Discovery;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Runtime.Handlers;
|
||||
using Elsa.Workflows.Runtime.Models;
|
||||
using Elsa.Workflows.Runtime.Options;
|
||||
using Elsa.Workflows.Runtime.Providers;
|
||||
using Elsa.Workflows.Runtime.Stimuli;
|
||||
using Elsa.Workflows.Runtime.Stores;
|
||||
using Elsa.Workflows.Runtime.Tasks;
|
||||
using Elsa.Workflows.Runtime.UIHints;
|
||||
|
|
@ -29,6 +31,7 @@ using Medallion.Threading.FileSystem;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.Features;
|
||||
|
||||
|
|
@ -45,6 +48,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
|
|||
/// A list of workflow builders configured during application startup.
|
||||
/// </summary>
|
||||
public IDictionary<string, Func<IServiceProvider, ValueTask<IWorkflow>>> Workflows { get; set; } = new Dictionary<string, Func<IServiceProvider, ValueTask<IWorkflow>>>();
|
||||
private ISet<Type> WorkflowTypes { get; } = new HashSet<Type>();
|
||||
|
||||
/// <summary>
|
||||
/// A factory that instantiates a concrete <see cref="IWorkflowRuntime"/>.
|
||||
|
|
@ -200,7 +204,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
|
|||
public WorkflowRuntimeFeature AddWorkflow(Type workflowType)
|
||||
{
|
||||
Workflows.Add(workflowType);
|
||||
Services.Configure<WorkflowJsonOptions>(options => options.RegisterTypeAlias(workflowType, workflowType.GetSimpleAssemblyQualifiedName()));
|
||||
WorkflowTypes.Add(workflowType);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +271,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
|
|||
Services.Configure(WorkflowInboxCleanupOptions);
|
||||
Services.Configure(WorkflowDispatcherOptions);
|
||||
Services.Configure(BookmarkQueuePurgeOptions);
|
||||
Services.Configure<SerializationTypeOptions>(RegisterWorkflowTypeAliases);
|
||||
Services.Configure<RuntimeOptions>(options => { options.Workflows = Workflows; });
|
||||
Services.Configure<WorkflowDispatcherOptions>(options =>
|
||||
{
|
||||
|
|
@ -305,7 +310,6 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
|
|||
|
||||
Services
|
||||
// Core.
|
||||
.AddSingleton<WorkflowTriggerEqualityComparer>()
|
||||
.AddScoped<ITriggerIndexer, TriggerIndexer>()
|
||||
.AddScoped<IWorkflowInstanceFactory, WorkflowInstanceFactory>()
|
||||
.AddScoped<IWorkflowHostFactory, WorkflowHostFactory>()
|
||||
|
|
@ -439,4 +443,34 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
|
|||
Services.TryAddScoped(WorkflowDispatchOutboxStore);
|
||||
Services.TryAddScoped<IWorkflowDispatchOutboxProcessor, WorkflowDispatchOutboxProcessor>();
|
||||
}
|
||||
|
||||
private void RegisterWorkflowTypeAliases(SerializationTypeOptions options)
|
||||
{
|
||||
WorkflowRuntimeTypeAliasRegistrar.Register(options, GetRegisteredWorkflowTypes());
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetRegisteredWorkflowTypes()
|
||||
{
|
||||
return WorkflowTypes
|
||||
.Concat(Workflows.Keys.Select(TryResolveWorkflowType).Where(type => type != null).Select(type => type!))
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
private static Type? TryResolveWorkflowType(string typeName)
|
||||
{
|
||||
Type? type;
|
||||
|
||||
try
|
||||
{
|
||||
type = Type.GetType(typeName, false);
|
||||
}
|
||||
catch (Exception e) when (e is ArgumentException or FileLoadException or FileNotFoundException or TypeLoadException or BadImageFormatException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return type != null && typeof(IWorkflow).IsAssignableFrom(type) && type is { IsAbstract: false, IsInterface: false, ContainsGenericParameters: false }
|
||||
? type
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ using Medallion.Threading;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Open.Linq.AsyncExtensions;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Runtime;
|
||||
|
||||
|
|
@ -31,9 +32,9 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IStimulusHasher _hasher;
|
||||
private readonly IDistributedLockProvider _distributedLockProvider;
|
||||
private readonly WorkflowTriggerEqualityComparer _triggerEqualityComparer;
|
||||
private readonly DistributedLockingOptions _lockingOptions;
|
||||
private readonly ILogger _logger;
|
||||
private readonly WorkflowTriggerEqualityComparer _triggerEqualityComparer;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
|
|
@ -49,8 +50,8 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
IServiceProvider serviceProvider,
|
||||
IStimulusHasher hasher,
|
||||
IDistributedLockProvider distributedLockProvider,
|
||||
ISerializationTypeRegistry workflowJsonTypeRegistry,
|
||||
IOptions<DistributedLockingOptions> lockingOptions,
|
||||
WorkflowTriggerEqualityComparer triggerEqualityComparer,
|
||||
ILogger<TriggerIndexer> logger)
|
||||
{
|
||||
_activityVisitor = activityVisitor;
|
||||
|
|
@ -62,8 +63,8 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
_serviceProvider = serviceProvider;
|
||||
_hasher = hasher;
|
||||
_distributedLockProvider = distributedLockProvider;
|
||||
_triggerEqualityComparer = new WorkflowTriggerEqualityComparer(workflowJsonTypeRegistry);
|
||||
_lockingOptions = lockingOptions.Value;
|
||||
_triggerEqualityComparer = triggerEqualityComparer;
|
||||
_logger = logger;
|
||||
_workflowDefinitionService = workflowDefinitionService;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@ using System.Reflection;
|
|||
using CShells.Features;
|
||||
using Elsa.Common;
|
||||
using Elsa.Common.RecurringTasks;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows.CommitStates;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Contracts;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Runtime.ActivationValidators;
|
||||
using Elsa.Workflows.Runtime.Comparers;
|
||||
using Elsa.Workflows.Runtime.Discovery;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Runtime.Handlers;
|
||||
|
|
@ -26,6 +25,7 @@ using Medallion.Threading.FileSystem;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.ShellFeatures;
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ public class WorkflowRuntimeFeature : IShellFeature
|
|||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
// Options.
|
||||
services.Configure<WorkflowJsonOptions>(RegisterWorkflowTypeAliases);
|
||||
services.Configure<SerializationTypeOptions>(RegisterWorkflowTypeAliases);
|
||||
services.Configure<RuntimeOptions>(options => { options.Workflows = Workflows; });
|
||||
services.Configure<WorkflowDispatcherOptions>(options =>
|
||||
{
|
||||
|
|
@ -227,7 +227,6 @@ public class WorkflowRuntimeFeature : IShellFeature
|
|||
.AddTransient<CShells.Lifecycle.IShellInitializer, Lifecycle.InitializePauseStateShellInitializer>()
|
||||
|
||||
// Core.
|
||||
.AddSingleton<WorkflowTriggerEqualityComparer>()
|
||||
.AddScoped<ITriggerIndexer, TriggerIndexer>()
|
||||
.AddScoped<IWorkflowInstanceFactory, WorkflowInstanceFactory>()
|
||||
.AddScoped<IWorkflowHostFactory, WorkflowHostFactory>()
|
||||
|
|
@ -237,7 +236,6 @@ public class WorkflowRuntimeFeature : IShellFeature
|
|||
.AddScoped(StimulusDispatcher)
|
||||
.AddScoped(WorkflowCancellationDispatcher)
|
||||
.AddScoped(RunTaskDispatcher)
|
||||
.AddScoped(DispatchWorkflowCommandHandler)
|
||||
.AddScoped(ActivityExecutionLogSink)
|
||||
.AddScoped(WorkflowExecutionLogSink)
|
||||
.AddSingleton(BackgroundActivityScheduler)
|
||||
|
|
@ -364,9 +362,33 @@ public class WorkflowRuntimeFeature : IShellFeature
|
|||
services.TryAddScoped<IWorkflowDispatchOutboxProcessor, WorkflowDispatchOutboxProcessor>();
|
||||
}
|
||||
|
||||
private void RegisterWorkflowTypeAliases(WorkflowJsonOptions options)
|
||||
private void RegisterWorkflowTypeAliases(SerializationTypeOptions options)
|
||||
{
|
||||
foreach (var workflowType in WorkflowTypes)
|
||||
options.RegisterTypeAlias(workflowType, workflowType.GetSimpleAssemblyQualifiedName());
|
||||
WorkflowRuntimeTypeAliasRegistrar.Register(options, GetRegisteredWorkflowTypes());
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetRegisteredWorkflowTypes()
|
||||
{
|
||||
return WorkflowTypes
|
||||
.Concat(Workflows.Keys.Select(TryResolveWorkflowType).Where(type => type != null).Select(type => type!))
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
private static Type? TryResolveWorkflowType(string typeName)
|
||||
{
|
||||
Type? type;
|
||||
|
||||
try
|
||||
{
|
||||
type = Type.GetType(typeName, false);
|
||||
}
|
||||
catch (Exception e) when (e is ArgumentException or FileLoadException or FileNotFoundException or TypeLoadException or BadImageFormatException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return type != null && typeof(IWorkflow).IsAssignableFrom(type) && type is { IsAbstract: false, IsInterface: false, ContainsGenericParameters: false }
|
||||
? type
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Runtime.Bookmarks;
|
||||
using Elsa.Workflows.Runtime.Stimuli;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Runtime;
|
||||
|
||||
internal static class WorkflowRuntimeTypeAliasRegistrar
|
||||
{
|
||||
public static void Register(SerializationTypeOptions options, IEnumerable<Type> workflowTypes)
|
||||
{
|
||||
options.AddTypeAlias<EventBookmarkPayload>();
|
||||
options.AddTypeAlias<ExecuteWorkflowPayload>();
|
||||
options.AddTypeAlias<RunTaskBookmarkPayload>();
|
||||
options.AddTypeAlias<BookmarkTokenPayload>();
|
||||
options.AddTypeAlias<EventTokenPayload>();
|
||||
options.AddTypeAlias<ExecuteWorkflowResult>();
|
||||
options.AddTypeAlias<WorkflowInterruptedPayload>();
|
||||
options.AddTypeAlias<BackgroundActivityStimulus>();
|
||||
options.AddTypeAlias<BulkDispatchWorkflowsStimulus>();
|
||||
options.AddTypeAlias<DispatchWorkflowStimulus>();
|
||||
options.AddTypeAlias<EventStimulus>();
|
||||
options.AddTypeAlias<ExecuteWorkflowStimulus>();
|
||||
options.AddTypeAlias<RunTaskStimulus>();
|
||||
|
||||
foreach (var workflowType in workflowTypes.Where(IsConcreteWorkflowType).Distinct())
|
||||
options.RegisterTypeAlias(workflowType, workflowType.GetSimpleAssemblyQualifiedName());
|
||||
}
|
||||
|
||||
private static bool IsConcreteWorkflowType(Type type)
|
||||
{
|
||||
return typeof(IWorkflow).IsAssignableFrom(type) && type is { IsAbstract: false, IsInterface: false, ContainsGenericParameters: false };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Common.Serialization;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
|
@ -9,7 +11,9 @@ namespace Elsa.Workflows.IntegrationTests.Serialization.JsonSerialization;
|
|||
|
||||
public class SerializationTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
private readonly IServiceProvider _services = new TestApplicationBuilder(testOutputHelper).Build();
|
||||
private readonly IServiceProvider _services = new TestApplicationBuilder(testOutputHelper)
|
||||
.ConfigureServices(services => services.Configure<SerializationTypeOptions>(options => options.AddTypeAlias<TestObject>()))
|
||||
.Build();
|
||||
|
||||
[Theory(DisplayName = "write")]
|
||||
[InlineData(typeof(JsonObject), "JsonObjectIsland")]
|
||||
|
|
@ -182,4 +186,4 @@ public class SerializationTests(ITestOutputHelper testOutputHelper)
|
|||
public class TestObject
|
||||
{
|
||||
public string? Data { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ using System.Dynamic;
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Serialization.ReferenceHandlers;
|
||||
using Elsa.Workflows.Services;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Serialization.Polymorphism;
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ public class Tests
|
|||
public void Test1()
|
||||
{
|
||||
var model = CreateModel();
|
||||
var expectedJson = File.ReadAllText("Serialization/Polymorphism/data.json").Trim();
|
||||
var expectedJson = File.ReadAllText("Serialization/Polymorphism/data.json").TrimEnd();
|
||||
var actualJson = JsonSerializer.Serialize(model, GetSerializerOptions());
|
||||
Assert.Equal(expectedJson, actualJson);
|
||||
}
|
||||
|
|
@ -74,6 +76,10 @@ public class Tests
|
|||
private JsonSerializerOptions GetSerializerOptions()
|
||||
{
|
||||
var referenceHandler = new CrossScopedReferenceHandler();
|
||||
var workflowJsonTypeRegistry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(new SerializationTypeOptions()));
|
||||
workflowJsonTypeRegistry.RegisterType(typeof(Model), nameof(Model));
|
||||
workflowJsonTypeRegistry.RegisterType(typeof(CustomDictionary), nameof(CustomDictionary));
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
ReferenceHandler = referenceHandler,
|
||||
|
|
@ -84,7 +90,7 @@ public class Tests
|
|||
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
options.Converters.Add(JsonMetadataServices.TimeSpanConverter);
|
||||
options.Converters.Add(new PolymorphicObjectConverterFactory(new WorkflowJsonOptions()));
|
||||
options.Converters.Add(new PolymorphicObjectConverterFactory(workflowJsonTypeRegistry));
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"$id":"1","text":"Hello World","number":123,"flag":true,"items":{"$id":"2","$values":[{"$id":"3","text":"Hello","number":1,"flag":true,"items":{"$id":"4","$values":[{"$id":"5","number":0,"flag":false,"metadata":{"Foo":"Bar","Number":123,"Flag":true,"Models":{"$id":"6","$values":[{"$id":"7","text":"Hello","number":1,"flag":true,"metadata":{"Foo":"Bar","Number":123,"Flag":true,"Models":{"$ref":"6"},"CustomDictionary":{"$id":"8","content-type":["application/json"],"_type":"Elsa.Workflows.IntegrationTests.Serialization.Polymorphism.CustomDictionary, Elsa.Workflows.IntegrationTests"}}},{"$id":"9","text":"World","number":2,"flag":false}],"_type":"System.Collections.Generic.List\u00601[[Elsa.Workflows.IntegrationTests.Serialization.Polymorphism.Model, Elsa.Workflows.IntegrationTests]], System.Private.CoreLib"},"CustomDictionary":{"$ref":"8"}}}]}},{"$id":"10","text":"World","number":2,"flag":false}]},"metadata":{"Foo":"Bar","Number":123,"Flag":true,"Models":{"$ref":"6"},"CustomDictionary":{"$ref":"8"}},"payload":{"$id":"11","text":"Payload","number":0,"flag":false,"_type":"Elsa.Workflows.IntegrationTests.Serialization.Polymorphism.Model, Elsa.Workflows.IntegrationTests"},"hashSet":{"$id":"12","$values":[{"$id":"13","text":"I\u0027m a model in a set!","number":0,"flag":false}],"_type":"System.Collections.Generic.HashSet\u00601[[Elsa.Workflows.IntegrationTests.Serialization.Polymorphism.Model, Elsa.Workflows.IntegrationTests]], System.Private.CoreLib"},"properties":{"$id":"14","Hello":{"$id":"15","text":"Hello","number":1,"flag":true},"World":{"$id":"16","text":"World","number":2,"flag":false}}}
|
||||
{"$id":"1","text":"Hello World","number":123,"flag":true,"items":{"$id":"2","$values":[{"$id":"3","text":"Hello","number":1,"flag":true,"items":{"$id":"4","$values":[{"$id":"5","number":0,"flag":false,"metadata":{"Foo":"Bar","Number":123,"Flag":true,"Models":{"$id":"6","$values":[{"$id":"7","text":"Hello","number":1,"flag":true,"metadata":{"Foo":"Bar","Number":123,"Flag":true,"Models":{"$ref":"6"},"CustomDictionary":{"$id":"8","content-type":["application/json"],"_type":"CustomDictionary"}}},{"$id":"9","text":"World","number":2,"flag":false}],"_type":"List\u003CModel\u003E"},"CustomDictionary":{"$ref":"8"}}}]}},{"$id":"10","text":"World","number":2,"flag":false}]},"metadata":{"Foo":"Bar","Number":123,"Flag":true,"Models":{"$ref":"6"},"CustomDictionary":{"$ref":"8"}},"payload":{"$id":"11","text":"Payload","number":0,"flag":false,"_type":"Model"},"hashSet":{"$id":"12","$values":[{"$id":"13","text":"I\u0027m a model in a set!","number":0,"flag":false}],"_type":"HashSet\u003CModel\u003E"},"properties":{"$id":"14","Hello":{"$id":"15","text":"Hello","number":1,"flag":true},"World":{"$id":"16","text":"World","number":2,"flag":false}}}
|
||||
|
|
|
|||
|
|
@ -314,11 +314,15 @@ public class IngressRateLimitingTests
|
|||
app => app.UseWorkflowsApiRateLimiting("elsa/api", PolicyName),
|
||||
options => AddFixedWindowLimiter(options, "other"));
|
||||
|
||||
app.Configure();
|
||||
await app.StartAsync();
|
||||
var client = app.GetTestClient();
|
||||
var exception = await Record.ExceptionAsync(async () =>
|
||||
{
|
||||
app.Configure();
|
||||
await app.StartAsync();
|
||||
var client = app.GetTestClient();
|
||||
await client.GetAsync("/elsa/api/ping");
|
||||
});
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync("/elsa/api/ping"));
|
||||
Assert.IsType<InvalidOperationException>(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Api.Endpoints.IncidentStrategies.List;
|
||||
using Elsa.Workflows.IncidentStrategies;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Api.UnitTests.Endpoints.IncidentStrategies;
|
||||
|
||||
public class ListTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ReturnsWorkflowJsonTypeIdentifier_ForIncidentStrategyTypeName()
|
||||
{
|
||||
var options = new SerializationTypeOptions();
|
||||
options.RegisterTypeAlias(typeof(ContinueWithIncidentsStrategy), nameof(ContinueWithIncidentsStrategy));
|
||||
options.RegisterLegacySimpleAssemblyQualifiedName(typeof(ContinueWithIncidentsStrategy));
|
||||
var registry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(options));
|
||||
var endpoint = new List([new ContinueWithIncidentsStrategy()], registry);
|
||||
|
||||
var response = await endpoint.ExecuteAsync(CancellationToken.None);
|
||||
|
||||
var descriptor = Assert.Single(response.Items);
|
||||
Assert.Equal(nameof(ContinueWithIncidentsStrategy), descriptor.TypeName);
|
||||
Assert.True(registry.TryGetType(typeof(ContinueWithIncidentsStrategy).GetSimpleAssemblyQualifiedName(), out var legacyType));
|
||||
Assert.Equal(typeof(ContinueWithIncidentsStrategy), legacyType);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Common.Serialization;
|
||||
using Elsa.Expressions.Services;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
|
||||
|
|
@ -8,7 +9,6 @@ using Elsa.Workflows.Models;
|
|||
using Elsa.Workflows.Serialization.Configurators;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Helpers;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ public sealed class ActivityJsonConverterTests
|
|||
Converters =
|
||||
{
|
||||
sut,
|
||||
new TypeJsonConverter(new WorkflowJsonOptions()),
|
||||
new TypeJsonConverter(),
|
||||
new PolymorphicObjectConverterFactory()
|
||||
},
|
||||
TypeInfoResolver = new ModifiableJsonTypeInfoResolver(new CustomConstructorConfigurator().GetModifiers())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,396 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Expressions.Options;
|
||||
using Elsa.Expressions.Services;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Exceptions;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Services;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Serialization.Converters;
|
||||
|
||||
public sealed class SerializationTypeResolverTests
|
||||
{
|
||||
private static readonly string UnsafeAssemblyQualifiedTypeAlias = typeof(System.Text.StringBuilder).AssemblyQualifiedName!;
|
||||
private readonly SerializationTypeRegistry _workflowJsonTypeRegistry = new(Microsoft.Extensions.Options.Options.Create(new SerializationTypeOptions()));
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
public SerializationTypeResolverTests()
|
||||
{
|
||||
_workflowJsonTypeRegistry.RegisterType(typeof(ExceptionState), nameof(ExceptionState));
|
||||
_workflowJsonTypeRegistry.RegisterType(typeof(FaultException), nameof(FaultException));
|
||||
_workflowJsonTypeRegistry.RegisterType(typeof(RegisteredPayload), "RegisteredPayload");
|
||||
_options = CreateOptions(_workflowJsonTypeRegistry);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("String", typeof(string))]
|
||||
[InlineData("String[]", typeof(string[]))]
|
||||
[InlineData("String[][]", typeof(string[][]))]
|
||||
[InlineData("List<String>", typeof(List<string>))]
|
||||
[InlineData("List<String[]>", typeof(List<string[]>))]
|
||||
[InlineData("List<List<String>>", typeof(List<List<string>>))]
|
||||
[InlineData("ExceptionState", typeof(ExceptionState))]
|
||||
[InlineData("FaultException", typeof(FaultException))]
|
||||
[InlineData("ObjectDictionary", typeof(IDictionary<string, object>))]
|
||||
public void When_DeserializeRegisteredTypeAlias_Then_ReturnsExpectedType(string typeAlias, Type expectedType)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize(typeAlias), _options);
|
||||
|
||||
Assert.Equal(expectedType, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializeRegisteredLegacyAssemblyQualifiedTypeAlias_Then_ReturnsExpectedType()
|
||||
{
|
||||
var typeAlias = typeof(RegisteredPayload).GetSimpleAssemblyQualifiedName();
|
||||
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonString(typeAlias), _options);
|
||||
|
||||
Assert.Equal(typeof(RegisteredPayload), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializeRegisteredTypeAliasWithDifferentCasing_Then_ReturnsExpectedType()
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonString("registeredpayload"), _options);
|
||||
|
||||
Assert.Equal(typeof(RegisteredPayload), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializeRegisteredLegacyGenericCollectionTypeAlias_Then_ReturnsExpectedType()
|
||||
{
|
||||
var typeAlias = typeof(List<RegisteredPayload>).GetSimpleAssemblyQualifiedName();
|
||||
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonString(typeAlias), _options);
|
||||
|
||||
Assert.Equal(typeof(List<RegisteredPayload>), result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(string), "String")]
|
||||
[InlineData(typeof(string[]), "String[]")]
|
||||
[InlineData(typeof(string[][]), "String[][]")]
|
||||
[InlineData(typeof(List<string>), "List<String>")]
|
||||
[InlineData(typeof(List<string[]>), "List<String[]>")]
|
||||
[InlineData(typeof(List<List<string>>), "List<List<String>>")]
|
||||
[InlineData(typeof(ExceptionState), "ExceptionState")]
|
||||
[InlineData(typeof(FaultException), "FaultException")]
|
||||
public void When_SerializeSupportedType_Then_EmitsAliasThatCanBeDeserialized(Type type, string expectedAlias)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(type, _options);
|
||||
var alias = JsonSerializer.Deserialize<string>(json);
|
||||
var result = JsonSerializer.Deserialize<Type>(json, _options);
|
||||
|
||||
Assert.Equal(expectedAlias, alias);
|
||||
Assert.Equal(type, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(IEnumerable<string>), "List<String>", typeof(List<string>))]
|
||||
[InlineData(typeof(ICollection<string>), "List<String>", typeof(List<string>))]
|
||||
[InlineData(typeof(IList<string>), "List<String>", typeof(List<string>))]
|
||||
[InlineData(typeof(IReadOnlyCollection<string>), "List<String>", typeof(List<string>))]
|
||||
[InlineData(typeof(IReadOnlyList<string>), "List<String>", typeof(List<string>))]
|
||||
[InlineData(typeof(ISet<string>), "HashSet<String>", typeof(HashSet<string>))]
|
||||
public void When_SerializeInterfaceCollectionType_Then_EmitsInstantiableAlias(Type type, string expectedAlias, Type expectedRoundTripType)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(type, _options);
|
||||
var alias = JsonSerializer.Deserialize<string>(json);
|
||||
var result = JsonSerializer.Deserialize<Type>(json, _options);
|
||||
|
||||
Assert.Equal(expectedAlias, alias);
|
||||
Assert.Equal(expectedRoundTripType, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(JsonIslandValues))]
|
||||
public void When_SerializeSpecialJsonIslandType_Then_CanBeDeserialized(object value, Type expectedType)
|
||||
{
|
||||
_workflowJsonTypeRegistry.RegisterType(typeof(JObject), nameof(JObject));
|
||||
_workflowJsonTypeRegistry.RegisterType(typeof(JArray), nameof(JArray));
|
||||
|
||||
var json = JsonSerializer.Serialize(value, _options);
|
||||
var result = JsonSerializer.Deserialize<object>(json, _options);
|
||||
|
||||
Assert.IsType(expectedType, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(System.Text.StringBuilder))]
|
||||
[InlineData(typeof(System.Text.StringBuilder[]))]
|
||||
[InlineData(typeof(List<System.Text.StringBuilder>))]
|
||||
public void When_SerializeUnsupportedType_Then_EmitsSafeUnregisteredTypeAlias(Type type)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(type, _options);
|
||||
var alias = JsonSerializer.Deserialize<string>(json);
|
||||
var result = JsonSerializer.Deserialize<Type>(json, _options);
|
||||
|
||||
Assert.StartsWith("UnregisteredClrType:", alias);
|
||||
Assert.Equal(typeof(Exception), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_SerializeExceptionStateWithUnregisteredExceptionType_Then_DoesNotThrow()
|
||||
{
|
||||
var exceptionState = ExceptionState.FromException(new NullReferenceException("Test"));
|
||||
|
||||
var json = JsonSerializer.Serialize(exceptionState, _options);
|
||||
var result = JsonSerializer.Deserialize<ExceptionState>(json, _options)!;
|
||||
|
||||
Assert.Contains("UnregisteredClrType:", json);
|
||||
Assert.Equal(typeof(Exception), result.Type);
|
||||
Assert.Equal("Test", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_ConfigureWorkflowsFeature_Then_RegistersCoreAliases()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var module = services.CreateModule();
|
||||
module.UseWorkflows();
|
||||
module.Apply();
|
||||
using var serviceProvider = services.BuildServiceProvider();
|
||||
var registry = serviceProvider.GetRequiredService<ISerializationTypeRegistry>();
|
||||
|
||||
var aliasRegistered = registry.TryGetAlias(typeof(NullReferenceException), out var alias);
|
||||
var typeRegistered = registry.TryGetType(nameof(NullReferenceException), out var type);
|
||||
|
||||
Assert.True(aliasRegistered);
|
||||
Assert.Equal(nameof(NullReferenceException), alias);
|
||||
Assert.True(typeRegistered);
|
||||
Assert.Equal(typeof(NullReferenceException), type);
|
||||
Assert.True(registry.TryGetAlias(typeof(MemoryStorageDriver), out var memoryStorageDriverAlias));
|
||||
Assert.Equal(nameof(MemoryStorageDriver), memoryStorageDriverAlias);
|
||||
Assert.True(registry.TryGetType(nameof(MemoryStorageDriver), out var memoryStorageDriverType));
|
||||
Assert.Equal(typeof(MemoryStorageDriver), memoryStorageDriverType);
|
||||
Assert.True(registry.TryGetType(typeof(MemoryStorageDriver).GetSimpleAssemblyQualifiedName(), out var legacyMemoryStorageDriverType));
|
||||
Assert.Equal(typeof(MemoryStorageDriver), legacyMemoryStorageDriverType);
|
||||
Assert.True(registry.TryGetAlias(typeof(Elsa.Workflows.IncidentStrategies.ContinueWithIncidentsStrategy), out var incidentStrategyAlias));
|
||||
Assert.Equal(nameof(Elsa.Workflows.IncidentStrategies.ContinueWithIncidentsStrategy), incidentStrategyAlias);
|
||||
Assert.True(registry.TryGetType(typeof(Elsa.Workflows.IncidentStrategies.ContinueWithIncidentsStrategy).GetSimpleAssemblyQualifiedName(), out var legacyIncidentStrategyType));
|
||||
Assert.Equal(typeof(Elsa.Workflows.IncidentStrategies.ContinueWithIncidentsStrategy), legacyIncidentStrategyType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_TypeAliasExistsOnlyInExpressionOptions_Then_WorkflowJsonDoesNotResolveIt()
|
||||
{
|
||||
var expressionOptions = new ExpressionOptions();
|
||||
expressionOptions.RegisterTypeAlias(typeof(ExpressionOnlyPayload), "ExpressionOnlyPayload");
|
||||
var expressionRegistry = new WellKnownTypeRegistry(Microsoft.Extensions.Options.Options.Create(expressionOptions));
|
||||
|
||||
Assert.True(expressionRegistry.TryGetType("ExpressionOnlyPayload", out _));
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Type>(JsonString("ExpressionOnlyPayload"), _options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_SerializePolymorphicObjectWithUnregisteredType_Then_OmitsTypeMetadata()
|
||||
{
|
||||
var json = JsonSerializer.Serialize<object>(new UnregisteredPayload { Name = "Alice" }, _options);
|
||||
|
||||
var result = JsonSerializer.Deserialize<object>(json, _options);
|
||||
|
||||
Assert.DoesNotContain("\"_type\"", json);
|
||||
var payload = Assert.IsAssignableFrom<IDictionary<string, object>>(result);
|
||||
Assert.Equal("Alice", payload["name"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializeUnknownAssemblyQualifiedTypeAlias_Then_ThrowsJsonException()
|
||||
{
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Type>(JsonString(UnsafeAssemblyQualifiedTypeAlias), _options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializeUnknownGenericElementTypeAlias_Then_ThrowsJsonException()
|
||||
{
|
||||
var typeAlias = $"List<{UnsafeAssemblyQualifiedTypeAlias}>";
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Type>(JsonString(typeAlias), _options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializePolymorphicObjectWithRegisteredTypeAlias_Then_ReturnsTypedObject()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"name": "Alice",
|
||||
"_type": "RegisteredPayload"
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<object>(json, _options);
|
||||
|
||||
var payload = Assert.IsType<RegisteredPayload>(result);
|
||||
Assert.Equal("Alice", payload.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("IEnumerable<String>", typeof(List<string>))]
|
||||
[InlineData("ICollection<String>", typeof(List<string>))]
|
||||
[InlineData("IList<String>", typeof(List<string>))]
|
||||
[InlineData("IReadOnlyCollection<String>", typeof(List<string>))]
|
||||
[InlineData("IReadOnlyList<String>", typeof(List<string>))]
|
||||
[InlineData("ISet<String>", typeof(HashSet<string>))]
|
||||
public void When_DeserializePolymorphicCollectionInterface_Then_ReturnsConcreteCollection(string typeAlias, Type expectedType)
|
||||
{
|
||||
var json = $$"""
|
||||
{
|
||||
"_items": ["Alice"],
|
||||
"_type": "{{typeAlias}}"
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<object>(json, _options);
|
||||
|
||||
Assert.IsType(expectedType, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializePolymorphicObjectWithNonInstantiableType_Then_ThrowsJsonException()
|
||||
{
|
||||
_workflowJsonTypeRegistry.RegisterType(typeof(AbstractPayload), "AbstractPayload");
|
||||
var json = """
|
||||
{
|
||||
"_type": "AbstractPayload"
|
||||
}
|
||||
""";
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<object>(json, _options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializePolymorphicObjectWithUnknownAssemblyQualifiedType_Then_ThrowsJsonException()
|
||||
{
|
||||
var json = $$"""
|
||||
{
|
||||
"capacity": 16,
|
||||
"_type": {{JsonString(UnsafeAssemblyQualifiedTypeAlias)}}
|
||||
}
|
||||
""";
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<object>(json, _options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializePolymorphicObjectWithoutTypeJsonConverterAndUnknownAssemblyQualifiedType_Then_ThrowsJsonException()
|
||||
{
|
||||
var options = CreatePolymorphicOnlyOptions(_workflowJsonTypeRegistry);
|
||||
var json = $$"""
|
||||
{
|
||||
"capacity": 16,
|
||||
"_type": {{JsonString(UnsafeAssemblyQualifiedTypeAlias)}}
|
||||
}
|
||||
""";
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<object>(json, options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializeDictionaryObjectPayloadWithRegisteredTypeAlias_Then_ReturnsTypedObjectValue()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"payload": {
|
||||
"name": "Alice",
|
||||
"_type": "RegisteredPayload"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<IDictionary<string, object>>(json, _options)!;
|
||||
|
||||
var payload = Assert.IsType<RegisteredPayload>(result["payload"]);
|
||||
Assert.Equal("Alice", payload.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_DeserializeDictionaryObjectPayloadWithUnknownAssemblyQualifiedType_Then_ThrowsJsonException()
|
||||
{
|
||||
var json = $$"""
|
||||
{
|
||||
"payload": {
|
||||
"capacity": 16,
|
||||
"_type": {{JsonString(UnsafeAssemblyQualifiedTypeAlias)}}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<IDictionary<string, object>>(json, _options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_RegistryChangesAfterLegacyResolutionAttempt_Then_LegacyResolutionUsesCurrentRegistry()
|
||||
{
|
||||
var typeAlias = typeof(LateRegisteredPayload).GetSimpleAssemblyQualifiedName();
|
||||
Assert.False(SerializationTypeResolver.TryResolveType(_workflowJsonTypeRegistry, typeAlias, out _));
|
||||
|
||||
_workflowJsonTypeRegistry.RegisterType(typeof(LateRegisteredPayload), "LateRegisteredPayload");
|
||||
|
||||
Assert.True(SerializationTypeResolver.TryResolveType(_workflowJsonTypeRegistry, typeAlias, out var result));
|
||||
Assert.Equal(typeof(LateRegisteredPayload), result);
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateOptions(ISerializationTypeRegistry workflowJsonTypeRegistry) => new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters =
|
||||
{
|
||||
new PolymorphicObjectConverterFactory(workflowJsonTypeRegistry),
|
||||
new TypeJsonConverter(workflowJsonTypeRegistry)
|
||||
}
|
||||
};
|
||||
|
||||
private static JsonSerializerOptions CreatePolymorphicOnlyOptions(ISerializationTypeRegistry workflowJsonTypeRegistry) => new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters =
|
||||
{
|
||||
new PolymorphicObjectConverterFactory(workflowJsonTypeRegistry)
|
||||
}
|
||||
};
|
||||
|
||||
private static string JsonString(string value) => JsonSerializer.Serialize(value);
|
||||
|
||||
public static TheoryData<object, Type> JsonIslandValues() => new()
|
||||
{
|
||||
{ new JObject { ["name"] = "Alice" }, typeof(JObject) },
|
||||
{ new JArray("Alice", "Bob"), typeof(JArray) },
|
||||
{ new JsonObject { ["name"] = "Alice" }, typeof(JsonObject) },
|
||||
{ new JsonArray("Alice", "Bob"), typeof(JsonArray) }
|
||||
};
|
||||
|
||||
public sealed class RegisteredPayload
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UnregisteredPayload
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public abstract class AbstractPayload
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public sealed class LateRegisteredPayload
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ExpressionOnlyPayload
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
using Elsa.Workflows.Exceptions;
|
||||
using Elsa.Workflows.IncidentStrategies;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Helpers;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Serialization.Converters;
|
||||
|
||||
public class WorkflowJsonTypeResolverTests
|
||||
{
|
||||
private static readonly string UnsafeAssemblyQualifiedTypeAlias = typeof(System.Text.StringBuilder).AssemblyQualifiedName!;
|
||||
private static readonly string TrustedAssemblyQualifiedTypeAlias = typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName();
|
||||
private readonly JsonSerializerOptions _options;
|
||||
private readonly JsonSerializerOptions _strictOptions;
|
||||
|
||||
public WorkflowJsonTypeResolverTests()
|
||||
{
|
||||
_options = CreateOptions();
|
||||
_strictOptions = CreateOptions(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_ResolvesClrAssemblyQualifiedType()
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize(UnsafeAssemblyQualifiedTypeAlias), _options);
|
||||
|
||||
Assert.Equal(typeof(System.Text.StringBuilder), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_RejectsUnregisteredClrAssemblyQualifiedType_WhenLegacyClrTypeNamesDisabled()
|
||||
{
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize(UnsafeAssemblyQualifiedTypeAlias), _strictOptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_ResolvesTrustedClrAssemblyQualifiedType()
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize(TrustedAssemblyQualifiedTypeAlias), _options);
|
||||
|
||||
Assert.Equal(typeof(UnregisteredPayload), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_ResolvesIncidentStrategyClrType()
|
||||
{
|
||||
var typeAlias = typeof(ContinueWithIncidentsStrategy).GetSimpleAssemblyQualifiedName();
|
||||
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize(typeAlias), _options);
|
||||
|
||||
Assert.Equal(typeof(ContinueWithIncidentsStrategy), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_ResolvesFaultExceptionClrType()
|
||||
{
|
||||
var typeAlias = typeof(FaultException).GetSimpleAssemblyQualifiedName();
|
||||
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize(typeAlias), _options);
|
||||
|
||||
Assert.Equal(typeof(FaultException), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PolymorphicObjectConverter_ResolvesClrAssemblyQualifiedType()
|
||||
{
|
||||
var json = $$"""
|
||||
{
|
||||
"name": "Alice",
|
||||
"_type": {{JsonSerializer.Serialize(typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName())}}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<object>(json, _options);
|
||||
|
||||
var payload = Assert.IsType<UnregisteredPayload>(result);
|
||||
Assert.Equal("Alice", payload.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PolymorphicObjectConverter_RejectsUnregisteredClrAssemblyQualifiedType_WhenLegacyClrTypeNamesDisabled()
|
||||
{
|
||||
var json = $$"""
|
||||
{
|
||||
"name": "Alice",
|
||||
"_type": {{JsonSerializer.Serialize(typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName())}}
|
||||
}
|
||||
""";
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<object>(json, _strictOptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PolymorphicObjectConverter_ResolvesRegisteredTypeAlias()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"name": "Alice",
|
||||
"_type": "RegisteredPayload"
|
||||
}
|
||||
""";
|
||||
|
||||
var result = JsonSerializer.Deserialize<object>(json, _options);
|
||||
|
||||
var payload = Assert.IsType<RegisteredPayload>(result);
|
||||
Assert.Equal("Alice", payload.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PolymorphicObjectConverter_WritesClrTypeMetadataForUnregisteredTypes()
|
||||
{
|
||||
var json = JsonSerializer.Serialize<object>(new UnregisteredPayload { Name = "Alice" }, _options);
|
||||
var result = JsonSerializer.Deserialize<object>(json, _options);
|
||||
|
||||
Assert.Contains("\"_type\"", json);
|
||||
var payload = Assert.IsType<UnregisteredPayload>(result);
|
||||
Assert.Equal("Alice", payload.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PolymorphicObjectConverter_RejectsUnregisteredClrTypeMetadata_WhenLegacyClrTypeNamesDisabled()
|
||||
{
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Serialize<object>(new UnregisteredPayload { Name = "Alice" }, _strictOptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_WritesUnregisteredTypesAsClrTypeNames()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(typeof(UnregisteredPayload), _options);
|
||||
var alias = JsonSerializer.Deserialize<string>(json);
|
||||
var result = JsonSerializer.Deserialize<Type>(json, _options);
|
||||
|
||||
Assert.Equal(typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName(), alias);
|
||||
Assert.Equal(typeof(UnregisteredPayload), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_RejectsUnregisteredClrTypeMetadata_WhenLegacyClrTypeNamesDisabled()
|
||||
{
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Serialize(typeof(UnregisteredPayload), _strictOptions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputJsonConverter_WritesUnregisteredTypesAsClrTypeNames()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
options.Converters.Add(new OutputJsonConverter<UnregisteredPayload>(Microsoft.Extensions.Options.Options.Create(new WorkflowJsonOptions())));
|
||||
var output = new Output<UnregisteredPayload>(new MemoryBlockReference("result"));
|
||||
|
||||
var json = JsonSerializer.Serialize(output, options);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
Assert.Equal(typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName(), doc.RootElement.GetProperty("typeName").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputJsonConverter_RejectsUnregisteredClrTypeMetadata_WhenLegacyClrTypeNamesDisabled()
|
||||
{
|
||||
var workflowJsonOptions = new WorkflowJsonOptions
|
||||
{
|
||||
AllowLegacyClrTypeNames = false
|
||||
};
|
||||
var options = CreateOptions();
|
||||
options.Converters.Add(new OutputJsonConverter<UnregisteredPayload>(Microsoft.Extensions.Options.Options.Create(workflowJsonOptions)));
|
||||
var output = new Output<UnregisteredPayload>(new MemoryBlockReference("result"));
|
||||
|
||||
Assert.Throws<JsonException>(() => JsonSerializer.Serialize(output, options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_ResolvesRegisteredLegacyAssemblyQualifiedName()
|
||||
{
|
||||
var typeAlias = typeof(RegisteredPayload).GetSimpleAssemblyQualifiedName();
|
||||
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize(typeAlias), _strictOptions);
|
||||
|
||||
Assert.Equal(typeof(RegisteredPayload), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_ResolvesRegisteredAliasesCaseInsensitively()
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize("registeredpayload"), _strictOptions);
|
||||
|
||||
Assert.Equal(typeof(RegisteredPayload), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_WritesPreferredAlias_WhenTypeHasMultipleAliases()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(typeof(FlowJoinMode), _strictOptions);
|
||||
|
||||
Assert.Equal("\"FlowJoinMode\"", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeJsonConverter_RoundTripsNullableValueTypeAliases_WhenLegacyClrTypeNamesDisabled()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(typeof(int?), _strictOptions);
|
||||
var result = JsonSerializer.Deserialize<Type>(JsonSerializer.Serialize("int32?"), _strictOptions);
|
||||
|
||||
Assert.Equal("\"Int32?\"", json);
|
||||
Assert.Equal(typeof(int?), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShellWorkflowsFeature_RegistersWorkflowJsonTypeAliases()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
new Elsa.Workflows.ShellFeatures.WorkflowsFeature().ConfigureServices(services);
|
||||
var workflowJsonOptions = services.BuildServiceProvider().GetRequiredService<IOptions<WorkflowJsonOptions>>().Value;
|
||||
|
||||
var faultStrategy = WorkflowJsonTypeResolver.ResolveType(workflowJsonOptions, nameof(FaultStrategy), false);
|
||||
|
||||
Assert.Equal(typeof(FaultStrategy), faultStrategy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShellFlowchartFeature_RegistersFlowScopeTypeAlias()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
new Elsa.Workflows.ShellFeatures.FlowchartFeature().ConfigureServices(services);
|
||||
var workflowJsonOptions = services.BuildServiceProvider().GetRequiredService<IOptions<WorkflowJsonOptions>>().Value;
|
||||
|
||||
var flowScope = WorkflowJsonTypeResolver.ResolveType(workflowJsonOptions, "FlowScope", false);
|
||||
|
||||
Assert.Equal(typeof(FlowScope), flowScope);
|
||||
}
|
||||
|
||||
private JsonSerializerOptions CreateOptions(bool allowLegacyClrTypeNames = true)
|
||||
{
|
||||
var workflowJsonOptions = new WorkflowJsonOptions
|
||||
{
|
||||
AllowLegacyClrTypeNames = allowLegacyClrTypeNames
|
||||
};
|
||||
workflowJsonOptions.RegisterWorkflowTypeAliases();
|
||||
workflowJsonOptions.RegisterTypeAlias(typeof(RegisteredPayload), "RegisteredPayload");
|
||||
workflowJsonOptions.RegisterTypeAlias(typeof(FaultStrategy), nameof(FaultStrategy));
|
||||
|
||||
return new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters =
|
||||
{
|
||||
new PolymorphicObjectConverterFactory(workflowJsonOptions),
|
||||
new TypeJsonConverter(workflowJsonOptions)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class RegisteredPayload
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UnregisteredPayload
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Services;
|
||||
|
||||
public class VariableMapperTests
|
||||
{
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions = new();
|
||||
private readonly SerializationTypeRegistry _registry = new(Microsoft.Extensions.Options.Options.Create(new SerializationTypeOptions()));
|
||||
private readonly VariableMapper _mapper;
|
||||
|
||||
public VariableMapperTests()
|
||||
{
|
||||
_workflowJsonOptions.RegisterTypeAlias(typeof(string), "String");
|
||||
_workflowJsonOptions.RegisterTypeAlias(typeof(WorkflowStorageDriver), nameof(WorkflowStorageDriver));
|
||||
_workflowJsonOptions.RegisterTypeAlias(typeof(MemoryStorageDriver), typeof(MemoryStorageDriver).GetSimpleAssemblyQualifiedName());
|
||||
_mapper = new(NullLogger<VariableMapper>.Instance, _workflowJsonOptions);
|
||||
_registry.RegisterType(typeof(string), "String");
|
||||
_registry.RegisterType(typeof(WorkflowStorageDriver), nameof(WorkflowStorageDriver));
|
||||
_registry.RegisterType(typeof(MemoryStorageDriver), typeof(MemoryStorageDriver).GetSimpleAssemblyQualifiedName());
|
||||
_mapper = new(_registry, NullLogger<VariableMapper>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -38,13 +38,21 @@ public class VariableMapperTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_ResolvesRegisteredStorageDriverAssemblyQualifiedName()
|
||||
public void Map_ResolvesRegisteredMemoryStorageDriverAssemblyQualifiedName()
|
||||
{
|
||||
var variable = _mapper.Map(new VariableModel("id", "name", "String", "value", typeof(MemoryStorageDriver).GetSimpleAssemblyQualifiedName()));
|
||||
|
||||
Assert.Equal(typeof(MemoryStorageDriver), variable.StorageDriverType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_WritesRegisteredStorageDriverAlias()
|
||||
{
|
||||
var model = _mapper.Map(new Variable<string>("name", "") { StorageDriverType = typeof(WorkflowStorageDriver) });
|
||||
|
||||
Assert.Equal(nameof(WorkflowStorageDriver), model.StorageDriverTypeName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_DoesNotLoadUnregisteredStorageDriverAssemblyQualifiedName()
|
||||
{
|
||||
|
|
@ -56,37 +64,10 @@ public class VariableMapperTests
|
|||
[Fact]
|
||||
public void Map_DoesNotUseRegisteredNonStorageDriverAliasAsStorageDriver()
|
||||
{
|
||||
_workflowJsonOptions.RegisterTypeAlias(typeof(string), "NotAStorageDriver");
|
||||
_registry.RegisterType(typeof(string), "NotAStorageDriver");
|
||||
|
||||
var variable = _mapper.Map(new VariableModel("id", "name", "String", "value", "NotAStorageDriver"));
|
||||
|
||||
Assert.Null(variable.StorageDriverType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_WritesUnregisteredVariableTypeAsClrTypeName()
|
||||
{
|
||||
var variable = new Variable<UnregisteredPayload>("payload", new());
|
||||
|
||||
var model = _mapper.Map(variable);
|
||||
|
||||
Assert.Equal(typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName(), model.TypeName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_RejectsUnregisteredVariableType_WhenLegacyClrTypeNamesDisabled()
|
||||
{
|
||||
var workflowJsonOptions = new WorkflowJsonOptions
|
||||
{
|
||||
AllowLegacyClrTypeNames = false
|
||||
};
|
||||
var mapper = new VariableMapper(NullLogger<VariableMapper>.Instance, workflowJsonOptions);
|
||||
var variable = new Variable<UnregisteredPayload>("payload", new());
|
||||
|
||||
Assert.Throws<JsonException>(() => mapper.Map(variable));
|
||||
}
|
||||
|
||||
private sealed class UnregisteredPayload
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,51 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Management.Mappers;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Management.UnitTests.Mappers;
|
||||
|
||||
public class VariableDefinitionMapperTests
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory = Substitute.For<IServiceScopeFactory>();
|
||||
private readonly WorkflowJsonOptions _workflowJsonOptions = new();
|
||||
private readonly SerializationTypeOptions _workflowJsonTypeOptions = new();
|
||||
private readonly VariableDefinitionMapper _mapper;
|
||||
|
||||
public VariableDefinitionMapperTests()
|
||||
{
|
||||
_workflowJsonOptions.RegisterTypeAlias(typeof(string), "String");
|
||||
_workflowJsonOptions.RegisterTypeAlias(typeof(MemoryStorageDriver), nameof(MemoryStorageDriver));
|
||||
_mapper = CreateMapper(_workflowJsonOptions);
|
||||
_workflowJsonTypeOptions.RegisterTypeAlias(typeof(string), "String");
|
||||
_workflowJsonTypeOptions.RegisterTypeAlias(typeof(MemoryStorageDriver), nameof(MemoryStorageDriver));
|
||||
_mapper = CreateMapper(_workflowJsonTypeOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_ResolvesUnregisteredClrTypeName_WhenLegacyClrTypeNamesEnabled()
|
||||
public void Map_DoesNotResolveUnregisteredClrTypeName()
|
||||
{
|
||||
var definition = new VariableDefinition("id", "payload", typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName(), false, null, null);
|
||||
|
||||
var variable = _mapper.Map(definition);
|
||||
|
||||
Assert.IsType<Variable<UnregisteredPayload>>(variable);
|
||||
Assert.Null(variable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_DoesNotResolveUnregisteredClrTypeName_WhenLegacyClrTypeNamesDisabled()
|
||||
public void Map_ResolvesRegisteredLegacyClrTypeName()
|
||||
{
|
||||
var workflowJsonOptions = new WorkflowJsonOptions
|
||||
{
|
||||
AllowLegacyClrTypeNames = false
|
||||
};
|
||||
var mapper = CreateMapper(workflowJsonOptions);
|
||||
var workflowJsonTypeOptions = new SerializationTypeOptions();
|
||||
workflowJsonTypeOptions.RegisterTypeAlias(typeof(UnregisteredPayload), nameof(UnregisteredPayload));
|
||||
workflowJsonTypeOptions.RegisterLegacySimpleAssemblyQualifiedName(typeof(UnregisteredPayload));
|
||||
var mapper = CreateMapper(workflowJsonTypeOptions);
|
||||
var definition = new VariableDefinition("id", "payload", typeof(UnregisteredPayload).GetSimpleAssemblyQualifiedName(), false, null, null);
|
||||
|
||||
var variable = mapper.Map(definition);
|
||||
|
||||
Assert.Null(variable);
|
||||
Assert.IsType<Variable<UnregisteredPayload>>(variable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -59,21 +59,22 @@ public class VariableDefinitionMapperTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_RejectsUnregisteredVariableType_WhenLegacyClrTypeNamesDisabled()
|
||||
public void Map_WritesRegisteredVariableTypeAsAlias()
|
||||
{
|
||||
var workflowJsonOptions = new WorkflowJsonOptions
|
||||
{
|
||||
AllowLegacyClrTypeNames = false
|
||||
};
|
||||
var mapper = CreateMapper(workflowJsonOptions);
|
||||
var workflowJsonTypeOptions = new SerializationTypeOptions();
|
||||
workflowJsonTypeOptions.RegisterTypeAlias(typeof(UnregisteredPayload), nameof(UnregisteredPayload));
|
||||
var mapper = CreateMapper(workflowJsonTypeOptions);
|
||||
var variable = new Variable<UnregisteredPayload>("payload", new());
|
||||
|
||||
Assert.Throws<JsonException>(() => mapper.Map(variable));
|
||||
var definition = mapper.Map(variable);
|
||||
|
||||
Assert.Equal(nameof(UnregisteredPayload), definition.TypeName);
|
||||
}
|
||||
|
||||
private VariableDefinitionMapper CreateMapper(WorkflowJsonOptions workflowJsonOptions)
|
||||
private VariableDefinitionMapper CreateMapper(SerializationTypeOptions workflowJsonTypeOptions)
|
||||
{
|
||||
return new(Microsoft.Extensions.Options.Options.Create(workflowJsonOptions), _scopeFactory, NullLogger<VariableDefinitionMapper>.Instance);
|
||||
var workflowJsonTypeRegistry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(workflowJsonTypeOptions));
|
||||
return new(workflowJsonTypeRegistry, _scopeFactory, NullLogger<VariableDefinitionMapper>.Instance);
|
||||
}
|
||||
|
||||
private sealed class UnregisteredPayload
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ using Elsa.Workflows.Management.Filters;
|
|||
using Elsa.Workflows.Management.Mappers;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Management.UnitTests.Services;
|
||||
|
||||
|
|
@ -61,7 +62,8 @@ public class WorkflowDefinitionExporterRegressionTests
|
|||
{
|
||||
var activitySerializer = Substitute.For<IActivitySerializer>();
|
||||
var scopeFactory = Substitute.For<IServiceScopeFactory>();
|
||||
var variableDefinitionMapper = new VariableDefinitionMapper(Microsoft.Extensions.Options.Options.Create(new WorkflowJsonOptions()), scopeFactory, NullLogger<VariableDefinitionMapper>.Instance);
|
||||
var workflowJsonTypeRegistry = SerializationTypeRegistry.CreateDefault();
|
||||
var variableDefinitionMapper = new VariableDefinitionMapper(workflowJsonTypeRegistry, scopeFactory, NullLogger<VariableDefinitionMapper>.Instance);
|
||||
var workflowDefinitionMapper = new WorkflowDefinitionMapper(activitySerializer, _workflowDefinitionService, variableDefinitionMapper);
|
||||
var fileNameSanitizer = new DefaultFileNameSanitizer();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Runtime.Comparers;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Serialization.Converters;
|
||||
using Elsa.Workflows.Serialization.Options;
|
||||
using Elsa.Workflows.Services;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.UnitTests.Comparers;
|
||||
|
||||
|
|
@ -27,8 +28,8 @@ public class WorkflowTriggerEqualityComparerTests
|
|||
/// A simple payload class that mimics real trigger payloads like HttpEndpointBookmarkPayload.
|
||||
/// </summary>
|
||||
private record TestPayload(string Path, string Method);
|
||||
|
||||
private record TypePayload(Type TargetType);
|
||||
private record TypedPayload(Type PayloadType, object Value);
|
||||
private record NestedPayload(string Name);
|
||||
|
||||
[Fact(DisplayName = "Fresh and round-tripped triggers with identical logical content should be considered equal")]
|
||||
public void FreshAndRoundTrippedTriggers_ShouldBeEqual()
|
||||
|
|
@ -41,7 +42,7 @@ public class WorkflowTriggerEqualityComparerTests
|
|||
var roundTrippedPayload = SimulatePayloadRoundTrip(freshPayload);
|
||||
var loadedTrigger = CreateTrigger("trigger-1", roundTrippedPayload);
|
||||
|
||||
var comparer = new WorkflowTriggerEqualityComparer();
|
||||
var comparer = new WorkflowTriggerEqualityComparer(CreateTypeRegistry());
|
||||
|
||||
// Act
|
||||
var areEqual = comparer.Equals(freshTrigger, loadedTrigger);
|
||||
|
|
@ -72,7 +73,7 @@ public class WorkflowTriggerEqualityComparerTests
|
|||
var newTriggers = new List<StoredTrigger> { freshTrigger };
|
||||
|
||||
// Act: this is exactly what TriggerIndexer.IndexTriggersInternalAsync does
|
||||
var diff = Diff.For(currentTriggers, newTriggers, new WorkflowTriggerEqualityComparer());
|
||||
var diff = Diff.For(currentTriggers, newTriggers, new WorkflowTriggerEqualityComparer(CreateTypeRegistry()));
|
||||
|
||||
// Assert: the diff should find no changes.
|
||||
// Before the fix, it reported Removed=[existingTrigger] and Added=[freshTrigger]
|
||||
|
|
@ -82,22 +83,6 @@ public class WorkflowTriggerEqualityComparerTests
|
|||
Assert.Single(diff.Unchanged);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Comparer should use configured type aliases when normalizing trigger payloads")]
|
||||
public void FreshAndRoundTrippedTriggers_WithRegisteredTypeAlias_ShouldBeEqual()
|
||||
{
|
||||
var workflowJsonOptions = new WorkflowJsonOptions();
|
||||
workflowJsonOptions.RegisterTypeAlias(typeof(CustomAliasTarget), "CustomAlias");
|
||||
var payload = new TypePayload(typeof(CustomAliasTarget));
|
||||
var roundTrippedPayload = SimulatePayloadRoundTrip(payload, workflowJsonOptions);
|
||||
var freshTrigger = CreateTrigger("trigger-1", payload);
|
||||
var loadedTrigger = CreateTrigger("trigger-1", roundTrippedPayload);
|
||||
var comparer = new WorkflowTriggerEqualityComparer(workflowJsonOptions);
|
||||
|
||||
var areEqual = comparer.Equals(freshTrigger, loadedTrigger);
|
||||
|
||||
Assert.True(areEqual);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Documents the underlying System.Text.Json casing behavior that necessitated the fix")]
|
||||
public void PayloadSerializationMismatch_ProducesDifferentJson()
|
||||
{
|
||||
|
|
@ -126,6 +111,50 @@ public class WorkflowTriggerEqualityComparerTests
|
|||
Assert.Equal("{\"Path\":\"/api/test\",\"Method\":\"GET\"}", freshJson);
|
||||
Assert.Equal("{\"path\":\"/api/test\",\"method\":\"GET\"}", roundTrippedJson);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Comparer serializes typed payload properties using the registered type aliases")]
|
||||
public void TypedPayloadProperties_ShouldSerializeWithRegisteredAliases()
|
||||
{
|
||||
var payload = new TypedPayload(typeof(NestedPayload), new NestedPayload("orders"));
|
||||
var comparer = new WorkflowTriggerEqualityComparer(CreateTypeRegistry());
|
||||
var trigger = CreateTrigger("trigger-1", payload);
|
||||
var expectedAliasPayload = JsonSerializer.Deserialize<object>(
|
||||
"""
|
||||
{
|
||||
"payloadType": "NestedPayload",
|
||||
"value": {
|
||||
"name": "orders",
|
||||
"_type": "NestedPayload"
|
||||
}
|
||||
}
|
||||
""",
|
||||
PayloadSerializerOptions);
|
||||
var assemblyQualifiedPayload = JsonSerializer.Deserialize<object>(
|
||||
$$"""
|
||||
{
|
||||
"payloadType": "{{typeof(NestedPayload).AssemblyQualifiedName}}",
|
||||
"value": {
|
||||
"name": "orders",
|
||||
"_type": "{{typeof(NestedPayload).AssemblyQualifiedName}}"
|
||||
}
|
||||
}
|
||||
""",
|
||||
PayloadSerializerOptions);
|
||||
var missingTypePayload = JsonSerializer.Deserialize<object>(
|
||||
"""
|
||||
{
|
||||
"payloadType": "NestedPayload",
|
||||
"value": {
|
||||
"name": "orders"
|
||||
}
|
||||
}
|
||||
""",
|
||||
PayloadSerializerOptions);
|
||||
|
||||
Assert.True(comparer.Equals(trigger, CreateTrigger("trigger-2", expectedAliasPayload!)));
|
||||
Assert.False(comparer.Equals(trigger, CreateTrigger("trigger-3", assemblyQualifiedPayload!)));
|
||||
Assert.False(comparer.Equals(trigger, CreateTrigger("trigger-4", missingTypePayload!)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IPayloadSerializer options: camelCase with case-insensitive deserialization.
|
||||
|
|
@ -162,20 +191,13 @@ public class WorkflowTriggerEqualityComparerTests
|
|||
return deserialized!;
|
||||
}
|
||||
|
||||
private static object SimulatePayloadRoundTrip(object payload, WorkflowJsonOptions workflowJsonOptions)
|
||||
private static ISerializationTypeRegistry CreateTypeRegistry()
|
||||
{
|
||||
var options = CreatePayloadSerializerOptions(workflowJsonOptions);
|
||||
var json = JsonSerializer.Serialize(payload, options);
|
||||
return JsonSerializer.Deserialize<object>(json, options)!;
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreatePayloadSerializerOptions(WorkflowJsonOptions workflowJsonOptions)
|
||||
{
|
||||
var options = new JsonSerializerOptions(PayloadSerializerOptions);
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
options.Converters.Add(new PolymorphicObjectConverterFactory(workflowJsonOptions));
|
||||
options.Converters.Add(new TypeJsonConverter(workflowJsonOptions));
|
||||
return options;
|
||||
var registry = new SerializationTypeRegistry(Microsoft.Extensions.Options.Options.Create(new SerializationTypeOptions()));
|
||||
registry.RegisterType(typeof(TestPayload), nameof(TestPayload));
|
||||
registry.RegisterType(typeof(TypedPayload), nameof(TypedPayload));
|
||||
registry.RegisterType(typeof(NestedPayload), nameof(NestedPayload));
|
||||
return registry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -198,6 +220,4 @@ public class WorkflowTriggerEqualityComparerTests
|
|||
Hash = hash,
|
||||
Payload = payload
|
||||
};
|
||||
|
||||
private sealed class CustomAliasTarget;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
using System.Reflection;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Features;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Runtime.Options;
|
||||
using Elsa.Workflows.Runtime.Providers;
|
||||
using NSubstitute;
|
||||
using RuntimeFeature = Elsa.Workflows.Runtime.Features.WorkflowRuntimeFeature;
|
||||
using ShellRuntimeFeature = Elsa.Workflows.Runtime.ShellFeatures.WorkflowRuntimeFeature;
|
||||
using Elsa.Common.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.UnitTests.Features;
|
||||
|
||||
public class WorkflowRuntimeFeatureTests
|
||||
{
|
||||
private readonly RuntimeFeature _feature = new(Substitute.For<IModule>());
|
||||
private readonly ShellRuntimeFeature _shellFeature = new();
|
||||
|
||||
[Fact]
|
||||
public void AddWorkflow_Throws_WhenTypeDoesNotImplementWorkflow()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _feature.AddWorkflow(typeof(NotAWorkflow)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(NonInstantiableWorkflowTypes))]
|
||||
public void AddWorkflow_Throws_WhenWorkflowTypeIsNotInstantiable(Type workflowType)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _feature.AddWorkflow(workflowType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShellAddWorkflow_Throws_WhenTypeDoesNotImplementWorkflow()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _shellFeature.AddWorkflow(typeof(NotAWorkflow)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(NonInstantiableWorkflowTypes))]
|
||||
public void ShellAddWorkflow_Throws_WhenWorkflowTypeIsNotInstantiable(Type workflowType)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _shellFeature.AddWorkflow(workflowType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddWorkflow_AllowsClosedGenericWorkflowType()
|
||||
{
|
||||
var workflowType = typeof(GenericWorkflow<int>);
|
||||
|
||||
_feature.AddWorkflow(workflowType);
|
||||
|
||||
Assert.Contains(workflowType.GetSimpleAssemblyQualifiedName(), _feature.Workflows.Keys);
|
||||
Assert.Contains(workflowType.FullName!, _feature.Workflows.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShellAddWorkflow_AllowsClosedGenericWorkflowType()
|
||||
{
|
||||
var workflowType = typeof(GenericWorkflow<int>);
|
||||
|
||||
_shellFeature.AddWorkflow(workflowType);
|
||||
|
||||
Assert.Contains(workflowType.GetSimpleAssemblyQualifiedName(), _shellFeature.Workflows.Keys);
|
||||
Assert.Contains(workflowType.FullName!, _shellFeature.Workflows.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkflowsAdd_RegistersWorkflowTypeAlias()
|
||||
{
|
||||
var workflowType = typeof(GenericWorkflow<int>);
|
||||
var options = new SerializationTypeOptions();
|
||||
|
||||
_feature.Workflows.Add(workflowType);
|
||||
|
||||
RegisterWorkflowTypeAliases(_feature, options);
|
||||
|
||||
Assert.Equal(workflowType, options.AliasTypeDictionary[workflowType.GetSimpleAssemblyQualifiedName()]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkflowsAdd_DoesNotThrow_WhenLegacyKeyAlreadyExists()
|
||||
{
|
||||
var workflowType = typeof(GenericWorkflow<int>);
|
||||
_feature.Workflows.Add(workflowType.FullName!, _ => new ValueTask<IWorkflow>(new GenericWorkflow<int>()));
|
||||
|
||||
_feature.Workflows.Add(workflowType);
|
||||
|
||||
Assert.Contains(workflowType.GetSimpleAssemblyQualifiedName(), _feature.Workflows.Keys);
|
||||
Assert.Contains(workflowType.FullName!, _feature.Workflows.Keys);
|
||||
Assert.Same(_feature.Workflows[workflowType.FullName!], _feature.Workflows[workflowType.GetSimpleAssemblyQualifiedName()]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClrWorkflowsProvider_MaterializesWorkflowOnce_WhenCanonicalAndLegacyKeysExist()
|
||||
{
|
||||
CountingWorkflow.CreatedCount = 0;
|
||||
var builder = Substitute.For<IWorkflowBuilder>();
|
||||
var builderFactory = Substitute.For<IWorkflowBuilderFactory>();
|
||||
var provider = new ClrWorkflowsProvider(
|
||||
Microsoft.Extensions.Options.Options.Create(new RuntimeOptions { Workflows = _feature.Workflows }),
|
||||
builderFactory,
|
||||
Substitute.For<IServiceProvider>());
|
||||
builderFactory.CreateBuilder().Returns(builder);
|
||||
builder.BuildWorkflowAsync(Arg.Any<CancellationToken>()).Returns(new Workflow());
|
||||
|
||||
_feature.Workflows.Add(typeof(CountingWorkflow));
|
||||
var workflows = await provider.GetWorkflowsAsync();
|
||||
|
||||
Assert.Single(workflows);
|
||||
Assert.Equal(1, CountingWorkflow.CreatedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShellWorkflowsAdd_RegistersWorkflowTypeAlias()
|
||||
{
|
||||
var workflowType = typeof(GenericWorkflow<int>);
|
||||
var options = new SerializationTypeOptions();
|
||||
|
||||
_shellFeature.Workflows.Add(workflowType);
|
||||
|
||||
RegisterWorkflowTypeAliases(_shellFeature, options);
|
||||
|
||||
Assert.Equal(workflowType, options.AliasTypeDictionary[workflowType.GetSimpleAssemblyQualifiedName()]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkflowsAdd_Throws_WhenTypeDoesNotImplementWorkflow()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _feature.Workflows.Add(typeof(NotAWorkflow)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeFeature_DependsOnWorkflowsFeature()
|
||||
{
|
||||
var dependencyTypes = typeof(RuntimeFeature)
|
||||
.GetCustomAttributes<DependsOnAttribute>()
|
||||
.Select(x => x.Type);
|
||||
|
||||
Assert.Contains(typeof(WorkflowsFeature), dependencyTypes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterWorkflowTypeAliases_RegistersOnlyTrackedWorkflowTypes()
|
||||
{
|
||||
var workflowType = typeof(GenericWorkflow<int>);
|
||||
var options = new SerializationTypeOptions();
|
||||
_feature.AddWorkflow(workflowType);
|
||||
_feature.Workflows[typeof(NotAWorkflow).AssemblyQualifiedName!] = _ => new ValueTask<IWorkflow>(new GenericWorkflow<int>());
|
||||
|
||||
RegisterWorkflowTypeAliases(_feature, options);
|
||||
|
||||
Assert.Equal(workflowType, options.AliasTypeDictionary[workflowType.GetSimpleAssemblyQualifiedName()]);
|
||||
Assert.DoesNotContain(typeof(NotAWorkflow).AssemblyQualifiedName!, options.AliasTypeDictionary.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShellRegisterWorkflowTypeAliases_RegistersOnlyTrackedWorkflowTypes()
|
||||
{
|
||||
var workflowType = typeof(GenericWorkflow<int>);
|
||||
var options = new SerializationTypeOptions();
|
||||
_shellFeature.AddWorkflow(workflowType);
|
||||
_shellFeature.Workflows[typeof(NotAWorkflow).AssemblyQualifiedName!] = _ => new ValueTask<IWorkflow>(new GenericWorkflow<int>());
|
||||
|
||||
RegisterWorkflowTypeAliases(_shellFeature, options);
|
||||
|
||||
Assert.Equal(workflowType, options.AliasTypeDictionary[workflowType.GetSimpleAssemblyQualifiedName()]);
|
||||
Assert.DoesNotContain(typeof(NotAWorkflow).AssemblyQualifiedName!, options.AliasTypeDictionary.Keys);
|
||||
}
|
||||
|
||||
public static TheoryData<Type> NonInstantiableWorkflowTypes() => new()
|
||||
{
|
||||
typeof(IWorkflow),
|
||||
typeof(WorkflowBase),
|
||||
typeof(GenericWorkflow<>)
|
||||
};
|
||||
|
||||
private sealed class NotAWorkflow
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class GenericWorkflow<T> : IWorkflow
|
||||
{
|
||||
public ValueTask BuildAsync(IWorkflowBuilder builder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CountingWorkflow : IWorkflow
|
||||
{
|
||||
public static int CreatedCount { get; set; }
|
||||
|
||||
public CountingWorkflow()
|
||||
{
|
||||
CreatedCount++;
|
||||
}
|
||||
|
||||
public ValueTask BuildAsync(IWorkflowBuilder builder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterWorkflowTypeAliases(object feature, SerializationTypeOptions options)
|
||||
{
|
||||
feature.GetType()
|
||||
.GetMethod("RegisterWorkflowTypeAliases", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(feature, new object[] { options });
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue