EF Core Azure Cosmos DB 提供者

此資料庫提供者可讓 Entity Framework Core 與 Azure Cosmos DB 搭配使用。 Entity Framework Core 專案的維護包含此提供者。

強烈建議您在閱讀本節之前先熟悉 Azure Cosmos DB 文件

注意

此提供者僅適用于適用于 NoSQL 的 Azure Cosmos DB。

安裝

安裝 Microsoft.EntityFrameworkCore.Cosmos NuGet 套件

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

開始使用

提示

您可以檢視本文中的 GitHut 範例

至於其他提供者,第一步是呼叫 UseCosmos

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OrdersDB");

警告

為簡便起見,此處將端點和索引鍵進行了硬式編碼,但這些內容在生產應用程式中應予以安全儲存

在此範例中,Order 是簡單實體,其參考了擁有的類型StreetAddress

public class Order
{
    public int Id { get; set; }
    public int? TrackingNumber { get; set; }
    public string PartitionKey { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}

遵循一般的 EF 模式儲存及查詢資料:

using (var context = new OrderContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.Add(
        new Order
        {
            Id = 1, ShippingAddress = new StreetAddress { City = "London", Street = "221 B Baker St" }, PartitionKey = "1"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.FirstAsync();
    Console.WriteLine($"First order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

重要

如果要建立需要的容器或插入種子資料 (如果存在於模型中),就必須呼叫 EnsureCreatedAsync。 但是,EnsureCreatedAsync 僅應在部署期間呼叫,在一般作業期間呼叫可能會導致效能問題。

Azure Cosmos DB 選項

您也可以使用單一連接字串設定 Azure Cosmos DB 提供者,並指定其他選項來自訂連線:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OptionsDB",
        options =>
        {
            options.ConnectionMode(ConnectionMode.Gateway);
            options.WebProxy(new WebProxy());
            options.LimitToEndpoint();
            options.Region(Regions.AustraliaCentral);
            options.GatewayModeMaxConnectionLimit(32);
            options.MaxRequestsPerTcpConnection(8);
            options.MaxTcpConnectionsPerEndpoint(16);
            options.IdleTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.OpenTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.RequestTimeout(TimeSpan.FromMinutes(1));
        });

提示

如需上述每個選項效果的詳細描述,請參閱 Azure Cosmos DB 選項文件

Cosmos 專用模型自訂

根據預設,所有實體類型都會對應至相同容器,並以衍生的內容命名 (在此案例中為 "OrderContext")。 若要變更預設容器名稱,請使用 HasDefaultContainer

modelBuilder.HasDefaultContainer("Store");

若要將實體類型對應至不同的容器,請使用 ToContainer

modelBuilder.Entity<Order>()
    .ToContainer("Orders");

為了識別其指定項目代表 EF Core 的實體類型,即使沒有衍生的實體類型,也會新增鑑別子值。 鑑別子的名稱和值可以變更

如果不會在同一個容器中儲存其他實體類型,就可以呼叫 HasNoDiscriminator 將鑑別子移除:

modelBuilder.Entity<Order>()
    .HasNoDiscriminator();

分割區索引鍵

根據預設,EF Core 會建立容器,並將分割區索引鍵設定為 , "__partitionKey" 而不會在插入專案時提供任何值。 但是,若要充分利用 Azure Cosmos DB 的效能功能, 應該使用謹慎選取的資料分割索引鍵 。 您可以透過呼叫 HasPartitionKey 來設定分割區索引鍵:

modelBuilder.Entity<Order>()
    .HasPartitionKey(o => o.PartitionKey);

注意

只要分割區索引鍵屬性會轉換成字串,就不限於任何類型。

分割區索引鍵屬性設定完成後,應該都會具有 null 以外的值。 新增 WithPartitionKey 呼叫可使查詢成為單一分割區。

using (var context = new OrderContext())
{
    context.Add(
        new Order
        {
            Id = 2, ShippingAddress = new StreetAddress { City = "New York", Street = "11 Wall Street" }, PartitionKey = "2"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.WithPartitionKey("2").LastAsync();
    Console.WriteLine($"Last order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

通常建議將分割區索引鍵新增至主索引鍵,因為這樣最能反映伺服器語意,並能最佳化某些項目,例如 FindAsync

佈建的輸送量

如果您使用 EF Core 來建立 Azure Cosmos DB 資料庫或容器,您可以 藉由呼叫 CosmosModelBuilderExtensions.HasAutoscaleThroughputCosmosModelBuilderExtensions.HasManualThroughput 來設定資料庫的布建輸送量 。 例如:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

呼叫 CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughputCosmosEntityTypeBuilderExtensions.HasManualThroughput 可設定容器的佈建輸送量。 例如:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

內嵌實體

注意

相關實體類型預設會設定為擁有。 若要防止特定實體類型,請呼叫 ModelBuilder.Entity

針對 Azure Cosmos DB,擁有的實體會內嵌在與擁有者相同的專案中。 若要變更屬性名稱,請使用 ToJsonProperty

modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.ToJsonProperty("Address");
        sa.Property(p => p.Street).ToJsonProperty("ShipsToStreet");
        sa.Property(p => p.City).ToJsonProperty("ShipsToCity");
    });

使用此組態,來自以上範例的順序會以如下方式儲存:

{
    "Id": 1,
    "PartitionKey": "1",
    "TrackingNumber": null,
    "id": "1",
    "Address": {
        "ShipsToCity": "London",
        "ShipsToStreet": "221 B Baker St"
    },
    "_rid": "6QEKAM+BOOABAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKAM+BOOA=/docs/6QEKAM+BOOABAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-692e763901d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163674
}

同時也會內嵌自有實體的集合。 在下一個範例中,我們將使用 Distributor 類別和 StreetAddress 的集合:

public class Distributor
{
    public int Id { get; set; }
    public string ETag { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

自有實體不需提供明確索引鍵值來儲存:

var distributor = new Distributor
{
    Id = 1,
    ShippingCenters = new HashSet<StreetAddress>
    {
        new StreetAddress { City = "Phoenix", Street = "500 S 48th Street" },
        new StreetAddress { City = "Anaheim", Street = "5650 Dolly Ave" }
    }
};

using (var context = new OrderContext())
{
    context.Add(distributor);

    await context.SaveChangesAsync();
}

它們會以如下方式保存:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        },
        {
            "City": "Anaheim",
            "Street": "5650 Dolly Ave"
        }
    ],
    "_rid": "6QEKANzISj0BAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKANzISj0=/docs/6QEKANzISj0BAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-7b2b439701d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163705
}

在內部,EF Core 一律需要具有所有追蹤實體的唯一索引鍵值。 根據預設,為擁有類型的集合所建立主索引鍵,由指向擁有者的外部索引鍵屬性和與 JSON 陣列中索引對應的 int 屬性組成。 若要擷取這些值,可以使用項目 API:

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers: {firstDistributor.ShippingCenters.Count}");

    var addressEntry = context.Entry(firstDistributor.ShippingCenters.First());
    var addressPKProperties = addressEntry.Metadata.FindPrimaryKey().Properties;

    Console.WriteLine(
        $"First shipping center PK: ({addressEntry.Property(addressPKProperties[0].Name).CurrentValue}, {addressEntry.Property(addressPKProperties[1].Name).CurrentValue})");
    Console.WriteLine();
}

提示

必要時,您可以變更自有實體類型的預設主索引鍵,但應明確提供索引鍵值。

基本類型集合

系統會自動探索及對應支援的基本類型集合,例如 stringint。 所有可實作 IReadOnlyList<T>IReadOnlyDictionary<TKey,TValue> 的類型都是支援的集合。 例如,請考慮以下的實體類型:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

清單和字典都可使用一般方式填入並插入資料庫:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
context.SaveChanges();

這會產生下列 JSON 文件:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

然後,您可以再次使用一般方式更新這些集合:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

context.SaveChanges();

限制:

  • 僅支援包含字串索引鍵的字典
  • 目前不支援查詢基本集合的內容。 如果這些功能對您很重要,請投票支持 #16926#25700#25701

使用已中斷連線的實體

每個項目都必須具有 id 值,該值對於特定分割區索引鍵是唯一的。 根據預設,EF Core 會使用 “|” 作為分隔符號,將鑑別子和主索引鍵值相結合以產生值。 僅當實體進入 Added 狀態時才會產生索引鍵值。 在連結實體時,如果它們在 .NET 類型上不具有 id 屬性來儲存值,則可能會發生問題。

若要解決此限制,您可以手動建立並設定 id 值,或先將實體標記為已新增,然後將其變更為所需狀態:

using (var context = new OrderContext())
{
    var distributorEntry = context.Add(distributor);
    distributorEntry.State = EntityState.Unchanged;

    distributor.ShippingCenters.Remove(distributor.ShippingCenters.Last());

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers is now: {firstDistributor.ShippingCenters.Count}");

    var distributorEntry = context.Entry(firstDistributor);
    var idProperty = distributorEntry.Property<string>("__id");
    Console.WriteLine($"The distributor 'id' is: {idProperty.CurrentValue}");
}

以下是輸出 JSON:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        }
    ],
    "_rid": "JBwtAN8oNYEBAAAAAAAAAA==",
    "_self": "dbs/JBwtAA==/colls/JBwtAN8oNYE=/docs/JBwtAN8oNYEBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9377-d7a1ae7c01d5\"",
    "_attachments": "attachments/",
    "_ts": 1572917100
}

使用 ETag 的開放式並行存取

請呼叫 UseETagConcurrency,以將實體類型設定為使用開放式並行存取。 此呼叫會建立陰影狀態_etag 屬性,並將其設定為並行權杖。

modelBuilder.Entity<Order>()
    .UseETagConcurrency();

若要更輕鬆解決並行錯誤,您可以使用 IsETagConcurrency 將 ETag 對應至 CLR 屬性。

modelBuilder.Entity<Distributor>()
    .Property(d => d.ETag)
    .IsETagConcurrency();