[Из песочницы] Создание пользовательских миграционных операций в Entity Framework 6

Миграции в Entity Framework (EF) представляют собой строго типизированный подход для выполнения распространенных операций, таких как создание, изменение и удаление таблиц, столбцов, индексов, и т.д. Однако реализация базовых операций достаточно ограничена и не поддерживает весь спектр параметров, которые поддерживает та или иная СУБД.До EF 6, единственным способом обхода данного ограничения было использование операции Sql, которая позволяет выполнить произвольную команду SQL при выполнении миграции. В EF 6 также появилась возможность реализации пользовательских строго типизированных операций.

Создание собственных операцийБазовая реализация операции CreateIndex позволяет задать список колонок, по которым строится индекс, а также позволяет указать является ли индекс уникальным и/или кластерным. Однако, например, команда CREATE INDEX Microsoft SQL Server поддерживает так же указание направлений сортировки, задание списка включенных колонок, параметров хранения и других дополнительных ограничений.Рассмотрим реализацию данной расширенной операции миграции.

Для начала создадим класс ExtendedCreateIndexOperation, который наследуется от абстрактного класса MigrationOperation и переопределим свойство IsDestructiveChange, которое указывает, может ли наша операция привести к потере данных.

Начнем с возможности поддержки указания направлений сортировки по колонкам. Для этого создадим вспомогательный класс IndexColumnModel, который будет в себе содержать информацию о названии колонки и направление сортировки по ней.

IndexColumnModel.cs namespace CustomMigrations.Infrastructure.Migrations.Models { using System;

///

/// Index column sort direction. /// internal enum SortDirection { Ascending, Descending }

///

/// Represents information about an index column. /// internal class IndexColumnModel { /// /// Gets or sets the name of the column. /// /// /// The name of the column. /// public string Name { get; set; }

///

/// Gets or sets the sort direction. /// /// /// The sort direction. /// public SortDirection SortDirection { get; set; } } } Далее, добавим коллекцию строк, которая будет содержать список имен колонок таблицы, которые необходимо включить в индекс, а также другие необходимые параметры.

В результате получим следующий класс:

ExtendedCreateIndexOperation.cs namespace CustomMigrations.Infrastructure.Migrations.Operations { using System; using System.Collections.Generic; using System.Data.Entity.Migrations.Model; using System.Linq; using Models;

///

/// Represents creating an extended database index. /// internal class ExtendedCreateIndexOperation: MigrationOperation { private readonly ICollection _columns = new List(); private readonly ICollection _includes = new List(); private string _name; private string _table;

///

/// Initializes a new instance of the class. /// /// /// Use anonymous type syntax to specify arguments e.g. 'new { SampleArgument = «MyValue» }'. /// public ExtendedCreateIndexOperation (object anonymousArguments = null) : base (anonymousArguments) { }

///

/// Gets the columns collection. /// /// /// The columns collection. /// public ICollection Columns { get { return _columns; } }

///

/// Gets the non-key columns to be added to the leaf level of the nonclustered index. /// /// /// The the non-key columns to be added to the leaf level of the nonclustered index. /// public ICollection Includes { get { return _includes; } }

///

/// Gets or sets the name of the table. /// /// /// The name of the table. /// /// Table name is null or whitespace.; value public string Table { get { return _table; } set { if (string.IsNullOrWhiteSpace (value)) { throw new ArgumentException («Table name is null or whitespace.», «value»); } _table = value; } }

///

/// Gets or sets the name of the index. /// /// /// The name of the index. /// public string Name { get { return _name? IndexOperation.BuildDefaultName ( _columns.Where (c => c!= null && ! string.IsNullOrWhiteSpace (c.Name)).Select (c => c.Name)); } set { _name = value; } }

///

/// Gets or sets a value indicating if this is a unique index. /// /// /// The value indicating if this is a unique index. /// public bool IsUnique { get; set; }

///

/// Gets or sets whether this is a clustered index. /// /// /// Whether this is a clustered index. /// public bool IsClustered { get; set; }

///

/// Gets or sets the WHERE option (). /// /// /// The WHERE option. /// public string Where { get; set; }

///

/// Gets or sets the WITH option ( [ ,…n ]). /// /// /// The WITH option. /// public string With { get; set; }

///

/// Gets or sets the ON option (partition_scheme_name (column_name) | filegroup_name | default). /// /// /// The ON option. /// public string On { get; set; }

///

/// Gets or sets the FILESTREAM_ON option (filestream_filegroup_name | partition_scheme_name | «NULL»). /// /// /// The FILESTREAM_ON option. /// public string FileStreamOn { get; set; }

public override bool IsDestructiveChange { get { return false; } } } } Подключение операций Хорошо, операция создана, но необходимо добавить возможность ее использования.Для этого добавим метод расширения CreateIndex, который будет создавать экземпляр класса ExtendedCreateIndexOperation, передавать ему необходимые значения параметров и добавлять его в список операций текущей миграции.

