Enhance Multitenancy with Runtime Tenant Management and Task Handling (#6173)

* Work in progress: Add DefaultTenantService for tenant management

Introduce `DefaultTenantService` and its corresponding interface `ITenantService` to manage tenant operations such as finding, getting, and listing tenants. Update `MultitenantBackgroundService` to utilize `DefaultTenantService` for handling tenant lifecycle events. This enhancement standardizes tenant operations and improves the maintainability of the multitenancy feature.

* WIP

* Add multitenancy event handlers and task interfaces

Implemented new interfaces IBackgroundTaskStarter and ITaskExecutor to manage task lifecycle events efficiently. Introduced new classes such as RunBackgroundTasks, RunStartupTasks, and StartRecurringTasks for handling tenant activation and deactivation events. Modified TaskExecutor to implement these interfaces and adjusted tenant registration logic to invoke these new handlers.

* Refactor multitenancy and task management services.

Remove background and recurring task runners, and integrate tenant activation and deactivation into the multitenancy feature. Enable multitenancy in the server application and create a new service for tenant activation and deactivation. This refactor simplifies the management of tenant-specific tasks and enhances the modularity of the platform.

* Refactor background service to use startup tasks

Replaced hosted service implementation with startup tasks for executing multi-tenant tasks and EF Core migrations. Introduced `PriorityAttribute` to manage task execution order, ensuring migrations run before other services that require database access. This simplifies tenant activation with an ordered task execution and removes redundant classes.

* Refactor MultitenancyFeature service registrations

Reorganized service registrations for better clarity and maintainability. Changed the registration of some services to use factory delegates for retrieving existing services to ensure correct dependencies. This refactor improves the flexibility of the tenant lifecycle event handling.

* Update V3_3 migration files

* Add tenant management endpoints and enhance tenant handling

Implemented tenant management endpoints including Add, Get, List, and Update. Enhanced tenant handling by introducing configuration and store-based providers, and improved error logging for tenant updates. Adjusted various internal functionalities to better support multitenancy features through different persistence providers.

* Implement tenant deletion endpoint and refactor migration setup.

Introduce a new API endpoint to handle tenant deletions while providing appropriate responses based on successful or unsuccessful attempts. Refactor migration handling by replacing startup tasks with hosted services across various modules to streamline the migration execution process.

* Add and integrate ConfigurationJsonConverter

Introduce a `ConfigurationJsonConverter` to handle JSON serialization and deserialization of `IConfiguration` objects. This change centralizes configuration serialization logic, leading to cleaner and more maintainable code. Updated various parts of the codebase to use the new serialization utility, ensuring a consistent approach throughout the application.

* Refactor JSON conversion and update tenant endpoint.

Removed unused workflow references and streamlined JSON handling in `ConfigurationJsonConverter`. Simplified tenant ID handling by removing `IIdentityGenerator` and setting a default value for `UpdatedTenant.Id`.

* Add logging for cancelled recurring tasks

Integrated ILogger to log information when a recurring task is canceled due to an OperationCanceledException. This change enhances troubleshooting by providing clearer insights into task cancellations and their underlying reasons, improving maintainability and observability of the task execution process.

* Disable multitenancy support and adjust default Tenant ID.

Multitenancy is now disabled by setting 'useMultitenancy' to false in the configuration. Additionally, the default Tenant's ID has been changed from null to an empty string to prevent potential null reference issues.

* Remove MultitenantHostedService abstraction file

The MultitenantHostedService.cs file was removed as it is no longer necessary. Its responsibilities have likely been refactored or integrated into another service, indicating a simplification or restructuring of the multitenancy handling in the codebase.

* Rename PriorityAttribute to OrderAttribute for clarity.

This change improves the clarity of the code by renaming PriorityAttribute to OrderAttribute, reflecting its actual purpose. All occurrences of the attribute in the codebase have been updated accordingly to maintain consistency. This makes the intent of the code more understandable for future maintenance and development.

* Fix message key retrieval in ProduceMessage activity

Update the ProduceMessage activity to use GetOrDefault for retrieving the message key. This change ensures that a null key is used if no explicit key is provided or if the key is empty or whitespace, preventing potential errors during message production.

* Refactor multitenancy and scheduling services.

Removed DefaultTenantContextInitializer interface and class, refactored tenant activation/deactivation to use try-catch logging, and updated tenant context handling to use IDisposable for context push. New activities and workflows added in Elsa.Server.Web, and scheduling services enhanced to schedule jobs with explicit job keys and groups. Also, adjusted configurations to enable multitenancy, providing improved maintainability and flexibility.

* Remove Example1 activities and disable multitenancy

Deleted Example1Activity, Example1Workflow, and FirstActivity classes to clean up unused code and simplify the codebase. Disabled multitenancy by setting useMultitenancy to false, likely to streamline configuration and resource utilization.

* Fix and normalize URL path concatenation.

Ensure that the base URLs in both base path providers consistently end with a forward slash. This normalization prevents potential issues with endpoint routing and path concatenation, improving overall URL construction robustness.
This commit is contained in:
Sipke Schoorstra 2024-12-03 15:07:55 +01:00 committed by GitHub
parent 5ac63f1485
commit 692937d7d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
229 changed files with 2718 additions and 695 deletions

View file

@ -5,7 +5,7 @@ on:
branches:
- 'main'
release:
types: [ prereleased, published ]
types: [ prereleased ]
env:
base_version: '3.3.0'
feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json'

View file

@ -131,6 +131,7 @@
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
@ -167,6 +168,7 @@
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Define the modules to update
mods=("Alterations" "Runtime" "Management" "Identity" "Labels")
mods=("Alterations" "Runtime" "Management" "Identity" "Labels" "Tenants")
# Define the list of providers
providers=("MySql" "SqlServer" "Sqlite" "PostgreSql" "Oracle")

View file

@ -63,8 +63,4 @@
<PackageReference Include="Proto.Persistence.SqlServer"/>
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\" />
</ItemGroup>
</Project>

View file

@ -13,6 +13,7 @@ using Elsa.EntityFrameworkCore.Modules.Alterations;
using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.EntityFrameworkCore.Modules.Tenants;
using Elsa.Extensions;
using Elsa.Features.Services;
using Elsa.Identity.Multitenancy;
@ -24,6 +25,7 @@ using Elsa.MongoDb.Modules.Alterations;
using Elsa.MongoDb.Modules.Identity;
using Elsa.MongoDb.Modules.Management;
using Elsa.MongoDb.Modules.Runtime;
using Elsa.MongoDb.Modules.Tenants;
using Elsa.OpenTelemetry.Middleware;
using Elsa.Secrets.Extensions;
using Elsa.Secrets.Management.Tasks;
@ -73,6 +75,7 @@ const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit;
const MassTransitBroker massTransitBroker = MassTransitBroker.Memory;
const bool useMultitenancy = false;
const bool useTenantsFromConfiguration = false;
const bool useAgents = false;
const bool useSecrets = false;
const bool disableVariableWrappers = false;
@ -146,7 +149,7 @@ services
ef.UseSqlServer(sqlServerConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
@ -180,7 +183,7 @@ services
ef.UseSqlServer(sqlServerConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
@ -251,7 +254,7 @@ services
}
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
@ -376,7 +379,7 @@ services
ef.UseSqlServer(sqlServerConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString);
else if(sqlDatabaseProvider == SqlDatabaseProvider.MySql)
else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql)
ef.UseMySql(mySqlConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
@ -452,8 +455,8 @@ services
asb.AzureServiceBusOptions = options => configuration.GetSection("AzureServiceBus").Bind(options);
});
}
if(useKafka)
if (useKafka)
{
elsa.UseKafka(kafka =>
{
@ -468,10 +471,10 @@ services
.UseAgentPersistence(persistence => persistence.UseEntityFrameworkCore(ef => ef.UseSqlite(sp => sp.GetSqliteConnectionString())))
.UseAgentsApi()
;
services.Configure<AgentsOptions>(options => builder.Configuration.GetSection("Agents").Bind(options));
}
if (useSecrets)
{
elsa
@ -482,11 +485,11 @@ services
if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
management.UseEntityFrameworkCore(ef =>
ef.UseSqlServer(sqlServerConnectionString)
);
);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
management.UseEntityFrameworkCore(ef =>
management.UseEntityFrameworkCore(ef =>
ef.UsePostgreSql(postgresConnectionString)
);
);
else
management.UseEntityFrameworkCore(ef =>
{
@ -502,16 +505,44 @@ services
{
elsa.UseTenants(tenants =>
{
tenants.ConfigureOptions(options =>
tenants.ConfigureMultitenancy(options =>
{
configuration.GetSection("Multitenancy").Bind(options);
options.TenantResolverPipelineBuilder
.Append<HostTenantResolver>()
.Append<RoutePrefixTenantResolver>()
.Append<HeaderTenantResolver>()
.Append<ClaimsTenantResolver>();
});
tenants.UseConfigurationBasedTenantsProvider();
if (useTenantsFromConfiguration)
{
tenants.UseConfigurationBasedTenantsProvider(options => configuration.GetSection("Multitenancy").Bind(options));
}
else
{
tenants.UseStoreBasedTenantsProvider();
tenants.UseTenantManagement(management =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
management.UseMongoDb();
if (persistenceProvider == PersistenceProvider.Dapper)
throw new NotSupportedException("Dapper is not supported for tenant management.");
if (persistenceProvider == PersistenceProvider.EntityFrameworkCore)
{
management.UseEntityFrameworkCore(ef =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) ef.UseSqlite(sqliteConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString);
if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString);
});
}
});
tenants.UseTenantManagementEndpoints();
}
});
elsa.UseTenantHttpRouting();
@ -562,7 +593,7 @@ app.UseAuthentication();
app.UseAuthorization();
// Multitenancy.
if(useMultitenancy)
if (useMultitenancy)
app.UseTenants();
// Elsa API endpoints for designer.

View file

@ -12,6 +12,9 @@
"System.Net.Http": "Warning"
}
},
"HostBuilder": {
"reloadConfigOnChange": true
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;",
@ -37,6 +40,19 @@
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;"
}
}
},
{
"Id": "tenant-1",
"Name": "Tenant 12 dd",
"Configuration": {
"Http": {
"Prefix": "/tenant-1",
"Host": "localhost:5001"
},
"ConnectionStrings": {
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;"
}
}
}
]
},
@ -81,7 +97,7 @@
"Roles": [
"admin"
],
"TenantId": null
"TenantId": ""
},
{
"Id": "b0cd0e506e713a9d",

View file

@ -1,17 +1,18 @@
using Elsa.Common.Multitenancy;
using Elsa.Tenants;
using JetBrains.Annotations;
namespace Elsa.Testing.Shared.Services;
[UsedImplicitly]
public class TestTenantsProvider(params string[] tenantIds) : ITenantsProvider
{
public ValueTask<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default)
public Task<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default)
{
var tenants = tenantIds.Select(id => new Tenant {Id = id, Name = id});
return new(tenants);
return Task.FromResult(tenants);
}
public async ValueTask<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
public async Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
{
var query = (await ListAsync(cancellationToken)).AsQueryable();
return filter.Apply(query).FirstOrDefault();

View file

@ -18,6 +18,7 @@
<PackageReference Include="DistributedLock.Core" />
<PackageReference Include="LinqKit.Core" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">

View file

@ -7,7 +7,9 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Ccontexts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Ccontracts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Centities/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Ceventargs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Cextensions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Cimplementations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Cmodels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Cnotifications/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=multitenancy_005Cresults/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -1,18 +0,0 @@
using Elsa.Common.Multitenancy.HostedServices;
using Elsa.Common.RecurringTasks;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Common.Features;
public class BackgroundTasksFeature(IModule module) : FeatureBase(module)
{
public override void Apply()
{
Services.AddSingleton<RecurringTaskScheduleManager>();
Services.AddScoped<TaskExecutor>();
Services.AddStartupTask<ConfigureRecurringTasksScheduleStartupTask>();
}
}

View file

@ -1,5 +1,8 @@
using Elsa.Common.Multitenancy;
using Elsa.Common.Multitenancy.EventHandlers;
using Elsa.Common.Multitenancy.HostedServices;
using Elsa.Common.RecurringTasks;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
@ -26,9 +29,7 @@ public class MultitenancyFeature(IModule module) : FeatureBase(module)
public override void ConfigureHostedServices()
{
Module
.ConfigureHostedService<StartupTasksRunner>(1)
.ConfigureHostedService<BackgroundTasksRunner>(1)
.ConfigureHostedService<RecurringTasksRunner>(1)
.ConfigureHostedService<ActivateTenants>(-1)
;
}
@ -38,9 +39,24 @@ public class MultitenancyFeature(IModule module) : FeatureBase(module)
.AddSingleton<ITenantScopeFactory, DefaultTenantScopeFactory>()
.AddSingleton<ITenantAccessor, DefaultTenantAccessor>()
.AddSingleton<ITenantFinder, DefaultTenantFinder>()
.AddSingleton<ITenantContextInitializer, DefaultTenantContextInitializer>()
.AddSingleton<ITenantService, DefaultTenantService>()
.AddSingleton<RunBackgroundTasks>()
.AddSingleton<ITenantActivatedEvent>(sp => sp.GetRequiredService<RunBackgroundTasks>())
.AddSingleton<ITenantDeactivatedEvent>(sp => sp.GetRequiredService<RunBackgroundTasks>())
.AddSingleton<StartRecurringTasks>()
.AddSingleton<ITenantActivatedEvent>(sp => sp.GetRequiredService<StartRecurringTasks>())
.AddSingleton<ITenantDeactivatedEvent>(sp => sp.GetRequiredService<StartRecurringTasks>())
.AddSingleton<ITenantActivatedEvent, RunStartupTasks>()
.AddSingleton<RecurringTaskScheduleManager>()
.AddSingleton<TenantEventsManager>()
.AddStartupTask<ConfigureRecurringTasksScheduleStartupTask>()
.AddScoped<DefaultTenantsProvider>()
.AddScoped<DefaultTenantResolver>()
.AddScoped<ITaskExecutor, TaskExecutor>()
.AddScoped<IBackgroundTaskStarter, TaskExecutor>()
.AddScoped(_tenantsProviderFactory);
}
}

