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

Примечание.

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

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

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

Соглашения на основе моделей

На этой странице рассматривается API DbModelBuilder для пользовательских соглашений. Этот 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 в контексте. Обновите класс ProductContext следующим образом:

    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 для нескольких свойств все они станут частью составного ключа. Одним из предостережением этого является то, что при указании нескольких свойств ключа необходимо также указать порядок для этих свойств. Это можно сделать, вызвав метод HasColumnOrder, как показано ниже:

    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 Key и столбца string Name. Если мы рассмотрим модель в конструкторе, она будет выглядеть следующим образом:

composite Key

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

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

 

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

Другой способ определения соглашений — использовать класс соглашения для инкапсулирования соглашения. При использовании класса Соглашения создается тип, наследующий от класса Convention в пространстве имен System.Data.Entity.ModelConfiguration.Conventions.

Мы можем создать класс соглашения с соглашением 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());
    }

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

Одна из вещей, которые могут быть полезными для соглашения о уровне типов, заключается в изменении соглашения об именовании таблиц либо для сопоставления с существующей схемой, которая отличается от EF по умолчанию, либо для создания новой базы данных с другим соглашением об именовании. Для этого сначала требуется метод, который может принимать 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)));

Это соглашение настраивает каждый тип в нашей модели для сопоставления с именем таблицы, возвращаемой из метода GetTableName. Это соглашение эквивалентно вызову метода 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();
    }

Примечание.

Универсальная версия GetService — это метод расширения в пространстве имен System.Data.Entity.DependencyResolution, необходимо добавить инструкцию using в контекст, чтобы использовать ее.

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

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

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

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

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

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

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

tpt Example

Это можно избежать и поддерживать сопоставление TPH по умолчанию несколькими способами:

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

 

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

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

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

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

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

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

 

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

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

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

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

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

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