共用方式為


EF Core 7.0 的新功能

EF Core 7.0 (EF7) 於 2022 年 11 月發行。

提示

您可以從 GitHub 下載範例程式碼來執行和偵錯範例。 每個區段都會連結到該區段專屬的原始程式碼。

EF7 以 .NET 6 為目標,因此可以搭配 .NET 6 (LTS).NET 7 使用。

範例模型

下列許多範例會搭配部落格、文章、標籤和作者使用簡單的模型:

public class Blog
{
    public Blog(string name)
    {
        Name = name;
    }

    public int Id { get; private set; }
    public string Name { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Post
{
    public Post(string title, string content, DateTime publishedOn)
    {
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
    }

    public int Id { get; private set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedOn { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
    public Author? Author { get; set; }
    public PostMetadata? Metadata { get; set; }
}

public class FeaturedPost : Post
{
    public FeaturedPost(string title, string content, DateTime publishedOn, string promoText)
        : base(title, content, publishedOn)
    {
        PromoText = promoText;
    }

    public string PromoText { get; set; }
}

public class Tag
{
    public Tag(string id, string text)
    {
        Id = id;
        Text = text;
    }

    public string Id { get; private set; }
    public string Text { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Author
{
    public Author(string name)
    {
        Name = name;
    }

    public int Id { get; private set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; } = null!;
    public List<Post> Posts { get; } = new();
}

有些範例也會使用匯總類型,這些類型在不同的範例中以不同方式對應。 連絡人有一個匯總類型:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

後置元數據的第二個匯總類型:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

提示

您可以在 BlogsContext.cs中找到範例模型。

JSON 數據行

大部分關係資料庫都支援包含 JSON 檔的數據行。 這些數據行中的 JSON 可以使用查詢來鑽研。 例如,這允許依檔元素進行篩選和排序,以及將專案從檔投影到結果中。 JSON 數據行可讓關係資料庫承擔文件資料庫的某些特性,在兩者之間建立有用的混合式。

EF7 包含與 JSON 數據行無關的提供者支援,以及 SQL Server 的實作。 此支援允許將從 .NET 類型建置的匯總對應至 JSON 檔。 一般 LINQ 查詢可用於匯總,這些查詢會轉譯成鑽研 JSON 所需的適當查詢建構。 EF7 也支援更新和儲存 JSON 檔的變更。

注意

針對EF7后,已規劃對 JSON 的 SQLite 支援。 PostgreSQL 和 Pomelo MySQL 提供者已經包含 JSON 資料行的一些支援。 我們將與這些提供者的作者合作,將所有提供者的 JSON 支援一致。

對應至 JSON 數據行

在 EF Core 中,匯總類型是使用 OwnsOneOwnsMany來定義。 例如,請考慮用來儲存連絡資訊之範例模型的匯總類型:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

這接著可用於「擁有者」實體類型,例如,用來儲存作者的聯繫人詳細數據:

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; }
}

匯總類型是使用 OwnsOne在 中OnModelCreating設定的:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

提示

此處顯示的程式代碼來自 JsonColumnsSample.cs

根據預設,關係資料庫提供者會將這類匯總類型對應至與擁有實體類型的相同數據表。 也就是說,和 Address 類別的每個屬性ContactDetails都會對應至數據表中的數據Authors行。

一些具有聯繫人詳細數據的已儲存作者看起來會像這樣:

作者

Id 名稱 Contact_Address_Street Contact_Address_City Contact_Address_Postcode Contact_Address_Country Contact_Phone
1 麥迪·蒙塔奎拉 1 主街 坎伯威克·格林 CW1 5ZH UK 01632 12345
2 Jeremy Likness 2 主街 奇格利 CW1 5ZH UK 01632 12346
3 丹尼爾·羅斯 3 主街 坎伯威克·格林 CW1 5ZH UK 01632 12347
4 亞瑟·維克斯 15a Main St 奇格利 CW1 5ZH 英國 01632 22345
5 布裡斯·蘭布森 4 主街 奇格利 CW1 5ZH UK 01632 12349

如有需要,組成匯總的每個實體類型都可以改為對應至自己的數據表:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToTable("Contacts");
            ownedNavigationBuilder.OwnsOne(
                contactDetails => contactDetails.Address, ownedOwnedNavigationBuilder =>
                {
                    ownedOwnedNavigationBuilder.ToTable("Addresses");
                });
        });
}

然後,相同的數據會儲存在三個數據表中:

作者

Id 名稱
1 麥迪·蒙塔奎拉
2 Jeremy Likness
3 丹尼爾·羅斯
4 亞瑟·維克斯
5 布裡斯·蘭布森

連絡人

AuthorId 手機
1 01632 12345
2 01632 12346
3 01632 12347
4 01632 22345
5 01632 12349

位址

ContactDetailsAuthorId 路/街 縣/市 郵遞區號 Country
1 1 主街 坎伯威克·格林 CW1 5ZH UK
2 2 主街 奇格利 CW1 5ZH 英國
3 3 主街 坎伯威克·格林 CW1 5ZH 英國
4 15a Main St 奇格利 CW1 5ZH 英國
5 4 主街 奇格利 CW1 5ZH UK

現在,對於有趣的部分。 在EF7中 ContactDetails ,匯總類型可以對應至 JSON 數據行。 設定匯總類型時,這只需要呼叫一次 ToJson()

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToJson();
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

資料表 Authors 現在會包含 JSON 資料行,以 ContactDetails 填入每個作者的 JSON 檔:

作者

Id 名稱 Contact
1 麥迪·蒙塔奎拉 {
  “Phone”:“01632 12345”,
  “Address”: {
    “City”:“Camberwick Green”,
    “Country”:“UK”,
    “Postcode”:“CW1 5ZH”,
    “Street”:“1 Main St”
  }
}
2 Jeremy Likness {
  “Phone”:“01632 12346”,
  “Address”: {
    “City”:“Chigley”,
    “Country”:“UK”,
    “Postcode”:“CH1 5ZH”,
    “Street”:“2 Main St”
  }
}
3 丹尼爾·羅斯 {
  “Phone”:“01632 12347”,
  “Address”: {
    “City”:“Camberwick Green”,
    “Country”:“UK”,
    “Postcode”:“CW1 5ZH”,
    “Street”:“3 Main St”
  }
}
4 亞瑟·維克斯 {
  “Phone”:“01632 12348”,
  “Address”: {
    “City”:“Chigley”,
    “Country”:“UK”,
    “Postcode”:“CH1 5ZH”,
    “Street”:“15a Main St”
  }
}
5 布裡斯·蘭布森 {
  “Phone”:“01632 12349”,
  “Address”: {
    “City”:“Chigley”,
    “Country”:“UK”,
    “Postcode”:“CH1 5ZH”,
    “Street”:“4 Main St”
  }
}

提示

使用適用於 Azure Cosmos DB 的 EF Core 提供者時,這種匯總的使用方式與 JSON 檔對應的方式非常類似。 JSON 資料行會將 EF Core 與文件資料庫搭配使用的功能帶到內嵌在關係資料庫中的檔。

上述 JSON 檔非常簡單,但此對應功能也可以與更複雜的文件結構搭配使用。 例如,請考慮範例模型中的另一個匯總類型,用來代表有關文章的元數據:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

這個匯總類型包含數個巢狀類型和集合。 和 OwnsManyOwnsOne呼叫可用來對應此匯總類型:

modelBuilder.Entity<Post>().OwnsOne(
    post => post.Metadata, ownedNavigationBuilder =>
    {
        ownedNavigationBuilder.ToJson();
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopSearches);
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopGeographies);
        ownedNavigationBuilder.OwnsMany(
            metadata => metadata.Updates,
            ownedOwnedNavigationBuilder => ownedOwnedNavigationBuilder.OwnsMany(update => update.Commits));
    });

提示

ToJson 只有在匯總根目錄上才能將整個匯總對應至 JSON 檔。

透過此對應,EF7 可以建立並查詢複雜的 JSON 檔,如下所示:

{
  "Views": 5085,
  "TopGeographies": [
    {
      "Browsers": "Firefox, Netscape",
      "Count": 924,
      "Latitude": 110.793,
      "Longitude": 39.2431
    },
    {
      "Browsers": "Firefox, Netscape",
      "Count": 885,
      "Latitude": 133.793,
      "Longitude": 45.2431
    }
  ],
  "TopSearches": [
    {
      "Count": 9359,
      "Term": "Search #1"
    }
  ],
  "Updates": [
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1996-02-17T19:24:29.5429092Z",
      "Commits": []
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "2019-11-24T19:24:29.5429093Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1997-05-28T19:24:29.5429097Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        },
        {
          "Comment": "Commit #2",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    }
  ]
}

注意

尚不支援將空間類型直接對應至 JSON。 上述檔使用 double 值作為因應措施。 如果這是您感興趣的專案,請在 JSON 數據行中投票支持空間類型。

注意

尚不支援將基本類型集合對應至 JSON。 上述檔會使用值轉換器,將集合轉換成逗號分隔字串。 投票給 Json:如果這是您感興趣的專案,請新增基本類型 集合的支援。

注意

尚未支援將擁有的類型對應至 JSON 與 TPT 或 TPC 繼承。 如果這是您感興趣的專案,請投票支援 具有 TPT/TPC 繼承對應的 JSON 屬性。

查詢 JSON 數據行

對 JSON 資料行的查詢的運作與查詢 EF Core 中任何其他匯總類型相同。 也就是說,只要使用LINQ! 以下列出一些範例。

查詢所有生活在 Chigley 中的作者:

var authorsInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .ToListAsync();

此查詢會在使用 SQL Server 時產生下列 SQL:

SELECT [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$')
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

請注意,使用 JSON_VALUE 從 JSON 檔案內的 取得 City Address

Select 可用來從 JSON 檔擷取和項目元素:

var postcodesInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .Select(author => author.Contact.Address.Postcode)
    .ToListAsync();

此查詢會產生下列 SQL:

SELECT CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

以下是在篩選和投影中執行更多動作的範例,也會依 JSON 檔中的電話號碼排序:

var orderedAddresses = await context.Authors
    .Where(
        author => (author.Contact.Address.City == "Chigley"
                   && author.Contact.Phone != null)
                  || author.Name.StartsWith("D"))
    .OrderBy(author => author.Contact.Phone)
    .Select(
        author => author.Name + " (" + author.Contact.Address.Street
                  + ", " + author.Contact.Address.City
                  + " " + author.Contact.Address.Postcode + ")")
    .ToListAsync();

此查詢會產生下列 SQL:

SELECT (((((([a].[Name] + N' (') + CAST(JSON_VALUE([a].[Contact],'$.Address.Street') AS nvarchar(max))) + N', ') + CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max))) + N' ') + CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))) + N')'
FROM [Authors] AS [a]
WHERE (CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' AND CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max)) IS NOT NULL) OR ([a].[Name] LIKE N'D%')
ORDER BY CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max))

當 JSON 檔包含集合時,結果中可以投影這些集合:

var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000)
    .AsNoTracking()
    .Select(
        post => new
        {
            post.Author!.Name, post.Metadata!.Views, Searches = post.Metadata.TopSearches, Commits = post.Metadata.Updates
        })
    .ToListAsync();

此查詢會產生下列 SQL:

SELECT [a].[Name], CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int), JSON_QUERY([p].[Metadata],'$.TopSearches'), [p].[Id], JSON_QUERY([p].[Metadata],'$.Updates')
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000

注意

涉及 JSON 集合的更複雜的查詢需要 jsonpath 支援。 如果這是您感興趣的專案,請投票支援 jsonpath 查詢

提示

請考慮建立索引以改善 JSON 檔中的查詢效能。 例如,請參閱 使用 SQL Server 時編制 Json 數據的 索引。

更新 JSON 數據行

SaveChangesSaveChangesAsync 會以正常方式運作,以更新 JSON 資料行。 如需大量變更,將會更新整份檔。 例如,取代作者的大部分 Contact 檔:

var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));

jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };

await context.SaveChangesAsync();

在此情況下,會以參數的形式傳遞整個新檔:

info: 8/30/2022 20:21:24.392 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"Phone":"01632 88346","Address":{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"2 Riverside"}}' (Nullable = false) (Size = 114), @p1='2'], CommandType='Text', CommandTimeout='30']

然後,SQL 中會用到 UPDATE

UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;

不過,如果只有子文件變更,EF Core 會使用 JSON_MODIFY 命令只更新子檔。 例如,變更 Address 檔案內的 Contact

var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));

brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");

await context.SaveChangesAsync();

產生下列參數:

info: 10/2/2022 15:51:15.895 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"4 Riverside"}' (Nullable = false) (Size = 80), @p1='5'], CommandType='Text', CommandTimeout='30']

這會透過JSON_MODIFY呼叫在 中使用UPDATE

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;

最後,如果只有單一屬性變更,EF Core 會再次使用 「JSON_MODIFY」 命令,這次只修補變更的屬性值。 例如:

var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));

arthur.Contact.Address.Country = "United Kingdom";

await context.SaveChangesAsync();

產生下列參數:

info: 10/2/2022 15:54:05.112 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']

這再次與 搭配 JSON_MODIFY使用:

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;

ExecuteUpdate 和 ExecuteDelete (大量更新)

根據預設,EF Core 會追蹤實體的變更,然後在呼叫其中SaveChanges一個方法時傳送更新至資料庫。 變更只會針對實際變更的屬性和關聯性傳送。 此外,追蹤的實體會與傳送至資料庫的變更保持同步。 此機制是將一般用途插入、更新和刪除傳送至資料庫的有效便利方式。 這些變更也會批處理,以減少資料庫往返次數。

不過,在資料庫上執行更新或刪除命令有時很有用,而不需要涉及變更追蹤器。 EF7 會使用新的 ExecuteUpdateExecuteDelete 方法來啟用此功能。 這些方法會套用至 LINQ 查詢,並根據該查詢的結果來更新或刪除資料庫中的實體。 許多實體可以使用單一命令來更新,而且實體不會載入記憶體中,這表示這可能會導致更有效率的更新和刪除。

不過,請記住:

  • 必須明確指定要進行的特定變更;EF Core 不會自動偵測它們。
  • 任何追蹤的實體都不會保持同步。
  • 其他命令可能需要以正確的順序傳送,以免違反資料庫條件約束。 例如,刪除相依專案,才能刪除主體。

這一切表示 ExecuteUpdateExecuteDelete 方法會補充現有的機制,而不是取代。SaveChanges

基本 ExecuteDelete 範例

提示

此處顯示的程式代碼來自 ExecuteDeleteSample.cs

呼叫 ExecuteDeleteExecuteDeleteAsyncDbSet ,會立即從資料庫刪除該 DbSet 實體的所有實體。 例如,若要刪除所有 Tag 實體:

