* Enable multitenancy support and normalize tenant ID handling. - Activate multitenancy in `Program.cs`. - Introduce `NormalizeTenantId` method for consistent tenant ID usage. - Update tenant-related classes and features to support normalization logic. * Add ADR for adopting empty string as the default tenant ID - Standardized the tenant ID for the default tenant to use an empty string (`""`) instead of `null`. - Documented the rationale and migration considerations in ADR 0007. - Updated ADR table of contents and graph for new entry. * Apply suggestion from @sfmskywalker * Update doc/adr/graph.dot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Normalize spacing and improve readability in `Program.cs`. Fix multitenancy condition formatting. * Fix ADR numbering and update TOC * Add ADRs for flowchart execution model, tenant deletion event, merge modes, and default tenant ID - Introduced ADR 0005: Token-centric flowchart execution model for improved loop and join handling. - Added ADR 0006: Tenant Deleted event for distinct handling of tenant removal. - Documented ADR 0007: Explicit merge modes for flowchart joins, improving reliability and configurability. - Included ADR 0008: Standardization of empty string as the default tenant ID for consistency and clarity. * Add unit tests for tenant ID normalization and multitenancy pipeline invoker - Added comprehensive unit tests for tenant ID normalization to ensure consistent handling of null, empty, and valid IDs. - Introduced tests for the multitenancy pipeline invoker covering various tenant resolution scenarios. - Updated solution to include new unit testing projects for `Elsa.Tenants` and `Elsa.Common`. * Update unit tests for `ActivityConstructionResult` - Refactor test parameterization to verify `HasExceptions` property more explicitly. - Simplify exception creation logic in helper methods. - Improve test assertions by combining act and assert phases where applicable. * Enable configuration-based multitenancy with tenant-specific settings - Introduced a configuration-based tenant provider to streamline tenant initialization and customization. - Added tenant ID handling filters to ensure tenant ID is applied and filtered automatically. - Deprecated the `CommonPersistenceFeature` in favor of modular persistence feature extension. * Update database indexes to include `TenantId` for multitenancy support - Added `TenantId` to unique constraints on `Triggers` table across all EFCore providers. - Adjusted index names to reflect the updated constraints. - Updated trigger configuration to ensure uniqueness includes `TenantId`. * Add tenant filtering to `DefaultWorkflowDefinitionStorePopulator` - Introduced `ITenantAccessor` to support tenant-specific filtering of workflow definitions. - Updated logic to skip workflows not matching the current tenant. * Update doc/adr/toc.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove `CommonPersistenceFeature` as it has been deprecated * Add tenant-specific filtering to workflow import logic in `DefaultWorkflowDefinitionStorePopulator` * Replace hardcoded tenant ID with `Tenant.DefaultTenantId` in integration tests * Update database indexes and migration logic to support `TenantId` for multitenancy - Added `TenantId` to unique constraints on the `Triggers` table and updated index names. - Included logic to drop outdated indexes without `TenantId` during migration. - Adjusted tests to account for `TenantId` in workflow identity and indexing scenarios. * Remove `TenantId` from workflow identity construction in concurrent trigger indexing tests * Introduce `SelectiveMockLockProvider` for precise lock mocking in tests - Added `SelectiveMockLockProvider` to allow targeted lock mocking without affecting unrelated background operations. - Updated test services to use `SelectiveMockLockProvider` in place of `TestDistributedLockProvider`. - Refactored `DistributedLockResilienceTests` to support selective mocking for deterministic and reliable assertions. * Update Elsa.sln Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Normalize tenant ID handling in `DefaultWorkflowDefinitionStorePopulator` for consistent filtering * Refactor `TenantResolverResult` to support explicit resolved/unresolved state handling - Updated `TenantResolverResult` to include an explicit `_isResolved` property. - Adjusted `ResolveTenantId()` and `IsResolved` logic for improved clarity and robustness. - Simplified tenant resolution invocation in `TenantResolverBase`. - Removed redundant normalization in `DefaultTenantResolverPipelineInvoker`. * Normalize tenant ID handling in `DefaultWorkflowDefinitionStorePopulator` and `ClrWorkflowsProvider`. * Refactor `DefaultWorkflowDefinitionStorePopulatorTests`: streamline object initializations and add tenant-specific test coverage for `PopulateStoreAsync`. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2.9 KiB
2.9 KiB
8. Empty String as Default Tenant ID
Date: 2026-01-27
Status
Accepted
Context
The multitenancy system in Elsa supports an optional mode where, when multitenancy is disabled, the system assumes a single tenant. When enabled, there's still a default tenant involved. The convention has been to use null as the tenant ID for the default tenant.
However, this convention created several issues:
- Dictionary compatibility: The
DefaultTenantResolverPipelineInvokerattempts to build a dictionary of tenants by their ID usingToDictionary(x => x.Id), which throws an exception because dictionaries do not support null keys. - Inconsistency: The codebase used
null, empty string (""), and string literal"default"interchangeably to refer to the default tenant across different parts of the system (e.g., in configuration files and database records). - Code clarity: Using
nullas a sentinel value for "default" is implicit and can be unclear to developers reading the code.
Decision
We will standardize on using an empty string ("") as the tenant ID for the default tenant instead of null. This decision includes:
- Define a constant: Add
Tenant.DefaultTenantId = ""to explicitly document the convention. - Update Tenant.Default: Change
Tenant.Default.Idfromnull!to use theDefaultTenantIdconstant. - Add normalization helper: Create a
NormalizeTenantId()extension method that convertsnullto empty string, ensuring backwards compatibility with code that still uses null. - Apply normalization consistently: Use the normalization method in:
- Dictionary creation in
DefaultTenantResolverPipelineInvoker - Tenant lookups in
TenantResolverContext - Any other places where tenant IDs are compared or used as dictionary keys
- Dictionary creation in
Consequences
Positive
- No more exceptions: Empty string is a valid dictionary key, eliminating the runtime exception in
DefaultTenantResolverPipelineInvoker. - Backwards compatible: The
NormalizeTenantId()extension method ensures that existing code usingnullor empty string will work correctly. - Explicit convention: The
DefaultTenantIdconstant makes the convention clear and self-documenting. - Simplified logic: Reduces the need for null-checking throughout the multitenancy code.
- Consistency: Aligns with parts of the codebase that were already using empty string (e.g., in configuration files).
Negative
- Migration consideration: Existing data stores that have
nulltenant IDs will need to be normalized to empty strings, though the normalization helper provides a runtime solution. - String vs null semantics: Some developers may find using empty string less intuitive than null for representing "no tenant", though this is mitigated by the explicit constant.
Neutral
- The empty string convention is common in multitenancy systems and aligns with string-based identifier patterns used elsewhere in the codebase.