EF Core Azure Cosmos DB プロバイダー

このデータベース プロバイダーにより、Azure Cosmos DB と共に Entity Framework Core を使用できます。 このプロバイダーは、Entity Framework Core プロジェクトの一部として保守管理されています。

このセクションを読む前に、Azure Cosmos DB のドキュメントを理解することを強くお勧めします。

注意

このプロバイダーは、Azure Cosmos DB for NoSQL でのみ機能します。

インストール

Microsoft.EntityFrameworkCore.Cosmos NuGet パッケージをインストールします。

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

はじめに

ヒント

この記事のサンプルは GitHub で確認できます。

他のプロバイダーと同様に、最初の手順は 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 のオプション

1 つの接続文字列で 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.HasAutoscaleThroughput または CosmosModelBuilderExtensions.HasManualThroughput を呼び出して、データベース用にプロビジョニングされたスループットを構成できます。 次に例を示します。

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

コンテナー用にプロビジョニングされたスループットを構成するには、CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput または CosmosEntityTypeBuilderExtensions.HasManualThroughput を呼び出します。 次に例を示します。

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

埋め込みエンティティ

Note

関連するエンティティ型が既定で所有されるように構成されています。 特定のエンティティ型に対してこれを防ぐには、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();