Table of Contents

Customizing the migration process

Choosing the right customization approach

Before implementing a migration customization, use the table below to select the interface that fits your use case:

Use case Approach Interfaces When it runs
Wrap the migration call to execute code before and/or after the entire migration (notifications, external system updates, etc.) Migration decorator IMigrator Before and/or after the migration
Insert custom commands at a specific point within the migration execution pipeline Migration interceptor IMigrationInterceptor + ICommand During migration command building, on every migration
Insert a custom command that should only execute once at a specific point in the migration pipeline Migration interceptor (run once) IMigrationInterceptor + IRunOnceCommand During migration command building; once, then skipped
Execute business logic per tenant after a successful migration (repositories, services available) Tenant migration interceptor ITenantDatabaseMigrationInterceptor After each successful migration, once per tenant
One-shot data correction per tenant — skipped on subsequent migrations Tenant migration interceptor (run once) IRunOnceTenantDatabaseMigrationInterceptor After migration; once per tenant, then skipped
Insert data with explicit auto-incremented ID values Identity scope IMigrationInterceptor + ICommand (using IInsertIdentityScope) Inside an ICommand.Execute method
flowchart TD
    Start([Start]) --> Q1{Where to run the code?}
    Q1 -->|Before/after the migration| A[Migration decorator]
    Q1 -->|Inside the migration pipeline| Q2{Run only once?}
    Q1 -->|After migration, per tenant| Q3{Run only once per tenant?}
    Q2 -->|No| B[Migration interceptor]
    Q2 -->|Yes| C[Migration interceptor - run once]
    Q3 -->|No| D[Tenant migration interceptor]
    Q3 -->|Yes| E[Tenant migration interceptor - run once]
Note

ITenantDatabaseMigrationInterceptor and IRunOnceTenantDatabaseMigrationInterceptor also run in single-tenant mode. In that mode, the interceptors execute once per migration for the single virtual tenant.

Executing operations before and after the migration

It is possible to perform operations before and / or after a migration using the C# decorator pattern with the IMigrator interface.
Each module of a cluster can define the operations it needs to execute.

Note

The decorator pattern is a design pattern that allows behavior to be added to the database migration, dynamically, without affecting its behavior. The decorator pattern is often useful by allowing the functionality of the migration to be extended without being modified.

Warning

In a multi-tenant context, you will not be able to use entity nor entity view repositories to perform operations on all tenants at the same time.

Before starting you must decide which module will contain the decorator.
Then open the Application layer business assembly C# project of the module.
Make sure that the project contains a reference to the Scrutor package.
Then you can create your decorator class :

public class MyMigratorDecorator : IMigrator
{
    private readonly IMigrator _baseMigrator;

    public MyMigratorDecorator(IMigrator baseMigrator)
    {
        _baseMigrator = baseMigrator;
    }

    /// <inheritdoc />
    public async Task<Result<int>> ExecuteAsync(MigrationOptions? options = null)
    {
        // Place operations to launch before the migration here

        Result<int> result = await _baseMigrator.ExecuteAsync(options);
        if (result.IsFailed)
        {
            return result;
        }

        // Place operations to launch after the migration here

        return result;
    }
}
Warning

Always check the result of the call to the base implementation before the post-migration code.

Next, you need to create or update the Startup class at the root of the project and register your decorator in the ConfigureServices method for IMigrator using the Decorate method :

public static class Startup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.Decorate<IMigrator, MyMigratorDecorator>();
    }
}

Customizing the migration

The IMigrationInterceptor interface exposes methods that can be used to add code or commands that will be executed at different stages of the migration.
This approach is more versatile that using a migration decorator that can only execute operations before and after a migration.
Each module of a cluster can define one or several migration interceptors that will all be used during a migration.
Interceptors and commands are created in the Application layer business assembly C# project of a module.

Note

This is the preferred method for customized database migration. It's simple to implement and well integrated into the migration process. What's more, in the case of a command implementation, you benefit from an instance of the FluentMigrator.Migration class, with all the features provided by FluentMigrator.

Creating commands to execute during the migration