DbMigrationExtensions.cs namespace CustomMigrations.Infrastructure.Extensions { using System; using System.Collections.Generic; using System.Data.Entity.Migrations; using System.Data.Entity.Migrations.Infrastructure; using System.Linq; using Migrations.Models; using Migrations.Operations;

internal static class DbMigrationExtensions { ///

/// Adds an operation to create an table index. /// /// The database migration instance. /// /// The name of the table to create the index on. Schema name is optional, if no schema is specified /// then dbo is assumed. /// /// The name of the columns to create the index on. /// The includes. /// /// A value indicating if this is a unique index. If no value is supplied a non-unique index will be /// created. /// /// /// The name to use for the index in the database. If no value is supplied a unique name will be /// generated. /// /// A value indicating whether or not this is a clustered index. /// The WHERE option (). /// The WITH option ( [ ,…n ]). /// The ON option (partition_scheme_name (column_name) | filegroup_name | default). /// The FILESTREAM_ON option (filestream_filegroup_name | partition_scheme_name | «NULL»). /// /// Additional arguments that may be processed by providers. Use anonymous type syntax to /// specify arguments e.g. 'new { SampleArgument = «MyValue» }'. /// /// /// migration /// or /// columns /// /// /// Table name is null or whitespace.; table /// or /// Columns collection is empty.; columns /// public static void CreateIndex (this DbMigration migration, string table, ICollection columns, ICollection includes = null, bool unique = false, string name = null, bool clustered = false, string where = null, string with = null, string on = null, string fileStreamOn = null, object anonymousArguments = null) { if (migration == null) { throw new ArgumentNullException («migration»); }

if (string.IsNullOrWhiteSpace (table)) { throw new ArgumentException («Table name is null or whitespace.», «table»); }

if (columns == null) { throw new ArgumentNullException («columns»); }

if (! columns.Any ()) { throw new ArgumentException («Columns collection is empty.», «columns»); }

var createIndexOperation = new ExtendedCreateIndexOperation (anonymousArguments) { Table = table, IsUnique = unique, Name = name, IsClustered = clustered, Where = where, With = with, On = on, FileStreamOn = fileStreamOn };

foreach (IndexColumnModel column in columns) { createIndexOperation.Columns.Add (column); }

if (includes!= null) { foreach (string column in includes) { createIndexOperation.Includes.Add (column); } }

((IDbMigration) migration).AddOperation (createIndexOperation); } } } Генерация SQL кода Для того чтобы научить EF преобразовывать пользовательские операции в SQL код, необходимо расширить возможности имеющегося базового генератора.Для этого необходимо создать наследника от класса SqlServerMigrationSqlGeneratorи и переопределить метод Generate (MigrationOperation). Данный метод вызывается только при обработке операций, которые неизвестны базовому генератору SQL.

Опишем необходимые преобразования из нашей операции в соответствующее выражение на языке SQL.

CustomSqlServerMigrationSqlGenerator.cs namespace CustomMigrations.Infrastructure.Migrations { using System; using System.Collections.Generic; using System.Data.Entity.Migrations.Model; using System.Data.Entity.Migrations.Utilities; using System.Data.Entity.SqlServer; using System.Linq; using Models; using Operations;

///

/// Custom provider to convert provider agnostic migration operations into SQL commands /// that can be run against a Microsoft SQL Server database. /// internal sealed class CustomSqlServerMigrationSqlGenerator: SqlServerMigrationSqlGenerator { private static readonly IDictionary SortDirectionDescriptionMap = new Dictionary { {SortDirection.Ascending, «ASC»}, {SortDirection.Descending, «DESC»} };

///

/// Generates SQL for a . /// Allows derived providers to handle additional operation types. /// Generated SQL should be added using the Statement method. /// /// The operation to produce SQL for. /// migrationOperation protected override void Generate (MigrationOperation migrationOperation) { if (migrationOperation == null) { throw new ArgumentNullException («migrationOperation»); }

var createIndexOperation = migrationOperation as ExtendedCreateIndexOperation; if (createIndexOperation == null) { return; }

Generate (createIndexOperation); }

///

/// Generates SQL for a /// . /// /// The operation to produce SQL for. /// createIndexOperation private void Generate (ExtendedCreateIndexOperation createIndexOperation) { if (createIndexOperation == null) { throw new ArgumentNullException («createIndexOperation»); }

