пользовательские соглашения Code First

Примечание

Только в EF6 и более поздних версиях. Функции, API и другие возможности, описанные на этой странице, появились в Entity Framework 6. При использовании более ранней версии могут быть неприменимы некоторые или все сведения.

при использовании Code First модель вычисляется из классов с помощью набора соглашений. соглашения Code First по умолчанию определяют, как свойство станет первичным ключом сущности, имя таблицы, которой сопоставлена сущность, и точность и масштаб десятичного столбца по умолчанию.

иногда эти соглашения по умолчанию не подходят для вашей модели, и их необходимо обойти, настроив множество отдельных сущностей с помощью заметок к данным или API Fluent. настраиваемые соглашения о Code First позволяют определять собственные соглашения, предоставляющие параметры конфигурации по умолчанию для вашей модели. В этом пошаговом руководстве будут рассмотрены различные типы пользовательских соглашений и способы их создания.

Соглашения Model-Based

На этой странице рассматривается API Дбмоделбуилдер для пользовательских соглашений. Этот API должен быть достаточным для создания наиболее настраиваемых соглашений. Однако существует также возможность создавать соглашения на основе модели, которые управляют завершающей моделью после ее создания — для обработки сложных сценариев. Дополнительные сведения см. в разделе соглашения на основе модели.

 

Наша модель

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

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }
    }

    public class Product
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public decimal? Price { get; set; }
        public DateTime? ReleaseDate { get; set; }
        public ProductCategory Category { get; set; }
    }

    public class ProductCategory
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public List<Product> Products { get; set; }
    }

 

Введение в пользовательские соглашения

Давайте напишем соглашение, которое настраивает любое свойство с именем KEY в качестве первичного ключа для типа сущности.

Соглашения включены в построителе моделей, доступ к которым можно получить путем переопределения OnModelCreating в контексте. Обновите класс Продуктконтекст следующим образом:

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Properties()
                        .Where(p => p.Name == "Key")
                        .Configure(p => p.IsKey());
        }
    }

Теперь любое свойство в нашей модели с именем Key будет настроено как первичный ключ любой сущности.

Мы также можем сделать наши соглашения более конкретными путем фильтрации по типу свойства, которое мы собираемся настроить:

    modelBuilder.Properties<int>()
                .Where(p => p.Name == "Key")
                .Configure(p => p.IsKey());

Это приведет к настройке всех свойств, именуемых Key, на первичный ключ своей сущности, но только в том случае, если они являются целым числом.

Интересная особенность метода IsKey заключается в том, что он является аддитивным. Это означает, что при вызове IsKey на нескольких свойствах все они становятся частью составного ключа. Одним из этих особенностей является то, что при указании нескольких свойств для ключа необходимо также указать порядок этих свойств. Это можно сделать, вызвав метод Хасколумнордер, как показано ниже:

    modelBuilder.Properties<int>()
                .Where(x => x.Name == "Key")
                .Configure(x => x.IsKey().HasColumnOrder(1));

    modelBuilder.Properties()
                .Where(x => x.Name == "Name")
                .Configure(x => x.IsKey().HasColumnOrder(2));

Этот код настраивает типы в нашей модели для использования составного ключа, состоящего из ключевого столбца int и столбца строкового имени. При просмотре модели в конструкторе она будет выглядеть следующим образом:

composite Key

еще один пример соглашений о свойствах — настройка всех свойств DateTime в модели для соответствия типу datetime2 в SQL Server вместо DateTime. Это можно сделать следующим образом:

    modelBuilder.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));

 

Классы соглашений

Другим способом определения соглашений является использование класса соглашения для инкапсуляции вашего соглашения. При использовании класса соглашения создается тип, который наследуется от класса соглашения в пространстве имен System. Data. Entity. Моделконфигуратион. соглашений.

Мы можем создать класс соглашения с соглашением datetime2, которое мы показали ранее, выполнив следующие действия.

    public class DateTime2Convention : Convention
    {
        public DateTime2Convention()
        {
            this.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));        
        }
    }

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

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties<int>()
                    .Where(p => p.Name.EndsWith("Key"))
                    .Configure(p => p.IsKey());

        modelBuilder.Conventions.Add(new DateTime2Convention());
    }

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

 