An interceptor allows the customization of the commands Neos will execute during the migration.
Custom commands can be created by implementing the ICommand interface. Another interface IRunOnceCommand allows you to implement commands that will be executed only once.

For example, when an application is deployed in production, if we mark as required an entity property that previously was not, we will need to updated existing database records to remove null values for this property during the migration.
In order to do that, we will create a new command class :

private class ReplaceNullOrderNotesCommand : ICommand
{
    public void Execute(FluentMigrator.Migration migration, ICommandExecutionArgs args)
    {
        if (args.DatabaseType == DatabaseType.SqlServer)
        {
            // Setting a value using raw SQL
            // Useful when access to other columns of the row is needed
            // Here we set the Notes column value with a concatenation of a text value and the ID column value
            migration.Execute.Sql("UPDATE Order SET Notes = 'Default notes ' + CONVERT(NVARCHAR(100), ID) WHERE Notes IS NULL");
        }
        else if (args.DatabaseType == DatabaseType.PostgreSQL)
        {
            // Setting a value using the Fluent Migrator syntax
            // Here we set the Notes column value with a text value
            migration.Update.Table("Order").Set(new { Notes = "Default notes" }).Where(new { Notes = DBNull.Value });
        }
    }
}

The Execute method contains the code that will be executed by the migration. Here we set a default value in the Notes column of the Order table since the column can no longer contain null values.

The migration parameter is a Fluent Migrator object that allows you to interact with the database. You can check the syntax to use to manipulate data here.

Warning

You should only need to use the Insert, Update and Delete methods to handle data in tables.
Database structure changes should always be handled in Neos Studio.

The args parameters contains information on the context of migration, notably the type of the database being updated.

This other example lets you create database objects not yet managed by Neos Studio. The command below will create the GetProductName function, which will be used in Neos Studio via a ProgrammableObject.

public class CreateGetProductNameFunction : IRunOnceCommand
{
    public string CommandName => "CreateGetProductNameFunction";