using (IndentedTextWriter writer = Writer ()) { writer.Write («CREATE »);

if (createIndexOperation.IsUnique) { writer.Write («UNIQUE »); }

if (createIndexOperation.IsClustered) { writer.Write («CLUSTERED »); }

writer.Write («INDEX »); writer.WriteLine (Quote (createIndexOperation.Name)); writer.Indent++; writer.Write («ON »); writer.Write (Name (createIndexOperation.Table));

writer.Write (»(»); writer.Write (string.Join (»,», createIndexOperation.Columns.Where (c => c!= null && ! string.IsNullOrWhiteSpace (c.Name)) .Select (c => Quote (c.Name) + » » + SortDirectionDescriptionMap[c.SortDirection]))); writer.Write (»)»);

// Skip the INCLUDE part for clustered indexes. if (! createIndexOperation.IsClustered && createIndexOperation.Includes.Count > 0) { writer.WriteLine (); writer.Write («INCLUDE (»); writer.Write (string.Join (»,», createIndexOperation.Includes.Where (c =>! string.IsNullOrWhiteSpace©).Select (Quote))); writer.Write (»)»); }

if (! string.IsNullOrWhiteSpace (createIndexOperation.Where)) { writer.WriteLine (); writer.Write («WHERE »); writer.Write (createIndexOperation.Where); }

if (! string.IsNullOrWhiteSpace (createIndexOperation.With)) { writer.WriteLine (); writer.Write («WITH (»); writer.Write (createIndexOperation.With); writer.Write (»)»); }

if (! string.IsNullOrWhiteSpace (createIndexOperation.On)) { writer.WriteLine (); writer.Write («ON »); writer.Write (createIndexOperation.On); }

if (! string.IsNullOrWhiteSpace (createIndexOperation.FileStreamOn)) { writer.WriteLine (); writer.Write («FILESTREAM_ON »); writer.Write (createIndexOperation.On); }

Statement (writer); } } } } Как видите, создание операций позволяет также добавить в них дополнительную логику проверок (в данном случае пропускается генерация кода для включенных колонок, в случае, если индекс является кластерным).

Итак, все необходимые приготовления завершены, и теперь необходимо подключить наш генератор.

Его можно зарегистрировать в соответствующих настройках миграций:

Configuration.cs namespace CustomMigrations.Migrations { using System.Data.Entity; using System.Data.Entity.Migrations; using System.Data.Entity.SqlServer;

internal sealed class Configuration: DbMigrationsConfiguration { public Configuration () { SetSqlGenerator (SqlProviderServices.ProviderInvariantName, new CustomSqlServerMigrationSqlGenerator ()); } } } Или в настройках DbConfigration приложения:

CustomDbConfiguration.cs namespace CustomMigrations.Migrations { using System.Data.Entity; using System.Data.Entity.SqlServer;

public class CustomDbConfiguration: DbConfiguration { public CustomDbConfiguration () { SetMigrationSqlGenerator (SqlProviderServices.ProviderInvariantName, () => new CustomSqlServerMigrationSqlGenerator ()); } } } Небольшие улучшения Для того чтобы удобнее было добавлять колонки без явного создания экземпляров класса IndexColumnModel добавим несколько методов расширения для строк, а также оператор неявного преобразования типов в сам класс.StringExtensions.cs namespace CustomMigrations.Infrastructure.Extensions { using System; using Migrations.Models;

internal static class StringExtensions { ///

/// Creates the index column model with the ascending sort direction. /// /// The column name. /// The index column model. /// value public static IndexColumnModel Ascending (this string value) { if (value == null) { throw new ArgumentNullException («value»); }

return new IndexColumnModel {Name = value, SortDirection = SortDirection.Ascending}; }

///

/// Creates the index column model with the descending sort direction. /// /// The column name. /// The index column model. /// value public static IndexColumnModel Descending (this string value) { if (value == null) { throw new ArgumentNullException («value»); }

return new IndexColumnModel {Name = value, SortDirection = SortDirection.Descending}; } } } IndexColumnModel.cs namespace CustomMigrations.Infrastructure.Migrations.Models { using System;

///

/// Index column sort direction. /// internal enum SortDirection { Ascending, Descending }

///

/// Represents information about an index column. /// internal class IndexColumnModel { /// /// Gets or sets the name of the column. /// /// /// The name of the column. /// public string Name { get; set; }

///

/// Gets or sets the sort direction. /// /// /// The sort direction. /// public SortDirection SortDirection { get; set; }

///

/// Performs an implicit conversion from to . /// /// The value. /// /// The result of the conversion. /// /// value public static implicit operator IndexColumnModel (string value) { if (value == null) { throw new ArgumentNullException («value»); } return new IndexColumnModel {Name = value, SortDirection = SortDirection.Ascending}; } } } Проверка результатов работы Для иллюстрации результатов работы сравним создание индекса с параметрами по старинке и с использованием нашей операции.Было:

Sql («CREATE NONCLUSTERED INDEX [IX_Column1_Column2_Column3] ON [dbo].[TestTable]» + »(» + » [Column1] DESC,» + » [Column2] ASC,» + » [Column3] ASC» + »)» + » INCLUDE (» + » [Column4],» + » [Column5]» + »)» + » WHERE [Column6] = 'Some filter'» + » WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]»);

… Стало:

this.CreateIndex («dbo.TestTable», columns: new[] {«Column1».Descending (), «Column2».Ascending (), «Column3»}, includes: new[] {«Column4», «Column5»}, where:»[Column6] = 'Some filter'», with: «SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF», on:»[PRIMARY]» );

… Результат работы операции можно оценить, выполнив команду Update-Database -Script в консоли Package Manager Console.

CREATE INDEX [IX_Column1_Column2_Column3] ON [dbo].[TestTable]([Column1] DESC, [Column2] ASC, [Column3] ASC) INCLUDE ([Column4], [Column5]) WHERE [Column6] = 'Some filter' WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]

… Полный исходный код доступен здесь.

© Habrahabr.ru