Настраиваемые атрибуты

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

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class NonUnicode : Attribute
    {
    }

Теперь создадим соглашение, чтобы применить этот атрибут к нашей модели:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
                .Configure(c => c.IsUnicode(false));

В соответствии с этим соглашением мы можем добавить атрибут не в Юникоде к любому из наших строковых свойств, что означает, что столбец в базе данных будет храниться как varchar, а не nvarchar.

Обратите внимание на то, что при помещении атрибута не в Юникоде для любого, кроме строкового свойства, будет выдано исключение. Это происходит потому, что нельзя настроить параметр GetString в Юникоде для любого типа, кроме строки. В этом случае вы можете сделать соглашение более конкретным, чтобы оно отфильтровывао все, что не является строкой.

Хотя приведенное выше соглашение работает при определении пользовательских атрибутов, существует еще один API, который гораздо проще в использовании, особенно если требуется использовать свойства из класса Attribute.

В этом примере мы будем обновлять наш атрибут и изменим его на атрибут an в Юникоде, поэтому он выглядит следующим образом:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    internal class IsUnicode : Attribute
    {
        public bool Unicode { get; set; }

        public IsUnicode(bool isUnicode)
        {
            Unicode = isUnicode;
        }
    }

После этого можно задать логическое значение для нашего атрибута, чтобы указать, что свойство должно быть в Юникоде. Это можно сделать в соответствии с соглашением, которое мы уже получили, обращаясь к Клрпроперти класса конфигурации следующим образом:

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
                .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));

Это достаточно просто, но существует более лаконичный способ достижения этого, используя метод HAVING API соглашений. Метод HAVING имеет параметр типа Func < PropertyInfo, T, > который принимает объект PropertyInfo, аналогичный методу Where, но предполагает возврат объекта. Если возвращенный объект имеет значение null, свойство не будет настроено. Это означает, что вы можете отфильтровывать свойства с ним, как и там, но при этом он также будет захватывать возвращаемый объект и передавать его в метод configure. Это работает следующим образом:

    modelBuilder.Properties()
                .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
                .Configure((config, att) => config.IsUnicode(att.Unicode));

Пользовательские атрибуты — это не единственная причина использования метода HAVING. Это полезно в любом месте, где необходимо указать, что вы фильтруете при настройке типов или свойств.

 

Настройка типов

Пока все наши соглашения были для свойств, но есть другая область API соглашений для настройки типов в модели. Опыт похож на те, которые мы видели до сих пор, но параметры внутри настройки будут находиться в сущности, а не на уровне свойств.

