Поделиться через


Пользовательские операции миграции

API MigrationBuilder позволяет выполнять множество различных операций во время миграции, но это далеко не является исчерпывающим. Однако API также расширяемый, что позволяет определять собственные операции. Существует два способа расширения API: использование метода Sql() или определение пользовательских объектов MigrationOperation.

Чтобы проиллюстрировать, давайте рассмотрим реализацию операции, которая создает пользователя базы данных с помощью каждого подхода. В наших миграциях мы хотим дать возможность писать следующий код:

migrationBuilder.CreateUser("SQLUser1", "Password");

Использование MigrationBuilder.Sql()

Самый простой способ реализации пользовательской операции — определить метод расширения, который вызывает MigrationBuilder.Sql(). Вот пример, который генерирует правильный Transact-SQL-запрос.

public static OperationBuilder<SqlOperation> CreateUser(
    this MigrationBuilder migrationBuilder,
    string name,
    string password)
    => migrationBuilder.Sql($"CREATE USER {name} WITH PASSWORD '{password}';");

Подсказка

В SQL Server используйте функцию EXEC, если инструкция должна быть первой или единственной в пакете SQL. Кроме того, может потребоваться обходить ошибки синтаксического анализа в сценариях идемпотентной миграции, которые могут возникнуть, если в таблице отсутствуют упомянутые столбцы в данный момент.

Если миграция должна поддерживать несколько поставщиков баз данных, можно использовать свойство MigrationBuilder.ActiveProvider. Ниже приведен пример, поддерживающий Microsoft SQL Server и PostgreSQL.

public static OperationBuilder<SqlOperation> CreateUser(
    this MigrationBuilder migrationBuilder,
    string name,
    string password)
{
    switch (migrationBuilder.ActiveProvider)
    {
        case "Npgsql.EntityFrameworkCore.PostgreSQL":
            return migrationBuilder
                .Sql($"CREATE USER {name} WITH PASSWORD '{password}';");

        case "Microsoft.EntityFrameworkCore.SqlServer":
            return migrationBuilder
                .Sql($"CREATE USER {name} WITH PASSWORD = '{password}';");
    }

    throw new Exception("Unexpected provider.");
}

Этот подход работает только в том случае, если вы знаете каждого поставщика, где будет применяться пользовательская операция.

Использование миграционной операции

Чтобы отделить пользовательскую операцию от SQL, можно определить свой собственный MigrationOperation для её представления. Затем операция передается поставщику, чтобы определить соответствующий SQL для создания.

public class CreateUserOperation : MigrationOperation
{
    public string Name { get; set; }
    public string Password { get; set; }
}

При таком подходе метод расширения просто должен добавить одну из этих операций в MigrationBuilder.Operations.

public static OperationBuilder<CreateUserOperation> CreateUser(
    this MigrationBuilder migrationBuilder,
    string name,
    string password)
{
    var operation = new CreateUserOperation { Name = name, Password = password };
    migrationBuilder.Operations.Add(operation);

    return new OperationBuilder<CreateUserOperation>(operation);
}

Этот подход требует, чтобы каждый поставщик знал, как создать SQL для этой операции в службе IMigrationsSqlGenerator. Ниже приведен пример переопределения генератора SQL Server для обработки новой операции.

public class MyMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public MyMigrationsSqlGenerator(
        MigrationsSqlGeneratorDependencies dependencies,
        ICommandBatchPreparer commandBatchPreparer)
        : base(dependencies, commandBatchPreparer)
    {
    }

    protected override void Generate(
        MigrationOperation operation,
        IModel model,
        MigrationCommandListBuilder builder)
    {
        if (operation is CreateUserOperation createUserOperation)
        {
            Generate(createUserOperation, builder);
        }
        else
        {
            base.Generate(operation, model, builder);
        }
    }

    private void Generate(
        CreateUserOperation operation,
        MigrationCommandListBuilder builder)
    {
        var sqlHelper = Dependencies.SqlGenerationHelper;
        var stringMapping = Dependencies.TypeMappingSource.FindMapping(typeof(string));

        builder
            .Append("CREATE USER ")
            .Append(sqlHelper.DelimitIdentifier(operation.Name))
            .Append(" WITH PASSWORD = ")
            .Append(stringMapping.GenerateSqlLiteral(operation.Password))
            .AppendLine(sqlHelper.StatementTerminator)
            .EndCommand();
    }
}

Замените службу генератора SQL миграций по умолчанию обновленной.

protected override void OnConfiguring(DbContextOptionsBuilder options)
    => options
        .UseSqlServer(_connectionString)
        .ReplaceService<IMigrationsSqlGenerator, MyMigrationsSqlGenerator>();