Adds the Persistence vNext proof of concept, including provider-neutral schema/document abstractions, relational and document provider POCs, Elsa integration, runtime-defined entities, physicalization planning, workflow runtime evaluation, and focused tests.
15 KiB
Persistence vNext Roadmap
Vision
Persistence vNext should let modular applications declare persistence intent once and let database providers materialize that intent using the best physical model available to them.
For Elsa, this means modules can contribute storage manifests, indexes, and runtime entity definitions without owning SQL Server, SQLite, PostgreSQL, Oracle, MySQL, MongoDB, or EF Core migration projects.
The long-term model is:
- portable by default
- provider-optimized when needed
- document-first for dynamic/runtime entities
- explicit-index driven for portable queries
- extensible enough for relational and document databases
- independent enough to become reusable outside Elsa
Non-Goals
Persistence vNext should not become:
- a universal ORM
- a LINQ provider
- an EF Core replacement for all data access
- a lowest-common-denominator database API
- a reporting/BI engine
- a magic cross-database query translator
Its job is to own modular persistence manifests, schema/index materialization, versioning, and a focused document/index query model.
Design Principles
- Modules declare intent, providers own physical shape.
- Schema authority is the manifest, not EF Core migrations.
- Runtime-defined entities use document storage, not table-per-entity by default.
- Portable queries run through declared indexes.
- Advanced provider behavior is exposed through capabilities and opt-in policies.
- Provider-specific escape hatches are acceptable when explicit.
- The core abstractions must not depend on Elsa, EF Core, MongoDB, or any one provider.
- Start with simple, reliable append-only versioning before automatic diffing.
- Optimize hot paths without sacrificing the default portable path.
Target Package Shape
General-Purpose Packages
-
ModularPersistence.Abstractions- storage manifests
- storage units
- fields
- indexes
- versions
- provider capabilities
- planner, renderer, migrator contracts
-
ModularPersistence.Document- document descriptors
- collection/index plans
- portable document session abstractions
- index-first query model
-
ModularPersistence.Relational- relational plans
- SQL operation model
- relational schema history conventions
- SQL execution abstractions
-
ModularPersistence.Sqlite- SQLite type mapping
- SQL rendering
- schema history
- portable document store implementation
-
ModularPersistence.SqlServer- SQL Server type mapping
- SQL rendering
- schema history
- optimized index strategies
-
ModularPersistence.PostgreSql- PostgreSQL type mapping
- JSONB options
- expression/generated indexes where useful
-
ModularPersistence.MongoDb- collection/index planning
- native MongoDB indexes
- document session implementation
- optional schema validation
Elsa Packages
-
Elsa.Persistence.VNext- Elsa module integration
- shell feature wiring
- options
- module discovery
- startup migration orchestration
-
Elsa.Persistence.VNext.RuntimeEntities- runtime entity definitions
- admin-defined schema lifecycle
- generated API/UI integration points
- permissions and audit hooks
Core Concepts
Storage Manifest
A module or runtime admin defines:
- schema name
- schema version
- storage units
- fields
- keys
- indexes
- validation metadata
- provider requirements
- optional physicalization policy
Storage Unit
A storage unit is provider-neutral. Providers map it differently:
- relational: table or document table/index table
- MongoDB: collection or shared collection
- Cosmos DB: container/partition strategy
- in-memory: dictionary plus indexes
Document Store
Runtime entities and many module aggregates should be stored as documents:
IdTypeTenantIdVersionCreatedAtUpdatedAtData- optional metadata
Indexes
Indexes are the portable query surface.
Portable queries should only target declared indexes. Providers decide how to implement them:
- relational generic field index table
- relational typed index table
- SQL generated/computed column
- MongoDB native index
- in-memory lookup table
Physicalization Policy
Each storage unit or index can choose a policy:
-
PortableDocument- safest default
- shared document table/collection plus generic indexes
-
OptimizedIndexes- provider creates stronger typed/materialized indexes
- still document-backed
-
NativePhysicalized- provider may create typed tables, collection-per-entity, generated columns, JSONB expression indexes, etc.
- opt-in for hot paths
Roadmap
Phase 0: Research And Validation
Goal: validate the design against Elsa's real persistence surface.
Tasks:
- Inventory current EF Core modules, stores, migrations, and provider packages.
- Classify stores into document-friendly, relational-specific, and high-performance hot paths.
- Identify query patterns per module.
- Compare YesSQL, Marten, MongoDB, EF Core, FluentMigrator, Liquibase, and Orchard Core data migrations.
- Decide which Elsa module is the first production candidate.
Exit criteria:
- Persistence inventory completed.
- First production module candidate selected.
- Provider priorities selected.
Recommended first candidates:
- Secrets
- Labels
- Tenants
- simple diagnostics stores
Avoid first:
- workflow runtime hot paths
- workflow instance queries
- execution log history
Phase 1: Extract Clean Core Abstractions
Goal: turn the POC into clean reusable contracts.
Tasks:
- Rename POC concepts into neutral package names.
- Separate core abstractions from Elsa.
- Finalize descriptor vocabulary:
- manifest
- storage unit
- field
- key
- index
- version
- capability
- Add provider capability checks.
- Add validation errors and diagnostics.
- Add contract tests for descriptors and planners.
Exit criteria:
- No Elsa dependencies in core packages.
- Descriptors support relational and document planning.
- Provider capability validation exists.
- SQLite and SQL Server render tests still pass.
Phase 2: Portable Document Store MVP
Goal: implement a YesSQL-like portable document store over SQLite first.
Tasks:
- Define
IDocumentStore. - Define document envelope:
IdStorageUnitVersionContent- timestamps
- Define explicit save and query request records.
- Implement SQLite document table.
- Implement generic field index table.
- Implement save/load/delete.
- Implement index maintenance in the same transaction.
- Implement basic optimistic concurrency.
Exit criteria:
- Can store and load documents.
- Can maintain declared indexes.
- Can query by declared indexes.
- Startup materialization is idempotent.
Phase 3: Portable Query Model
Goal: define useful queries without becoming an ORM.
Tasks:
- Define index-first query API.
- Support:
- equals
- not equals
- in
- range
- starts-with where supported
- null checks
- sorting
- paging
- Add provider capability validation for unsupported operations.
- Add query-plan diagnostics.
- Add tests ensuring unindexed portable queries fail clearly.
Exit criteria:
- Modules can query common indexes portably.
- Unsupported query shapes produce clear errors.
- Query API does not expose provider-specific behavior by accident.
Phase 4: SQL Server And PostgreSQL Providers
Goal: prove the relational provider model across serious relational engines.
Tasks:
- Implement SQL Server document store.
- Implement PostgreSQL document store.
- Support generic indexes.
- Add provider-specific optimized index options:
- SQL Server computed columns where useful
- PostgreSQL JSONB/expression indexes where useful
- Add transaction and locking strategy.
- Add provider integration tests.
Exit criteria:
- Same document/index tests pass on SQLite, SQL Server, and PostgreSQL.
- Provider differences are isolated to provider packages.
- Startup race conditions are handled.
Phase 5: MongoDB Provider
Goal: prove native document database support.
Tasks:
- Implement MongoDB document store.
- Decide shared collection versus collection-per-type defaults.
- Map storage units to collections.
- Map declared indexes to MongoDB indexes.
- Add optimistic concurrency.
- Add basic transaction support when available.
- Add provider capability checks for unsupported relational assumptions.
Exit criteria:
- Same document/index tests pass on MongoDB.
- MongoDB uses native indexes.
- No relational tables/index assumptions leak into the document provider.
Phase 6: Elsa Integration
Goal: consume the general packages from Elsa.
Tasks:
- Add Elsa-specific registration package.
- Let modules register manifests through module features.
- Add startup materialization service.
- Add shell feature support.
- Add options for selected provider and connection settings.
- Add diagnostics endpoint for applied manifests and provider status.
Exit criteria:
- An Elsa module can declare storage intent without EF Core.
- Elsa startup materializes the selected provider.
- Applied versions are observable.
Phase 7: First Elsa Module Migration
Goal: migrate one real module safely.
Recommended module: Secrets.
Tasks:
- Implement Secrets document store.
- Preserve existing API behavior.
- Add import path from EF Core Secrets tables if needed.
- Add dual-read or explicit migration utility if required.
- Add provider tests for SQLite, SQL Server, and MongoDB.
Exit criteria:
- Secrets works without EF Core migrations.
- Existing tests pass.
- Provider-specific persistence packages are no longer needed for Secrets vNext.
Phase 8: Runtime-Defined Entities
Goal: let admins define entities at runtime.
Tasks:
- Define
RuntimeEntityDefinition. - Add draft/published/retired lifecycle.
- Add validation rules.
- Add index definitions.
- Add provider capability checks before publish.
- Materialize published definitions.
- Generate CRUD endpoints.
- Expose admin UI integration points.
- Add audit trail for schema changes.
Exit criteria:
- Admin can define an entity.
- System validates and publishes it.
- Data can be created, queried by indexed fields, updated, and deleted.
- No physical table per entity is required by default.
Phase 9: Physicalization And Performance
Goal: optimize hot entities and indexes without giving up portability.
Tasks:
- Add storage policy:
PortableDocumentOptimizedIndexesNativePhysicalized
- Add provider-specific physicalization planners.
- Add operational commands to promote/demote indexes.
- Add benchmarks.
- Add guidance for when to physicalize.
Exit criteria:
- Portable defaults remain simple.
- Hot-path indexes can be optimized.
- Provider-specific behavior is explicit and observable.
Local progress note: the POC now includes provider-neutral physicalization policies with schema validation plus SQLite and MongoDB planners. SQLite emits dedicated table/index DDL operations; MongoDB emits dedicated collection/native-index operations. This proves the extension point, but representative benchmarks and operational promote/demote commands are still needed.
Phase 10: Workflow Runtime Evaluation
Goal: determine whether high-throughput workflow runtime persistence should use vNext.
Tasks:
- Benchmark workflow instance queries.
- Benchmark bookmark and trigger queries.
- Evaluate lock/concurrency semantics.
- Evaluate event/log volume storage.
- Decide where specialized persistence remains better.
Exit criteria:
- Clear decision for runtime stores.
- No premature migration of hot workflow internals.
Likely outcome:
- Management/catalog/metadata stores are good candidates.
- Runtime execution hot paths may need specialized providers or optimized physicalization.
Local progress note: the POC includes a documented go/no-go decision and evaluator. The current recommendation is to use vNext for management/module metadata first, gate hot lookups behind benchmarks and physicalization, and keep queues/logs on specialized providers until runtime contract tests prove otherwise.
Phase 11: Hardening
Goal: make the framework production-safe.
Tasks:
- Startup locking.
- Transaction strategy.
- Retry strategy.
- Idempotency guarantees.
- Version history repair tools.
- Provider diagnostics.
- Backup/restore guidance.
- Multi-tenant isolation model.
- Security and permission model for runtime schemas.
- Roll-forward migration guidance.
Exit criteria:
- Safe concurrent startup.
- Clear recovery story.
- Provider failures are diagnosable.
Phase 12: General-Purpose OSS Extraction
Goal: publish the reusable framework outside Elsa.
Tasks:
- Finalize package names.
- Remove Elsa-specific concepts.
- Add examples:
- ASP.NET Core modular monolith
- SQLite
- SQL Server
- MongoDB
- Add documentation.
- Add provider compatibility matrix.
- Define semantic versioning policy.
Exit criteria:
- Elsa consumes the general packages.
- Non-Elsa sample app works.
- Public API is stable enough for external users.
Migration Strategy For Elsa
- Keep current EF Core persistence intact.
- Introduce vNext side-by-side.
- Use vNext for new experimental modules first.
- Migrate small modules next.
- Provide import utilities for existing data.
- Avoid dual-write unless absolutely necessary.
- Do not migrate runtime hot paths until benchmarks justify it.
Major Risks
- Query abstraction grows too large.
- Provider capabilities become hard to reason about.
- Document indexes become expensive on relational providers.
- Runtime-defined entities introduce product/security complexity.
- Automatic diffs become unreliable.
- Physicalization policies create operational burden.
- MongoDB and relational transaction semantics diverge.
Key Decisions To Make Early
- Package naming and ownership.
- Whether this starts as Elsa-internal or separate repo.
- First production Elsa module.
- Provider priority order.
- Whether PostgreSQL or SQL Server follows SQLite.
- MongoDB shared collection versus collection-per-type default.
- How much query expressiveness is allowed in portable mode.
- Runtime entity UI scope.
Suggested Execution Order
- Finish core abstractions in the POC.
- Build SQLite document store.
- Convert Secrets to use the document store.
- Add SQL Server provider.
- Add MongoDB provider.
- Add Elsa integration.
- Add runtime-defined entities.
- Add physicalization policies.
- Decide whether to extract as OSS.
Success Criteria
This initiative succeeds if:
- modules declare persistence once
- provider-specific migrations are no longer required per module
- relational and MongoDB providers both work
- runtime-defined entities can be stored without table-per-entity
- portable queries are predictable and index-driven
- hot paths have opt-in physicalization
- Elsa remains database agnostic
- the reusable core stays small enough to understand