await context.Tags.ExecuteDeleteAsync();

使用 SQL Server 時,這會執行下列 SQL:

DELETE FROM [t]
FROM [Tags] AS [t]

更有趣的是,查詢可以包含篩選。 例如:

await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();

這會執行下列 SQL:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'

查詢也可以使用更複雜的篩選,包括流覽至其他類型。 例如,僅從舊的部落格文章中刪除標籤:

await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();

其會執行:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

基本 ExecuteUpdate 範例

提示

此處所示的程式碼來自 ExecuteUpdateSample.cs

ExecuteUpdateExecuteUpdateAsync 的行為與方法非常類似 ExecuteDelete 。 主要差異在於更新需要知道要更新的屬性,以及如何更新它們。 這是使用對 SetProperty的一或多個呼叫來達成。 例如,若要更新 Name 每個部落格的 :

await context.Blogs.ExecuteUpdateAsync(
    s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));

的第一個參數 SetProperty 會指定要更新的屬性,在此案例中為 Blog.Name。 第二個參數會指定應該如何計算新值;在這裡情況下,藉由取得現有的值並附加 "*Featured!*"。 產生的 SQL 為:

UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]

ExecuteDelete如同 ,查詢可用來篩選要更新的實體。 此外,對的多個呼叫 SetProperty 可用來更新目標實體上的多個屬性。 例如,若要更新 Title 2022 之前發行之所有文章的 和 Content

await context.Posts
    .Where(p => p.PublishedOn.Year < 2022)
    .ExecuteUpdateAsync(s => s
        .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")")
        .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")"));

在此情況下,產生的 SQL 稍微複雜一點:

UPDATE [p]
SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')',
    [p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')'
FROM [Posts] AS [p]
WHERE DATEPART(year, [p].[PublishedOn]) < 2022

最後,同樣地,篩選 ExecuteDelete條件可以參考其他數據表。 例如,若要更新舊文章中的所有標記:

await context.Tags
    .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));

這會產生:

UPDATE [t]
SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

如需 和ExecuteDelete的詳細資訊ExecuteUpdate和程式代碼範例,請參閱ExecuteUpdate和ExecuteDelete

繼承和多個數據表

ExecuteUpdateExecuteDelete 只能處理單一數據表。 當使用不同的 繼承對應策略時,這有影響。 一般而言,使用 TPH 對應策略時沒有任何問題,因為只有一個數據表可修改。 例如,刪除所有 FeaturedPost 實體:

await context.Set<FeaturedPost>().ExecuteDeleteAsync();

使用 TPH 對應時產生下列 SQL:

DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'

使用 TPC 對應策略時,也沒有任何問題,因為只需要變更單一數據表:

DELETE FROM [f]
FROM [FeaturedPosts] AS [f]

不過,使用 TPT 對應策略時嘗試此動作將會失敗,因為它需要從兩個不同的數據表刪除數據列。

將篩選新增至查詢通常表示作業會因為 TPC 和 TPT 策略而失敗。 這是因為數據列可能需要從多個數據表中刪除。 例如,這個查詢:

await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();

使用 TPH 時產生下列 SQL:

DELETE FROM [p]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%')

但在使用 TPC 或 TPT 時失敗。

提示

問題 #10879 追蹤新增支援,以在這些案例中自動傳送多個命令。 如果這是您想要看到已實作的內容,請投票處理此問題。

ExecuteDelete 和關聯性

如上所述,您可能需要刪除或更新相依實體,才能刪除關聯性主體。 例如,每個 Post 都是與其相關聯 之 相 Author依的 。 這表示如果文章仍然參考作者,則無法刪除作者;這樣做會違反資料庫中的外鍵條件約束。 例如,嘗試執行下列動作:

await context.Authors.ExecuteDeleteAsync();

會導致 SQL Server 發生下列例外狀況:

Microsoft.Data.SqlClient.SqlException (0x80131904):D ELETE 語句與 REFERENCE 條件約束 “FK_Posts_Authors_AuthorId” 衝突。 資料庫 「TphBlogsContext」 資料表 「dbo 發生衝突」。貼文“,數據行 'AuthorId'。 陳述式已經結束。

若要修正此問題,我們必須先刪除貼文,或藉由將外鍵屬性設定 AuthorId 為 null,以斷絕每個貼文與其作者之間的關聯性。 例如,使用 delete 選項:

await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();

提示

TagWith 可用來標記 ExecuteDeleteExecuteUpdate 標記一般查詢的方式。

這會產生兩個不同的命令:要刪除相依專案的第一個:

-- Deleting posts...

DELETE FROM [p]
FROM [Posts] AS [p]

第二個要刪除主體:

-- Deleting authors...

DELETE FROM [a]
FROM [Authors] AS [a]

重要

根據預設,多個 ExecuteDeleteExecuteUpdate 命令不會包含在單一交易中。 不過, DbContext 交易 API 可以用一般方式在交易中包裝這些命令。

提示

在單一來回行程中傳送這些命令取決於 問題 #10879。 如果這是您想要看到已實作的內容,請投票處理此問題。

在資料庫中設定 串聯刪除 可能非常實用。 在我們的模型中,和 Post 之間的Blog關聯性是必要的,這會導致 EF Core 依慣例設定串聯刪除。 這表示從資料庫刪除部落格時,也會刪除其所有相依文章。 接著,它會刪除所有部落格和文章,我們只需要刪除部落格:

await context.Blogs.ExecuteDeleteAsync();

這會導致下列 SQL:

DELETE FROM [b]
FROM [Blogs] AS [b]

這在刪除部落格時,也會讓已設定的串聯刪除所有相關文章。

更快速的 SaveChanges

在 EF7 中,和 SaveChangesAsyncSaveChanges效能已大幅改善。 在某些情況下,儲存變更的速度現在快於 EF Core 6.0 的四倍!

這些改進大部分都來自:

  • 執行較少的往返資料庫
  • 產生更快的 SQL

這些改進的一些範例如下所示。

注意

如需這些變更的深入討論,請參閱 .NET 部落格上的宣佈 Entity Framework Core 7 Preview 6:Performance Edition

提示

此處顯示的程式代碼來自 SaveChangesPerformanceSample.cs

取消不必要的交易

所有新式關係資料庫都保證單一 SQL 語句的交易性。 也就是說,即使發生錯誤,語句永遠不會只部分完成。 EF7 可避免在這些情況下啟動明確的交易。

例如,查看記錄以取得下列 對 SaveChanges的呼叫:

await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();

顯示在 EF Core 6.0 中, INSERT 命令會由命令包裝,以開始並認可交易:

dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Blogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

EF7 會在此偵測到不需要交易,因此會移除這些呼叫:

info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);

這會移除兩個資料庫往返,這可能會對整體效能造成巨大差異,尤其是在資料庫呼叫延遲很高時。 在一般生產系統中,資料庫不會與應用程式位於相同的計算機上。 這表示延遲通常相對較高,使得此優化在真實世界生產系統中特別有效。

改善簡單的身分識別插入 SQL

上述案例會插入具有索引鍵數據行的單一 IDENTITY 數據列,而沒有其他資料庫產生的值。 EF7 會使用 OUTPUT INSERTED來簡化此案例中的 SQL。 雖然這種簡化對許多其他案例而言無效,但改善仍然很重要,因為這種單一數據列插入在許多應用程式中非常常見。

插入多個數據列

在EF Core 6.0 中,插入多個數據列的預設方法是由 SQL Server 支援具有觸發程式之數據表的限制所驅動。 我們想要確保即使少數使用者在其數據表中有觸發程式,默認體驗也能運作。 這表示我們無法使用簡單 OUTPUT 子句,因為在 SQL Server 上,這 不適用於觸發程式。 相反地,插入多個實體時,EF Core 6.0 會產生一些相當捲積的 SQL。 例如,這個對的呼叫 SaveChanges

for (var i = 0; i < 4; i++)
{
    await context.AddAsync(new Blog { Name = "Foo" + i });
}

await context.SaveChangesAsync();

使用 EF Core 6.0 對 SQL Server 執行時,會產生下列動作:

dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position
      INTO @inserted0;

      SELECT [i].[Id] FROM @inserted0 i
      ORDER BY [i].[_Position];
dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

重要

雖然這很複雜,但批處理這類的多個插入仍然比為每個插入傳送單一命令要快得多。

在 EF7 中,如果您的數據表包含觸發程式,您仍然可以取得此 SQL,但針對常見的案例,我們現在會產生更有效率的命令,如果仍然有些複雜,則命令:

info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position;

交易已消失,如同在單一插入案例中,因為 MERGE 是受隱含交易保護的單一語句。 此外,臨時表已消失,而且 OUTPUT 子句現在會將產生的標識碼直接傳回用戶端。 這比 EF Core 6.0 快四倍,視應用程式與資料庫之間的延遲等環境因素而定。

觸發程序

如果數據表有觸發程式,則上述程式代碼中的呼叫 SaveChanges 會擲回例外狀況:

未處理的例外狀況。 Microsoft.EntityFrameworkCore.DbUpdateException:
無法儲存變更,因為目標數據表具有資料庫觸發程式。 請據以設定實體類型,如需詳細資訊,請參閱 https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
如果語句包含不含 INTO 子句的 OUTPUT 子句,DML 語句的目標數據表 'BlogsWithTriggers' 就無法有任何啟用的觸發程式。

下列程式代碼可用來通知 EF Core 資料表具有觸發程式:

modelBuilder
    .Entity<BlogWithTrigger>()
    .ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));

EF7 接著會在傳送此數據表的插入和更新命令時還原為 EF Core 6.0 SQL。

如需詳細資訊,包括使用觸發程式自動設定所有對應數據表的慣例,請參閱 EF7 重大變更檔中具有觸發程式的 SQL Server 數據表現在需要特殊的 EF Core 設定。

插入圖形的往返次數較少

請考慮插入包含新主體實體的實體圖表,以及具有參考新主體之外鍵的新相依實體。 例如:

await context.AddAsync(
    new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();

如果主體的主鍵是由資料庫產生,則在插入主體之前,在相依專案中為外鍵設定的值是未知的。 EF Core 會產生兩次往返,讓這個 --1 插入主體並取回新的主鍵,第二個來插入相依專案並設定外鍵值。 由於有兩個語句,因此需要交易,這表示總共有四個往返:

dbug: 10/1/2022 13:12:02.517 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 13:12:02.517 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);
info: 10/1/2022 13:12:02.529 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (5ms) [Parameters=[@p1='6', @p2='My first post' (Nullable = false) (Size = 4000), @p3='6', @p4='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Post] USING (
      VALUES (@p1, @p2, 0),
      (@p3, @p4, 1)) AS i ([BlogId], [Title], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([BlogId], [Title])
      VALUES (i.[BlogId], i.[Title])
      OUTPUT INSERTED.[Id], i._Position;
dbug: 10/1/2022 13:12:02.531 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

不過,在某些情況下,在插入主體之前,已知主鍵值。 這包括:

  • 未自動產生的索引鍵值
  • 用戶端上產生的索引鍵值,例如 Guid 密鑰
  • 批次在伺服器上產生的索引鍵值,例如使用hi-lo值產生器時

在 EF7 中,這些案例現在已優化為單一來回行程。 例如,在 SQL Server 上的情況中, Blog.Id 主鍵可以設定為使用 hi-lo 產生策略:

modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();

SaveChanges上述呼叫現在已針對插入進行單次優化。

dbug: 10/1/2022 21:51:55.805 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 21:51:55.806 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='9', @p1='MyBlog' (Nullable = false) (Size = 4000), @p2='10', @p3='9', @p4='My first post' (Nullable = false) (Size = 4000), @p5='11', @p6='9', @p7='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Id], [Name])
      VALUES (@p0, @p1);
      INSERT INTO [Posts] ([Id], [BlogId], [Title])
      VALUES (@p2, @p3, @p4),
      (@p5, @p6, @p7);
dbug: 10/1/2022 21:51:55.807 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

請注意,這裡仍然需要交易。 這是因為插入會分成兩個不同的數據表。

EF7 在其他情況下,EF Core 6.0 也會使用單一批次來建立多個批次。 例如,刪除數據列並將數據列插入相同的數據表時。

SaveChanges 的值

如此處的一些範例所示,將結果儲存至資料庫可能是複雜的業務。 這就是使用EF Core 之類的專案真正顯示其值的位置。 EF Core:

  • 將多個插入、更新和刪除命令批處理,以減少往返
  • 找出是否需要明確交易
  • 決定插入、更新和刪除實體的順序,以免違反資料庫條件約束
  • 確保有效率地傳回資料庫產生的值,並傳播回實體
  • 使用主鍵產生的值自動設定外鍵值
  • 偵測並行衝突

此外,對於其中許多情況,不同的資料庫系統需要不同的 SQL。 EF Core 資料庫提供者可與 EF Core 搭配運作,以確保會針對每個案例傳送正確且有效率的命令。

每個具體類型的數據表 (TPC) 繼承對應

根據預設,EF Core 會將 .NET 類型的繼承階層對應至單一資料庫數據表。 這稱為 數據表個別階層 (TPH) 對應策略。 EF Core 5.0 引進了 每個類型數據表 (TPT) 策略,可支援將每個 .NET 類型對應至不同的資料庫數據表。 EF7 引進每個具體類型的數據表 (TPC) 策略。 TPC 也會將 .NET 類型對應至不同的數據表,但以解決 TPT 策略的一些常見效能問題的方式。

提示

此處顯示的程式代碼來自 TpcInheritanceSample.cs

提示

EF 小組在 .NET 數據社群月臺的情節中示範並深入討論 TPC 對應。 和所有社群站立劇集一樣,您現在可以在YouTube上觀看TPC劇集。

TPC 資料庫架構

TPC 策略與 TPT 策略類似,不同數據表是針對階層中的每個具體類型所建立,但不會針對抽象類型建立數據表,因此名稱為“table-per-concrete-type”。 如同 TPT,數據表本身會指出已儲存物件的類型。 不過,不同於 TPT 對應,每個數據表都包含具體類型及其基底類型中每個屬性的數據行。 TPC 資料庫架構會反正規化。

例如,請考慮對應此階層:

public abstract class Animal
{
    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

使用 SQL Server 時,為此階層建立的數據表如下:

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Value] decimal(18,2) NOT NULL,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [FavoriteAnimalId] int NULL,
    CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

請注意:

  • Pet 類型沒有數據表Animal,因為這些數據表位於abstract物件模型中。 請記住,C# 不允許抽象類型的實例,因此沒有將抽象類型實例儲存至資料庫的情況。

  • 基底類型中的屬性對應會針對每個具體類型重複。 例如,每個數據表都有一個數據行 Name ,而 Cat 和 Dog 都有一個數據行 Vet

  • 將一些資料儲存到此資料庫中會產生下列結果:

Cats 表格

Id 名稱 FoodId 獸醫 EducationLevel
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly 幼稚園
8 巴克斯特 5dc5019e-6f72-454b-d4b0-08da7aca624f 雙塞爾寵物醫院 BSc

狗桌

Id 名稱 FoodId 獸醫 FavoriteToy
3 快顯通知 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly 松鼠先生

FarmAnimals 數據表

Id 名稱 FoodId 種類
4 克萊德 1d495075-f527-4498-d4af-08da7aca624f 100.00 equus africanus asinus

人類數據表

Id 名稱 FoodId FavoriteAnimalId
5 溫迪 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 凱蒂 null 8

請注意,與 TPT 對應不同,單一物件的所有資訊都包含在單一數據表中。 而且,與 TPH 對應不同,在模型從未使用的任何數據表中,沒有數據行和數據列的組合。 我們將在下面看到這些特性對於查詢和記憶體的重要性。

設定 TPC 繼承

當階層與 EF Core 對應階層時,繼承階層中的所有類型都必須明確包含在模型中。 您可以針對每個類型在 上DbContext建立DbSet屬性來完成此作業:

public DbSet<Animal> Animals => Set<Animal>();
public DbSet<Pet> Pets => Set<Pet>();
public DbSet<FarmAnimal> FarmAnimals => Set<FarmAnimal>();
public DbSet<Cat> Cats => Set<Cat>();
public DbSet<Dog> Dogs => Set<Dog>();
public DbSet<Human> Humans => Set<Human>();

或者, Entity 在 中使用 OnModelCreating方法:

modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();

重要

這與舊版 EF6 行為不同,如果衍生類型的對應基底類型包含在相同的元件中,就會自動探索它們。

不需要執行任何其他動作,才能將階層對應為 TPH,因為它是預設策略。 不過,從EF7開始,TPH 可以藉由在階層的基底類型上呼叫 UseTphMappingStrategy 來明確:

modelBuilder.Entity<Animal>().UseTphMappingStrategy();

若要改用 TPT,請將此值變更為 UseTptMappingStrategy

modelBuilder.Entity<Animal>().UseTptMappingStrategy();

同樣地, UseTpcMappingStrategy 用來設定 TPC:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

在每個案例中,用於每個類型的數據表名稱都是取自 DbSetDbContext上的屬性名稱,或者可以使用ToTable產生器方法或 屬性來[Table]設定

TPC 查詢效能

對於查詢,TPC 策略是 TPT 的改善,因為它可確保指定實體實例的資訊一律儲存在單一數據表中。 這表示當對應階層很大且具有許多具體(通常是分葉)類型時,TPC 策略會很有用,每個類型都有大量的屬性,而且大多數查詢中只會使用一小部分類型。

針對三個簡單 LINQ 查詢所產生的 SQL 可用來觀察 TPC 相較於 TPH 和 TPT 時,TPC 在何處表現良好。 這些查詢包括:

  1. 傳回階層中所有型別實體的查詢:

    context.Animals.ToList();
    
  2. 從階層中型別子集傳回實體的查詢:

    context.Pets.ToList();
    
  3. 只從階層中的單一分葉類型傳回實體的查詢:

    context.Cats.ToList();
    

TPH 查詢

使用 TPH 時,這三個查詢只會查詢單一數據表,但在歧視性數據行上有不同的篩選:

  1. TPH SQL 會傳回階層中所有型別的實體:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Species], [a].[Value], [a].[FavoriteAnimalId], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    
  2. TPH SQL 會從階層中的類型子集傳回實體:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] IN (N'Cat', N'Dog')
    
  3. TPH SQL 只會從階層中的單一分葉類型傳回實體:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] = N'Cat'
    