View file

@ -1,56 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Elsa.Common.Multitenancy;
/// <summary>
/// A hosted service that executes a task for each tenant.
/// </summary>
public abstract class MultitenantBackgroundService(IServiceScopeFactory serviceScopeFactory) : BackgroundService
{
private bool _disposed;
protected IServiceScope ServiceScope { get; private set; } = default!;
protected ICollection<Tenant> Tenants { get; private set; } = default!;
protected IDictionary<Tenant, TenantScope> TenantScopes { get; private set; } = default!;
public override async Task StartAsync(CancellationToken cancellationToken)
{
ServiceScope = serviceScopeFactory.CreateScope();
var tenantScopeFactory = ServiceScope.ServiceProvider.GetRequiredService<ITenantScopeFactory>();
var tenantsProvider = ServiceScope.ServiceProvider.GetRequiredService<ITenantsProvider>();
Tenants = (await tenantsProvider.ListAsync(cancellationToken)).ToList();
TenantScopes = Tenants.ToDictionary(x => x, x => tenantScopeFactory.CreateScope(x));
foreach (var entry in TenantScopes)
await StartAsync(entry.Value, cancellationToken);
await base.StartAsync(cancellationToken);
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
if(_disposed)
return;
foreach (var entry in TenantScopes)
await StopAsync(entry.Value, cancellationToken);
foreach (var entry in TenantScopes)
await entry.Value.DisposeAsync();
ServiceScope.Dispose();
await base.StopAsync(cancellationToken);
_disposed = true;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
foreach (var entry in TenantScopes)
await ExecuteAsync(entry.Value, stoppingToken);
}
protected virtual Task StartAsync(TenantScope tenantScope, CancellationToken stoppingToken) => Task.CompletedTask;
protected virtual Task ExecuteAsync(TenantScope tenantScope, CancellationToken stoppingToken) => Task.CompletedTask;
protected virtual Task StopAsync(TenantScope tenantScope, CancellationToken stoppingToken) => Task.CompletedTask;
}

