elsa-workflows / elsa-workflows/elsa-core
Multitenancy does not work properly
- Dominant language
- C#
- Stars
- 7.9k
- Forks
- 1.5k
- Avg merge
- 15h 22m
- Merged PRs (30d)
- 114
Description
## Description
If I create a new tenant by the API or by the ITenantStore service then one of the handlers will set the TenantId of the tenant. This new TenantId will be the TenantId of the DbContext (the Id of the active Tenant).
The responsible handler for this behavior is this: `src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/ApplyTenantId.cs`.
At the other side when I start the program the ` StartAsync` metod of the `ActivateTenants` class will query the tenants from the store by the `ListAsync` method of the `EFCoreTenantStore` service.
However this query won't include any tenant where the TenantId is not null (or differs from the TenantId of the active Tenant), so my app won't be able to activate the newly created Tenant. The situation is the same if I just call the `RefreshAsync` method of the `ITenantService` after the tenant creation.
I guess this behavior is expected for every other tables to be able to filter out the data corresponding to the inactive tenants if I use the shared database strategy, but I think this is a wrong behavior for the tenants table.
## Related snippets
### ApplyTenantId
```c#
namespace Elsa.EntityFrameworkCore.EntityHandlers;
///
/// Represents a handler for applying the tenant ID to an entity before saving changes.
///
public class ApplyTenantId : IEntitySavingHandler
{
///
public ValueTask HandleAsync(ElsaDbContextBase dbContext, EntityEntry entry, CancellationToken cancellationToken = default)
{
if (entry.Entity is Entity entity)
entity.TenantId = dbContext.TenantId.NullIfEmpty();
return default;
}
}
```
### ActivateTenants
```c#
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);
}
}
```
### DefaultTenantService
```c#
public async Task ActivateTenantsAsync(CancellationToken cancellationToken = default)
{
await RefreshAsync(cancellationToken);
}
public async Task DeactivateTenantsAsync(CancellationToken cancellationToken = default)
{
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
var tenants = dictionary.Values.ToArray();
foreach (var tenant in tenants)
await UnregisterTenantAsync(tenant, cancellationToken);
}
public async Task RefreshAsync(CancellationToken cancellationToken = default)
{
await _refreshLock.WaitAsync(cancellationToken);
try
{
await using var scope = scopeFactory.CreateAsyncScope();
var tenantsProvider = scope.ServiceProvider.GetRequiredService();
var currentTenants = await GetTenantsDictionaryAsync(cancellationToken);
var currentTenantIds = currentTenants.Keys;
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id.EmptyIfNull());
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);
}
}
finally
{
_refreshLock.Release();
}
}
```
### StoreTenantsProvider
```c#
namespace Elsa.Tenants.Providers;
[UsedImplicitly]
public class StoreTenantsProvider(ITenantStore store) : ITenantsProvider
{
public async Task> ListAsync(CancellationToken cancellationToken = default)
{
return await store.ListAsync(cancellationToken);
}
public Task FindAsync(TenantFilter filter, CancellationToken cancellationToken = default)
{
return store.FindAsync(filter, cancellationToken);
}
}
```
I use the 3.5.0-rc1 packages
Contributor guide
Assessment
This issue has not been assessed yet.