We are testing a service to automatically provision the database and user for a tenant whenever a tenant is created, ensuring proper tenant isolation at a database level. Although the provisioning is working as we expect, the events that should be triggered after a tenant is created are not.
The current implementation consists of two handlers that listen to TenantCreatedEto:
1. Local handler (order -1): Runs first. Provisions database and sets connection string to the new tenant:
// Inside TenantDatabaseProvisioner.ProvisionAsync()
await CreateDatabaseAsync(hostConnectionString, databaseName);
await CreateUserAsync(hostConnectionString, userName, password);
await GrantPrivilegesAsync(hostConnectionString, databaseName, userName);
var tenantConnectionString = BuildTenantConnectionString(hostConnectionString, databaseName, userName, password);
var tenant = await tenantRepository.FindByIdAsync(tenantId);
tenant.SetDefaultConnectionString(tenantConnectionString);
await tenantRepository.UpdateAsync(tenant, true); // Still within the original UoW
2. Default MyAppTenantDatabaseMigrationHandler: This is default ABP code to run migrations/seeding:
private async Task MigrateAndSeedForTenantAsync(Guid tenantId, string adminEmail, string adminPassword)
{
using (_currentTenant.Change(tenantId))
{
// Creates a NEW Unit of Work
using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
var tenantConfiguration = await _tenantStore.FindAsync(tenantId);
// THIS CHECK FAILS - configuration/connection string is still null here, so migrations don't run
if (tenantConfiguration?.ConnectionStrings != null &&
!tenantConfiguration.ConnectionStrings.Default.IsNullOrWhiteSpace())
{
foreach (var migrator in _dbSchemaMigrators)
{
await migrator.MigrateAsync();
}
}
await uow.CompleteAsync();
}
// Seed data
// ...
}
}
Problem: the distributed handler creates a separate UoW to run the migrations in with _unitOfWorkManager.Begin(requiresNew: true), which queries the database before the original UoW commits. Result: tenantConfiguration.ConnectionStrings.Default is null, so migrations don't run when they should.
- Are there built-in mechanisms for this tenant database provisioning scenario?
- What would be the recommended approach for running migrations when the connection string is set dynamically during tenant creation?
Thanks!
4 Answer(s)
-
0
- Solution
There is no built‑in “auto‑provision DB + user + connection string + then run migrations” pipeline in ABP. The default
MyAppTenantDatabaseMigrationHandlerassumes the tenant’s connection string is already set before theTenantCreatedEtois published. In your flow it is not, so the handler behaves as designed and skips migrations.You basically have two safe options:
Option A – Move provisioning + migrations into the tenant creation flow (recommended)
Instead of relying on
TenantCreatedEtofor this scenario, override the tenant creation application service so that:- Tenant is created (host DB).
- DB/user is provisioned.
- Tenant connection string is updated and committed.
- Migrations are executed for that tenant.
The key point is to separate the “create tenant” transaction from the “provision + migrate” transaction so that the migration step sees a committed connection string.
A minimal outline:
public class MyTenantAppService : SaasTenantAppService { private readonly ITenantRepository _tenantRepository; private readonly ICurrentTenant _currentTenant; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IEnumerable<IMyAppDbSchemaMigrator> _dbSchemaMigrators; private readonly ITenantDatabaseProvisioner _provisioner; public MyTenantAppService( ITenantRepository tenantRepository, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, IEnumerable<IMyAppDbSchemaMigrator> dbSchemaMigrators, ITenantDatabaseProvisioner provisioner, // ... base deps ) : base(/* ... */) { _tenantRepository = tenantRepository; _currentTenant = currentTenant; _unitOfWorkManager = unitOfWorkManager; _dbSchemaMigrators = dbSchemaMigrators; _provisioner = provisioner; } public override async Task<SaasTenantDto> CreateAsync(SaasTenantCreateDto input) { // 1. Create tenant in host DB var tenantDto = await base.CreateAsync(input); // 2. Provision DB/user + build connection string var tenantConnectionString = await _provisioner.ProvisionAsync(tenantDto.Id, /* host conn info, etc. */); // 3. Persist connection string in a SEPARATE UoW and commit using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) { var tenant = await _tenantRepository.GetAsync(tenantDto.Id); tenant.SetDefaultConnectionString(tenantConnectionString); await _tenantRepository.UpdateAsync(tenant, autoSave: true); await uow.CompleteAsync(); } // 4. Run migrations for the tenant in its own UoW await MigrateAndSeedForTenantAsync(tenantDto.Id); return tenantDto; } private async Task MigrateAndSeedForTenantAsync(Guid tenantId) { using (_currentTenant.Change(tenantId)) { using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false)) { foreach (var migrator in _dbSchemaMigrators) { await migrator.MigrateAsync(); } await uow.CompleteAsync(); } // seed data here if needed, again with ICurrentTenant.Change in effect } } }Then disable/remove the default
MyAppTenantDatabaseMigrationHandler(either don’t register it or replace it with an empty implementation), because you’re now explicitly handling migrations after connection string setup.This avoids the race you are seeing: when migrations run, the tenant row (with connection string) is already committed and visible.
Option B – Publish a custom event after setting the connection string
If you strongly prefer an event‑driven approach:
- Keep your local handler as is but end the UoW after you update the tenant (or explicitly create a
requiresNew: trueUoW when updating the tenant and complete it). - After the tenant connection string is successfully committed, publish a custom event (e.g.
TenantDatabaseProvisionedEto). - Subscribe to
TenantDatabaseProvisionedEtowith a handler that:- uses
_currentTenant.Change(tenantId) - starts its own
requiresNew: trueUoW - executes the migrators.
- uses
Pseudocode in the provisioner:
// still inside the original tenant-created flow, but now we commit the connection string using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) { var tenant = await _tenantRepository.GetAsync(tenantId); tenant.SetDefaultConnectionString(tenantConnectionString); await _tenantRepository.UpdateAsync(tenant, autoSave: true); await uow.CompleteAsync(); } // After commit, publish custom event await _distributedEventBus.PublishAsync( new TenantDatabaseProvisionedEto { TenantId = tenantId });Handler:
public class TenantDatabaseProvisionedHandler : IDistributedEventHandler<TenantDatabaseProvisionedEto>, ITransientDependency { private readonly ICurrentTenant _currentTenant; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IEnumerable<IMyAppDbSchemaMigrator> _dbSchemaMigrators; public async Task HandleEventAsync(TenantDatabaseProvisionedEto eventData) { using (_currentTenant.Change(eventData.TenantId)) using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false)) { foreach (var migrator in _dbSchemaMigrators) { await migrator.MigrateAsync(); } await uow.CompleteAsync(); } } }Because the connection string is already committed when this event is handled,
_tenantStore.FindAsync/ the connection resolver will see it correctly.
- Sources
- Changing current tenant context for migrations/seeding:
https://abp.io/docs/latest/framework/architecture/multi-tenancy#change-the-current-tenant - Example of handling tenant‑specific migrations & seeding via a custom
TenantDatabaseMigrationHandler:
https://abp.io/support/questions/8896
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
-
0
hi
the distributed handler creates a separate UoW to run the migrations in with _unitOfWorkManager.Begin(requiresNew: true), which queries the database before the original UoW commits. Result: tenantConfiguration.ConnectionStrings.Default is null, so migrations don't run when they should.
The
TenantCreatedEtoevent should be published when a tenant is created. Which means the new tenant is already in the database.Thanks.
-
0
[maliming] said: hi
the distributed handler creates a separate UoW to run the migrations in with _unitOfWorkManager.Begin(requiresNew: true), which queries the database before the original UoW commits. Result: tenantConfiguration.ConnectionStrings.Default is null, so migrations don't run when they should.
The
TenantCreatedEtoevent should be published when a tenant is created. Which means the new tenant is already in the database.Thanks.
As I understand, the tenant technically is already in the database, but the transaction is not yet committed at that point, correct? Since if my
TenantCreatedEtohandler throws, the transaction is rolled back and it's like the entity was never added.So, since the transaction is not yet committed, when
MyAppTenantDatabaseMigrationHandlerstarts a completely new UoW withrequiresNew=truebefore applying the migrations, it will not "see" the connection string I just set to the tenant in the previous handler, causing the migrations to not be applied. That is the problem. -
0
hi
The
TenantCreatedEtowill be published when the current unit of work is completed. If the current unit of work failed, abp will not publish it.Can you share your code? Because Saas moudule doesn't have this problem.
Thanks.