基于模型的约定
注意
仅限 EF6 及更高版本 - 此页面中讨论的功能、API 等已引入实体框架 6。 如果使用的是早期版本,则部分或全部信息不适用。
基于模型的约定是基于约定的模型配置的一种高级方法。 在大多数情况下,应使用 DbModelBuilder 上的自定义 Code First 约定 API。 在使用基于模型的约定之前,建议先了解 DbModelBuilder API 的约定。
基于模型的约定允许创建影响属性和表的约定,这些属性和表无法通过标准约定进行配置。 例如,每个层次结构模型的表中的鉴别器列和独立关联列。
创建约定
创建基于模型的约定的第一步是选择何时需要在管道中将约定应用于模型。 有两种类型的模型约定:概念 (C-Space) 和存储 (S-Space)。 C-Space 约定应用于应用程序生成的模型,而 S-Space 约定应用于表示数据库并控制自动生成列的命名方式等操作的模型版本。
模型约定是一个从 IConceptualModelConvention 或 IStoreModelConvention 扩展的类。 这些接口都接受类型为 MetadataItem 的泛型类型,该类型用于筛选约定适用的数据类型。
添加约定
模型约定的添加方式与常规约定类相同。 在 OnModelCreating 方法中,将约定添加到模型的约定列表中。
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
public class BlogContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Comment> Comments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add<MyModelBasedConvention>();
}
}
还可使用 Conventions.AddBefore 或 Conventions.AddAfter 方法将约定添加到另一个约定<><>。 有关实体框架适用的约定的详细信息,请参阅备注部分。
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.AddAfter<IdKeyDiscoveryConvention>(new MyModelBasedConvention());
}
示例:鉴别器模型约定
重命名 EF 生成的列是不能与其他约定 API 一起执行的操作示例。 在这种情况下,使用模型约定是唯一的选项。
使用基于模型的约定配置生成的列的示例之一是自定义鉴别器列的命名方式。 下面是一个基于模型的简单约定示例,该约定将名为“Discriminator”模型的每一列重命名为“EntityType”。 这包括开发人员简单命名为“Discriminator”的列。 由于“Discriminator”列是生成的列,因此需要在 S-Space 中运行。
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
class DiscriminatorRenamingConvention : IStoreModelConvention<EdmProperty>
{
public void Apply(EdmProperty property, DbModel model)
{
if (property.Name == "Discriminator")
{
property.Name = "EntityType";
}
}
}
示例:常规 IA 重命名约定
另一个更复杂的基于模型的约定的示例是配置独立关联 (IA) 命名的方式。 在这种情况下,模型约定适用,因为 IA 由 EF 生成,并且不在 DbModelBuilder API 可以访问的模型中。
EF 生成 IA 时,会创建名为 EntityType_KeyName 的列。 例如,对于具有名为 CustomerId 关键列的名为 Customer 的关联,它将生成名为 Customer_CustomerId 的列。 以下约定将“_”字符从为 IA 生成的列名中去除。
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
// Provides a convention for fixing the independent association (IA) foreign key column names.
public class ForeignKeyNamingConvention : IStoreModelConvention<AssociationType>
{
public void Apply(AssociationType association, DbModel model)
{
// Identify ForeignKey properties (including IAs)
if (association.IsForeignKey)
{
// rename FK columns
var constraint = association.Constraint;
if (DoPropertiesHaveDefaultNames(constraint.FromProperties, constraint.ToRole.Name, constraint.ToProperties))
{
NormalizeForeignKeyProperties(constraint.FromProperties);
}
if (DoPropertiesHaveDefaultNames(constraint.ToProperties, constraint.FromRole.Name, constraint.FromProperties))
{
NormalizeForeignKeyProperties(constraint.ToProperties);
}
}
}
private bool DoPropertiesHaveDefaultNames(ReadOnlyMetadataCollection<EdmProperty> properties, string roleName, ReadOnlyMetadataCollection<EdmProperty> otherEndProperties)
{
if (properties.Count != otherEndProperties.Count)
{
return false;
}
for (int i = 0; i < properties.Count; ++i)
{
if (!properties[i].Name.EndsWith("_" + otherEndProperties[i].Name))
{
return false;
}
}
return true;
}
private void NormalizeForeignKeyProperties(ReadOnlyMetadataCollection<EdmProperty> properties)
{
for (int i = 0; i < properties.Count; ++i)
{
int underscoreIndex = properties[i].Name.IndexOf('_');
if (underscoreIndex > 0)
{
properties[i].Name = properties[i].Name.Remove(underscoreIndex, 1);
}
}
}
}
扩展现有约定
如果需要编写类似于实体框架已应用于模型的约定之一,可以始终扩展该约定,以避免从头开始重写它。 示例之一是将现有的 ID 匹配约定替换为自定义 ID 匹配约定。 重写密钥约定的一个附加好处是,只有在尚未检测到或显式配置键时,才调用重写的方法。 此处提供了由实体框架使用的约定列表:http://msdn.microsoft.com/library/system.data.entity.modelconfiguration.conventions.aspx。
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Linq;
// Convention to detect primary key properties.
// Recognized naming patterns in order of precedence are:
// 1. 'Key'
// 2. [type name]Key
// Primary key detection is case insensitive.
public class CustomKeyDiscoveryConvention : KeyDiscoveryConvention
{
private const string Id = "Key";
protected override IEnumerable<EdmProperty> MatchKeyProperty(
EntityType entityType, IEnumerable<EdmProperty> primitiveProperties)
{
Debug.Assert(entityType != null);
Debug.Assert(primitiveProperties != null);
var matches = primitiveProperties
.Where(p => Id.Equals(p.Name, StringComparison.OrdinalIgnoreCase));
if (!matches.Any())
{
matches = primitiveProperties
.Where(p => (entityType.Name + Id).Equals(p.Name, StringComparison.OrdinalIgnoreCase));
}
// If the number of matches is more than one, then multiple properties matched differing only by
// case--for example, "Key" and "key".
if (matches.Count() > 1)
{
throw new InvalidOperationException("Multiple properties match the key convention");
}
return matches;
}
}
然后,我们需要在现有密钥约定之前添加新约定。 添加 CustomKeyDiscoveryConvention 后,可以删除 IdKeyDiscoveryConvention。 如果不删除现有的 IdKeyDiscoveryConvention,则此约定仍将优先于 ID 发现约定,因为它先运行,但在未找到“密钥”属性的情况下,将运行“ID”约定。 我们看到此行为,是因为每个约定都会将模型视为由以前的约定更新的模型(而不是单独操作模型,并全部合并在一起),因此,如果以前的约定更新了列名称以匹配自定义约定中感兴趣的内容(在此之前,该名称不是感兴趣的内容),则它将应用于该列。
public class BlogContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Comment> Comments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new CustomKeyDiscoveryConvention());
modelBuilder.Conventions.Remove<IdKeyDiscoveryConvention>();
}
}
说明
此处的 MSDN 文档中提供了当前由实体框架应用的约定列表:http://msdn.microsoft.com/library/system.data.entity.modelconfiguration.conventions.aspx。 此列表直接从源代码拉取。 GitHub 上提供了 实体框架 6 的源代码,并且实体框架使用的许多约定都是基于自定义模型约定的良好起点。