View file

@ -1,31 +0,0 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Elsa.Common.Multitenancy;
/// <summary>
/// A hosted service that runs the start and stop methods for each tenant.
/// </summary>
[UsedImplicitly]
public abstract class MultitenantHostedService(IServiceScopeFactory serviceScopeFactory) : IHostedService
{
public virtual async Task StartAsync(CancellationToken cancellationToken) => await RunThroughTenantsAsync(StartAsync, cancellationToken);
public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
protected virtual Task StartAsync(TenantScope tenantScope, CancellationToken cancellationToken) => Task.CompletedTask;
private async Task RunThroughTenantsAsync(Func<TenantScope, CancellationToken, Task> action, CancellationToken cancellationToken)
{
using var serviceScope = serviceScopeFactory.CreateScope();
var tenantScopeFactory = serviceScope.ServiceProvider.GetRequiredService<ITenantScopeFactory>();
var tenantsProvider = serviceScope.ServiceProvider.GetRequiredService<ITenantsProvider>();
var tenants = (await tenantsProvider.ListAsync(cancellationToken)).ToList();
foreach (var tenant in tenants)
{
await using var tenantScope = tenantScopeFactory.CreateScope(tenant);
await action(tenantScope, cancellationToken);
}
}
}

View file

@ -40,9 +40,4 @@ public abstract class TenantResolverBase : ITenantResolver
/// Automatically resolves the tenant if the tenant ID is not null.
/// </summary>
protected TenantResolverResult AutoResolve(string? tenantId) => tenantId == null ? Unresolved() : Resolved(tenantId);
public Task<Tenant?> ResolveAsync(CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,7 @@
namespace Elsa.Common.Multitenancy;
public interface IBackgroundTaskStarter
{
Task StartAsync(IBackgroundTask task, CancellationToken cancellationToken);
Task StopAsync(IBackgroundTask task, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Common.Multitenancy;
public interface ITaskExecutor
{
Task ExecuteTaskAsync(ITask task, CancellationToken cancellationToken);
}

View file

@ -6,5 +6,7 @@ public interface ITenantAccessor
/// Get the current <see cref="Multitenancy.Tenant"/>.
/// </summary>
/// <returns>Current tenant or null.</returns>
Tenant? Tenant { get; set; }
Tenant? Tenant { get; }
IDisposable PushContext(Tenant? tenant);
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Common.Multitenancy;
public interface ITenantActivatedEvent
{
Task TenantActivatedAsync(TenantActivatedEventArgs args);
}

View file

@ -1,6 +0,0 @@
namespace Elsa.Common.Multitenancy;
public interface ITenantContextInitializer
{
Task InitializeAsync(string tenantId, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Common.Multitenancy;
public interface ITenantDeactivatedEvent
{
Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args);
}

View file

@ -0,0 +1,68 @@
namespace Elsa.Common.Multitenancy;
public interface ITenantService
{
/// <summary>
/// Finds a tenant by the given id.
/// </summary>
/// <param name="id">The tenant id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The tenant or null if not found.</returns>
Task<Tenant?> FindAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Finds a tenant using the given filter.
/// </summary>
/// <param name="filter">The filter.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a tenant by the given id.
/// </summary>
/// <param name="id">The tenant id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The tenant.</returns>
/// <exception cref="KeyNotFoundException">Thrown if the tenant is not found.</exception>
Task<Tenant> GetAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a tenant by the given id.
/// </summary>
/// <param name="filter">The filter.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The tenant.</returns>
/// <exception cref="KeyNotFoundException">Thrown if the tenant is not found.</exception>
Task<Tenant> GetAsync(TenantFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Lists all the tenants.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The tenants.</returns>
Task<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Lists all the tenants using the given filter.
/// </summary>
/// <param name="filter">The filter.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The tenants.</returns>
Task<IEnumerable<Tenant>> ListAsync(TenantFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Activates all tenants.
/// </summary>
Task ActivateTenantsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Deactivates all tenants.
/// </summary>
Task DeactivateTenantsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Invokes the <see cref="ITenantsProvider"/> and caches the result.
/// When new tenants are added, lifecycle events are triggered to ensure background tasks are updated.
/// </summary>
Task RefreshAsync(CancellationToken cancellationToken = default);
}

View file

@ -8,10 +8,10 @@ public interface ITenantsProvider
/// <summary>
/// Lists all the tenants.
/// </summary>
ValueTask<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default);
Task<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Finds a tenant by the given filter.
/// </summary>
ValueTask<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default);
Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default);
}

View file

@ -22,7 +22,7 @@ public class Tenant : Entity
public static readonly Tenant Default = new()
{
Id = null!,
Id = string.Empty,
Name = "Default"
};
}

View file

@ -0,0 +1,3 @@
namespace Elsa.Common.Multitenancy;
public record TenantActivatedEventArgs(Tenant Tenant, TenantScope TenantScope, CancellationToken CancellationToken) : TenantEventArgs(Tenant, TenantScope, CancellationToken);

View file

@ -0,0 +1,3 @@
namespace Elsa.Common.Multitenancy;
public record TenantDeactivatedEventArgs(Tenant Tenant, TenantScope TenantScope, CancellationToken CancellationToken) : TenantEventArgs(Tenant, TenantScope, CancellationToken);

View file

@ -0,0 +1,3 @@
namespace Elsa.Common.Multitenancy;
public record TenantEventArgs(Tenant Tenant, TenantScope TenantScope, CancellationToken CancellationToken);

View file

@ -0,0 +1,34 @@
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Common.Multitenancy.EventHandlers;
public class RunBackgroundTasks : ITenantActivatedEvent, ITenantDeactivatedEvent
{
private readonly ICollection<Task> _runningTasks = new List<Task>();
private CancellationTokenSource _cancellationTokenSource = default!;
public Task TenantActivatedAsync(TenantActivatedEventArgs args)
{
var cancellationToken = args.CancellationToken;
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var tenantScope = args.TenantScope;
var backgroundTasks = tenantScope.ServiceProvider.GetServices<IBackgroundTask>();
var backgroundTaskStarter = tenantScope.ServiceProvider.GetRequiredService<IBackgroundTaskStarter>();
foreach (var backgroundTask in backgroundTasks)
{
var task = backgroundTaskStarter.StartAsync(backgroundTask, _cancellationTokenSource.Token);
if (!task.IsCompleted) _runningTasks.Add(task);
}
return Task.CompletedTask;
}
public Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args)
{
_cancellationTokenSource.Cancel();
_runningTasks.Clear();
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,19 @@
using System.Reflection;
using Elsa.Common.RecurringTasks;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Common.Multitenancy.EventHandlers;
public class RunStartupTasks : ITenantActivatedEvent
{
public async Task TenantActivatedAsync(TenantActivatedEventArgs args)
{
var cancellationToken = args.CancellationToken;
var tenantScope = args.TenantScope;
var tasks = tenantScope.ServiceProvider.GetServices<IStartupTask>().OrderBy(x => x.GetType().GetCustomAttribute<OrderAttribute>()?.Order ?? 0f).ToList();
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<ITaskExecutor>();
foreach (var task in tasks)
await taskExecutor.ExecuteTaskAsync(task, cancellationToken);
}
}

View file

@ -0,0 +1,49 @@
using Elsa.Common.RecurringTasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Elsa.Common.Multitenancy.EventHandlers;
public class StartRecurringTasks(RecurringTaskScheduleManager scheduleManager, ILogger<StartRecurringTasks> logger) : ITenantActivatedEvent, ITenantDeactivatedEvent
{
private readonly ICollection<ScheduledTimer> _scheduledTimers = new List<ScheduledTimer>();
private CancellationTokenSource _cancellationTokenSource = default!;
public async Task TenantActivatedAsync(TenantActivatedEventArgs args)
{
var cancellationToken = args.CancellationToken;
_cancellationTokenSource = new CancellationTokenSource();
var tenantScope = args.TenantScope;
var tasks = tenantScope.ServiceProvider.GetServices<IRecurringTask>().ToList();
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<ITaskExecutor>();
foreach (var task in tasks)
{
var schedule = scheduleManager.GetScheduleFor(task.GetType());
var timer = schedule.CreateTimer(async () =>
{
try
{
await taskExecutor.ExecuteTaskAsync(task, _cancellationTokenSource.Token);
}
catch (OperationCanceledException e)
{
logger.LogInformation(e, "Task {TaskType} was cancelled", task.GetType().Name);
}
});
_scheduledTimers.Add(timer);
await task.StartAsync(cancellationToken);
}
}
public async Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args)
{
var tenantScope = args.TenantScope;
_cancellationTokenSource.Cancel();
foreach (var timer in _scheduledTimers) await timer.DisposeAsync();
_scheduledTimers.Clear();
var tasks = tenantScope.ServiceProvider.GetServices<IRecurringTask>();
foreach (var task in tasks) await task.StopAsync(args.CancellationToken);
}
}

View file

@ -0,0 +1,18 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Hosting;
namespace Elsa.Common.Multitenancy.HostedServices;
[UsedImplicitly]
public class ActivateTenants(ITenantService tenantService) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
await tenantService.ActivateTenantsAsync(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await tenantService.DeactivateTenantsAsync(cancellationToken);
}
}