    public void Execute(FluentMigrator.Migration migration, ICommandExecutionArgs args)
    {
        switch (args.DatabaseType)
        {
            case DatabaseType.Oracle:
                migration.Execute
                    .Sql(@"CREATE FUNCTION GetProductName(IdOfProduct IN NUMBER) RETURN NVARCHAR2
IS
  product_name NVARCHAR2(100);
BEGIN
  SELECT ""ProductName""
  INTO product_name
  FROM ""Product""
  WHERE ""ProductId"" = IdOfProduct;

  RETURN(product_name);
END;");
                break;

            case DatabaseType.PostgreSQL:
                migration.Execute
                    .Sql(@"CREATE FUNCTION ""GetProductName"" (""IdOfProduct"" integer)
RETURNS character varying(100)
AS $$
DECLARE
  ""NameOfProduct"" character varying(100);
BEGIN
  SELECT ""ProductName"" INTO ""NameOfProduct""
  FROM ""Product""
  WHERE ""ProductId"" = ""IdOfProduct"";

  RETURN ""NameOfProduct"";
END;
$$ LANGUAGE plpgsql;");
                break;

            case DatabaseType.SqlServer:
                migration.Execute
                    .Sql(@"CREATE FUNCTION [dbo].[GetProductName] (@IdOfProduct int)
RETURNS VARCHAR(100)
AS
BEGIN
  DECLARE @NameOfProduct VARCHAR(100)

  SELECT @NameOfProduct = ProductName
  FROM Product
  WHERE ProductId = @IdOfProduct

  RETURN @NameOfProduct
END");
                break;

            default:
                throw new NotSupportedException();
        }
    }
}

As this command implements the IRunOnceCommand interface, it will only be executed once.

Note

Compared with the ICommand interface, IRunOnceCommand requires implementation of the CommandName property. This type of command is stored in the $NeosObject system table, after successful execution. Before execution, the presence of the command in the table is tested, thus guaranteeing a unique execution.

Verifying and resetting the run-once marker

Storage format for manual maintenance:

  • Table: $NeosObject
  • Key columns: ObjectType, ObjectName
  • Stored value for ObjectType: Command
  • Stored value for ObjectName: the exact value of CommandName

Example:

ObjectType ObjectName
Command CreateGetProductNameFunction

Maintenance operations:

  • To force the command to run again on the next migration, delete the corresponding row from $NeosObject.
  • To prevent the command from running, insert the row in $NeosObject in advance with the exact CommandName value.

Example queries:

SELECT *
FROM "$NeosObject"
WHERE "ObjectType" = 'Command'
    AND "ObjectName" = 'CreateGetProductNameFunction';

DELETE
FROM "$NeosObject"
WHERE "ObjectType" = 'Command'
    AND "ObjectName" = 'CreateGetProductNameFunction';
Warning

Use the exact persisted value of CommandName. If you change CommandName in code, Neos will consider it a different run-once command and will create a new marker.

Creating a migration interceptor class

Interceptors are classes that implement the IMigrationInterceptor interface.
This interface exposes methods that allow you to execute code or commands at different stages of the migration.

OnCommandsCreatedAsync

Executed after the existing and expected database schemas are compared and the standard migration commands to execute have been determined.
Allows to add custom commands to the list of commands or totally replace it.

The data parameter contains information on the context of the migration.

The commandList parameter contains the command list. You can add custom commands to the list. See here for more information.

Here is an example adding the custom command that we created earlier that replaces null values in the Notes column of the Order table before the commands that (re)activate constraints :

public class MyMigrationInterceptor : IMigrationInterceptor
{
    public Task<ICommandList> OnCommandsCreatedAsync(CommandsCreatedEventData data, ICommandList commandList)
    {
        if (data.ExistingSchema.Columns.Exists(c => c.TableName == "Order" && c.Name == "Notes" && c.Nullable)
            && data.ExpectedSchema.Columns.Exists(c => c.TableName == "Order" && c.Name == "Notes" && !c.Nullable))
        {
            commandList.AddBefore<ISetNotNullConstraint>(new ReplaceNullOrderNotesCommand());
        }

        return Task.FromResult(commandList);
    }
}

Registering a migration interceptor

After a migration interceptor has been created, you need to create or update the Startup class at the root of the project and register it in the ConfigureServices method using the AddMigrationInterceptor method :

public static class Startup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddMigrationInterceptor<MyMigrationInterceptor>();
    }
}

Command execution order during the migration

The migration process runs in two phases:

  1. Detection phase: The system compares the current database schema with the expected schema (defined by your entities and data tables). It generates a list of commands to execute (create tables, columns, indexes, etc.).
  2. Execution phase: The commands are executed in a precise, predefined order.

Standard migration commands determined by Neos to migrate from the current database structure to the one configured in the metadata are always executed in the same order. The ICommandList interface allows you to add custom commands between them.

Phase Command Description
AddFirst(ICommand) Custom commands inserted here
Exclusions ExcludeTable Excludes a table from the migration
ExcludePrimaryKey Excludes a primary key from the migration
ExcludeForeignKey Excludes a foreign key from the migration
ExcludeIndex Excludes an index from the migration
Drop operations DropView Drops a view
DropCheckConstraint Drops a check constraint
DropForeignKey Drops a foreign key
DropPrimaryKey Drops a primary key
DropIndex Drops an index
Remove constraints DeleteNotNullConstraint Deletes a not null constraint
DeleteDefaultValueConstraint Deletes a default value constraint
Column modifications RenameColumn Renames a column
Table & column creation CreateTable Creates a table
CreateColumn Creates a column
Column alterations ChangeColumnTypeToLocalizableString Changes a column type to manage localizable strings (JSON)
AlterColumn Alters a column
Cleanup DropTable Drops a table
DropDependentComputedColumns Drops dependent computed columns
DropComputedColumn Drops a computed column
DropColumn Drops a column
Constraints, views & indexes SetNotNullConstraint Sets a not null constraint
CreateDependentComputedColumns Creates dependent computed columns
CreateView Creates a view
CreatePrimaryKey Creates a primary key
CreateCheckConstraint Creates a check constraint
RenameIndex Renames an index
CreateIndex Creates an index
CreateForeignKey Creates a foreign key
Finalization UpdateSchemaVersion Updates the schema version
AddLast(ICommand) Custom commands inserted here
Note

During the Drop operations and Remove constraints phases, constraints are removed from the database before structural updates are applied. This allows schema modifications to proceed without causing constraint violations. Constraints are then recreated during the Constraints, views & indexes phase at the end of the migration.
Therefore, if you need to add a custom command that manipulates data, it is recommended to add it just before the ISetNotNullConstraint command.

Caution

When using AddFirst(), your custom command will execute before any table creation. On a new database, tables do not exist yet at this stage. If your command attempts to read or modify data in tables, it will fail.

To safely manipulate data after tables are created, use AddAfter<ICreateTable>(...) or AddLast(...) instead.

You can use the ICommandList.AddBefore<TCommand>(ICommand) / ICommandList.AddAfter<TCommand>(ICommand) methods to insert custom commands at any stage of the migration. If the specified command is not found, the custom command will be inserted at the next step, respecting the order of precedence.

The parameter type TCommand of the generic methods ICommandList.AddBefore<TCommand>(ICommand) / ICommandList.AddAfter<TCommand>(ICommand) must be an interface corresponding to a standard command (e.g., IDropColumn, ICreateTable).

The following examples illustrate how custom commands are inserted into the command list.

Inserting before an existing command

When the specified command exists in the list, the custom command is inserted directly before it.

Initial command list: CreateTable → CreateColumn → AlterColumn → DropColumn → CreatePrimaryKey → CreateIndex

commandList.AddBefore<IDropColumn>(new CustomCommand());

Result: CreateTable → CreateColumn → AlterColumn → CustomCommand → DropColumn → CreatePrimaryKey → CreateIndex

Inserting before a non-existing command (fallback behavior)

When the specified command does not exist in the list, the custom command is inserted at the position where that command would have been, based on the standard execution order.

Initial command list (CreateTable is missing): AlterColumn → DropColumn → CreatePrimaryKey → CreateIndex

commandList.AddBefore<ICreateTable>(new CustomCommand());

Result (CustomCommand is inserted at the start, where CreateTable would have been): CustomCommand → AlterColumn → DropColumn → CreatePrimaryKey → CreateIndex

Inserting after a non-existing command

Similarly, when using AddAfter with a command that does not exist, the custom command is inserted just before the next existing command in the standard order.

Initial command list (CreatePrimaryKey is missing): CreateTable → CreateColumn → AlterColumn → DropColumn → CreateIndex

commandList.AddAfter<ICreatePrimaryKey>(new CustomCommand());

Result (CustomCommand is inserted before CreateIndex): CreateTable → CreateColumn → AlterColumn → DropColumn → CustomCommand → CreateIndex

Inserting at the end when no following command exists

When the specified command and all subsequent commands do not exist, the custom command is appended at the end of the list.

Initial command list (CreateIndex is missing): CreateTable → CreateColumn → AlterColumn → DropColumn → CreatePrimaryKey

commandList.AddBefore<ICreateIndex>(new CustomCommand());

Result (CustomCommand is appended at the end): CreateTable → CreateColumn → AlterColumn → DropColumn → CreatePrimaryKey → CustomCommand

Warning

When you specify a custom command type instead of a standard command interface (e.g., your own ICustomCommand), an InvalidOperationException will be thrown. Only the standard command interfaces listed above are allowed.

Executing operations after tenant database migration

It is possible to perform operations after the migration for all the tenants associated with the database thanks to the OnTenantDatabaseMigratedAsync method of the ITenantDatabaseMigrationInterceptor interceptor.
For example, when a shared database has 3 associated tenants, the interceptor will be executed 3 times.

Creating a tenant database migration interceptor

When an interceptor is executed, its OnTenantDatabaseMigratedAsync method is called. It is in this method that you can set your logic.
ITenants.GetCurrentTenant, INeosTenantInfoAccessor.NeosTenantInfo, repositories... are set for the current tenant.

Example :

internal class MyTenantDatabaseMigrationInterceptor : ITenantDatabaseMigrationInterceptor
{
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;

    public MyTenantDatabaseMigrationInterceptor(IProductRepository productRepository, IUnitOfWork unitOfWork)
    {
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result> OnTenantDatabaseMigratedAsync(TenantDatabaseMigratedEventData tenantDatabaseMigratedEventData)
    {
        if (!_productRepository.GetQuery().Any(c => c.Name == "MyProduct"))
        {
            _productRepository.Add(new()
            {
                Name = "MyProduct",
            });
        }

        return await _unitOfWork.SaveAsync();
    }
}

The TenantDatabaseMigratedEventData provides data to the interceptor:

Property Type Required Description
Tenant NeosTenantInfo Yes Informations on the tenant
ClientAdministratorLogin String No Client administrator login
PublisherAdministratorLogin String No Publisher administrator login
MigrationOptions MigrationOptions No Options of the migration
MigrationResult MigrationResult No Object indicating whether the migration has succeeded with additional informations (migration identifier, previous and current database schemas)
Note

You have the SchemaComparison helper class that allows comparing schema between two databases. The SchemaComparisonResult class represents the result of the comparison and provides information about the differences between a source and target database models. A collection of SchemaDifference is available that contains details of schema comparison.

Example :

internal class SampleTenantDatabaseMigrationInterceptor : ITenantDatabaseMigrationInterceptor
{
    private readonly IMigrationSettings _migrationSettings;
    private readonly IDatabase _database;

    public SampleTenantDatabaseMigrationInterceptor(IMigrationSettings migrationSettings, IDatabase database)
    {
        _migrationSettings = migrationSettings;
        _database = database;
    }

    public async Task<Result> OnTenantDatabaseMigratedAsync(TenantDatabaseMigratedEventData eventData)
    {
        SchemaComparisonResult result = SchemaComparison.Compare(
            eventData.MigrationResult.PreviousSchema,
            eventData.MigrationResult.CurrentSchema,
            _database.DatabaseType,
            _migrationSettings.QuotedIdentifiers);

        if (result.IsEqual)
        {
            // No database change
            return Result.Ok();
        }

        if (result.IsAdded(ObjectType.Table, "Product"))
        {
            // New table "Product"
            ...
        }

        if (result.IsAdded(ObjectType.Column, "Product.QuantityInStock"))
        {
            // New column "QuantityInStock" of the Product "Table"
            ...
        }

        if (result.HasChanged(ObjectType.Column, "Product.CategoryId"))
        {
            // Modifying the "CategoryId" column in the "Product" table
            ...
        }

        if (result.IsDeleted(ObjectType.ForeignKey, "FK_OrderDetail_Order"))
        {
            // Deleting the "FK_OrderDetail_Order" foreign key
            ...
        }

        SchemaDifference? diff =
            result.Differences.FirstOrDefault(
                d =>
                    d.UpdateAction == SchemaUpdateAction.Change &&
                    d.ObjectType == ObjectType.Column &&
                    string.Equals(d.Name, "Product.Price", System.StringComparison.OrdinalIgnoreCase));
        if (diff != null)
        {
            // Gets the old and new version of the "Price" column of the "Product" table
            INeosColumn oldColumn = (INeosColumn)diff.SourceObject;
            INeosColumn newColumn = (INeosColumn)diff.TargetObject;

            ...
        }

        ...

        return Result.Ok();
    }
}

Registering a tenant database migration interceptor

After a tenant migration interceptor has been created, you need to create or update the Startup class at the root of the project and register it in the ConfigureServices method using the AddTenantDatabaseMigrationInterceptor method :

public static class Startup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddTenantDatabaseMigrationInterceptor<MyTenantDatabaseMigrationInterceptor>();
    }
}

Executing a tenant database migration interceptor only once per tenant

Some tenant migration interceptors are not meant to run after every successful migration. This is typically the case when an interceptor performs a one-shot data correction or seeds data that should not be revisited once it has been applied successfully.

In that case, implement IRunOnceTenantDatabaseMigrationInterceptor instead of ITenantDatabaseMigrationInterceptor:

internal class MyTenantDatabaseMigrationInterceptor : IRunOnceTenantDatabaseMigrationInterceptor
{
    public string InterceptorName => "MyTenantDatabaseMigrationInterceptor_V1";

    public Task<Result> OnTenantDatabaseMigratedAsync(TenantDatabaseMigratedEventData tenantDatabaseMigratedEventData)
    {
        // One-shot migration logic for the current tenant.
        return Task.FromResult(Result.Ok());
    }
}

IRunOnceTenantDatabaseMigrationInterceptor uses the same registration method as any other tenant migration interceptor:

services.AddTenantDatabaseMigrationInterceptor<MyTenantDatabaseMigrationInterceptor>();

Behavior details:

  • Neos stores a marker only after a successful execution.
  • If the interceptor fails, no marker is stored and the interceptor is retried on the next migration.
  • The marker is scoped per tenant, including when several tenants share the same physical database.
  • The marker key is based on TenantId and InterceptorName, so InterceptorName must remain stable once deployed.
  • Avoid using nameof(...) for InterceptorName, because renaming the class would change the persisted key and re-execute the interceptor for every tenant.
  • In migration history, a run-once interceptor that does not execute again is reported with the Skipped execution status.

Verifying and resetting the run-once marker

Storage format for manual maintenance:

  • Table: $NeosObject
  • Key columns: ObjectType, ObjectName
  • Stored value for ObjectType: TenantMigrationInterceptor
  • Stored value for ObjectName: tenant-id:<TenantId>|interceptor:<InterceptorName>
  • In single-tenant mode, if the effective tenant identifier is empty, the stored value becomes tenant-id:|interceptor:<InterceptorName>

Example:

  • ObjectType: TenantMigrationInterceptor
  • ObjectName: tenant-id:42|interceptor:MyTenantDatabaseMigrationInterceptor_V1

Maintenance operations:

  • To force a re-execution for one tenant, delete the corresponding row from $NeosObject.
  • To prevent execution for one tenant, insert the row in $NeosObject in advance with the exact tenant id and InterceptorName.
  • To re-execute the interceptor for all tenants of a shared database, delete all rows matching the same InterceptorName.

Example queries:

SELECT *
FROM "$NeosObject"
WHERE "ObjectType" = 'TenantMigrationInterceptor'
    AND "ObjectName" = 'tenant-id:42|interceptor:MyTenantDatabaseMigrationInterceptor_V1';

DELETE
FROM "$NeosObject"
WHERE "ObjectType" = 'TenantMigrationInterceptor'
    AND "ObjectName" = 'tenant-id:42|interceptor:MyTenantDatabaseMigrationInterceptor_V1';

DELETE
FROM "$NeosObject"
WHERE "ObjectType" = 'TenantMigrationInterceptor'
    AND "ObjectName" LIKE 'tenant-id:%|interceptor:MyTenantDatabaseMigrationInterceptor_V1';
Warning

IRunOnceTenantDatabaseMigrationInterceptor does not make the business logic and the persisted run-once marker atomic. The marker is written only after OnTenantDatabaseMigratedAsync(...) returns success. If the interceptor has already committed business changes and the marker persistence fails afterwards (database error, transient network issue, concurrency problem, process interruption, ...), the migration is reported as failed and the interceptor will be retried on the next migration.

As a consequence, run-once tenant migration interceptors must still be designed to be idempotent, even when the business intention is to execute them only once. Prefer logic that can safely run several times, or that explicitly checks whether the targeted correction or seed has already been applied before writing again.

Warning

Use the exact persisted value of InterceptorName. If you rename the interceptor or change InterceptorName, the previously stored marker will no longer match and Neos will execute the interceptor again.

Use this mode only for logic that is truly one-shot. If an interceptor is expected to resynchronize data or configuration on every migration, keep using ITenantDatabaseMigrationInterceptor.

When is the tenant database migration interceptor executed ?

In multi-tenant mode

