* 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>
99 lines
3.6 KiB
C#
99 lines
3.6 KiB
C#
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>();
|
|
var tenantId = (tenantAccessor?.TenantId).NormalizeTenantId();
|
|
TenantId ??= tenantId;
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
} |