View file

@ -1,27 +0,0 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Common.Multitenancy.HostedServices;
[UsedImplicitly]
public class BackgroundTasksRunner(IServiceScopeFactory serviceScopeFactory) : MultitenantBackgroundService(serviceScopeFactory)
{
protected override async Task StartAsync(TenantScope tenantScope, CancellationToken stoppingToken)
{
var tasks = tenantScope.ServiceProvider.GetServices<IBackgroundTask>();
foreach (var task in tasks) await task.StartAsync(stoppingToken);
}
protected override async Task ExecuteAsync(TenantScope tenantScope, CancellationToken stoppingToken)
{
var tasks = tenantScope.ServiceProvider.GetServices<IBackgroundTask>();
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<TaskExecutor>();
foreach (var task in tasks) await taskExecutor.ExecuteTaskAsync(task, stoppingToken);
}
protected override async Task StopAsync(TenantScope tenantScope, CancellationToken stoppingToken)
{
var tasks = tenantScope.ServiceProvider.GetServices<IBackgroundTask>();
foreach (var task in tasks) await task.StopAsync(stoppingToken);
}
}

View file

@ -1,36 +0,0 @@
using Elsa.Common.RecurringTasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Common.Multitenancy.HostedServices;
[UsedImplicitly]
public class RecurringTasksRunner(IServiceScopeFactory serviceScopeFactory, RecurringTaskScheduleManager scheduleManager) : MultitenantBackgroundService(serviceScopeFactory)
{
private readonly ICollection<ScheduledTimer> _scheduledTimers = new List<ScheduledTimer>();
protected override async Task StartAsync(TenantScope tenantScope, CancellationToken stoppingToken)
{
var tasks = tenantScope.ServiceProvider.GetServices<IRecurringTask>().ToList();
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<TaskExecutor>();
foreach (var task in tasks)
{
var schedule = scheduleManager.GetScheduleFor(task.GetType());
var timer = schedule.CreateTimer(async () =>
{
await taskExecutor.ExecuteTaskAsync(task, stoppingToken);
});
_scheduledTimers.Add(timer);
await task.StartAsync(stoppingToken);
}
}
protected override async Task StopAsync(TenantScope tenantScope, CancellationToken stoppingToken)
{
foreach (var timer in _scheduledTimers) await timer.DisposeAsync();
_scheduledTimers.Clear();
var tasks = tenantScope.ServiceProvider.GetServices<IRecurringTask>();
foreach (var task in tasks) await task.StopAsync(stoppingToken);
}
}

View file

@ -1,18 +0,0 @@
using Elsa.Common.DistributedHosting;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Elsa.Common.Multitenancy.HostedServices;
[UsedImplicitly]
public class StartupTasksRunner(IServiceScopeFactory serviceScopeFactory, IOptions<DistributedLockingOptions> options) : MultitenantHostedService(serviceScopeFactory)
{
protected override async Task StartAsync(TenantScope tenantScope, CancellationToken cancellationToken)
{
var tasks = tenantScope.ServiceProvider.GetServices<IStartupTask>();
var taskExecutor = tenantScope.ServiceProvider.GetRequiredService<TaskExecutor>();
foreach (var task in tasks)
await taskExecutor.ExecuteTaskAsync(task, cancellationToken);
}
}

View file

@ -1,27 +0,0 @@
using System.Reflection;
using Elsa.Common.DistributedHosting;
using Elsa.Common.RecurringTasks;
using Medallion.Threading;
using Microsoft.Extensions.Options;
namespace Elsa.Common.Multitenancy.HostedServices;
public class TaskExecutor(IDistributedLockProvider distributedLockProvider, IOptions<DistributedLockingOptions> options)
{
public async Task ExecuteTaskAsync(ITask task, CancellationToken cancellationToken)
{
var taskType = task.GetType();
var singleNodeTask = taskType.GetCustomAttribute<SingleNodeTaskAttribute>() != null;
if (singleNodeTask)
{
var resourceName = taskType.AssemblyQualifiedName!;
await using (await distributedLockProvider.AcquireLockAsync(resourceName, options.Value.LockAcquisitionTimeout, cancellationToken: cancellationToken))
await task.ExecuteAsync(cancellationToken);
}
else
{
await task.ExecuteAsync(cancellationToken);
}
}
}

