2025-09-27 18:39:18 +00:00
|
|
|
using Elsa.Common.Entities;
|
|
|
|
|
using Elsa.Common.Multitenancy;
|
|
|
|
|
using Elsa.Extensions;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
|
|
|
|
|
namespace Elsa.Persistence.EFCore;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// An optional base class to implement with some opinions on certain converters to install for certain DB providers.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema
|
|
|
|
|
{
|
|
|
|
|
private static readonly ISet<EntityState> ModifiedEntityStates = new HashSet<EntityState>
|
|
|
|
|
{
|
|
|
|
|
EntityState.Added,
|
|
|
|
|
EntityState.Modified,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
protected IServiceProvider ServiceProvider { get; }
|
|
|
|
|
private readonly ElsaDbContextOptions? _elsaDbContextOptions;
|
|
|
|
|
public string? TenantId { get; set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The default schema used by Elsa.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static string ElsaSchema { get; set; } = "Elsa";
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
|
public string Schema { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The table used to store the migrations history.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static string MigrationsHistoryTable { get; set; } = "__EFMigrationsHistory";
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initializes a new instance of the <see cref="ElsaDbContextBase"/> class.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected ElsaDbContextBase(DbContextOptions options, IServiceProvider serviceProvider) : base(options)
|
|
|
|
|
{
|
|
|
|
|
ServiceProvider = serviceProvider;
|
|
|
|
|
_elsaDbContextOptions = options.FindExtension<ElsaDbContextOptionsExtension>()?.Options;
|
|
|
|
|
|
|
|
|
|
// ReSharper disable once VirtualMemberCallInConstructor
|
|
|
|
|
Schema = !string.IsNullOrWhiteSpace(_elsaDbContextOptions?.SchemaName) ? _elsaDbContextOptions.SchemaName : ElsaSchema;
|
|
|
|
|
|
|
|
|
|
var tenantAccessor = serviceProvider.GetService<ITenantAccessor>();
|
Implement null TenantId for tenant-agnostic entities (#7226)
* 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>
2026-02-02 09:59:01 +00:00
|
|
|
var tenantId = (tenantAccessor?.TenantId).NormalizeTenantId();
|
|
|
|
|
TenantId ??= tenantId;
|
2025-09-27 18:39:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
await OnBeforeSavingAsync(cancellationToken);
|
|
|
|
|
return await base.SaveChangesAsync(cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(Schema))
|
|
|
|
|
modelBuilder.HasDefaultSchema(Schema);
|
|
|
|
|
|
|
|
|
|
var additionalConfigurations = _elsaDbContextOptions?.GetModelConfigurations(this);
|
|
|
|
|
|
|
|
|
|
additionalConfigurations?.Invoke(modelBuilder);
|
|
|
|
|
|
|
|
|
|
using var scope = ServiceProvider.CreateScope();
|
|
|
|
|
var entityTypeHandlers = scope.ServiceProvider.GetServices<IEntityModelCreatingHandler>().ToList();
|
|
|
|
|
|
|
|
|
|
foreach (var entityType in modelBuilder.Model.GetEntityTypes().ToList())
|
|
|
|
|
{
|
|
|
|
|
foreach (var handler in entityTypeHandlers)
|
|
|
|
|
handler.Handle(this, modelBuilder, entityType);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task OnBeforeSavingAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
using var scope = ServiceProvider.CreateScope();
|
|
|
|
|
var handlers = scope.ServiceProvider.GetServices<IEntitySavingHandler>().ToList();
|
|
|
|
|
foreach (var entry in ChangeTracker.Entries().Where(IsModifiedEntity))
|
|
|
|
|
{
|
|
|
|
|
foreach (var handler in handlers)
|
|
|
|
|
await handler.HandleAsync(this, entry, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Determine if an entity was modified.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private bool IsModifiedEntity(EntityEntry entityEntry)
|
|
|
|
|
{
|
|
|
|
|
return ModifiedEntityStates.Contains(entityEntry.State) && entityEntry.Entity is Entity;
|
|
|
|
|
}
|
|
|
|
|
}
|