Одна из моментов, с которой соглашения об уровне типа может быть действительно полезна для, — изменение соглашения об именовании таблиц для соответствия существующей схеме, которая отличается от EF Default, или создание новой базы данных с другим соглашением об именовании. Для этого нам нужен метод, который может принять TypeInfo для типа в нашей модели и вернуть имя таблицы для этого типа:

    private string GetTableName(Type type)
    {
        var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Этот метод принимает тип и возвращает строку, в которой используется нижний регистр с символами подчеркивания вместо CamelCase. В нашей модели это означает, что класс ProductCategory будет сопоставлен с таблицей с именем product_category вместо ProductCategories.

После этого метод можно вызвать в соответствии со следующим соглашением:

    modelBuilder.Types()
                .Configure(c => c.ToTable(GetTableName(c.ClrType)));

Это соглашение настраивает каждый тип в нашей модели для соотнесения с именем таблицы, возвращаемым методомического TableName. это соглашение эквивалентно вызову метода ToTable для каждой сущности в модели с помощью API Fluent.

Обратите внимание, что при вызове ToTable EF будет принимать строку, которую вы задаете как точное имя таблицы, без какого-либо множественного преобразования, которое обычно выполняется при определении имен таблиц. Именно поэтому имя таблицы в соглашении product_category вместо product_categories. Мы можем разрешить это в нашем соглашении, сделав вызов службы во множественном числе.

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

    private string GetTableName(Type type)
    {
        var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();

        var result = pluralizationService.Pluralize(type.Name);

        result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Примечание

Универсальная версия метода WebService является методом расширения в пространстве имен System. Data. Entity. Infrastructure. Депенденциресолутион. для его использования необходимо добавить инструкцию using в контекст.

ToTable и наследование

Еще один важный аспект ToTable заключается в том, что при явном сопоставлении типа с заданной таблицей можно изменить стратегию сопоставления, которую будет использовать EF. Если вы вызываете ToTable для каждого типа в иерархии наследования, передавая имя типа в качестве имени таблицы, как мы делали ранее, то изменим стратегию сопоставления таблиц по иерархии (иерархия) по умолчанию до типа "одна таблица на тип" (TPT). Лучший способ описать это — вхис конкретный пример:

    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Manager : Employee
    {
        public string SectionManaged { get; set; }
    }

По умолчанию сотрудники и менеджер сопоставляются с одной и той же таблицей в базе данных. Таблица будет содержать как сотрудников, так и руководителей со столбцом дискриминатора, который сообщит, какой тип экземпляра хранится в каждой строке. Это сопоставление с иерархической таблицей, так как для иерархии существует одна таблица. Однако при вызове ToTable для обоих классе каждый тип будет сопоставлен с собственной таблицей, также известной как TPT, так как каждый тип имеет свою собственную таблицу.

    modelBuilder.Types()
                .Configure(c=>c.ToTable(c.ClrType.Name));

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

tpt Example

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

  1. Вызовите ToTable с тем же именем таблицы для каждого типа в иерархии.
  2. Вызовите ToTable только для базового класса иерархии, в нашем примере — Employee.

 

Порядок выполнения

соглашения работают при последнем wins-процессе, то же, что и Fluent API. Это означает, что при написании двух соглашений, которые настраивают один и тот же параметр одного и того же свойства, последний для выполнения WINS. Например, в коде ниже максимальной длины всех строк задано значение 500, но затем все свойства, имена которых называются в модели, будут иметь максимальную длину 250.

    modelBuilder.Properties<string>()
                .Configure(c => c.HasMaxLength(500));

    modelBuilder.Properties<string>()
                .Where(x => x.Name == "Name")
                .Configure(c => c.HasMaxLength(250));

Поскольку в соответствии с соглашением для установки параметра Max length равным 250, после того, как для всех строк задается значение 500, все свойства, имена которых называются в нашей модели, будут иметь значение MaxLength, равное 250, а любые другие строки, например описания, будут состоять из 500. Использование соглашений таким образом означает, что вы можете предоставить общее соглашение для типов или свойств в модели, а затем переопределить их для поднаборов, которые отличаются.

Fluent API и заметки к данным также можно использовать для переопределения соглашения в конкретных случаях. в нашем примере выше, если мы использовали api Fluent для установки максимальной длины свойства, мы могли бы разместить его до или после соглашения, поскольку более конкретное Fluent API будет выиграно по более общему соглашению о конфигурации.

 

Встроенные соглашения

так как соглашения Code First по умолчанию могут повлиять на пользовательские соглашения, может быть полезно добавить соглашения, которые будут выполняться до или после другого соглашения. Для этого можно использовать методы Аддбефоре и Аддафтер коллекции соглашений для производного DbContext. Следующий код добавляет созданный ранее класс соглашения, чтобы он выполнялся перед встроенным соглашением об обнаружении ключа.

    modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());

Это будет наиболее частое применение при добавлении соглашений, которые должны выполняться до или после встроенных соглашений. список встроенных соглашений можно найти здесь: System. Data. Entity. моделконфигуратион. Conventions Namespace.

Можно также удалить соглашения, которые не должны применяться к модели. Чтобы удалить соглашение, используйте метод Remove. Ниже приведен пример удаления Плурализингтабленамеконвентион.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }