EF Core 2.0 中的新功能

.NET Standard 2.0

EF Core 现在面向 .NET Standard 2.0,这意味着它可以与 .NET Core 2.0、.NET Framework 4.6.1 以及实现 .NET Standard 2.0 的其他库配合使用。 有关受支持的 .NET 实现的详细信息,请参阅支持的 .NET 实现

Modeling

表拆分

现在可以将两个或多个实体类型映射到将共享主键列所在的同一个表,并且每行对应于两个或多个实体。

若要使用表拆分标识性关系(其中外键属性构成主键),需要在所有共享该表的实体类型之间进行配置:

modelBuilder.Entity<Product>()
    .HasOne(e => e.Details).WithOne(e => e.Product)
    .HasForeignKey<ProductDetails>(e => e.Id);
modelBuilder.Entity<Product>().ToTable("Products");
modelBuilder.Entity<ProductDetails>().ToTable("Products");

有关此功能的详细信息,请阅读 有关表拆分的部分

拥有的类型

拥有的实体类型可以与另一个拥有的实体类型共享相同的 .NET 类型,但由于仅凭 .NET 类型无法识别,因此必须从另一个实体类型导航到它。 包含定义导航的实体即为所有者。 查询所有者时,固有类型将默认包含在内。

按照约定,将为被拥有的类型创建影子主键,并通过表拆分将其映射到与所有者相同的表。 这允许使用自己的类型,类似于 EF6 中使用复杂类型的方式:

modelBuilder.Entity<Order>().OwnsOne(p => p.OrderDetails, cb =>
    {
        cb.OwnsOne(c => c.BillingAddress);
        cb.OwnsOne(c => c.ShippingAddress);
    });

public class Order
{
    public int Id { get; set; }
    public OrderDetails OrderDetails { get; set; }
}

public class OrderDetails
{
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}

有关此功能的详细信息,请阅读 有关拥有实体类型的部分

模型级查询筛选器

EF Core 2.0 包括我们称之为模型级查询筛选器的新功能。 此功能允许 LINQ 查询谓词(通常传递给 LINQ Where 查询运算符的布尔表达式)直接在元数据模型中的实体类型(通常位于 OnModelCreating 中)上定义。 此类筛选器会自动应用于涉及这些实体类型的任何 LINQ 查询,包括间接引用的实体类型,例如使用 Include 或直接导航属性引用。 此功能的一些常见应用程序包括:

  • 软删除 - 实体类型定义 IsDeleted 属性。
  • 多租户 - 实体类型定义 TenantId 属性。

下面是演示上面列出的两种方案的功能的简单示例:

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    public int TenantId { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>().HasQueryFilter(
            p => !p.IsDeleted
            && p.TenantId == this.TenantId);
    }
}

我们定义了一个模型级筛选器,该筛选器实现了对 Post 实体类型实例的多租户和软删除。 请注意实例级属性的使用 DbContextTenantId. 模型级筛选器将使用正确上下文实例(即正在执行查询的上下文实例)中的值。

可以使用 IgnoreQueryFilters() 运算符为单个 LINQ 查询禁用筛选器。

局限性

  • 不允许导航引用。 可以根据反馈添加此功能。
  • 筛选器只能在层次结构的根实体类型上定义。

数据库标量函数映射

EF Core 2.0 包括 Paul Middleton 的重要贡献,它使数据库标量函数能够映射到方法存根,以便可以在 LINQ 查询中使用,并将其转换为 SQL。

下面是有关如何使用该功能的简要说明:

在你的DbContext上声明一个静态方法,并使用DbFunctionAttribute进行注释:

public class BloggingContext : DbContext
{
    [DbFunction]
    public static int PostReadCount(int blogId)
    {
        throw new NotImplementedException();
    }
}

此类方法会自动注册。 注册后,可以将 LINQ 查询中方法的调用转换为 SQL 中的函数调用:

var query =
    from p in context.Posts
    where BloggingContext.PostReadCount(p.Id) > 5
    select p;

有几点需要注意:

  • 按照约定,该方法的名称在生成 SQL 时用作函数的名称(在本例中为用户定义的函数),但在方法注册期间可以重写名称和架构。
  • 目前仅支持标量函数。
  • 必须在数据库中创建映射函数。 EF Core 迁移不会负责创建该对象。

代码优先的自包含类型配置

在 EF6 中,可以通过派生自 EntityTypeConfiguration 来封装特定实体类型的代码第一个配置。 在 EF Core 2.0 中,我们将此模式带回:

class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.HasKey(c => c.AlternateKey);
        builder.Property(c => c.Name).HasMaxLength(200);
    }
}

...
// OnModelCreating
builder.ApplyConfiguration(new CustomerConfiguration());

高性能

DbContext 池

在 ASP.NET Core 应用程序中使用 EF Core 的基本模式通常涉及将自定义 DbContext 类型注册到依赖项注入系统中,稍后通过控制器中的构造函数参数获取该类型的实例。 这意味着为每个请求创建 DbContext 的新实例。

在版本 2.0 中,我们将引入一种在依赖项注入中注册自定义 DbContext 类型的新方法,以透明方式引入可重用 DbContext 实例池。 要使用 DbContext 池,在服务注册期间使用 AddDbContextPool 而不是 AddDbContext

services.AddDbContextPool<BloggingContext>(
    options => options.UseSqlServer(connectionString));

如果使用此方法,则当控制器请求 DbContext 实例时,我们将首先检查池中是否有可用的实例。 请求处理完成后,实例上的任何状态将重置,实例本身将返回到池。

这在概念上类似于连接池在 ADO.NET 提供程序中运行的方式,并具有节省 DbContext 实例初始化的一些成本的优势。

局限性

