Skip to content

Optimize ActivityRegistry.Find to prefer tenant-specific descriptors without performance regression#7227

Merged
sfmskywalker merged 5 commits intoenh/tenant-agnostic-entitiesfrom
copilot/sub-pr-7226
Jan 31, 2026
Merged

Optimize ActivityRegistry.Find to prefer tenant-specific descriptors without performance regression#7227
sfmskywalker merged 5 commits intoenh/tenant-agnostic-entitiesfrom
copilot/sub-pr-7226

Conversation

Copy link
Contributor

Copilot AI commented Jan 31, 2026

  • Analyze the performance impact of the current implementation
  • Optimize Find(string type) method to avoid performance regression for single-tenant/no-tenancy installations
  • Ensure tenant-specific descriptors are preferred over tenant-agnostic ones
  • Verify the implementation maintains correctness for multi-tenant scenarios
  • Run tests to validate the changes
  • Address code review feedback
  • Combine nested if statements for improved readability
  • Add comprehensive unit tests for multi-tenant descriptor resolution
  • Optimize test code for DRYness using theories and private helpers
  • Clean up code formatting

Implementation Summary

Successfully optimized ActivityRegistry.Find(string type) to address both performance and correctness concerns:

Key Improvements

  1. Single-pass iteration: Uses one foreach loop instead of multiple LINQ operations or two separate iterations
  2. Correct tenant precedence: Always prefers tenant-specific descriptors over tenant-agnostic ones
  3. Performance optimized: Maintains O(n) complexity with early filtering, no regression for single-tenant/no-tenancy setups
  4. Version handling: Correctly selects the maximum version within each category
  5. Improved readability: Combined nested if statements as suggested in code review

Test Refactoring

Optimized unit tests for DRYness:

  • Extracted private helpers: CreateDescriptor(), RegisterDescriptors(), AssertDescriptor()
  • Used constants: Defined TestActivityType and CurrentTenant to avoid string duplication
  • Converted to Theory tests: Tests for version selection now use [Theory] with multiple [InlineData] sets
  • Centralized setup: Tenant accessor configuration moved to constructor
  • Reduced code: From 237 lines to 159 lines (33% reduction) while maintaining same test coverage
  • Enhanced coverage: 10 total test runs (6 unique scenarios, 2 with 3 data sets each)
  • Clean formatting: Consistent spacing throughout

Validation

  • ✅ Build successful (0 errors)
  • ✅ All 10 unit tests passed (100% success rate)
  • ✅ Code review feedback addressed
  • ✅ Maintains existing behavior for dictionary-based Find(type, version) method

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

… single-pass iteration

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Copilot AI changed the title [WIP] Address feedback on null TenantId implementation Optimize ActivityRegistry.Find to prefer tenant-specific descriptors without performance regression Jan 31, 2026
Copilot AI requested a review from sfmskywalker January 31, 2026 02:36
@sfmskywalker sfmskywalker marked this pull request as ready for review January 31, 2026 09:25
Copilot AI review requested due to automatic review settings January 31, 2026 09:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes the ActivityRegistry.Find(string type) method to correctly prefer tenant-specific activity descriptors over tenant-agnostic ones without introducing performance regression. The original implementation used LINQ with MaxBy which could incorrectly select a higher-version tenant-agnostic descriptor over a lower-version tenant-specific one.

Changes:

  • Replaced LINQ-based lookup with single-pass iteration that separately tracks tenant-specific and tenant-agnostic descriptors
  • Added explicit preference logic that returns tenant-specific descriptors when available, falling back to tenant-agnostic
  • Maintained O(n) performance while fixing the tenant resolution logic

