实体框架自定义代码优先约定(EF6以后)

时间:2023-01-20 07:03:23

仅限EF6仅向前 - 此页面中讨论的功能,API等在实体框架6中引入。如果您使用的是早期版本,则部分或全部信息不适用。

使用Code First时,您的模型是使用一组约定从您的类计算的。默认的Code First Conventions确定哪些属性成为实体的主键,实体映射到的表的名称,以及默认情况下十进制列具有的精度和比例。

有时,这些默认约定对于您的模型并不理想,您必须通过使用Data Annotations或Fluent API配置许多单个实体来解决这些问题。自定义代码优先约定允许您定义自己的约定,为您的模型提供配置默认值。在本演练中,我们将探讨不同类型的自定义约定以及如何创建它们。

此页面介绍了用于自定义约定的DbModelBuilder API。此API应足以创作大多数自定义约定。但是,还可以创建基于模型的约定 - 一旦创建后操纵最终模型的约定 - 来处理高级场景。有关更多信息,请参阅基于模型的约定(EF6以上版本)

让我们从定义一个可以与我们的约定一起使用的简单模型开始。将以下类添加到项目中。

     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()); modelBuilder.Properties()
.Where(x => x.Name == "Name")
.Configure(x => x.IsKey().HasColumnOrder());

此代码将配置模型中的类型,以使组合键由int Key列和字符串Name列组成。如果我们在设计器中查看模型,它将如下所示:

实体框架自定义代码优先约定(EF6以后)

属性约定的另一个示例是在我的模型中配置所有DateTime属性以映射到SQL Server中的datetime2类型而不是datetime。您可以通过以下方式实现此目的:

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

定义约定的另一种方法是使用约定类来封装您的约定。使用Convention类时,您将创建一个继承System.Data.Entity.ModelConfiguration.Conventions命名空间中的Convention类的类型。

我们可以通过执行以下操作来创建具有我们之前显示的datetime2约定的Convention类:

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

要告诉EF使用此约定,请将其添加到OnModelCreating中的Conventions集合中,如果您一直按照演练进行操作,则它将如下所示:

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

如您所见,我们将约定的实例添加到约定集合中。继承自Convention提供了一种跨团队或项目分组和共享约定的便捷方式。例如,您可以拥有一个类库,其中包含所有组织项目使用的一组通用约定。

自定义属性

 约定的另一个重要用途是在配置模型时启用新属性。为了说明这一点,让我们创建一个属性,我们可以使用该属性将String属性标记为非Unicode。
   [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;
}
}

一旦我们有了这个,我们可以在我们的属性上设置一个bool来告诉约定一个属性是否应该是Unicode。我们可以通过访问配置类的ClrProperty来实现这一点,如下所示:

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

这很容易,但通过使用convention API的Having方法,有一种更简洁的方法来实现这一点。Having方法有一个类型为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));

自定义属性不是使用Having方法的唯一原因,在配置类型或属性时,您需要在任何地方推断您正在过滤的内容。

到目前为止,我们所有的约定都是针对属性的,但是还有另一个约定API区域用于配置模型中的类型。这种体验类似于我们目前看到的约定,但configure中的选项将在实体而不是属性级别。

类型级约定可以真正有用的一件事是更改表命名约定,要么映射到不同于EF默认值的现有模式,要么创建具有不同命名约定的新数据库。为此,我们首先需要一个方法,它可以接受模型中类型的TypeInfo,并返回该类型的表名应该是什么:

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

此方法接受一个类型并返回一个字符串,该字符串使用带有下划线的小写而不是CamelCase。在我们的模型中,这意味着ProductCategory类将映射到名为product_category而不是ProductCategories的表。

一旦我们有了这个方法,我们就可以在这样的约定中调用它:

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

此约定将我们模型中的每个类型配置为映射到从GetTableName方法返回的表名。此约定相当于使用Fluent API为模型中的每个实体调用ToTable方法。

有一点需要注意的是,当你调用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[] + "_" + m.Value[]); return result.ToLower();
}

注意:GetService的通用版本是System.Data.Entity.Infrastructure.DependencyResolution命名空间中的扩展方法,您需要在上下文中添加using语句才能使用它。

ToTable和继承

ToTable的另一个重要方面是,如果您将类型显式映射到给定表,那么您可以更改EF将使用的映射策略。如果为继承层次结构中的每种类型调用ToTable,将类型名称作为表的名称传递,就像我们上面所做的那样,那么您将默认的Table-Per-Hierarchy(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; }
}

默认情况下,employee和manager都映射到数据库中的同一个表(Employees)。该表将包含一个带有鉴别器列的员工和经理,该列将告诉您每行中存储的实例类型。这是TPH映射,因为层次结构有一个表。但是,如果在两个classe上调用ToTable,则每个类型将被映射到其自己的表,也称为TPT,因为每个类型都有自己的表。

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

上面的代码将映射到如下所示的表结构:

实体框架自定义代码优先约定(EF6以后)

您可以通过以下几种方式避免这种情况,并维护默认的TPH映射:

  1. 使用层次结构中每种类型的相同表名调用ToTable。
  2. 仅在层次结构的基类上调用ToTable,在我们的示例中将是employee。

约定以最后的方式运行,与Fluent API相同。这意味着如果你编写两个约定来配置相同属性的相同选项,那么最后一个执行获胜。例如,在下面的代码中,所有字符串的最大长度都设置为500,但我们将模型中名为Name的所有属性配置为最大长度为250。

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

因为将max length设置为250的约定是在将所有字符串设置为500的约定之后,所以我们模型中名为Name的所有属性将具有250的MaxLength,而任何其他字符串(例如描述)将为500。这种方式意味着您可以为模型中的类型或属性提供一般约定,然后将它们覆盖在不同的子集上。

Fluent API和Data Annotations也可用于在特定情况下覆盖约定。在上面的示例中,如果我们使用Fluent API来设置属性的最大长度,那么我们可以在约定之前或之后放置它,因为更具体的Fluent API将胜过更一般的配置约定。

由于自定义约定可能受默认的Code First约定的影响,因此添加约定以在另一个约定之前或之后运行会很有用。为此,您可以在派生的DbContext上使用Conventions集合的AddBefore和AddAfter方法。以下代码将添加我们之前创建的约定类,以便它将在内置键发现约定之前运行。

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

在添加需要在内置约定之前或之后运行的约定时,这将是最有用的,可以在此处找到内置约定的列表:System.Data.Entity.ModelConfiguration.Conventions Namespace

您还可以删除不希望应用于模型的约定。要删除约定,请使用Remove方法。以下是删除PluralizingTableNameConvention的示例。

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