When the application is executed using the neos run -mt command or when the application is deployed and configured with multiTenancy=true option, the interceptor is executed in several cases :

  • After the database migration process
    In a shared database, the interceptor is called sequentially for each tenant. There is no parallelization to avoid database access problems and facilitate debugging.
    In a separate database, the interceptor is called only once per database on the single tenant it is associated with.

    Example with a migration request on a shared database containing 3 tenants :

sequenceDiagram
    autonumber
    participant A as Requester
    participant B as Business Cluster - Backend
    A->>B: Request migration
    B->>B: Migration successful
    B->>B: OnTenantDatabaseMigratedAsync on tenant 1
    B->>B: OnTenantDatabaseMigratedAsync on tenant 2
    B->>B: OnTenantDatabaseMigratedAsync on tenant 3
    B->>A: Migration result
  • During the tenant initialization after a successful migration
sequenceDiagram
    autonumber
    participant A as Requester
    participant B as Business Cluster - Backend
    A->>B: Publish "TenantInit" to create Tenant 'MyTenant'
    B->>B: Tenant initialization
    B->>B: Migration successful
    B->>B: OnTenantDatabaseMigratedAsync on tenant 'MyTenant'
    B->>A: TenantInit result

Single tenant mode (automatic migration)

When a application is in single tenant mode, the backend runs database migrations automatically.

Warning

Even if the application is not in multi-tenant mode, OnTenantDatabaseMigratedAsync will be called as we consider that there is one tenant in the database.

By default, in development mode, when you execute the neos run command, the backend runs migrations and when successful, the interceptor is executed.

graph
    A(Business Cluster - Backend)-->B(Automatic migration : true)
    B-->C{Run migration}
    C-->|Successful| D[Run OnTenantDatabaseMigratedAsync]
    C-->|Failed| E[OnTenantDatabaseMigratedAsync not executed]

Inserting data and forcing the value of auto-incremented properties

When you need to import data during the migration, you may be inserting data that contain auto-incremented values, like an "ID" property for example. By default, this value is automatically set by the database and you cannot specify a the value you want.

When you create the ICommand class that will import data, you can inject and use an IInsertIdentityScope to force an auto-incremented property to use a specified value. This value will automatically be incremented for each subsequent insertions.

Here is an example. You can find a complete example in the technical demos cluster in the TechnicalDemos.slnx > Business > Core.Application > ImportDataMigration folder :

internal class ImportDataCommand : ICommand
{
    private readonly IInsertIdentityScopeService _insertIdentityScopeService;
    private readonly DatabaseContext _databaseContext;

    public ImportDataCommand(IInsertIdentityScopeService insertIdentityScopeService, DatabaseContext databaseContext)
    {
        _insertIdentityScopeService = insertIdentityScopeService;
        // Need to add the Persistence project to references.
        _databaseContext = databaseContext;
    }

    public void Execute(Migration migration, ICommandExecutionArgs args)
    {
        // Example using connection with SQL
        migration.Execute.WithConnection((connection, transaction) =>
        {
            using IDbCommand hasCompanyCommand = connection.CreateCommand();
            hasCompanyCommand.CommandText = "SELECT EXISTS(SELECT 1 FROM \"Company\")";
            bool hasCompany = (bool)hasCompanyCommand.ExecuteScalar()!;
            if (!hasCompany)
            {
                using (IInsertIdentityScope insertIdentityScope = _insertIdentityScopeService.Start(connection, "Company", "Id", transaction))
                {
                    // Insert the first company with the ID 5.
                    using IDbCommand insertCommand = connection.CreateCommand();
                    insertCommand.Transaction = transaction;
                    insertCommand.CommandText = "INSERT INTO \"Company\" (\"Id\", \"Name\") VALUES (5, 'AKANEA')";
                    insertCommand.ExecuteNonQuery();
                    // If not explicitly specified, then dispose will stop the scope automatically.
                    insertIdentityScope.StopScope();
                }

                // The second company will have the ID 6.
                using IDbCommand insert2Command = connection.CreateCommand();
                insert2Command.Transaction = transaction;
                insert2Command.CommandText = "INSERT INTO \"Company\" (\"Name\") VALUES ('IRIUM')";
                insert2Command.ExecuteNonQuery();
            }
        });
    }
}