所有這些查詢都應該能正常執行,特別是在歧視性數據行上使用適當的資料庫索引。

TPT 查詢

使用 TPT 時,所有這些查詢都需要聯結多個數據表,因為任何指定特定類型的數據會分割到許多數據表:

  1. TPT SQL 會傳回階層中所有型別的實體:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [f].[Species], [f].[Value], [h].[FavoriteAnimalId], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
        WHEN [h].[Id] IS NOT NULL THEN N'Human'
        WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id]
    LEFT JOIN [Humans] AS [h] ON [a].[Id] = [h].[Id]
    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  2. TPT SQL 會從階層中的類型子集傳回實體:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  3. TPT SQL 只會從階層中的單一分葉類型傳回實體:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    INNER JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    

注意

EF Core 使用「歧視性合成」來判斷數據的來源數據表,因此要使用的正確類型。 這可運作,因為 LEFT JOIN 會傳回相依標識符數據行的 Null (子數據表)不是正確的類型。 因此,對於狗來說, [d].[Id] 將是非 Null,而所有其他(具體)標識碼都將是 Null。

所有這些查詢都可能會因為數據表聯結而發生效能問題。 這就是為什麼 TPT 絕不是查詢效能的好選擇。

TPC 查詢

TPC 會針對所有這些查詢改善 TPT,因為需要查詢的數據表數目會減少。 此外,每個數據表的結果都會使用 UNION ALL結合,這比數據表聯結快得多,因為它不需要在數據列之間執行任何比對或重複數據列。

  1. TPC SQL 會傳回階層中所有類型的實體:

    SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
    FROM [FarmAnimals] AS [f]
    UNION ALL
    SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator]
    FROM [Humans] AS [h]
    UNION ALL
    SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  2. TPC SQL 會從階層中的類型子集傳回實體:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  3. TPC SQL 只會從階層中的單一分葉類型傳回實體:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel]
    FROM [Cats] AS [c]
    

即使所有這些查詢的 TPC 都優於 TPT,但傳回多個類型的實例時,TPH 查詢仍然更好。 這是 TPH 是 EF Core 使用的預設策略的其中一個原因。

如查詢的 SQL #3 所示,在查詢單一分葉類型的實體時,TPC 會非常出色。 查詢只會使用單一數據表,而且不需要篩選。

TPC 插入和更新

TPC 在儲存新實體時也執行得很好,因為這只需要將單一數據列插入單一數據表中。 TPH 也是如此。 使用 TPT 時,數據列必須插入許多數據表中,這樣效能會比較低。

更新通常也是如此,不過在此情況下,如果所有更新的數據行都位於相同的數據表中,即使是針對 TPT,差異可能並不重要。

空間考慮

當有許多子類型且經常未使用的屬性時,TPT 和 TPC 都可以使用小於 TPH 的記憶體。 這是因為 TPH 資料表中的每個資料列都必須儲存 NULL 每個未使用屬性的 。 在實務上,這很少是個問題,但在儲存具有這些特性的大量數據時,可能值得考慮。

提示

如果您的資料庫系統支援它 (e.g. SQL Server),請考慮針對很少填入的 TPH 資料行使用「疏鬆數據行」。

產生金鑰

選擇的繼承對應策略會產生和管理主鍵值的方式。 TPH 中的索引鍵很容易,因為每個實體實例都是以單一數據表中的單一數據列來表示。 您可以使用任何類型的索引鍵值產生,而且不需要額外的條件約束。

針對 TPT 策略,數據表中一律會有一個數據列對應至階層的基底類型。 此數據列可以使用任何類型的密鑰產生,而其他數據表的索引鍵會使用外鍵條件約束連結至此數據表。

TPC 的情況會變得更複雜一點。 首先,請務必瞭解 EF Core 要求階層中的所有實體都必須有唯一索引鍵值,即使實體具有不同的類型也一樣。 因此,使用我們的範例模型,Dog 不能有與 Cat 相同的標識碼索引鍵值。 其次,與 TPT 不同,沒有一個通用數據表可以做為索引鍵值存住且可以產生的單一位置。 這表示無法使用簡單的 Identity 數據行。

對於支援時序的資料庫,可以使用每個數據表之默認條件約束中所參考的單一序列來產生索引鍵值。 這是上述 TPC 數據表中使用的策略,其中每個數據表都有下列專案:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence 是EF Core 所建立的資料庫序列。 使用適用於 SQL Server 的 EF Core 資料庫提供者時,預設會針對 TPC 階層使用此策略。 支持順序之其他資料庫的資料庫提供者應該有類似的預設值。 其他使用序列的重要產生策略,例如 Hi-Lo 模式,也可以與 TPC 搭配使用。

雖然標準身分識別數據行無法與 TPC 搭配使用,但如果每個數據表都設定了適當的種子且遞增,則可能會使用 Identity 數據行,讓每個數據表產生的值永遠不會衝突。 例如:

modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

SQLite 不支援序列或身分識別種子/增量,因此搭配 TPC 策略使用 SQLite 時,不支援產生整數索引鍵值。 不過,客戶端產生或全域唯一索引鍵,例如,任何資料庫都支援 GUID 金鑰,包括 SQLite。

外部索引鍵條件約束

TPC 對應策略會建立反正規化的 SQL 架構,這是某些資料庫 Purists 反對它的原因之一。 例如,請考慮外鍵資料行 FavoriteAnimalId。 此數據行中的值必須符合某些動物的主鍵值。 使用 TPH 或 TPT 時,可以使用簡單的 FK 條件約束在資料庫中強制執行。 例如:

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

但是,使用 TPC 時,動物的主要索引鍵會儲存在該動物的具體類型數據表中。 例如,貓的主鍵會儲存在數據行中 Cats.Id ,而狗的主鍵則儲存在數據行中 Dogs.Id 等等。 這表示無法為此關聯性建立 FK 條件約束。

實際上,只要應用程式未嘗試插入無效的數據,就不是問題。 例如,如果EF Core 插入所有數據並使用導覽來關聯實體,則保證 FK 數據行會隨時包含有效的 PK 值。

摘要和指引

總而言之,當您的程式代碼大多會查詢單一分葉類型的實體時,TPC 是很好的對應策略。 這是因為記憶體需求較小,而且沒有可能需要索引的歧視性數據行。 插入和更新也很有效率。

話說來,TPH 通常適用於大多數應用程式,而且是各種案例的良好預設值,因此,如果您不需要 TPC,請勿新增 TPC 的複雜性。 具體而言,如果您的程式代碼大多會查詢許多類型的實體,例如針對基底類型撰寫查詢,則傾向於 TPH over TPC。

只有在受外部因素限制的情況下,才使用 TPT。

自定義反向工程範本

您現在可以從資料庫反向工程 EF 模型時自定義 Scaffolded 程式代碼。 開始將預設樣本新增至您的專案:

dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates

然後,範本可以自定義,而且會自動由 dotnet ef dbcontext scaffoldScaffold-DbContext使用。

如需詳細資訊,請參閱 自定義反向工程範本

提示

EF 小組在 .NET 數據社群月臺一集中示範並深入討論反向工程範本。 和所有社群月臺劇集一樣,您現在可以在YouTube上觀看TT範本劇集。

模型建置慣例

EF Core 會使用元數據「模型」來描述應用程式的實體類型如何對應至基礎資料庫。 此模型是使用大約 60 個「慣例」的集合所建置。 接著可以使用對應屬性(也稱為「數據批注」)和/或呼叫 中的 OnModelCreatingAPI 來自定義慣例所建置的DbModelBuilder模型。

從EF7開始,應用程式現在可以移除或取代上述任何慣例,以及新增慣例。 模型建置慣例是控制模型組態的強大方式,但可能很複雜且難以正確。 在許多情況下,可以使用現有的 預先慣例模型組 態,輕鬆地指定屬性和類型的通用組態。

覆寫 方法會變更 DbContext.ConfigureConventions 所使用的DbContext慣例。 例如:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

提示

若要尋找所有內建模型建置慣例,請尋找實作 IConvention 介面的每個類別。

提示

此處顯示的程式代碼來自 ModelBuildingConventionsSample.cs

拿掉現有的慣例

有時候其中一個內建慣例可能不適合您的應用程式,在此情況下可以移除。

範例:不要為外鍵數據行建立索引

建立外鍵 (FK) 數據行的索引通常很合理,因此有內建慣例: ForeignKeyIndexConvention。 查看與 和關聯性Blog之實體類型的模型偵錯檢視,我們可以看到兩個Post索引已建立-一個用於 FK,另一個則用於 AuthorId BlogId FK。Author

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK Index
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      AuthorId
      BlogId

不過,索引會有額外負荷,而且 ,如這裡所述,可能不一定適合為所有 FK 數據行建立它們。 若要達成此目的, ForeignKeyIndexConvention 可以在建置模型時移除 :

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

現在查看模型的 Post 偵錯檢視,我們看到尚未建立 FK 上的索引:

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade

如有需要,仍然可以使用 來明確建立外鍵數據行的 IndexAttribute索引:

[Index("BlogId")]
public class Post
{
    // ...
}

或在 中使用 組 OnModelCreating態:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}

再次查看 Post 實體類型,它現在包含 BlogId 索引,但不包含 AuthorId 索引:

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      BlogId

提示

如果您的模型未使用對應屬性(也稱為數據批注)進行設定,則可以安全地移除結尾 AttributeConvention 的所有慣例,以加速模型建置。

新增慣例

移除現有的慣例是一個開始,但新增全新的模型建置慣例呢? EF7 也支援此功能!

範例:限制歧視性屬性的長度

每個 階層的數據表繼承對應策略 需要一個歧視性數據行來指定任何指定數據列中所代表的類型。 根據預設,EF 會針對歧視性使用未系結的字串數據行,以確保它會在任何歧視性長度上運作。 不過,限制歧視性字串的最大長度,可讓儲存和查詢更有效率。 讓我們建立會執行該作業的新慣例。

EF Core 模型建置慣例會根據正在建置的模型所做的變更來觸發。 這會讓模型保持最新狀態,因為已進行明確設定、套用對應屬性,以及執行其他慣例。 為了參與這項作業,每個慣例都會實作一或多個介面,以判斷何時觸發慣例。 例如,每當將新的實體類型加入模型時,就會觸發實作的慣例 IEntityTypeAddedConvention 。 同樣地,每當將索引鍵或外鍵新增至模型時,都會觸發實作 和的IKeyAddedConvention慣例IForeignKeyAddedConvention

知道要實作的介面可能很棘手,因為某個時間點對模型所做的設定可能會變更或移除。 例如,金鑰可能依慣例建立,但稍後會在明確設定不同的密鑰時加以取代。

讓我們先嘗試實作歧視性長度慣例,讓這更具體一點:

public class DiscriminatorLengthConvention1 : IEntityTypeBaseTypeChangedConvention
{
    public void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        var discriminatorProperty = entityTypeBuilder.Metadata.FindDiscriminatorProperty();
        if (discriminatorProperty != null
            && discriminatorProperty.ClrType == typeof(string))
        {
            discriminatorProperty.Builder.HasMaxLength(24);
        }
    }
}