新方法对 DbContext 方法中 OnConfiguring() 可以执行的操作引入了一些限制。

警告

如果在派生的 DbContext 类中维护自己的状态(例如私有字段),且这些状态不应跨请求共享,请避免使用 DbContext Pooling。 EF Core 只会重置在将 DbContext 实例添加到池之前已知的状态。

显式编译的查询

这是第二个可选的性能功能,旨在在大规模场景中提供优势。

手动或显式编译的查询 API 在以前版本的 EF 中也可用,在 LINQ to SQL 中也允许应用程序缓存查询转换,以便仅计算一次查询并执行多次。

尽管一般 EF Core 可以根据查询表达式的哈希表示形式自动编译和缓存查询,但此机制可以通过绕过哈希计算和缓存查找来获取较小的性能提升,从而允许应用程序通过调用委托来使用已编译的查询。

// Create an explicitly compiled query
private static Func<CustomerContext, int, Customer> _customerById =
    EF.CompileQuery((CustomerContext db, int id) =>
        db.Customers
            .Include(c => c.Address)
            .Single(c => c.Id == id));

// Use the compiled query by invoking it
using (var db = new CustomerContext())
{
   var customer = _customerById(db, 147);
}

更改跟踪

附件功能可以跟踪新的和现有实体的图表。

EF Core 支持通过各种机制自动生成键值。 使用此功能时,如果键属性为 CLR 默认值,则生成值-通常为零或 null。 这意味着可以将实体图传递给 DbContext.AttachDbSet.Attach EF Core 将标记已设置 Unchanged 密钥的实体,而没有密钥集的实体将被标记为 Added。 在使用生成的键时,可以轻松地附加包含新旧混合实体的图。 DbContext.UpdateDbSet.Update 的工作方式相同,但具有键集的实体被标记为 Modified 而不是 Unchanged

查询

改进了 LINQ 翻译

更大数量的查询可以成功执行,更多的逻辑将在数据库中(而非内存中)进行评估,从而减少不必要的数据从数据库中检索。

GroupJoin 改进

此项工作改进了为组联接生成的 SQL。 分组连接通常是可选导航属性子查询的结果。

FromSql 和 ExecuteSqlCommand 中的字符串内插

C# 6 引入了字符串内插,该功能允许 C# 表达式直接嵌入字符串文本中,从而在运行时提供生成字符串的好方法。 在 EF Core 2.0 中,我们向接受原始 SQL 字符串的两个主要 API 添加了对内插字符串的特殊支持: FromSqlExecuteSqlCommand。 此新支持允许以“安全”方式使用 C# 字符串内插。 也就是说,它可以防止在运行时动态构造 SQL 时可能发生的常见 SQL 注入错误。

以下是示例:

var city = "London";
var contactTitle = "Sales Representative";

using (var context = CreateContext())
{
    context.Set<Customer>()
        .FromSql($@"
            SELECT *
            FROM ""Customers""
            WHERE ""City"" = {city} AND
                ""ContactTitle"" = {contactTitle}")
            .ToArray();
  }

在此示例中,SQL 格式字符串中嵌入了两个变量。 EF Core 将生成以下 SQL:

@p0='London' (Size = 4000)
@p1='Sales Representative' (Size = 4000)

SELECT *
FROM ""Customers""
WHERE ""City"" = @p0
    AND ""ContactTitle"" = @p1

EF.Functions.Like()

我们添加了 EF.Functions 属性,EF Core 或提供程序可以使用此属性来定义映射到数据库函数或运算符的方法,以便在 LINQ 查询中调用这些方法。 此类方法的第一个示例为 Like():

var aCustomers =
    from c in context.Customers
    where EF.Functions.Like(c.Name, "a%")
    select c;

请注意,Like() 自带内存实现,在处理内存数据库或需要在客户端进行谓词评估时,这非常有用。

数据库管理

DbContext 构建的复数化机制

EF Core 2.0 引入了一个新的 IPluralizer 服务,该服务用于对实体类型名称和 DbSet 名称进行复数化。 默认实现是一个 no-op,因此这只是一个挂钩,人们可以轻松插入自己的复数化器。

开发人员集成自己复数处理器的方式如下:

public class MyDesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection services)
    {
        services.AddSingleton<IPluralizer, MyPluralizer>();
    }
}

public class MyPluralizer : IPluralizer
{
    public string Pluralize(string name)
    {
        return Inflector.Inflector.Pluralize(name) ?? name;
    }

    public string Singularize(string name)
    {
        return Inflector.Inflector.Singularize(name) ?? name;
    }
}

其他

将 ADO.NET SQLite 提供程序移动到 SQLitePCL.raw

这为我们在 Microsoft.Data.Sqlite 中提供了更可靠的解决方案,用于在不同平台上分发本机 SQLite 二进制文件。

每个模型只有一个供应商

显著增强了提供程序如何与模型交互,并简化了约定、注释和 Fluent API 如何与不同的提供程序配合使用。

EF Core 2.0 现在将为使用的每种提供程序构建不同的 IModel。 这通常对应用程序是透明的。 这简化了低级元数据 API 的使用,即任何访问常见关系元数据概念的操作都通过调用.Relational来进行,而不是通过.SqlServer.Sqlite等。

统一日志记录和诊断

日志记录(基于 ILogger)和诊断(基于 DiagnosticSource)机制现在共享更多代码。

发送到 ILogger 的消息的事件 ID 已在 2.0 中更改。 事件 ID 现在在 EF Core 代码中是唯一的。 这些消息现在还遵循用于结构化日志记录的标准模式,例如 MVC。

记录器类别也已更改。 现在可通过 DbLoggerCategory 访问一组已知的类别。

DiagnosticSource 事件现在使用与相应 ILogger 消息相同的事件 ID 名称。