View file

@ -11,6 +11,29 @@ public class DefaultTenantAccessor : ITenantAccessor
public Tenant? Tenant
{
get => CurrentTenantField.Value;
set => CurrentTenantField.Value = value;
internal set => CurrentTenantField.Value = value;
}
public IDisposable PushContext(Tenant? tenant)
{
return new CurrentTenantScope(tenant, Tenant, t => Tenant = t);
}
}
public class CurrentTenantScope : IDisposable
{
private readonly Tenant? _previousTenant;
private readonly Action<Tenant?> _apply;
public CurrentTenantScope(Tenant? newTenant, Tenant? previousTenant, Action<Tenant?> apply)
{
_previousTenant = previousTenant;
_apply = apply;
_apply(newTenant);
}
public void Dispose()
{
_apply(_previousTenant);
}
}

View file

@ -1,10 +0,0 @@
namespace Elsa.Common.Multitenancy;
public class DefaultTenantContextInitializer(ITenantFinder tenantFinder, ITenantAccessor tenantAccessor) : ITenantContextInitializer
{
public async Task InitializeAsync(string tenantId, CancellationToken cancellationToken = default)
{
var tenant = await tenantFinder.FindByIdAsync(tenantId, cancellationToken);
tenantAccessor.Tenant = tenant;
}
}

View file

@ -7,7 +7,7 @@ public class DefaultTenantResolver : TenantResolverBase
{
private readonly Tenant _defaultTenant = new()
{
Id = null!,
Id = string.Empty,
Name = "Default"
};

View file

@ -0,0 +1,124 @@
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Common.Multitenancy;
public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScopeFactory tenantScopeFactory, TenantEventsManager tenantEvents, ITenantAccessor tenantAccessor) : ITenantService, IAsyncDisposable
{
private readonly AsyncServiceScope _serviceScope = scopeFactory.CreateAsyncScope();
private IDictionary<string, Tenant>? _tenantsDictionary;
private IDictionary<Tenant, TenantScope>? _tenantScopesDictionary;
public async ValueTask DisposeAsync()
{
await _serviceScope.DisposeAsync();
}
public async Task<Tenant?> FindAsync(string id, CancellationToken cancellationToken = default)
{
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
return dictionary.TryGetValue(id, out var tenant) ? tenant : null;
}
public async Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
{
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
return filter.Apply(dictionary.Values.AsQueryable()).FirstOrDefault();
}
public async Task<Tenant> GetAsync(string id, CancellationToken cancellationToken = default)
{
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
return dictionary[id];
}
public async Task<Tenant> GetAsync(TenantFilter filter, CancellationToken cancellationToken = default)
{
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
return filter.Apply(dictionary.Values.AsQueryable()).First();
}
public async Task<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default)
{
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
return dictionary.Values;
}
public async Task<IEnumerable<Tenant>> ListAsync(TenantFilter filter, CancellationToken cancellationToken = default)
{
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
return filter.Apply(dictionary.Values.AsQueryable());
}
public async Task ActivateTenantsAsync(CancellationToken cancellationToken = default)
{
await RefreshAsync(cancellationToken);
}
public async Task DeactivateTenantsAsync(CancellationToken cancellationToken = default)
{
var tenants = _tenantsDictionary!.Values.ToArray();
foreach (var tenant in tenants)
await UnregisterTenantAsync(tenant, cancellationToken);
}
public async Task RefreshAsync(CancellationToken cancellationToken = default)
{
await using var scope = scopeFactory.CreateAsyncScope();
var tenantsProvider = scope.ServiceProvider.GetRequiredService<ITenantsProvider>();
var currentTenants = await GetTenantsDictionaryAsync(cancellationToken);
var currentTenantIds = currentTenants.Keys;
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id);
var newTenantIds = newTenants.Keys;
var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray();
var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray();
foreach (var removedTenantId in removedTenantIds)
{
var removedTenant = currentTenants[removedTenantId];
await UnregisterTenantAsync(removedTenant, cancellationToken);
}
foreach (var addedTenantId in addedTenantIds)
{
var addedTenant = newTenants[addedTenantId];
await RegisterTenantAsync(addedTenant, cancellationToken);
}
}
private async Task<IDictionary<string, Tenant>> GetTenantsDictionaryAsync(CancellationToken cancellationToken)
{
if (_tenantsDictionary == null)
{
_tenantsDictionary = new Dictionary<string, Tenant>();
_tenantScopesDictionary = new Dictionary<Tenant, TenantScope>();
var tenantsProvider = _serviceScope.ServiceProvider.GetRequiredService<ITenantsProvider>();
var tenants = await tenantsProvider.ListAsync(cancellationToken);
foreach (var tenant in tenants)
await RegisterTenantAsync(tenant, cancellationToken);
}
return _tenantsDictionary;
}
private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
var scope = tenantScopeFactory.CreateScope(tenant);
_tenantsDictionary![tenant.Id] = tenant;
_tenantScopesDictionary![tenant] = scope;
using (tenantAccessor.PushContext(tenant))
await tenantEvents.TenantActivatedAsync(new TenantActivatedEventArgs(tenant, scope, cancellationToken));
}
private async Task UnregisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
var scope = _tenantScopesDictionary![tenant];
_tenantsDictionary!.Remove(tenant.Id);
_tenantScopesDictionary!.Remove(tenant);
using (tenantAccessor.PushContext(tenant))
await tenantEvents.TenantDeactivatedAsync(new TenantDeactivatedEventArgs(tenant, scope, cancellationToken));
}
}

View file

@ -10,14 +10,14 @@ public class DefaultTenantsProvider : ITenantsProvider
{
private readonly Tenant[] _tenants = { Tenant.Default };
public ValueTask<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default)
public Task<IEnumerable<Tenant>> ListAsync(CancellationToken cancellationToken = default)
{
return new(_tenants);
return Task.FromResult<IEnumerable<Tenant>>(_tenants);
}
public ValueTask<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
public Task<Tenant?> FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
{
var queryable = _tenants.AsQueryable();
return new ValueTask<Tenant?>(filter.Apply(queryable).FirstOrDefault());
return Task.FromResult(filter.Apply(queryable).FirstOrDefault());
}
}

View file

@ -0,0 +1,42 @@
using System.Reflection;
using Elsa.Common.DistributedHosting;
using Elsa.Common.RecurringTasks;
using Medallion.Threading;
using Microsoft.Extensions.Options;
namespace Elsa.Common.Multitenancy;
public class TaskExecutor(IDistributedLockProvider distributedLockProvider, IOptions<DistributedLockingOptions> options) : ITaskExecutor, IBackgroundTaskStarter
{
public async Task ExecuteTaskAsync(ITask task, CancellationToken cancellationToken)
{
await ExecuteInternalAsync(task, () => task.ExecuteAsync(cancellationToken), cancellationToken);
}
public async Task StartAsync(IBackgroundTask task, CancellationToken cancellationToken)
{
await ExecuteInternalAsync(task, () => task.StartAsync(cancellationToken), cancellationToken);
}
public async Task StopAsync(IBackgroundTask task, CancellationToken cancellationToken)
{
await ExecuteInternalAsync(task, () => task.StopAsync(cancellationToken), cancellationToken);
}
private async Task ExecuteInternalAsync(ITask task, Func<Task> action, CancellationToken cancellationToken)
{
var taskType = task.GetType();
var singleNodeTask = taskType.GetCustomAttribute<SingleNodeTaskAttribute>() != null;
if (singleNodeTask)
{
var resourceName = taskType.AssemblyQualifiedName!;
await using (await distributedLockProvider.AcquireLockAsync(resourceName, options.Value.LockAcquisitionTimeout, cancellationToken: cancellationToken))
await action();
}
else
{
await action();
}
}
}