此慣例會實作 IEntityTypeBaseTypeChangedConvention,這表示每當實體類型的對應繼承階層變更時,就會觸發它。 然後,慣例會尋找並設定階層的字串歧視性屬性。

接著,在 中ConfigureConventions呼叫 Add ,即可使用此慣例:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ =>  new DiscriminatorLengthConvention1());
}

提示

方法不直接新增慣例的實例, Add 而是接受處理站來建立慣例的實例。 這可讓慣例使用 EF Core 內部服務提供者的相依性。 由於此慣例沒有相依性,所以服務提供者參數會命名為 _,表示永遠不會使用。

建置模型並查看 Post 實體類型會顯示此專案已運作-已將歧視性屬性設定為最大長度為 24:

 Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

但是,如果我們現在明確設定不同的歧視性屬性,會發生什麼事? 例如:

modelBuilder.Entity<Post>()
    .HasDiscriminator<string>("PostTypeDiscriminator")
    .HasValue<Post>("Post")
    .HasValue<FeaturedPost>("Featured");

查看模型的偵錯檢視,我們發現已不再設定歧視性長度!

 PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw

這是因為我們稍後在慣例中設定的歧視性屬性會在新增自定義歧視性時移除。 我們可以嘗試藉由在慣例上實作另一個介面來回應歧視性變更來修正此問題,但找出要實作的介面並不容易。

幸運的是,有一種不同的方法來解決此問題,使事情變得更容易。 很多時候,只要最終模型正確,模型在建置時看起來就無關緊要。 此外,我們想要套用的設定通常不需要觸發其他慣例來做出反應。 因此,我們的慣例可以實作 IModelFinalizingConvention。 模型完成慣例會在所有其他模型建置完成之後執行,因此可以存取模型的最終狀態。 模型完成慣例通常會逐一查看整個模型,以設定模型元素。 因此,在此案例中,我們會在模型中尋找每個歧視性,並加以設定:

public class DiscriminatorLengthConvention2 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                discriminatorProperty.Builder.HasMaxLength(24);
            }
        }
    }
}

使用這個新慣例建置模型之後,我們發現即使已自定義,仍可正確設定歧視性長度:

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

只是為了有趣,讓我們進一步進行一個步驟,並將最大長度設定為最長的歧視性值長度。

public class DiscriminatorLengthConvention3 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                var maxDiscriminatorValueLength =
                    entityType.GetDerivedTypesInclusive().Select(e => ((string)e.GetDiscriminatorValue()!).Length).Max();

                discriminatorProperty.Builder.HasMaxLength(maxDiscriminatorValueLength);
            }
        }
    }
}

現在,歧視性數據行最大長度是 8,也就是 “Featured” 的長度,這是使用中最長的歧視性值。

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)

提示

您可能想知道慣例是否也應該為歧視性數據行建立索引。 GitHub 上對此有討論。 簡短的答案是,有時候索引可能很有用,但大部分時候可能不是。 因此,最好視需要在這裡建立適當的索引,而不是有一律執行此動作的慣例。 但是,如果您不同意這一點,則上述慣例也可以輕鬆地修改以建立索引。

範例:所有字串屬性的預設長度

讓我們看看另一個範例,其中可以使用完成慣例--這次,設定任何字串屬性的預設最大長度,如 GitHub 上要求。 慣例看起來與上一個範例相當類似:

public class MaxStringLengthConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            property.Builder.HasMaxLength(512);
        }
    }
}

這個慣例很簡單。 它會尋找模型中的每個字串屬性,並將其最大長度設定為512。 查看的偵 Post錯檢視,我們看到所有字串屬性現在的長度上限為 512。

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(512)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Content但該屬性可能允許超過512個字元,否則我們所有的文章都將相當短! 這可以在不變更慣例的情況下完成,只要使用對應屬性來明確設定此屬性的長度上限:

[MaxLength(4000)]
public string Content { get; set; }

或使用中的 OnModelCreating程式代碼:

modelBuilder.Entity<Post>()
    .Property(post => post.Content)
    .HasMaxLength(4000);

現在,所有屬性的長度上限為512,但 Content 已明確設定4000:

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(4000)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

那麼,為什麼我們的慣例不會覆寫明確設定的最大長度? 答案是 EF Core 會持續追蹤每個組態的製作方式。 這會以 ConfigurationSource 列舉表示。 不同類型的組態包括:

  • Explicit:已在 中明確設定模型專案 OnModelCreating
  • DataAnnotation:模型專案是使用 CLR 類型上的對應屬性 (也稱為數據批注) 來設定
  • Convention:模型專案是由模型建置慣例所設定

慣例絕不會覆寫標示為 DataAnnotation 或的 Explicit組態。 使用「慣例產生器」來達成此目的,例如 IConventionPropertyBuilder,從屬性取得的 Builder 。 例如:

property.Builder.HasMaxLength(512);

在慣例產生器上呼叫 HasMaxLength 時,只有在對應屬性或 中OnModelCreating尚未設定它時,才會設定最大長度

這類產生器方法也有第二個參數: fromDataAnnotation。 如果慣例代表對應屬性進行組態,請將此值 true 設定為 。 例如:

property.Builder.HasMaxLength(512, fromDataAnnotation: true);

這會將 設定 ConfigurationSourceDataAnnotation,這表示值現在可以透過明確對應來 OnModelCreating覆寫,但不能透過非對應屬性慣例來覆寫。

最後,在我們離開此範例之前,如果我們 MaxStringLengthConvention 同時使用 和 DiscriminatorLengthConvention3 ,會發生什麼情況? 答案是,這取決於新增的順序,因為模型完成慣例會依新增的順序執行。 因此,如果 MaxStringLengthConvention 最後加入,則會執行最後一個,並將歧視性屬性的最大長度設定為 512。 因此,在此情況下,最好 DiscriminatorLengthConvention3 新增 last,以便只覆寫歧視性屬性的預設最大長度,同時將所有其他字串屬性保留為 512。

取代現有的慣例

有時候,我們不想完全移除現有的慣例,而是想要將它取代為基本上相同但行為變更的慣例。 這非常有用,因為現有的慣例已經實作它所需的介面,以便適當地觸發。

範例:選擇加入屬性對應

EF Core 會依慣例對應所有公用讀寫屬性。 這可能 不適用於 您定義實體類型的方式。 若要變更這項功能,我們可以將 取代 PropertyDiscoveryConvention 為不會對應任何屬性的自有實作,除非它在 中 OnModelCreating 明確對應或標示為名為 Persist的新屬性:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}

以下是新的慣例:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

提示

取代內建慣例時,新的慣例實作應該繼承自現有的慣例類別。 請注意,某些慣例具有關係型或提供者特定的實作,在此情況下,新的慣例實作應該繼承自使用中資料庫提供者最特定的現有慣例類別。

接著會使用 Replace 中的 ConfigureConventions方法註冊慣例:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
        serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
            serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}

提示

這是現有慣例具有相依性,由相依性物件表示的 ProviderConventionSetBuilderDependencies 案例。 這些是使用 GetRequiredService 從內部服務提供者取得,並傳遞至慣例建構函式。

此慣例的運作方式是從指定的實體類型取得所有可讀取的屬性和字段。 如果成員以 [Persist]屬性化,則會藉由呼叫 來對應:

entityTypeBuilder.Property(memberInfo);

另一方面,如果成員是本來已對應的屬性,則會使用 下列專案從模型排除:

entityTypeBuilder.Ignore(propertyInfo.Name);

請注意,此慣例允許對應字段(除了屬性之外),只要字段標示為 [Persist]即可。 這表示我們可以使用私用欄位作為模型中的隱藏金鑰。

例如,請考慮下列實體類型:

public class LaundryBasket
{
    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    public bool IsClean { get; set; }

    public List<Garment> Garments { get; } = new();
}

public class Garment
{
    public Garment(string name, string color)
    {
        Name = name;
        Color = color;
    }

    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    [Persist]
    public string Name { get; }

    [Persist]
    public string Color { get; }

    public bool IsClean { get; set; }

    public LaundryBasket? Basket { get; set; }
}

從這些實體類型建置的模型如下:

Model:
  EntityType: Garment
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Basket_id (no field, int?) Shadow FK Index
      Color (string) Required
      Name (string) Required
      TenantId (int) Required
    Navigations:
      Basket (LaundryBasket) ToPrincipal LaundryBasket Inverse: Garments
    Keys:
      _id PK
    Foreign keys:
      Garment {'Basket_id'} -> LaundryBasket {'_id'} ToDependent: Garments ToPrincipal: Basket ClientSetNull
    Indexes:
      Basket_id
  EntityType: LaundryBasket
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TenantId (int) Required
    Navigations:
      Garments (List<Garment>) Collection ToDependent Garment Inverse: Basket
    Keys:
      _id PK

請注意,通常, IsClean 會進行對應,但由於它沒有標示 [Persist] (大概因為清潔不是洗衣的持續性屬性),它現在被視為未對應的屬性。

提示

無法將此慣例實作為模型完成慣例,因為對應屬性會觸發許多其他慣例來執行,以進一步設定對應的屬性。

預存程序對應

根據預設,EF Core 會產生直接使用數據表或可更新檢視的插入、更新和刪除命令。 EF7 引進了將這些命令對應至預存程序的支援。

提示

EF Core 一律支援透過預存程式進行查詢。 EF7 中的新支持明確說明如何針對插入、更新和刪除使用預存程式。

重要

預存程序對應的支援並不表示建議使用預存程式。

預存程式會使用InsertUsingStoredProcedureUpdateUsingStoredProcedureDeleteUsingStoredProcedure來對應。OnModelCreating 例如,若要對應實體類型的預存程式 Person

modelBuilder.Entity<Person>()
    .InsertUsingStoredProcedure(
        "People_Insert",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(a => a.Name);
            storedProcedureBuilder.HasResultColumn(a => a.Id);
        })
    .UpdateUsingStoredProcedure(
        "People_Update",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        })
    .DeleteUsingStoredProcedure(
        "People_Delete",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        });

此組態會在使用 SQL Server 時對應至下列預存程式:

針對插入

CREATE PROCEDURE [dbo].[People_Insert]
    @Name [nvarchar](max)
AS
BEGIN
      INSERT INTO [People] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@Name);
END

更新

CREATE PROCEDURE [dbo].[People_Update]
    @Id [int],
    @Name_Original [nvarchar](max),
    @Name [nvarchar](max)
AS
BEGIN
    UPDATE [People] SET [Name] = @Name
    WHERE [Id] = @Id AND [Name] = @Name_Original
    SELECT @@ROWCOUNT
END

針對刪除

CREATE PROCEDURE [dbo].[People_Delete]
    @Id [int],
    @Name_Original [nvarchar](max)
AS
BEGIN
    DELETE FROM [People]
    OUTPUT 1
    WHERE [Id] = @Id AND [Name] = @Name_Original;
END

提示

預存程式不需要用於模型中的每一種類型,也不需要用於指定類型上的所有作業。 例如,如果只 DeleteUsingStoredProcedure 針對指定型別指定,EF Core 就會像插入和更新作業一樣產生 SQL,並且只使用預存程式進行刪除。

傳遞至每個方法的第一個自變數是預存程式名稱。 這可以省略,在此情況下,EF Core 會使用附加 「_Insert」、“_Update” 或 「_Delete」 的數據表名稱。 因此,在上述範例中,由於數據表稱為 「People」,因此可以移除預存程式名稱,而不需要變更功能。

第二個自變數是用來設定預存程式的輸入和輸出,包括參數、傳回值和結果數據行。

參數

參數必須以與預存程式定義中顯示的相同順序新增至產生器。

注意

參數可以命名,但 EF Core 一律會使用位置自變數來呼叫預存程式,而不是命名自變數。 投票給 [允許設定 sproc 對應以在依名稱呼叫時使用參數名稱進行調用 ] 是您感興趣的專案。

每個參數產生器方法的第一個自變數會指定參數所系結之模型中的屬性。 這可以是 Lambda 運算式:

storedProcedureBuilder.HasParameter(a => a.Name);

或字串,在對應 陰影屬性時特別有用:

storedProcedureBuilder.HasParameter("Name");

根據預設,參數會設定為「輸入」。 您可以使用巢狀產生器來設定「輸出」或「輸入/輸出」參數。 例如:

storedProcedureBuilder.HasParameter(
    document => document.RetrievedOn, 
    parameterBuilder => parameterBuilder.IsOutput());

參數不同類別有三種不同的產生器方法:

  • HasParameter 指定係結至指定屬性目前值的一般參數。
  • HasOriginalValueParameter 指定係結至指定屬性之原始值的參數。 原始值是屬性從資料庫查詢時所擁有的值,如果已知的話。 如果不知道這個值,則會改用目前的值。 原始值參數適用於並行令牌。
  • HasRowsAffectedParameter 指定參數,用來傳回受預存程序影響的數據列數目。

提示

原始值參數必須用於「更新」和「刪除」預存程式中的索引鍵值。 這可確保在支援可變索引鍵值的未來 EF Core 版本中,將會更新正確的數據列。

傳回值

EF Core 支援三種從預存程式傳回值的機制:

  • 輸出參數,如上所示。
  • 使用產生器方法指定的 HasResultColumn 結果數據行。
  • 傳回值,限制為傳回受影響的數據列數目,並使用產生器方法指定 HasRowsAffectedReturnValue

從預存程式傳回的值通常用於產生的、預設或計算值,例如來自 Identity 索引鍵或計算數據行的值。 例如,下列組態會指定四個結果數據行:

entityTypeBuilder.InsertUsingStoredProcedure(
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(document => document.Title);
            storedProcedureBuilder.HasResultColumn(document => document.Id);
            storedProcedureBuilder.HasResultColumn(document => document.FirstRecordedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RetrievedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RowVersion);
        });

這些是用來傳回:

  • 屬性產生的索引鍵值 Id
  • 資料庫為 FirstRecordedOn 屬性產生的預設值。
  • 資料庫 RetrievedOn 為屬性產生的計算值。
  • 屬性自動產生的 rowversion 並行令牌 RowVersion

此組態會在使用 SQL Server 時對應至下列預存程式:

CREATE PROCEDURE [dbo].[Documents_Insert]
    @Title [nvarchar](max)
AS
BEGIN
    INSERT INTO [Documents] ([Title])
    OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion]
    VALUES (@Title);
END

開放式並行存取

開放式並行 存取的運作方式與預存程式的運作方式相同,就像沒有一樣。 預存程式應該:

  • 在 子句中使用 WHERE 並行令牌,以確保只有在數據列具有有效的令牌時才會更新。 用於並行令牌的值通常是 ,但不必是並行令牌屬性的原始值。
  • 傳回受影響的數據列數目,讓EF Core 可以與受影響的預期數據列數目進行比較,並在值不相符時擲回 DbUpdateConcurrencyException

例如,下列 SQL Server 預存程式會使用 rowversion 自動並行令牌:

CREATE PROCEDURE [dbo].[Documents_Update]
    @Id [int],
    @RowVersion_Original [rowversion],
    @Title [nvarchar](max),
    @RowVersion [rowversion] OUT
AS
BEGIN
    DECLARE @TempTable table ([RowVersion] varbinary(8));
    UPDATE [Documents] SET
        [Title] = @Title
    OUTPUT INSERTED.[RowVersion] INTO @TempTable
    WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original
    SELECT @@ROWCOUNT;
    SELECT @RowVersion = [RowVersion] FROM @TempTable;
END

這是使用下列專案在 EF Core 中設定:

.UpdateUsingStoredProcedure(
    storedProcedureBuilder =>
    {
        storedProcedureBuilder.HasOriginalValueParameter(document => document.Id);
        storedProcedureBuilder.HasOriginalValueParameter(document => document.RowVersion);
        storedProcedureBuilder.HasParameter(document => document.Title);
        storedProcedureBuilder.HasParameter(document => document.RowVersion, parameterBuilder => parameterBuilder.IsOutput());
        storedProcedureBuilder.HasRowsAffectedResultColumn();
    });

請注意:

  • 會使用並行令牌的原始值 RowVersion
  • 預存程式會使用 WHERE 子句來確保只有在原始值相符時 RowVersion ,才會更新數據列。
  • 的新產生的值 RowVersion 會插入臨時表中。
  • 受影響的數據列數目 (@@ROWCOUNT) 和產生的 RowVersion 值會傳回。

將繼承階層對應至預存程式

EF Core 要求預存程式遵循階層中類型的數據表配置。 這表示:

  • 使用 TPH 對應的階層必須具有以單一對應數據表為目標的單一插入、更新及/或刪除預存程式。 插入和更新預存程式必須具有歧視性值的參數。
  • 使用 TPT 對應的階層必須具有每個類型的插入、更新和/或刪除預存程式,包括抽象類型。 EF Core 會視需要多次呼叫,以更新、插入和刪除所有數據表中的數據列。
  • 使用 TPC 對應的階層必須具有每個具體類型的插入、更新和/或刪除預存程式,但不能有抽象型別。

注意

如果使用每個具體類型的單一預存程式,不論對應策略為何,不論繼承對應策略為何,都投票支援使用每個具體類型的單一Sproc。

將擁有的類型對應至預存程式

針對擁有的類型設定預存程式是在巢狀擁有的類型產生器中完成。 例如:

modelBuilder.Entity<Person>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.OwnsOne(
            author => author.Contact,
            ownedNavigationBuilder =>
            {
                ownedNavigationBuilder.ToTable("Contacts");
                ownedNavigationBuilder
                    .InsertUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                        })
                    .UpdateUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
                        })
                    .DeleteUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
            });
    });

注意

目前插入、更新和刪除的預存程式只支持擁有的類型必須對應至個別的數據表。 也就是說,擁有的類型不能由擁有者數據表中的數據行表示。 如果這是您想要看到已移除的限制,請投票給 CUD sproc 對應 新增「數據表」分割支援。

將多對多聯結實體對應至預存程式

您可以設定預存程式的多對多聯結實體,做為多對多組態的一部分。 例如:

modelBuilder.Entity<Book>(
    entityTypeBuilder =>
    {
        entityTypeBuilder
            .HasMany(document => document.Authors)
            .WithMany(author => author.PublishedWorks)
            .UsingEntity<Dictionary<string, object>>(
                "BookPerson",
                builder => builder.HasOne<Person>().WithMany().OnDelete(DeleteBehavior.Cascade),
                builder => builder.HasOne<Book>().WithMany().OnDelete(DeleteBehavior.ClientCascade),
                joinTypeBuilder =>
                {
                    joinTypeBuilder
                        .InsertUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasParameter("AuthorsId");
                                storedProcedureBuilder.HasParameter("PublishedWorksId");
                            })
                        .DeleteUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasOriginalValueParameter("AuthorsId");
                                storedProcedureBuilder.HasOriginalValueParameter("PublishedWorksId");
                                storedProcedureBuilder.HasRowsAffectedResultColumn();
                            });
                });
    });

新增和改良的攔截器和事件

EF Core 攔截器 可啟用 EF Core 作業的攔截、修改和/或隱藏。 EF Core 也包含 傳統的 .NET 事件記錄

EF7 包含下列攔截器的增強功能:

此外,EF7 也包含下列專案的新傳統 .NET 事件:

下列各節示範使用這些新攔截功能的一些範例。

實體建立的簡單動作

提示

此處顯示的程式代碼來自 SimpleMaterializationSample.cs

新的 IMaterializationInterceptor 支援在建立實體實例之前和之後攔截,以及初始化該實例的屬性前後。 攔截器可以變更或取代每個點的實體實例。 這讓:

  • 設定驗證、計算值或旗標所需的未對應的屬性或呼叫方法。
  • 使用處理站建立實例。
  • 建立與 EF 不同的實體實例通常會建立,例如從快取或 Proxy 類型的實例。
  • 將服務插入實體實例。

例如,假設我們想要追蹤從資料庫擷取實體的時間,或許可以向用戶顯示該數據。 為了達成此目的,我們會先定義介面:

public interface IHasRetrieved
{
    DateTime Retrieved { get; set; }
}

使用介面與攔截器很常見,因為它允許相同的攔截器使用許多不同的實體類型。 例如:

public class Customer : IHasRetrieved
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? PhoneNumber { get; set; }

    [NotMapped]
    public DateTime Retrieved { get; set; }
}

請注意, [NotMapped] 屬性是用來指出只有在使用實體時才會使用這個屬性,而且不應該保存至資料庫。

攔截器接著必須實作適當的方法, IMaterializationInterceptor 並設定擷取的時間:

public class SetRetrievedInterceptor : IMaterializationInterceptor
{
    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasRetrieved hasRetrieved)
        {
            hasRetrieved.Retrieved = DateTime.UtcNow;
        }

        return instance;
    }
}

設定 時 DbContext,會註冊此攔截器的實例:

public class CustomerContext : DbContext
{
    private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();

    public DbSet<Customer> Customers
        => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .AddInterceptors(_setRetrievedInterceptor)
            .UseSqlite("Data Source = customers.db");
}

提示

此攔截器是無狀態的,這是常見的,因此會建立單一實例,並在所有 DbContext 實例之間共用。

現在,每當從資料庫查詢 時 CustomerRetrieved 就會自動設定 屬性。 例如:

await using (var context = new CustomerContext())
{
    var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
    Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}

產生輸出:

Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'

將服務插入實體

提示

此處顯示的程式代碼來自 InjectLoggerSample.cs

EF Core 已內建支援將一些特殊服務插入內容實例;例如,請參閱 不使用 Proxy 的延遲載入,其可藉由插入 ILazyLoader 服務來運作。

IMaterializationInterceptor可用來將此一般化為任何服務。 下列範例示範如何將 插入 ILogger 實體,使其可以執行自己的記錄。

注意

將服務插入實體會將這些實體類型與插入的服務結合,而有些人認為這是反模式。

和之前一樣,介面是用來定義可以執行的動作。

public interface IHasLogger
{
    ILogger? Logger { get; set; }
}

而將記錄的實體類型必須實作這個介面。 例如:

public class Customer : IHasLogger
{
    private string? _phoneNumber;

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public string? PhoneNumber
    {
        get => _phoneNumber;
        set
        {
            Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");

            _phoneNumber = value;
        }
    }

    [NotMapped]
    public ILogger? Logger { get; set; }
}

這次,攔截器必須實 IMaterializationInterceptor.InitializedInstance作 ,在建立每個實體實例之後呼叫,而且其屬性值已經初始化。 攔截器會從內容取得 ILogger ,並使用它初始化 IHasLogger.Logger

public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
    private ILogger? _logger;

    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasLogger hasLogger)
        {
            _logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
            hasLogger.Logger = _logger;
        }

        return instance;
    }
}

這次會針對每個 DbContext 實例使用攔截器的新實例,因為 ILogger 取得的 可以變更每個 DbContext 實例,而且 ILogger 會在攔截器上快取 :

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());

現在,每當 Customer.PhoneNumber 變更時,此變更就會記錄至應用程式的記錄檔。 例如:

info: CustomersLogger[1]
      Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.

LINQ 運算式樹狀結構攔截

提示

此處顯示的程式代碼來自 QueryInterceptionSample.cs

EF Core 會 使用 .NET LINQ 查詢。 這通常牽涉到使用 C#、VB 或 F# 編譯程式來建置運算式樹狀結構,然後由 EF Core 轉譯為適當的 SQL。 例如,請考慮傳回客戶頁面的方法:

Task<List<Customer>> GetPageOfCustomers(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .Skip(page * 20).Take(20).ToListAsync();
}

提示

此查詢會 EF.Property 使用 方法來指定要排序的屬性。 這可讓應用程式動態傳入屬性名稱,允許依實體類型的任何屬性排序。 請注意,依非索引數據行排序可能會變慢。

只要用於排序的 屬性一律會傳回穩定的排序,這樣就能正常運作。 但情況可能並不總是如此。 例如,上述 LINQ 查詢會在依 排序 Customer.City時,在 SQLite 上產生下列專案:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0

如果有多個客戶具有相同 City,則此查詢的排序並不穩定。 當使用者透過數據分頁時,這可能會導致遺失或重複的結果。

修正此問題的常見方法是依主鍵執行次要排序。 不過,EF7 不會手動將此新增至每個查詢,而是允許攔截可動態新增次要排序的查詢表達式樹狀結構。 為了方便進行這項操作,我們將再次使用 介面,這次對於具有整數主鍵的任何實體而言:

public interface IHasIntKey
{
    int Id { get; }
}

此介面是由感興趣的實體類型所實作:

public class Customer : IHasIntKey
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? City { get; set; }
    public string? PhoneNumber { get; set; }
}

然後我們需要實作的攔截器 IQueryExpressionInterceptor

public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
    public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
        => new KeyOrderingExpressionVisitor().Visit(queryExpression);

    private class KeyOrderingExpressionVisitor : ExpressionVisitor
    {
        private static readonly MethodInfo ThenByMethod
            = typeof(Queryable).GetMethods()
                .Single(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);

        protected override Expression VisitMethodCall(MethodCallExpression? methodCallExpression)
        {
            var methodInfo = methodCallExpression!.Method;
            if (methodInfo.DeclaringType == typeof(Queryable)
                && methodInfo.Name == nameof(Queryable.OrderBy)
                && methodInfo.GetParameters().Length == 2)
            {
                var sourceType = methodCallExpression.Type.GetGenericArguments()[0];
                if (typeof(IHasIntKey).IsAssignableFrom(sourceType))
                {
                    var lambdaExpression = (LambdaExpression)((UnaryExpression)methodCallExpression.Arguments[1]).Operand;
                    var entityParameterExpression = lambdaExpression.Parameters[0];

                    return Expression.Call(
                        ThenByMethod.MakeGenericMethod(
                            sourceType,
                            typeof(int)),
                        base.VisitMethodCall(methodCallExpression),
                        Expression.Lambda(
                            typeof(Func<,>).MakeGenericType(entityParameterExpression.Type, typeof(int)),
                            Expression.Property(entityParameterExpression, nameof(IHasIntKey.Id)),
                            entityParameterExpression));
                }
            }

            return base.VisitMethodCall(methodCallExpression);
        }
    }
}

這看起來可能相當複雜,而且是! 使用表達式樹狀架構通常不容易。 讓我們看看發生了什麼事:

  • 基本上,攔截器會 ExpressionVisitor封裝 。 訪客會 VisitMethodCall覆寫 ,每當查詢表達式樹狀結構中有方法的呼叫時,就會呼叫此方法。

  • 訪客會檢查這是我們感興趣的方法呼叫 OrderBy

  • 如果是,則訪客會進一步檢查泛型方法呼叫是否為實作介面 IHasIntKey 的類型。

  • 此時,我們知道方法呼叫的格式 OrderBy(e => ...)為 。 我們會從這個呼叫擷取 Lambda 運算式,並取得該運算式中使用的參數,也就是 e

  • 我們現在使用Expression.Call建立器方法來建置新的 MethodCallExpression 。 在這裡情況下,所呼叫的方法為 ThenBy(e => e.Id)。 我們會使用上述擷取的參數,以及介面屬性IHasIntKey的屬性存取Id來建置這個參數。

  • 這個呼叫的輸入是原始 OrderBy(e => ...)的 ,因此結束結果是 的 OrderBy(e => ...).ThenBy(e => e.Id)表達式。

  • 這個修改過的表達式會從訪客傳回,這表示LINQ查詢現在已經過適當修改以包含 ThenBy 呼叫。

  • EF Core 會繼續,並將此查詢表達式編譯為所使用之資料庫的適當 SQL。

此攔截器會以與第一個範例相同的方式註冊。 執行 GetPageOfCustomers 現在會產生下列 SQL:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City", "c"."Id"
LIMIT @__p_1 OFFSET @__p_0

這現在一律會產生穩定的訂單,即使有多個具有相同 的客戶也一 City樣。

唷! 這是許多程式代碼,可對查詢進行簡單的變更。 更糟的是,它甚至可能不適用於所有查詢。 難以撰寫可辨識它應該的所有查詢圖形的表達式訪客,而且不應該寫入任何查詢圖形。 例如,如果排序是在子查詢中完成,則這可能無法運作。

這讓我們到了攔截器的關鍵點,一律問自己是否有一種更容易的方式來做您想要的。 攔截器很強大,但很容易出錯。 他們說,就像說的那樣,這是一個簡單的方法,射殺自己在腳。

例如,假設我們改為變更方法 GetPageOfCustomers ,如下所示:

Task<List<Customer>> GetPageOfCustomers2(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .ThenBy(e => e.Id)
        .Skip(page * 20).Take(20).ToListAsync();
}

在此情況下, ThenBy 只會將 新增至查詢。 是,可能需要個別對每個查詢執行,但簡單、容易瞭解,而且一律會運作。

開放式並行攔截

提示

此處顯示的程式代碼來自 OptimisticConcurrencyInterceptionSample.cs

EF Core 支援 開放式並行模式 ,方法是檢查實際受更新或刪除影響的數據列數目與預期受影響的數據列數目相同。 這通常與並行令牌結合;也就是說,數據行值,只有在讀取預期的值之後,數據列尚未更新,才會符合其預期值。

EF 會擲回 DbUpdateConcurrencyException來發出違反開放式並行存取的訊號。 在 EF7 中, ISaveChangesInterceptor 有新的方法 ThrowingConcurrencyException ,而且 ThrowingConcurrencyExceptionAsync 會在 擲回 之前 DbUpdateConcurrencyException 呼叫。 這些攔截點允許隱藏例外狀況,可能加上異步資料庫變更來解決違規。

例如,如果兩個要求幾乎同時嘗試刪除相同的實體,則第二個刪除可能會失敗,因為資料庫中的數據列已不存在。 這可能是正確的結果,就是實體無論如何都已刪除。 下列攔截器示範如何完成此動作:

public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
    public InterceptionResult ThrowingConcurrencyException(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result)
    {
        if (eventData.Entries.All(e => e.State == EntityState.Deleted))
        {
            Console.WriteLine("Suppressing Concurrency violation for command:");
            Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);

            return InterceptionResult.Suppress();
        }

        return result;
    }

    public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
        => new(ThrowingConcurrencyException(eventData, result));
}

有數件事值得注意此攔截器:

  • 同時實作同步和異步攔截方法。 如果應用程式可以呼叫 SaveChangesSaveChangesAsync,則這很重要。 不過,如果所有應用程式程式代碼都是異步的,則只需要 ThrowingConcurrencyExceptionAsync 實作。 同樣地,如果應用程式永遠不會使用異步資料庫方法,則只需要 ThrowingConcurrencyException 實作。 這通常適用於具有同步處理和異步方法的所有攔截器。 (實作應用程式不會用來擲回的方法可能值得,只是在某些情況下,某些同步/異步程序代碼會在 中爬行。
  • 攔截器可以存取 EntityEntry 要儲存之實體的物件。 在此情況下,這會用來檢查刪除作業是否發生並行違規。
  • 如果應用程式使用關係資料庫提供者,則可以將 ConcurrencyExceptionEventData 物件轉換成 RelationalConcurrencyExceptionEventData 物件。 這會提供有關所執行之資料庫作業的其他關係型特定資訊。 在此情況下,關係型命令文字會列印至主控台。
  • InterceptionResult.Suppress() 回會告知 EF Core 隱藏即將採取的動作,在此案例中,擲回 DbUpdateConcurrencyException。 這項變更 EF Core 行為的能力,而不是只觀察 EF Core 正在執行的動作,是攔截器最強大的功能之一。

延遲初始化 連接字串

提示

此處顯示的程式代碼來自 LazyConnectionStringSample.cs

連接字串通常是從組態檔讀取的靜態資產。 設定 時DbContext,這些可以輕鬆地傳遞至 UseSqlServer 或類似專案。 不過,有時候 連接字串 可以變更每個內容實例。 例如,多租用戶系統中的每個租使用者可能有不同的 連接字串。

EF7 透過 改善 IDbConnectionInterceptor,更輕鬆地處理動態連線和 連接字串。 這一開始就是能夠設定 DbContext ,而不需要任何 連接字串。 例如:

services.AddDbContext<CustomerContext>(
    b => b.UseSqlServer());

然後,可以實作其中 IDbConnectionInterceptor 一種方法來設定連接,然後再使用它。 ConnectionOpeningAsync是不錯的選擇,因為它可以執行異步操作來取得 連接字串、尋找存取令牌等等。 例如,假設服務的範圍設定為瞭解目前租使用者的目前要求:

services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();

警告

每次需要時,執行 連接字串、存取令牌或類似的異步查閱可能會非常慢。 請考慮快取這些專案,並只定期重新整理快取的字串或令牌。 例如,存取令牌通常可在需要重新整理之前使用一段相當長的時間。

這可以使用建構函式插入來插入每個 DbContext 實例:

public class CustomerContext : DbContext
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

    public CustomerContext(
        DbContextOptions<CustomerContext> options,
        ITenantConnectionStringFactory connectionStringFactory)
        : base(options)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    // ...
}

接著,此服務會在建構內容的攔截器實作時使用:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(
        new ConnectionStringInitializationInterceptor(_connectionStringFactory));

最後,攔截器會使用此服務以異步方式取得 連接字串,並在第一次使用聯機時加以設定:

public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
    private readonly IClientConnectionStringFactory _connectionStringFactory;

    public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
        => throw new NotSupportedException("Synchronous connections not supported.");

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
        CancellationToken cancellationToken = new())
    {
        if (string.IsNullOrEmpty(connection.ConnectionString))
        {
            connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
        }

        return result;
    }
}

注意

連接字串 只會在第一次使用連接時取得。 之後,儲存在 上的 DbConnection 連接字串 將會使用,而不需要查閱新的 連接字串。

提示

此攔截器會覆寫要擲回的非異步ConnectionOpening方法,因為必須從異步程式代碼路徑呼叫服務以取得 連接字串。

記錄 SQL Server 查詢統計數據

提示

此處顯示的程式代碼來自 QueryStatisticsLoggerSample.cs

最後,讓我們建立兩個攔截器,一起運作,以將 SQL Server 查詢統計數據傳送至應用程式記錄。 若要產生統計數據,我們需要 IDbCommandInterceptor 執行兩件事。

首先,攔截器會使用 SET STATISTICS IO ON前置命令,這會告訴 SQL Server 在取用結果集之後,將統計數據傳送給用戶端:

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)
{
    command.CommandText = "SET STATISTICS IO ON;" + Environment.NewLine + command.CommandText;

    return new(result);
}

其次,攔截器會實作新的 DataReaderClosingAsync 方法,這個方法會在 完成取用結果之後 DbDataReader 呼叫,但在 關閉之前 呼叫。 當 SQL Server 傳送統計數據時,它會將它們放在讀取器上的第二個結果中,因此此時攔截器會藉由呼叫 NextResultAsync 來將統計數據填入連線,來讀取該結果。

public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
    DbCommand command,
    DataReaderClosingEventData eventData,
    InterceptionResult result)
{
    await eventData.DataReader.NextResultAsync();

    return result;
}

需要第二個攔截器,才能從連線取得統計數據,並將其寫出至應用程式的記錄器。 為此,我們將使用 IDbConnectionInterceptor,實作新的 ConnectionCreated 方法。 ConnectionCreated 會在 EF Core 建立連線之後立即呼叫 ,因此可用來執行該連線的其他組態。 在此情況下,攔截器會 ILogger 取得 ,然後攔截事件 SqlConnection.InfoMessage 以記錄訊息。

public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection result)
{
    var logger = eventData.Context!.GetService<ILoggerFactory>().CreateLogger("InfoMessageLogger");
    ((SqlConnection)eventData.Connection).InfoMessage += (_, args) =>
    {
        logger.LogInformation(1, args.Message);
    };
    return result;
}

重要

ConnectionCreating只有在 EF Core 建立 DbConnection時,才會呼叫 和 ConnectionCreated 方法。 如果應用程式建立 DbConnection 並將它傳遞至 EF Core,則不會呼叫它們。

執行一些使用這些攔截器的程式代碼會在記錄檔中顯示 SQL Server 查詢統計資料:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000), @p2='?' (Size = 4000), @p3='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Customers] USING (
      VALUES (@p0, @p1, 0),
      (@p2, @p3, 1)) AS i ([Name], [PhoneNumber], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name], [PhoneNumber])
      VALUES (i.[Name], i.[PhoneNumber])
      OUTPUT INSERTED.[Id], i._Position;
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 0, logical reads 5, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SELECT TOP(2) [c].[Id], [c].[Name], [c].[PhoneNumber]
      FROM [Customers] AS [c]
      WHERE [c].[Name] = N'Alice'
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.

查詢增強功能

EF7 包含 LINQ 查詢翻譯的許多改善。

GroupBy 作為最終運算符

提示

此處顯示的程式代碼來自 GroupByFinalOperatorSample.cs

EF7 支援在 GroupBy 查詢中使用 做為最終運算符。 例如這個 LINQ 查詢:

var query = context.Books.GroupBy(s => s.Price);

使用 SQL Server 時會轉譯為下列 SQL:

SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

注意

這種類型的 GroupBy 不會直接轉譯為 SQL,因此 EF Core 會在傳回的結果上進行分組。 不過,這並不會導致從伺服器傳輸任何其他數據。

GroupJoin 作為最終運算符

提示

此處顯示的程式代碼來自 GroupJoinFinalOperatorSample.cs

EF7 支援在 GroupJoin 查詢中使用 做為最終運算符。 例如這個 LINQ 查詢:

var query = context.Customers.GroupJoin(
    context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });

使用 SQL Server 時會轉譯為下列 SQL:

SELECT [c].[Id], [c].[Name], [t].[Id], [t].[Amount], [t].[CustomerId]
FROM [Customers] AS [c]
OUTER APPLY (
    SELECT [o].[Id], [o].[Amount], [o].[CustomerId]
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId]
) AS [t]
ORDER BY [c].[Id]

GroupBy 實體類型

提示

此處顯示的程式代碼來自 GroupByEntityTypeSample.cs

EF7 支援依實體類型分組。 例如這個 LINQ 查詢:

var query = context.Books
    .GroupBy(s => s.Author)
    .Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });

使用 SQLite 時會轉譯為下列 SQL:

SELECT [a].[Id], [a].[Name], MAX([b].[Price]) AS [MaxPrice]
FROM [Books] AS [b]
INNER JOIN [Author] AS [a] ON [b].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

請記住,依唯一屬性分組,例如主鍵,一律比依實體類型分組更有效率。 不過,依實體類型分組可用於索引鍵和無索引鍵實體類型。

此外,使用主鍵的實體類型進行分組一律會導致每個實體實例有一個群組,因為每個實體都必須有唯一的索引鍵值。 有時值得切換查詢的來源,讓不需要進行分組。 例如,下列查詢會傳回與上一個查詢相同的結果:

var query = context.Authors
    .Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });

此查詢會在使用 SQLite 時轉譯為下列 SQL:

SELECT [a].[Id], [a].[Name], (
    SELECT MAX([b].[Price])
    FROM [Books] AS [b]
    WHERE [a].[Id] = [b].[AuthorId]) AS [MaxPrice]
FROM [Authors] AS [a]

子查詢不會從外部查詢參考未分組的數據行

提示

此處顯示的程式代碼來自 UngroupedColumnsQuerySample.cs

在 EF Core 6.0 中,子 GROUP BY 句會參考外部查詢中的數據行,而這些數據行會因某些資料庫而失敗,而其他資料庫則沒有效率。 例如,請考慮下列查詢:

var query = from s in (from i in context.Invoices
                       group i by i.History.Month
                       into g
                       select new { Month = g.Key, Total = g.Sum(p => p.Amount), })
            select new
            {
                s.Month, s.Total, Payment = context.Payments.Where(p => p.History.Month == s.Month).Sum(p => p.Amount)
            };

在 SQL Server 上的 EF Core 6.0 中,這會轉譯為:

SELECT DATEPART(month, [i].[History]) AS [Month], COALESCE(SUM([i].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = DATEPART(month, [i].[History])) AS [Payment]
FROM [Invoices] AS [i]
GROUP BY DATEPART(month, [i].[History])

在 EF7 上,翻譯為:

SELECT [t].[Key] AS [Month], COALESCE(SUM([t].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = [t].[Key]) AS [Payment]
FROM (
    SELECT [i].[Amount], DATEPART(month, [i].[History]) AS [Key]
    FROM [Invoices] AS [i]
) AS [t]
GROUP BY [t].[Key]

只讀集合可用於 Contains

提示

此處顯示的程式代碼來自 ReadOnlySetQuerySample.cs

EF7 支援在 或中包含要搜尋的專案包含在或 IReadOnlyCollectionIReadOnlyListIReadOnlySet時使用 Contains 。 例如這個 LINQ 查詢:

IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));

使用 SQL Server 時會轉譯為下列 SQL:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[Customer1Id] AND [o].[Id] IN (1, 3, 5))

聚合函數的翻譯

EF7 為提供者引進更好的擴充性,以轉譯聚合函數。 此領域的這項工作和其他工作已跨提供者產生數個新的翻譯,包括:

注意

對自變數採取行動 IEnumerable 的聚合函數通常只會在查詢中 GroupBy 轉譯。 如果您想要移除這項限制,請投票給 JSON 資料行中的支持空間類型。

字串聚合函數

提示

此處顯示的程式代碼來自 StringAggregateFunctionsSample.cs

使用 JoinConcat 的查詢現在會在適當時轉譯。 例如:

var query = context.Posts
    .GroupBy(post => post.Author)
    .Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });

此查詢會在使用 SQL Server 時轉譯為下列各項:

SELECT [a].[Id], [a].[Name], COALESCE(STRING_AGG([p].[Title], N'|'), N'') AS [Books]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

結合其他字串函式時,這些轉譯允許伺服器上的某些複雜字串操作。 例如:

var query = context.Posts
    .GroupBy(post => post.Author!.Name)
    .Select(
        grouping =>
            new
            {
                PostAuthor = grouping.Key,
                Blogs = string.Concat(
                    grouping
                        .Select(post => post.Blog.Name)
                        .Distinct()
                        .Select(postName => "'" + postName + "' ")),
                ContentSummaries = string.Join(
                    " | ",
                    grouping
                        .Where(post => post.Content.Length >= 10)
                        .Select(post => "'" + post.Content.Substring(0, 10) + "' "))
            });

此查詢會在使用 SQL Server 時轉譯為下列各項:

SELECT [t].[Name], (N'''' + [t0].[Name]) + N''' ', [t0].[Name], [t].[c]
FROM (
    SELECT [a].[Name], COALESCE(STRING_AGG(CASE
        WHEN CAST(LEN([p].[Content]) AS int) >= 10 THEN COALESCE((N'''' + COALESCE(SUBSTRING([p].[Content], 0 + 1, 10), N'')) + N''' ', N'')
    END, N' | '), N'') AS [c]
    FROM [Posts] AS [p]
    LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
    GROUP BY [a].[Name]
) AS [t]
OUTER APPLY (
    SELECT DISTINCT [b].[Name]
    FROM [Posts] AS [p0]
    LEFT JOIN [Authors] AS [a0] ON [p0].[AuthorId] = [a0].[Id]
    INNER JOIN [Blogs] AS [b] ON [p0].[BlogId] = [b].[Id]
    WHERE [t].[Name] = [a0].[Name] OR ([t].[Name] IS NULL AND [a0].[Name] IS NULL)
) AS [t0]
ORDER BY [t].[Name]

空間聚合函數

提示

此處顯示的程式代碼來自 SpatialAggregateFunctionsSample.cs

現在,支援 NetTopologySuite 的資料庫提供者可以轉譯下列空間聚合函數:

提示

這些翻譯已由 SQL Server 和 SQLite 小組實作。 若為其他提供者,請連絡提供者維護人員,以在該提供者實作支援時新增支援。

例如:

var query = context.Caches
    .Where(cache => cache.Location.X < -90)
    .GroupBy(cache => cache.Owner)
    .Select(
        grouping => new { Id = grouping.Key, Combined = GeometryCombiner.Combine(grouping.Select(cache => cache.Location)) });

使用 SQL Server 時,此查詢會轉譯為下列 SQL:

SELECT [c].[Owner] AS [Id], geography::CollectionAggregate([c].[Location]) AS [Combined]
FROM [Caches] AS [c]
WHERE [c].[Location].Long < -90.0E0
GROUP BY [c].[Owner]

統計聚合函數

提示

此處顯示的程式代碼來自 StatisticalAggregateFunctionsSample.cs

已針對下列統計函式實作 SQL Server 翻譯:

提示

這些翻譯已由 SQL Server 小組實作。 若為其他提供者,請連絡提供者維護人員,以在該提供者實作支援時新增支援。

例如:

var query = context.Downloads
    .GroupBy(download => download.Uploader.Id)
    .Select(
        grouping => new
        {
            Author = grouping.Key,
            TotalCost = grouping.Sum(d => d.DownloadCount),
            AverageViews = grouping.Average(d => d.DownloadCount),
            VariancePopulation = EF.Functions.VariancePopulation(grouping.Select(d => d.DownloadCount)),
            VarianceSample = EF.Functions.VarianceSample(grouping.Select(d => d.DownloadCount)),
            StandardDeviationPopulation = EF.Functions.StandardDeviationPopulation(grouping.Select(d => d.DownloadCount)),
            StandardDeviationSample = EF.Functions.StandardDeviationSample(grouping.Select(d => d.DownloadCount))
        });

使用 SQL Server 時,此查詢會轉譯為下列 SQL:

SELECT [u].[Id] AS [Author], COALESCE(SUM([d].[DownloadCount]), 0) AS [TotalCost], AVG(CAST([d].[DownloadCount] AS float)) AS [AverageViews], VARP([d].[DownloadCount]) AS [VariancePopulation], VAR([d].[DownloadCount]) AS [VarianceSample], STDEVP([d].[DownloadCount]) AS [StandardDeviationPopulation], STDEV([d].[DownloadCount]) AS [StandardDeviationSample]
FROM [Downloads] AS [d]
INNER JOIN [Uploader] AS [u] ON [d].[UploaderId] = [u].[Id]
GROUP BY [u].[Id]

的翻譯 string.IndexOf

提示

此處顯示的程式代碼來自 MiscellaneousTranslationsSample.cs

EF7 現在會在 String.IndexOf LINQ 查詢中轉譯。 例如:

var query = context.Posts
    .Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
    .Where(post => post.IndexOfEntity > 0);

此查詢會在使用 SQL Server 時轉譯為下列 SQL:

SELECT [p].[Title], CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1 AS [IndexOfEntity]
FROM [Posts] AS [p]
WHERE (CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1) > 0

GetType實體類型的轉譯

提示

此處顯示的程式代碼來自 MiscellaneousTranslationsSample.cs

EF7 現在會在 Object.GetType() LINQ 查詢中轉譯。 例如:

var query = context.Posts.Where(post => post.GetType() == typeof(Post));

此查詢會在搭配 TPH 繼承使用 SQL Server 時轉譯為下列 SQL:

SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'Post'

請注意,此查詢只會 Post 傳回實際屬於 類型的 Post實例,而不是任何衍生型別的實例。 這與使用 isOfType的查詢不同,後者也會傳回任何衍生型別的實例。 例如,請考慮查詢:

var query = context.Posts.OfType<Post>();

這會轉譯為不同的 SQL:

      SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
      FROM [Posts] AS [p]

和會同時 Post 傳回和 FeaturedPost 實體。

支援 AT TIME ZONE

提示

此處顯示的程式代碼來自 MiscellaneousTranslationsSample.cs

EF7 引進 和 的新AtTimeZone函式DateTimeDateTimeOffset 這些函式會轉譯為 AT TIME ZONE 所產生 SQL 中的 子句。 例如:

var query = context.Posts
    .Select(
        post => new
        {
            post.Title,
            PacificTime = EF.Functions.AtTimeZone(post.PublishedOn, "Pacific Standard Time"),
            UkTime = EF.Functions.AtTimeZone(post.PublishedOn, "GMT Standard Time"),
        });

此查詢會在使用 SQL Server 時轉譯為下列 SQL:

SELECT [p].[Title], [p].[PublishedOn] AT TIME ZONE 'Pacific Standard Time' AS [PacificTime], [p].[PublishedOn] AT TIME ZONE 'GMT Standard Time' AS [UkTime]
FROM [Posts] AS [p]

提示

這些翻譯已由 SQL Server 小組實作。 若為其他提供者,請連絡提供者維護人員,以在該提供者實作支援時新增支援。

已篩選隱藏導覽上的 Include

提示

此處顯示的程式代碼來自 MiscellaneousTranslationsSample.cs

Include 方法現在可以搭配 EF.Property使用。 這甚至允許 篩選和排序 私人導覽屬性,或字段所代表的私人導覽。 例如:

var query = context.Blogs.Include(
    blog => EF.Property<ICollection<Post>>(blog, "Posts")
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

這相當於:

var query = context.Blogs.Include(
    blog => Posts
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

但不需要 Blog.Posts 公開存取。

使用 SQL Server 時,上述兩個查詢都會轉譯為:

SELECT [b].[Id], [b].[Name], [t].[Id], [t].[AuthorId], [t].[BlogId], [t].[Content], [t].[Discriminator], [t].[PublishedOn], [t].[Title], [t].[PromoText]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
    FROM [Posts] AS [p]
    WHERE [p].[Content] LIKE N'%.NET%'
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Title]

的Cosmos翻譯 Regex.IsMatch

提示

此處顯示的程式代碼來自 CosmosQueriesSample.cs

EF7 支援在針對 Azure Cosmos DB 的 LINQ 查詢中使用 Regex.IsMatch 。 例如:

var containsInnerT = await context.Triangles
    .Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
    .ToListAsync();

轉譯為下列 SQL:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))

DbContext API 和行為增強功能

EF7 包含各種與相關類別的小型改進 DbContext

提示

本節範例的程式代碼來自 DbContextApiSample.cs

Uninitialized DbSet 屬性的隱藏器

建構 時DbContext,EF Core 會自動初始化 上的DbContext公用、可DbSet設定屬性。 例如,請考慮下列 DbContext 定義:

public class SomeDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
}

屬性Blogs會設定為 實例,作為建構實例的DbContextDbSet<Blog>部分。 這可讓內容用於查詢,而不需要任何額外的步驟。

不過,在 C# 可為 Null 的參考型別引進之後,編譯程式現在會警告無法初始化不可為 Null 的屬性Blogs

[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

這是虛假警告;屬性會由EF Core 設定為非 Null 值。 此外,將屬性宣告為可為 Null 會使警告消失,但這不是個好主意,因為從概念上講,屬性不可為 Null,而且永遠不會是 Null。

EF7 包含 的 DiagnosticSuppressorDbSetDbContext其會阻止編譯程式產生此警告。

提示

此模式源自 C# 自動屬性非常有限的日子。 使用新式 C# 時,請考慮將自動屬性設為唯讀,然後在建構函式中 DbContext 明確初始化它們,或視需要從內容取得快取 DbSet 的實例。 例如: public DbSet<Blog> Blogs => Set<Blog>()

區分記錄中的取消與失敗

有時候應用程式會明確取消查詢或其他資料庫作業。 這通常是使用 CancellationToken 傳遞至執行作業的方法來完成。

在EF Core 6中,取消作業時所記錄的事件與作業因其他原因而失敗時所記錄的事件相同。 EF7 會特別針對已取消的資料庫作業引進新的記錄事件。 根據預設,這些新事件會記錄在 Debug 層級。 下表顯示相關事件及其預設記錄層級:

活動 描述 預設記錄層級
CoreEventId.QueryIterationFailed 處理查詢結果時發生錯誤。 LogLevel.Error
CoreEventId.SaveChangesFailed 嘗試儲存資料庫變更時發生錯誤。 LogLevel.Error
RelationalEventId.CommandError 執行資料庫命令時發生錯誤。 LogLevel.Error
CoreEventId.QueryCanceled 查詢已取消。 LogLevel.Debug
CoreEventId.SaveChangesCanceled 嘗試儲存變更時,資料庫命令已取消。 LogLevel.Debug
RelationalEventId.CommandCanceled DbCommand的執行已取消。 LogLevel.Debug

注意

查看例外狀況,而不是檢查取消令牌來偵測取消。 這表示不會透過取消令牌觸發的取消,仍會以這種方式偵測和記錄。

方法的新IPropertyEntityEntryINavigation多載

使用EF模型的程式代碼通常會有 IPropertyINavigation 表示屬性或導覽元數據。 然後,EntityEntry 會用來取得屬性/導覽值或查詢其狀態。 不過,在 EF7 之前,這需要將屬性的名稱或巡覽傳遞至 的方法EntityEntry,然後重新查閱 IPropertyINavigation 在 EF7 中, IProperty 可以改為直接傳遞 或 INavigation ,以避免額外的查閱。

例如,請考慮一個方法來尋找指定實體的所有同層級:

public static IEnumerable<TEntity> FindSiblings<TEntity>(
    this DbContext context, TEntity entity, string navigationToParent)
    where TEntity : class
{
    var parentEntry = context.Entry(entity).Reference(navigationToParent);

    return context.Entry(parentEntry.CurrentValue!)
        .Collection(parentEntry.Metadata.Inverse!)
        .CurrentValue!
        .OfType<TEntity>()
        .Where(e => !ReferenceEquals(e, entity));
}

這個方法會尋找指定實體的父代,然後將 反向 INavigation 傳遞至 Collection 父專案的方法。 接著,此元數據會用來傳回指定父代的所有同層級。 以下是其用法的範例:


Console.WriteLine($"Siblings to {post.Id}: '{post.Title}' are...");
foreach (var sibling in context.FindSiblings(post, nameof(post.Blog)))
{
    Console.WriteLine($"    {sibling.Id}: '{sibling.Title}'");
}

以及輸出:

Siblings to 1: 'Announcing Entity Framework 7 Preview 7: Interceptors!' are...
    5: 'Productivity comes to .NET MAUI in Visual Studio 2022'
    6: 'Announcing .NET 7 Preview 7'
    7: 'ASP.NET Core updates in .NET 7 Preview 7'

EntityEntry 適用於共用類型實體類型

EF Core 可以針對多個不同的實體類型使用相同的 CLR 類型。 這些稱為「共用類型實體類型」,通常用來對應字典類型與用於實體類型屬性的索引鍵/值組。 例如, BuildMetadata 您可以定義實體類型,而不定義專用 CLR 類型:

modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
    "BuildMetadata", b =>
    {
        b.IndexerProperty<int>("Id");
        b.IndexerProperty<string>("Tag");
        b.IndexerProperty<Version>("Version");
        b.IndexerProperty<string>("Hash");
        b.IndexerProperty<bool>("Prerelease");
    });

請注意,共用類型實體類型必須命名 - 在此情況下, 名稱為 BuildMetadata。 然後,針對使用名稱取得的實體類型,使用這些實體類型來存取 DbSet 這些實體類型。 例如:

public DbSet<Dictionary<string, object>> BuildMetadata
    => Set<Dictionary<string, object>>("BuildMetadata");

DbSet 可用來追蹤實體實例:

await context.BuildMetadata.AddAsync(
    new Dictionary<string, object>
    {
        { "Tag", "v7.0.0-rc.1.22426.7" },
        { "Version", new Version(7, 0, 0) },
        { "Prerelease", true },
        { "Hash", "dc0f3e8ef10eb1464b27f0fd4704f53c01226036" }
    });

然後執行查詢:

var builds = await context.BuildMetadata
    .Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
    .OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
    .ToListAsync();

現在,在 EF7 中,也有方法EntryDbSet可用來取得實例的狀態,即使尚未追蹤。 例如:

var state = context.BuildMetadata.Entry(build).State;

ContextInitialized 現在會記錄為 Debug

在 EF7 中 ContextInitialized ,事件會記錄在 Debug 層級。 例如:

dbug: 10/7/2022 12:27:52.379 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

在舊版中,它會記錄在 Information 層級。 例如:

info: 10/7/2022 12:30:34.757 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

如有需要,記錄層級可以變更回 Information

optionsBuilder.ConfigureWarnings(
    builder =>
    {
        builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
    });

IEntityEntryGraphIterator 可公開使用

在 EF7 中 IEntityEntryGraphIterator ,應用程式可以使用服務。 這是在探索要追蹤之實體圖表時,以及由的內部使用的服務 TrackGraph。 以下範例會逐一查看從某些起始實體觸達的所有實體:

var blogEntry = context.ChangeTracker.Entries<Blog>().First();
var found = new HashSet<object>();
var iterator = context.GetService<IEntityEntryGraphIterator>();
iterator.TraverseGraph(new EntityEntryGraphNode<HashSet<object>>(blogEntry, found, null, null), node =>
{
    if (node.NodeState.Contains(node.Entry.Entity))
    {
        return false;
    }

    Console.Write($"Found with '{node.Entry.Entity.GetType().Name}'");

    if (node.InboundNavigation != null)
    {
        Console.Write($" by traversing '{node.InboundNavigation.Name}' from '{node.SourceEntry!.Entity.GetType().Name}'");
    }

    Console.WriteLine();

    node.NodeState.Add(node.Entry.Entity);

    return true;
});

Console.WriteLine();
Console.WriteLine($"Finished iterating. Found {found.Count} entities.");
Console.WriteLine();

注意:

  • 當回呼委派傳 false回 時,反覆運算器會停止從指定的節點周遊。 此範例會追蹤已瀏覽的實體,並在實體已造訪時傳回 false 。 這可防止圖形中的迴圈產生無限迴圈。
  • 物件 EntityEntryGraphNode<TState> 可讓狀態四處傳遞,而不需要將它擷取到委派中。
  • 對於第一個以外的每個節點,它探索到的節點,以及它透過探索到的瀏覽,都會傳遞至回呼。

模型建置增強功能

EF7 包含模型建置中的各種小型改進。

提示

本節範例的程式代碼來自 ModelBuildingSample.cs

索引可以是遞增或遞減

根據預設,EF Core 會建立遞增索引。 EF7 也支援建立遞減索引。 例如:

modelBuilder
    .Entity<Post>()
    .HasIndex(post => post.Title)
    .IsDescending();

或者,使用 Index 對應屬性:

[Index(nameof(Title), AllDescending = true)]
public class Post
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Title { get; set; }
}

這在單一數據行上的索引很少有用,因為資料庫可以使用相同的索引來雙向排序。 不過,對於多個數據行的複合索引而言,情況並非如此,因為每個數據行上的順序可能很重要。 EF Core 支援這項功能,方法是允許多個數據行針對每個數據行定義不同的順序。 例如:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner })
    .IsDescending(false, true);

或者,使用對應屬性:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true })]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