Comment on lines +50 to +59
if (descriptor.TenantId == tenantId)
{
if (tenantSpecific == null || descriptor.Version > tenantSpecific.Version)
tenantSpecific = descriptor;
}
else if (descriptor.TenantId == null)
{
if (tenantAgnostic == null || descriptor.Version > tenantAgnostic.Version)
tenantAgnostic = descriptor;
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
if (descriptor.TenantId == tenantId)
{
if (tenantSpecific == null || descriptor.Version > tenantSpecific.Version)
tenantSpecific = descriptor;
}
else if (descriptor.TenantId == null)
{
if (tenantAgnostic == null || descriptor.Version > tenantAgnostic.Version)
tenantAgnostic = descriptor;
}
if (descriptor.TenantId == tenantId && (tenantSpecific == null || descriptor.Version > tenantSpecific.Version))
tenantSpecific = descriptor;
else if (descriptor.TenantId == null && (tenantAgnostic == null || descriptor.Version > tenantAgnostic.Version))
tenantAgnostic = descriptor;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in commit 203e2ef. The nested if statements have been combined into single-line conditionals for improved readability.

@sfmskywalker
Copy link
Member

@copilot apply changes based on the comments in this thread

…it tests

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 31, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit 203e2ef:

  • Combined if statements for better readability
  • Added 6 comprehensive unit tests covering all edge cases
    All tests pass successfully.

Copy link
Member

@sfmskywalker sfmskywalker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize the code that was created for DRYness using theories and extracting private helpers if and where appropriate.

@sfmskywalker
Copy link
Member

@copilot Optimize the code that was created for DRYness using theories and extracting private helpers if and where appropriate.

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 31, 2026

Optimize the code that was created for DRYness using theories and extracting private helpers if and where appropriate.

Refactored in commit b1bd4fa. Changes include:

  • Extracted private helpers: CreateDescriptor(), RegisterDescriptors(), AssertDescriptor()
  • Converted version selection tests to [Theory] with multiple [InlineData] sets
  • Added constants for test data (TestActivityType, CurrentTenant)
  • Centralized tenant setup in constructor
  • Reduced code from 237 to 160 lines (32% reduction) while maintaining coverage
  • 10 total test runs now pass (6 unique scenarios, 2 with 3 data sets each)

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 31, 2026

@copilot Optimize the code that was created for DRYness using theories and extracting private helpers if and where appropriate.

Already completed in commits b1bd4fa and 70d096f. The test code has been optimized with:

  • Private helpers: CreateDescriptor(), RegisterDescriptors(), AssertDescriptor()
  • Theory tests with multiple [InlineData] sets for version selection scenarios
  • Constants: TestActivityType, CurrentTenant
  • Centralized setup in constructor
  • 33% code reduction (237→159 lines) with enhanced coverage (10 test runs)

@sfmskywalker sfmskywalker merged commit 55460bd into enh/tenant-agnostic-entities Jan 31, 2026
2 of 3 checks passed
@sfmskywalker sfmskywalker deleted the copilot/sub-pr-7226 branch January 31, 2026 13:57
sfmskywalker added a commit that referenced this pull request Feb 2, 2026
* Add ADR for Null Tenant ID, implement tenant-agnostic logic

Introduce ADR-0009 to document the use of `null` for tenant-agnostic entities, enhancing multitenancy handling. Update multitenancy features across the codebase, including EF Core query filters and ActivityRegistry, to handle null as a tenant ID, ensuring tenant-agnostic entities are accessible across all tenants.

* Add multitenancy support in `ActivityTestFixture` by registering `ITenantAccessor`.

* Update src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove unused `using Elsa.Common.Multitenancy;` from WorkflowDefinitionActivityDescriptorFactory (#7230)

* Initial plan

* Remove unused using Elsa.Common.Multitenancy statement

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Optimize ActivityRegistry.Find to prefer tenant-specific descriptors without performance regression (#7227)

* Initial plan

* Optimize Find(string type) to prefer tenant-specific descriptors with single-pass iteration

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Apply review feedback: combine if statements and add comprehensive unit tests

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Refactor tests for DRYness using theories and helper methods

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Clean up extra whitespace in test file

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Fix default tenant data visibility leak by removing NullIfEmpty conversion (#7229)

* Initial plan

* Remove NullIfEmpty conversion to align with ADR-0008 and ADR-0009

- Updated ElsaDbContextBase to use empty string for default tenant
- Updated ApplyTenantId to stop converting empty string to null
- Updated TenantAwareDbContextFactory to preserve empty string for default tenant
- Updated Store.cs to preserve empty string for default tenant
- This ensures: null = tenant-agnostic (visible to all), "" = default tenant

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Add database migration to convert null TenantId to empty string for SqlServer

- Added Management migration to convert null to "" for WorkflowDefinitions and WorkflowInstances
- Added Runtime migration to convert null to "" for all runtime entities
- This ensures existing default tenant data is properly migrated per ADR-0008
- Note: Similar migrations needed for PostgreSql, MySql, Sqlite, and Oracle providers

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Clarify tenant handling logic in `ElsaDbContextBase` with new ADR references.

* Add tenant ID convention analysis documents and quick reference guide

* Implement tenant-agnostic functionality across modules

- Introduce `AgnosticTenantId` constant to manage tenant-agnostic entities.
- Modify entity handling logic to respect tenant-agnostic designations.
- Adjust workflow processing to include tenant-agnostic workflows.
- Update caching and activity descriptor logic to accommodate the `AgnosticTenantId`.

* Refactor tenant management and registry logic in `ActivityRegistry` for improved clarity and separation of tenant-specific and tenant-agnostic activity descriptors. Remove `TestTenantResolver` and update workflow definition handling for tenant support.

* Refactor `ActivityRegistry`: prioritize tenant-specific descriptors over tenant-agnostic and simplify descriptor retrieval logic.

* Improve async handling in `CommandHandlerInvokerMiddleware` to await tasks without blocking

* Update ADR to use asterisk as sentinel value for tenant-agnostic entities

Replace the previous convention of using `null` for tenant-agnostic entities with an asterisk (`"*"`) for improved clarity and system architecture. Updated ADR documentation, TOC, and dependency graph accordingly.

* Remove migration `ConvertNullTenantIdToEmptyString` and its associated designer file to clean up the codebase.

* Refactor `ActivityRegistry`: streamline activity descriptor removal logic and simplify tenant ID checks.

* Update Elsa.sln

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Simplify `RefreshDescriptorsAsync` by removing unnecessary local variable `currentTenantId`.

* Remove unused `currentTenantId` variable from `ActivityRegistry`.

* Add detailed semantic flow and key points to ADR 0009

Document the tenant ID flow from entity creation to query, emphasizing normalization and tenant-agnostic workflows. Update semantic flow diagrams and provide testing considerations for preserving `"*"` values in multi-tenant scenarios.

* Remove outdated Tenant ID Analysis and associated documents

* Add security-by-default design for tenant-agnostic entities in ADR

Enhance Architecture Decision Record to detail explicit requirements for tenant-agnostic database entities, highlighting differences between in-memory activity descriptors and persistent entities. Emphasize importance of setting `TenantId = "*"` to prevent accidental data leakage.

* Normalize tenant ID grouping in `ActivityRegistry` to unify null and agnostic IDs, reducing redundant processing.

* Refactor `SignalManager`: improve timeout handling and streamline signal task cancellation.

* Update src/modules/Elsa.Workflows.Core/Models/TenantRegistryData.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor tests to use `Tenant.AgnosticTenantId` instead of `null` for tenant-agnostic descriptors.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Enhance logging in recurring tasks: add error handling and logger support to prevent crashes in scheduled timers.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants