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_電話
1 麥迪·蒙塔奎拉 1 主街 坎伯威克·格林 CW1 5ZH 英國 01632 12345
2 Jeremy Likness 2 主街 奇格利 CW1 5ZH 英國 01632 12346
3 丹尼爾·羅斯 3 主街 坎伯威克·格林 CW1 5ZH 英國 01632 12347
4 亞瑟·維克斯 15a Main St 奇格利 CW1 5ZH 英國 01632 22345
5 布裡斯·蘭布森 4 主街 奇格利 CW1 5ZH 英國 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 英國
2 2 主街 奇格利 CW1 5ZH 英國
3 3 主街 坎伯威克·格林 CW1 5ZH 英國
4 15a Main St 奇格利 CW1 5ZH 英國
5 4 主街 奇格利 CW1 5ZH 英國

現在,對於有趣的部分。 在 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 麥迪·蒙塔奎拉 {
  「電話」:「01632 12345」,
  「Address」: {
    「City」:「Camberwick Green」,
    「Country」:「UK」,
    「Postcode」:「CW1 5ZH」,
    「Street」:「1 Main St」
  }
}
2 Jeremy Likness {
  「電話」:「01632 12346」,
  「Address」: {
    「City」:「Chigley」,
    「Country」:「UK」,
    「Postcode」:「CH1 5ZH」,
    「Street」:「2 Main St」
  }
}
3 丹尼爾·羅斯 {
  「電話」:「01632 12347」,
  「Address」: {
    「City」:「Camberwick Green」,
    「Country」:「UK」,
    「Postcode」:「CW1 5ZH」,
    「Street」:「3 Main St」
  }
}
4 亞瑟·維克斯 {
  「電話」:「01632 12348」,
  「Address」: {
    「City」:「Chigley」,
    「Country」:「UK」,
    「Postcode」:「CH1 5ZH」,
    「Street」:「15a Main St」
  }
}
5 布裡斯·蘭布森 {
  「電話」:「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 檔內的 取得 CityAddress

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):DELETE 子句與 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 每個未使用屬性的 。 在實務上,這很少是個問題,但在儲存具有這些特性的大量資料時,可能值得考慮。

提示

如果您的資料庫系統支援它(例如 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 上觀看 T4 範本劇集。

模型建置慣例

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

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

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

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

提示

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

提示

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

移除現有的慣例

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

範例:不要為外鍵資料行建立索引

建立外鍵 (FK) 資料行的索引通常很合理,因此有內建慣例: ForeignKeyIndexConvention 。 查看與 和 關聯性 Blog 之實體類型的模型 偵錯檢視 ,我們可以看到兩個 Post 索引已建立-一個用於 FK,另一個則用於 AuthorIdBlogId 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 會進行對應,但由於它沒有標示 [Perist] (大概因為清潔不是洗衣的持續性屬性),它現在被視為未對應的屬性。

提示

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

預存程序對應

根據預設,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」 的資料表名稱。 因此,在上述範例中,由於資料表稱為「人員」,因此可以移除預存程式名稱,而不需要變更功能。

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

參數

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

注意

參數可以命名,但 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 正在執行的動作,是攔截器最強大的功能之一。

延遲初始化連接字串

提示

此處顯示的程式碼來自 Lazy連線ionStringSample.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 包含 的 DiagnosticSuppressor DbSetDbContext 其會阻止編譯器產生此警告。

提示

此模式源自 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.NoActionDeleteBehaviorAttribute

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-ef EF 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

Choose Category data source type

然後,您可以使用一些簡單的程式碼系結至 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