Open Closed

Dynamically set tenant connection string on tenant creation and then run migrations #10277


User avatar
0
bmulinari created

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)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Solution

    There is no built‑in “auto‑provision DB + user + connection string + then run migrations” pipeline in ABP. The default MyAppTenantDatabaseMigrationHandler assumes the tenant’s connection string is already set before the TenantCreatedEto is 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 TenantCreatedEto for this scenario, override the tenant creation application service so that:

    1. Tenant is created (host DB).
    2. DB/user is provisioned.
    3. Tenant connection string is updated and committed.
    4. 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:

    1. Keep your local handler as is but end the UoW after you update the tenant (or explicitly create a requiresNew: true UoW when updating the tenant and complete it).
    2. After the tenant connection string is successfully committed, publish a custom event (e.g. TenantDatabaseProvisionedEto).
    3. Subscribe to TenantDatabaseProvisionedEto with a handler that:
      • uses _currentTenant.Change(tenantId)
      • starts its own requiresNew: true UoW
      • executes the migrators.

    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.


    1. 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.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    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 TenantCreatedEto event should be published when a tenant is created. Which means the new tenant is already in the database.

    Thanks.

  • User Avatar
    0
    bmulinari created

    [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 TenantCreatedEto event 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 TenantCreatedEto handler throws, the transaction is rolled back and it's like the entity was never added.

    So, since the transaction is not yet committed, when MyAppTenantDatabaseMigrationHandler starts a completely new UoW with requiresNew=true before 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.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    The TenantCreatedEto will 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.

Learn More, Pay Less
33% OFF
All Trainings!
Get Your Deal
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.2.0-preview. Updated on January 07, 2026, 08:03
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.