View file

@ -0,0 +1,36 @@
using Microsoft.Extensions.Logging;
namespace Elsa.Common.Multitenancy;
public class TenantEventsManager(IEnumerable<ITenantActivatedEvent> tenantActivatedEvents, IEnumerable<ITenantDeactivatedEvent> tenantDeactivatedEvents, ILogger<TenantEventsManager> logger)
{
public async Task TenantActivatedAsync(TenantActivatedEventArgs args)
{
foreach (var tenantActivatedEvent in tenantActivatedEvents)
{
try
{
await tenantActivatedEvent.TenantActivatedAsync(args);
}
catch (Exception e)
{
logger.LogError(e, "Error occurred while processing tenant activated event.");
}
}
}
public async Task TenantDeactivatedAsync(TenantDeactivatedEventArgs args)
{
foreach (var tenantDeactivatedEvent in tenantDeactivatedEvents)
{
try
{
await tenantDeactivatedEvent.TenantDeactivatedAsync(args);
}
catch (Exception e)
{
logger.LogError(e, "Error occurred while processing tenant deactivated event.");
}
}
}
}

View file

@ -24,4 +24,6 @@ public class TenantFilter
return queryable;
}
public static TenantFilter ById(string id) => new() { Id = id };
}

View file

@ -6,19 +6,16 @@ namespace Elsa.Common.Multitenancy;
/// Represents a tenant scope, which sets the current tenant for the duration of the scope.
/// After the scope is disposed, the original tenant is restored.
/// </summary>
public class TenantScope : IAsyncDisposable
public class TenantScope(IServiceScope serviceScope, ITenantAccessor tenantAccessor, Tenant? tenant) : IAsyncDisposable
{
public TenantScope(IServiceScope serviceScope, ITenantAccessor tenantAccessor, Tenant? tenant)
{
ServiceScope = serviceScope;
tenantAccessor.Tenant = tenant;
}
public IServiceScope ServiceScope { get; }
private readonly IDisposable _tenantContext = tenantAccessor.PushContext(tenant);
public IServiceScope ServiceScope { get; } = serviceScope;
public IServiceProvider ServiceProvider => ServiceScope.ServiceProvider;
public async ValueTask DisposeAsync()
{
_tenantContext.Dispose();
if (ServiceScope is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
else

View file

@ -0,0 +1,10 @@
namespace Elsa.Common.RecurringTasks;
/// <summary>
/// Configures a task with a priority.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class OrderAttribute(float order) : Attribute
{
public float Order { get; } = order;
}

View file

@ -0,0 +1,45 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
namespace Elsa.Common.Serialization;
public class ConfigurationJsonConverter : JsonConverter<IConfiguration>
{
public override IConfiguration? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var jsonElement = JsonElement.ParseValue(ref reader);
using var memoryStream = new MemoryStream();
using var writer = new Utf8JsonWriter(memoryStream);
jsonElement.WriteTo(writer);
writer.Flush();
memoryStream.Position = 0;
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonStream(memoryStream);
return configurationBuilder.Build();
}
public override void Write(Utf8JsonWriter writer, IConfiguration value, JsonSerializerOptions options)
{
var dictionary = new Dictionary<string, object?>();
foreach (var child in value.GetChildren())
dictionary[child.Key] = GetValue(child);
JsonSerializer.Serialize(writer, dictionary, options);
}
private static object? GetValue(IConfigurationSection section)
{
var children = section.GetChildren().ToList();
if (!children.Any())
return section.Value;
var dict = new Dictionary<string, object?>();
foreach (var child in children)
dict[child.Key] = GetValue(child);
return dict;
}
}

View file

@ -0,0 +1,11 @@
using System.Text.Json;
namespace Elsa.Common.Serialization;
public static class SerializerOptions
{
public static JsonSerializerOptions ConfigurationJsonSerializerOptions { get; } = new()
{
Converters = { new ConfigurationJsonConverter() }
};
}

View file

@ -0,0 +1,25 @@
using System.Text.Json;
using Microsoft.Extensions.Configuration;
namespace Elsa.Common.Serialization;
public static class Serializers
{
public static IConfiguration DeserializeConfiguration(JsonElement? json)
{
return DeserializeConfiguration(json?.GetRawText());
}
public static IConfiguration DeserializeConfiguration(string? json)
{
if (json == null)
return new ConfigurationBuilder().Build();
return JsonSerializer.Deserialize<IConfiguration>(json, SerializerOptions.ConfigurationJsonSerializerOptions)!;
}
public static string SerializeConfiguration(IConfiguration configuration)
{
return JsonSerializer.Serialize(configuration, SerializerOptions.ConfigurationJsonSerializerOptions);
}
}

View file

@ -10,6 +10,11 @@ namespace Elsa.Common.Services;
public class MemoryStore<TEntity>
{
private IDictionary<string, TEntity> Entities { get; set; } = new ConcurrentDictionary<string, TEntity>();
/// <summary>
/// Gets a queryable of all entities.
/// </summary>
public IQueryable<TEntity> Queryable => Entities.Values.AsQueryable();
/// <summary>
/// Adds an entity.

View file

@ -1,4 +1,5 @@
using Elsa.Common.Entities;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
@ -44,11 +45,10 @@ public abstract class PersistenceFeatureBase<TFeature, TDbContext> : FeatureBase
.MigrationsAssembly("Elsa.EntityFrameworkCore.Sqlite")
.MigrationsHistoryTable(ElsaDbContextBase.MigrationsHistoryTable, ElsaDbContextBase.ElsaSchema));
/// <inheritdoc />
public override void ConfigureHostedServices()
{
if (RunMigrations)
Module.ConfigureHostedService<RunMigrationsHostedService<TDbContext>>(-100); // Migrations need to run before other hosted services that depend on DB access.
ConfigureMigrations();
}
/// <inheritdoc />
@ -60,6 +60,11 @@ public abstract class PersistenceFeatureBase<TFeature, TDbContext> : FeatureBase
Services.AddDbContextFactory<TDbContext>(DbContextOptionsBuilder, DbContextFactoryLifetime);
}
protected virtual void ConfigureMigrations()
{
Services.AddStartupTask<RunMigrationsStartupTask<TDbContext>>();
}
/// <summary>
/// Adds a store to the service collection.
/// </summary>

View file

@ -1,21 +1,33 @@
using Elsa.Common.Multitenancy;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Elsa.EntityFrameworkCore;
/// <summary>
/// Executes EF Core migrations using the specified <see cref="DbContext"/> type.
/// </summary>
[UsedImplicitly]
public class RunMigrationsHostedService<TDbContext>(IServiceScopeFactory scopeFactory) : MultitenantHostedService(scopeFactory) where TDbContext : DbContext
public class RunMigrationsHostedService<TDbContext> : IHostedService where TDbContext : DbContext
{
/// <inheritdoc />
protected override async Task StartAsync(TenantScope tenantScope, CancellationToken cancellationToken)
private readonly IServiceScopeFactory _scopeFactory;
/// <summary>
/// Initializes a new instance of the <see cref="RunMigrationsHostedService{TDbContext}"/> class.
/// </summary>
public RunMigrationsHostedService(IServiceScopeFactory scopeFactory)
{
var dbContextFactory = tenantScope.ServiceProvider.GetRequiredService<IDbContextFactory<TDbContext>>();
var tenantDbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await tenantDbContext.Database.MigrateAsync(cancellationToken);
_scopeFactory = scopeFactory;
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _scopeFactory.CreateScope();
var dbContextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<TDbContext>>();
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await dbContext.Database.MigrateAsync(cancellationToken);
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View file

@ -0,0 +1,22 @@
using Elsa.Common;
using Elsa.Common.RecurringTasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore;
/// <summary>
/// Executes EF Core migrations using the specified <see cref="DbContext"/> type.
/// </summary>
[UsedImplicitly]
[SingleNodeTask]
[Order(-100)]
public class RunMigrationsStartupTask<TDbContext>(IDbContextFactory<TDbContext> dbContextFactory) : IStartupTask where TDbContext : DbContext
{
/// <inheritdoc /
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
var tenantDbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await tenantDbContext.Database.MigrateAsync(cancellationToken);
}
}

View file

@ -5,6 +5,7 @@ using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.EntityFrameworkCore.Modules.Tenants;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
@ -13,29 +14,22 @@ using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore.MySql;
[UsedImplicitly]
public class IdentityDbContextFactory : MySqlDesignTimeDbContextFactory<IdentityElsaDbContext>
{
}
public class IdentityDbContextFactory : MySqlDesignTimeDbContextFactory<IdentityElsaDbContext>;
[UsedImplicitly]
public class ManagementDbContextFactory : MySqlDesignTimeDbContextFactory<ManagementElsaDbContext>
{
}
public class ManagementDbContextFactory : MySqlDesignTimeDbContextFactory<ManagementElsaDbContext>;
[UsedImplicitly]
public class RuntimeDbContextFactory : MySqlDesignTimeDbContextFactory<RuntimeElsaDbContext>
{
}
public class RuntimeDbContextFactory : MySqlDesignTimeDbContextFactory<RuntimeElsaDbContext>;
[UsedImplicitly]
public class LabelsDbContextFactory : MySqlDesignTimeDbContextFactory<LabelsElsaDbContext>
{
}
public class LabelsDbContextFactory : MySqlDesignTimeDbContextFactory<LabelsElsaDbContext>;
[UsedImplicitly]
public class AlterationsDbContextFactories : MySqlDesignTimeDbContextFactory<AlterationsElsaDbContext>
{
}
public class AlterationsDbContextFactories : MySqlDesignTimeDbContextFactory<AlterationsElsaDbContext>;
[UsedImplicitly]
public class TenantsDbContextFactories : MySqlDesignTimeDbContextFactory<TenantsElsaDbContext>;
public class MySqlDesignTimeDbContextFactory<TDbContext> : DesignTimeDbContextFactoryBase<TDbContext> where TDbContext : DbContext
{

View file

@ -1,5 +1,4 @@
using Elsa.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Alterations
/// <inheritdoc />
public partial class V3_1 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_1(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Alterations
/// <inheritdoc />
public partial class V3_2 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_2(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Alterations
{
[DbContext(typeof(AlterationsElsaDbContext))]
[Migration("20241109102522_V3_3")]
[Migration("20241202095004_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -1,5 +1,4 @@
using Elsa.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Identity
/// <inheritdoc />
public partial class V3_1 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_1(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Identity
/// <inheritdoc />
public partial class V3_2 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_2(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Identity
{
[DbContext(typeof(IdentityElsaDbContext))]
[Migration("20241109102921_V3_3")]
[Migration("20241202095402_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -1,5 +1,4 @@
using Elsa.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Labels
/// <inheritdoc />
public partial class V3_1 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_1(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Labels
/// <inheritdoc />
public partial class V3_2 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_2(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Labels
{
[DbContext(typeof(LabelsElsaDbContext))]
[Migration("20241109103004_V3_3")]
[Migration("20241202095515_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -1,5 +1,4 @@
using Elsa.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Management
/// <inheritdoc />
public partial class V3_1 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_1(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Management
/// <inheritdoc />
public partial class V3_2 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_2(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Management
{
[DbContext(typeof(ManagementElsaDbContext))]
[Migration("20241109102839_V3_3")]
[Migration("20241202095327_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -1,5 +1,4 @@
using Elsa.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
/// <inheritdoc />
public partial class V3_1 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_1(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
/// <inheritdoc />
public partial class V3_2 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_2(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
{
[DbContext(typeof(RuntimeElsaDbContext))]
[Migration("20241109102737_V3_3")]
[Migration("20241202095210_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -0,0 +1,54 @@
// <auto-generated />
using Elsa.EntityFrameworkCore.Modules.Tenants;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Tenants
{
[DbContext(typeof(TenantsElsaDbContext))]
[Migration("20241202095623_V3_3")]
partial class V3_3
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Elsa")
.HasAnnotation("ProductVersion", "7.0.20")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Elsa.Common.Multitenancy.Tenant", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("Configuration")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("TenantId")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_Tenant_Name");
b.HasIndex("TenantId")
.HasDatabaseName("IX_Tenant_TenantId");
b.ToTable("Tenants", "Elsa");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,68 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Tenants
{
/// <inheritdoc />
public partial class V3_3 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_3(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
{
_schema = schema;
}
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "Elsa");
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Tenants",
schema: _schema.Schema,
columns: table => new
{
Id = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Name = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Configuration = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TenantId = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Tenants", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Tenant_Name",
schema: _schema.Schema,
table: "Tenants",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_Tenant_TenantId",
schema: _schema.Schema,
table: "Tenants",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Tenants",
schema: _schema.Schema);
}
}
}

View file

@ -0,0 +1,51 @@
// <auto-generated />
using Elsa.EntityFrameworkCore.Modules.Tenants;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Tenants
{
[DbContext(typeof(TenantsElsaDbContext))]
partial class TenantsElsaDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Elsa")
.HasAnnotation("ProductVersion", "7.0.20")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Elsa.Common.Multitenancy.Tenant", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("Configuration")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("TenantId")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_Tenant_Name");
b.HasIndex("TenantId")
.HasDatabaseName("IX_Tenant_TenantId");
b.ToTable("Tenants", "Elsa");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -5,6 +5,7 @@ using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.EntityFrameworkCore.Modules.Tenants;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
@ -13,29 +14,22 @@ using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore.Oracle;
[UsedImplicitly]
public class IdentityDbContextFactory : OracleDesignTimeDbContextFactory<IdentityElsaDbContext>
{
}
public class IdentityDbContextFactory : OracleDesignTimeDbContextFactory<IdentityElsaDbContext>;
[UsedImplicitly]
public class ManagementDbContextFactory : OracleDesignTimeDbContextFactory<ManagementElsaDbContext>
{
}
public class ManagementDbContextFactory : OracleDesignTimeDbContextFactory<ManagementElsaDbContext>;
[UsedImplicitly]
public class RuntimeDbContextFactory : OracleDesignTimeDbContextFactory<RuntimeElsaDbContext>
{
}
public class RuntimeDbContextFactory : OracleDesignTimeDbContextFactory<RuntimeElsaDbContext>;
[UsedImplicitly]
public class LabelsDbContextFactory : OracleDesignTimeDbContextFactory<LabelsElsaDbContext>
{
}
public class LabelsDbContextFactory : OracleDesignTimeDbContextFactory<LabelsElsaDbContext>;
[UsedImplicitly]
public class AlterationsDbContextFactories : OracleDesignTimeDbContextFactory<AlterationsElsaDbContext>
{
}
public class AlterationsDbContextFactories : OracleDesignTimeDbContextFactory<AlterationsElsaDbContext>;
[UsedImplicitly]
public class TenantsDbContextFactories : OracleDesignTimeDbContextFactory<TenantsElsaDbContext>;
public class OracleDesignTimeDbContextFactory<TDbContext> : DesignTimeDbContextFactoryBase<TDbContext> where TDbContext : DbContext
{

View file

@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata;
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Alterations
{
[DbContext(typeof(AlterationsElsaDbContext))]
[Migration("20241109102700_V3_3")]
[Migration("20241202095130_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -11,7 +11,7 @@ using Oracle.EntityFrameworkCore.Metadata;
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Identity
{
[DbContext(typeof(IdentityElsaDbContext))]
[Migration("20241109102957_V3_3")]
[Migration("20241202095500_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -11,7 +11,7 @@ using Oracle.EntityFrameworkCore.Metadata;
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Labels
{
[DbContext(typeof(LabelsElsaDbContext))]
[Migration("20241109103034_V3_3")]
[Migration("20241202095616_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata;
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management
{
[DbContext(typeof(ManagementElsaDbContext))]
[Migration("20241109102913_V3_3")]
[Migration("20241202095357_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata;
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime
{
[DbContext(typeof(RuntimeElsaDbContext))]
[Migration("20241109102830_V3_3")]
[Migration("20241202095317_V3_3")]
partial class V3_3
{
/// <inheritdoc />

View file

@ -0,0 +1,57 @@
// <auto-generated />
using Elsa.EntityFrameworkCore.Modules.Tenants;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Oracle.EntityFrameworkCore.Metadata;
#nullable disable
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Tenants
{
[DbContext(typeof(TenantsElsaDbContext))]
[Migration("20241202095654_V3_3")]
partial class V3_3
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Elsa")
.HasAnnotation("ProductVersion", "7.0.20")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Elsa.Common.Multitenancy.Tenant", b =>
{
b.Property<string>("Id")
.HasColumnType("NVARCHAR2(450)");
b.Property<string>("Configuration")
.IsRequired()
.HasColumnType("NVARCHAR2(2000)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("NVARCHAR2(450)");
b.Property<string>("TenantId")
.HasColumnType("NVARCHAR2(450)");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_Tenant_Name");
b.HasIndex("TenantId")
.HasDatabaseName("IX_Tenant_TenantId");
b.ToTable("Tenants", "Elsa");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Tenants
{
/// <inheritdoc />
public partial class V3_3 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_3(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
{
_schema = schema;
}
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "Elsa");
migrationBuilder.CreateTable(
name: "Tenants",
schema: _schema.Schema,
columns: table => new
{
Id = table.Column<string>(type: "NVARCHAR2(450)", nullable: false),
Name = table.Column<string>(type: "NVARCHAR2(450)", nullable: false),
Configuration = table.Column<string>(type: "NVARCHAR2(2000)", nullable: false),
TenantId = table.Column<string>(type: "NVARCHAR2(450)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Tenants", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Tenant_Name",
schema: _schema.Schema,
table: "Tenants",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_Tenant_TenantId",
schema: _schema.Schema,
table: "Tenants",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Tenants",
schema: _schema.Schema);
}
}
}

View file

@ -0,0 +1,54 @@
// <auto-generated />
using Elsa.EntityFrameworkCore.Modules.Tenants;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Oracle.EntityFrameworkCore.Metadata;
#nullable disable
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Tenants
{
[DbContext(typeof(TenantsElsaDbContext))]
partial class TenantsElsaDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Elsa")
.HasAnnotation("ProductVersion", "7.0.20")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Elsa.Common.Multitenancy.Tenant", b =>
{
b.Property<string>("Id")
.HasColumnType("NVARCHAR2(450)");
b.Property<string>("Configuration")
.IsRequired()
.HasColumnType("NVARCHAR2(2000)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("NVARCHAR2(450)");
b.Property<string>("TenantId")
.HasColumnType("NVARCHAR2(450)");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_Tenant_Name");
b.HasIndex("TenantId")
.HasDatabaseName("IX_Tenant_TenantId");
b.ToTable("Tenants", "Elsa");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -5,6 +5,7 @@ using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.EntityFrameworkCore.Modules.Tenants;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
@ -13,29 +14,22 @@ using Microsoft.EntityFrameworkCore;
namespace Elsa.EntityFrameworkCore.PostgreSql;
[UsedImplicitly]
public class IdentityDbContextFactory : PostgreSqlDesignTimeDbContextFactory<IdentityElsaDbContext>
{
}
public class IdentityDbContextFactory : PostgreSqlDesignTimeDbContextFactory<IdentityElsaDbContext>;
[UsedImplicitly]
public class ManagementDbContextFactory : PostgreSqlDesignTimeDbContextFactory<ManagementElsaDbContext>
{
}
public class ManagementDbContextFactory : PostgreSqlDesignTimeDbContextFactory<ManagementElsaDbContext>;
[UsedImplicitly]
public class RuntimeDbContextFactory : PostgreSqlDesignTimeDbContextFactory<RuntimeElsaDbContext>
{
}
public class RuntimeDbContextFactory : PostgreSqlDesignTimeDbContextFactory<RuntimeElsaDbContext>;
[UsedImplicitly]
public class LabelsDbContextFactory : PostgreSqlDesignTimeDbContextFactory<LabelsElsaDbContext>
{
}
public class LabelsDbContextFactory : PostgreSqlDesignTimeDbContextFactory<LabelsElsaDbContext>;
[UsedImplicitly]
public class AlterationsDbContextFactories : PostgreSqlDesignTimeDbContextFactory<AlterationsElsaDbContext>
{
}
public class AlterationsDbContextFactories : PostgreSqlDesignTimeDbContextFactory<AlterationsElsaDbContext>;
[UsedImplicitly]
public class TenantsDbContextFactories : PostgreSqlDesignTimeDbContextFactory<TenantsElsaDbContext>;
public class PostgreSqlDesignTimeDbContextFactory<TDbContext> : DesignTimeDbContextFactoryBase<TDbContext> where TDbContext : DbContext
{

View file

@ -1,5 +1,4 @@
using Elsa.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Alterations
/// <inheritdoc />
public partial class V3_1 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_1(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Alterations
/// <inheritdoc />
public partial class V3_2 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
private readonly IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
public V3_2(IElsaDbContextSchema schema)
{
_schema = schema;
}

View file

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Alterations
{
[DbContext(typeof(AlterationsElsaDbContext))]
[Migration("20241109102646_V3_3")]
[Migration("20241202095104_V3_3")]
partial class V3_3
{
/// <inheritdoc />

Some files were not shown because too many files have changed in this diff Show more