EF Core 2.0 的新功能

.NET 標準 2.0

EF Core 現在的目標是 .NET Standard 2.0,這意味著它可以與 .NET Core 2.0、.NET Framework 4.6.1 以及其他實作 .NET Standard 2.0 的函式庫相容。 更多支援內容請參閱 支援的 .NET 實作

建 模

檔案分割

現在可以將兩種或多種實體類型映射到同一個表格,主鍵欄位會共享,且每列對應兩個或多個實體。

要使用表格拆分,必須在所有共享該表格的實體類型間設定一個識別關係(其中外鍵屬性為主鍵):

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 使用或直接使用導覽屬性而間接參照的實體類型。 此功能的一些常見應用包括:

  • 軟刪除 - Entity Types 定義了 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實例的軟刪除。 請注意使用 DbContext 實例層級屬性: TenantId。 模型層級過濾器會使用正確的上下文實例(即執行查詢的上下文實例)的值。

可使用 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 pooling,請在服務註冊期間使用 AddDbContextPool 取代 AddDbContext

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

若使用此方法,當控制器請求 DbContext 實例時,我們會先檢查池中有沒有實例可用。 請求處理完成後,實例上的任何狀態會被重置,實例本身會被回傳回池。

這在概念上類似於 ADO.NET 提供者中的連線池運作方式,且其優點是節省部分 DbContext 實例初始化的成本。

局限性

新方法在 DbContext 的 OnConfiguring() 方法中引入了一些限制。

警告

如果你在衍生的 DbContext 類別中維護你自己的狀態(例如私有欄位),而這些狀態不應在請求間共享,請避免使用 DbContext Pooling。 EF Core 只會在將 DbContext 實例加入池之前,重設其已知的狀態。

顯式編譯查詢

這是第二個選擇加入的效能功能,旨在於高規模情境下提供優勢。

手動或明確編譯的查詢 API 在先前版本的 EF 以及 LINQ 轉 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);
}

變更追蹤

Attach 可以追蹤新舊實體的變動圖表

EF Core 支援透過多種機制自動產生關鍵值。 使用此功能時,若鍵屬性為 CLR 預設值,通常為零或空值,則會產生一個值。 這表示可以將實體圖傳送到 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。函數屬性,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 名稱。 預設實作是不執行任何操作,所以這是個掛鉤,讓大家可以輕鬆插入自己的複數化工具。

以下是開發者掛鉤自己的複數化工具時的樣貌:

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 二進位檔。

每個模型只能有一位提供者

大幅提升提供者與模型互動的方式,並簡化慣例、註解與流暢 API 在不同提供者間的運作方式。

EF Core 2.0 現在會為每個不同的提供者建置不同的 IModel 。 這通常對應用程式來說是透明的。 這促進了低階元資料 API 的簡化,使得任何對常見關聯性元資料概念的存取,一律透過呼叫 .Relational,而不再使用 .SqlServer.Sqlite 等方法。

整合日誌與診斷

日誌(基於 ILogger)與診斷機制(基於 DiagnosticSource)現在共享更多程式碼。

發送給 ILogger 的訊息事件 ID 在 2.0 版本中有所更改。 事件 ID 現在在 EF Core 程式碼中是唯一的。 這些訊息現在也遵循了例如MVC等標準的結構化日誌模式。

日誌記錄器類別也有所變動。 現在有一套知名的分類可透過 DbLoggerCategory 存取。

DiagnosticSource 事件現在使用與對應 ILogger 訊息相同的事件 ID 名稱。