這會在使用 SQL Server 時產生下列 SQL:

CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);

最後,您可以藉由提供索引名稱,透過相同的排序數據行集來建立多個索引。 例如:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_1")
    .IsDescending(false, true);

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_2")
    .IsDescending(true, true);

或者,使用對應屬性:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true }, Name = "IX_Blogs_Name_Owner_1")]
[Index(nameof(Name), nameof(Owner), IsDescending = new[] { true, true }, Name = "IX_Blogs_Name_Owner_2")]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

這會在 SQL Server 上產生下列 SQL:

CREATE INDEX [IX_Blogs_Name_Owner_1] ON [Blogs] ([Name], [Owner] DESC);
CREATE INDEX [IX_Blogs_Name_Owner_2] ON [Blogs] ([Name] DESC, [Owner] DESC);

複合索引鍵的對應屬性

EF7 引進了新的對應屬性(也稱為「數據批注」),以指定任何實體類型的主鍵屬性或屬性。 不同於 System.ComponentModel.DataAnnotations.KeyAttributePrimaryKeyAttribute 會放在實體類型類別上,而不是放在索引鍵屬性上。 例如:

[PrimaryKey(nameof(PostKey))]
public class Post
{
    public int PostKey { get; set; }
}

這使得它自然適合用來定義複合索引鍵:

[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
    public int PostId { get; set; }
    public int CommentId { get; set; }
    public string CommentText { get; set; } = null!;
}

在類別上定義索引也表示可用來將私用屬性或字段指定為索引鍵,即使建置 EF 模型時通常會忽略這些屬性。 例如:

[PrimaryKey(nameof(_id))]
public class Tag
{
    private readonly int _id;
}

DeleteBehavior mapping 屬性

EF7 引進對應屬性(也稱為「數據批注」),以指定 DeleteBehavior 關聯性的 。 例如,預設會使用 DeleteBehavior.Cascade 建立必要的關聯性。 這預設可以使用 變更為 DeleteBehavior.NoAction DeleteBehaviorAttribute

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [DeleteBehavior(DeleteBehavior.NoAction)]
    public Blog Blog { get; set; } = null!;
}

這會停用 Blog-Posts 關聯性的串聯刪除。

對應至不同數據行名稱的屬性

某些對應模式會導致相同的 CLR 屬性對應至每個不同數據表中的數據行。 EF7 可讓這些數據行有不同的名稱。 例如,請考慮簡單的繼承階層:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

使用 TPT 繼承對應策略,這些類型會對應至三個數據表。 不過,每個數據表中的主鍵數據行可能有不同的名稱。 例如:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Breed] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

EF7 允許使用巢狀數據表產生器來設定此對應:

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

透過 TPC 繼承對應, Breed 屬性也可以對應至不同資料表中的不同數據行名稱。 例如,請考慮下列 TPC 數據表:

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

EF7 支援此資料表對應:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });

單向多對多關聯性

EF7 支援 多對多關聯性 ,其中一端或另一端沒有導覽屬性。 例如,請考慮 PostTag 類型:

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
}
public class Tag
{
    public int Id { get; set; }
    public string TagName { get; set; } = null!;
}

請注意,此 Post 類型具有標籤清單的導覽屬性,但 Tag 該類型沒有貼文的導覽屬性。 在EF7中,這仍可設定為多對多關聯性,讓相同的 Tag 物件可用於許多不同的文章。 例如:

modelBuilder
    .Entity<Post>()
    .HasMany(post => post.Tags)
    .WithMany();

這會導致對應至適當的聯結數據表:

CREATE TABLE [Tags] (
    [Id] int NOT NULL IDENTITY,
    [TagName] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Tags] PRIMARY KEY ([Id])
);

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(64) NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id])
);

CREATE TABLE [PostTag] (
    [PostId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_PostId] FOREIGN KEY ([PostId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE
);

而關聯性可以用正常方式做為多對多。 例如,插入一些文章,從一般集合共用各種標籤:

var tags = new Tag[] { new() { TagName = "Tag1" }, new() { TagName = "Tag2" }, new() { TagName = "Tag2" }, };

await context.AddRangeAsync(new Blog { Posts =
{
    new Post { Tags = { tags[0], tags[1] } },
    new Post { Tags = { tags[1], tags[0], tags[2] } },
    new Post()
} });

await context.SaveChangesAsync();

實體分割

實體分割會將單一實體類型對應至多個數據表。 例如,請考慮具有三個保存客戶數據之數據表的資料庫:

  • Customers客戶信息的數據表
  • PhoneNumbers客戶的電話號碼數據表
  • Addresses客戶地址的數據表

以下是 SQL Server 中這些資料表的定義:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

每個數據表通常會對應到自己的實體類型,且類型之間具有關聯性。 不過,如果這三個數據表一律一起使用,則將其全部對應至單一實體類型會比較方便。 例如:

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

這會在 EF7 中呼叫 SplitToTable 實體類型中的每個分割來達成此目的。 例如,下列程式代碼會將 Customer 實體類型分割為如上所示的 CustomersPhoneNumbersAddresses 資料表:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

另請注意,如有必要,可以為每個數據表指定不同的主鍵數據行名稱。

SQL Server UTF-8 字串

以和 nvarchar 數據類型表示nchar的 SQL Server Unicode 字串會儲存為 UTF-16。 此外, charvarchar 數據類型 可用來儲存支援各種 字元集的非 Unicode 字串。

從 SQL Server 2019 開始, charvarchar 資料類型可用來改為使用 UTF-8 編碼來儲存 Unicode 字串。 藉由設定其中 一個 UTF-8 定序來達成 。 例如,下列程式代碼會設定 CommentText 資料行的可變長度 SQL Server UTF-8 字串:

modelBuilder
    .Entity<Comment>()
    .Property(comment => comment.CommentText)
    .HasColumnType("varchar(max)")
    .UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");

此組態會產生下列 SQL Server 資料行定義:

CREATE TABLE [Comment] (
    [PostId] int NOT NULL,
    [CommentId] int NOT NULL,
    [CommentText] varchar(max) COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8 NOT NULL,
    CONSTRAINT [PK_Comment] PRIMARY KEY ([PostId], [CommentId])
);

時態表支持擁有的實體

EF Core SQL Server 時態表 對應已在 EF7 中增強,以支援 數據表共用。 最值得注意的是,擁有之單一實體的預設對應會使用數據表共用。

例如,請考慮擁有者實體類型和 Employee 其擁有的實體類型 EmployeeInfo

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; } = null!;

    public EmployeeInfo Info { get; set; } = null!;
}

public class EmployeeInfo
{
    public string Position { get; set; } = null!;
    public string Department { get; set; } = null!;
    public string? Address { get; set; }
    public decimal? AnnualSalary { get; set; }
}

如果這些類型對應至相同的數據表,則 EF7 中的數據表可以建立時態表:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        tableBuilder =>
        {
            tableBuilder.IsTemporal();
            tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
            tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
        })
    .OwnsOne(
        employee => employee.Info,
        ownedBuilder => ownedBuilder.ToTable(
            "Employees",
            tableBuilder =>
            {
                tableBuilder.IsTemporal();
                tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
                tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
            }));

注意

問題 #29303 可更輕鬆地追蹤此設定。 如果這是您想要看到已實作的內容,請投票處理此問題。

改善的值產生

EF7 包含兩項大幅改善,可自動產生索引鍵屬性的值。

提示

本節中的範例程式代碼來自 ValueGenerationSample.cs

DDD 受防護類型的值產生

在領域驅動設計中,「受防護密鑰」可以改善金鑰屬性的類型安全性。 這可藉由將金鑰類型包裝在另一個類型中,這是使用密鑰的特定類型。 例如,下列程式代碼會 ProductId 定義產品密鑰的類型,以及 CategoryId 類別索引鍵的類型。

public readonly struct ProductId
{
    public ProductId(int value) => Value = value;
    public int Value { get; }
}

public readonly struct CategoryId
{
    public CategoryId(int value) => Value = value;
    public int Value { get; }
}

這些會接著用於 ProductCategory 實體類型:

public class Product
{
    public Product(string name) => Name = name;
    public ProductId Id { get; set; }
    public string Name { get; set; }
    public CategoryId CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

public class Category
{
    public Category(string name) => Name = name;
    public CategoryId Id { get; set; }
    public string Name { get; set; }
    public List<Product> Products { get; } = new();
}

這無法不小心將類別的標識符指派給產品,反之亦然。

警告

和許多 DDD 概念一樣,這種改良的類型安全性會犧牲額外的程式代碼複雜度。 例如,將產品標識碼指派給類別是否可能發生,值得考慮。 將專案保持簡單可能更有利於程式代碼基底。

此處顯示的受防護索引鍵類型會包裝 int 索引鍵值,這表示將用於對應的資料庫數據表中的整數值。 這可藉由定義 類型的值轉換器 來達成:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<ProductId>().HaveConversion<ProductIdConverter>();
    configurationBuilder.Properties<CategoryId>().HaveConversion<CategoryIdConverter>();
}

private class ProductIdConverter : ValueConverter<ProductId, int>
{
    public ProductIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

private class CategoryIdConverter : ValueConverter<CategoryId, int>
{
    public CategoryIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

注意

這裡的程式代碼會使用 struct 類型。 這表示它們具有適當的實值類型語意,可用來作為索引鍵。 如果 class 改用類型,則必須覆寫相等語意,或同時指定 值比較子

在EF7中,根據值轉換器的索引鍵類型可以使用自動產生的索引鍵值,只要基礎類型支援這個值即可。 這是使用 ValueGeneratedOnAdd的一般方式設定:

modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();

根據預設,當與 SQL Server 搭配使用時,這會產生 IDENTITY 數據行:

CREATE TABLE [Categories] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Categories] PRIMARY KEY ([Id]));

CREATE TABLE [Products] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

在插入實體時,會以一般方式使用它們來產生索引鍵值:

MERGE [Categories] USING (
VALUES (@p0, 0),
(@p1, 1)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;

SQL Server 的時序式金鑰產生

EF Core 支援使用 SQL Server IDENTITY 數據行或 Hi-Lo 模式,根據資料庫順序所產生的索引鍵區塊來產生索引鍵值。 EF7 引進附加至索引鍵數據行默認條件約束的資料庫順序支援。 以最簡單的形式,這只需要告訴 EF Core 使用索引鍵屬性的序列:

modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();

這會導致資料庫中定義的序列:

CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;

然後在索引鍵資料行預設條件約束中使用:

CREATE TABLE [Products] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [ProductSequence]),
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

注意

此形式的金鑰產生預設會用於使用 TPC 對應策略之實體類型階層中產生的索引鍵。

如有需要,序列可以指定不同的名稱和架構。 例如:

modelBuilder
    .Entity<Product>()
    .Property(product => product.Id)
    .UseSequence("ProductsSequence", "northwind");

序列的進一步設定是藉由在模型中明確設定來形成。 例如:

modelBuilder
    .HasSequence<int>("ProductsSequence", "northwind")
    .StartsAt(1000)
    .IncrementsBy(2);

移轉工具改善

EF7 在使用 EF Core 移轉命令行工具,包含兩項重大改善。

UseSqlServer etc. accept null

從組態檔讀取 連接字串 很常見,然後將該 連接字串 傳遞至 UseSqlServerUseSqlite,或另一個提供者的對等方法。 例如:

services.AddDbContext<BloggingContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));

套用移轉時,通常也會傳遞 連接字串。 例如:

dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

或者,使用 移轉套件組合時。

./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

在這裡情況下,即使未使用 連接字串 從組態讀取,應用程式啟動程式代碼仍會嘗試從組態讀取它,並將它傳遞至 UseSqlServer。 如果群組態無法使用,則這會導致將 Null 傳遞至 UseSqlServer。 在EF7中,只要稍後設定 連接字串,例如藉由傳遞--connection至命令行工具,就允許這樣做。

注意

已針對 UseSqlServerUseSqlite進行這項變更。 針對其他提供者,如果提供者尚未針對該提供者進行對等變更,請連絡提供者維護人員進行對等的變更。

偵測工具執行時機

使用 或 PowerShell 命令時,dotnet-efEF Core 會執行應用程式程式代碼。 有時可能需要偵測這種情況,以防止在設計時間執行不適當的程序代碼。 例如,在啟動時自動套用移轉的程式代碼應該不會在設計時間執行此動作。 在 EF7 中,您可以使用 旗標偵測 EF.IsDesignTime 到:

if (!EF.IsDesignTime)
{
    await context.Database.MigrateAsync();
}

EF Core 會在應用程式程式碼代表工具執行時,將設定IsDesignTimetrue為 。

Proxy 的效能增強功能

EF Core 支援動態產生的 Proxy 來 延遲載入變更追蹤。 使用這些 Proxy 時,EF7 包含兩項效能改善:

  • 現在會延遲建立 Proxy 類型。 這表示使用 Proxy 時的初始模型建置時間可能比 EF Core 6.0 快得多。
  • Proxy 現在可以搭配已編譯的模型使用。

以下是具有 449 個實體類型、6390 屬性和 720 關聯性的模型效能結果。

案例 方法 平均數 錯誤 StdDev
不含 Proxy 的 EF Core 6.0 TimeToFirstQuery 1.085 秒 0.0083 秒 0.0167 秒
使用變更追蹤 Proxy 的 EF Core 6.0 TimeToFirstQuery 13.01 秒 0.2040 秒 0.4110 秒
沒有 Proxy 的 EF Core 7.0 TimeToFirstQuery 1.442 秒 0.0134 s 0.0272 秒
使用變更追蹤 Proxy 的 EF Core 7.0 TimeToFirstQuery 1.446 秒 0.0160 秒 0.0323 秒
EF Core 7.0 搭配變更追蹤 Proxy 和已編譯的模型 TimeToFirstQuery 0.162 秒 0.0062 秒 0.0125 秒

因此,在此情況下,具有變更追蹤 Proxy 的模型可以準備好在 EF7 中執行第一個查詢,速度比 EF Core 6.0 快 80 倍。

一流的 Windows Forms 數據系結

Windows Forms 小組已大幅 改善 Visual Studio Designer 體驗。 這包括 與 EF Core 整合之數據系結 的新體驗。

簡單來說,新的體驗提供 Visual Studio U.I. 來建立 ObjectDataSource

選擇類別數據源類型

然後,您可以使用一些簡單的程式代碼系結至 EF Core DbSet

public partial class MainForm : Form
{
    private ProductsContext? dbContext;

    public MainForm()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        this.dbContext = new ProductsContext();

        this.dbContext.Categories.Load();
        this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        base.OnClosing(e);

        this.dbContext?.Dispose();
        this.dbContext = null;
    }
}

如需完整的逐步解說和可下載的 WinForms 範例應用程式,請參閱開始使用 Windows Forms