EF Core Azure Cosmos DB プロバイダー

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

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

注意

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

インストール

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 は、OrderStreetAddressへの参照を持つ単純なエンティティです。

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 は、パフォーマンスの問題を引き起こす可能性があるため、通常の操作ではなく、配置時にのみ呼び出す必要があります。

Cosmos のオプション

1 つの接続文字列を使用して 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));
        });

注意

これらのオプションのほとんどは EF Core 5.0 で導入されました。

ヒント

前述の各オプションの効果の詳細については、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 のパフォーマンス機能を十分に活用するには、慎重に選んだパーティション キーを使用する必要があります。 これを構成するには、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();
}

注意

WithPartitionKey は、5.0 EF Core で導入されました。

通常、プライマリ キーにパーティション キーを追加することが推奨されます。それにより、サーバーのセマンティクスが最もよく反映され、FindAsync などで、いくつかの最適化が可能になるためです。

埋め込みエンティティ

Cosmos の場合、所有エンティティは所有者と同じアイテムに埋め込まれます。 プロパティ名を変更するには、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();
}

ヒント

必要に応じて、所有されているエンティティ型の既定の主キーは変更できますが、その場合、キー値は明示的に指定する必要があります。

接続解除エンティティの使用

すべてのアイテムには、特定のパーティション キーに対して一意な id 値が必要です。 既定では、EF Core は、' | ' を区切り記号として使用して、識別子と主キーの値を連結して値を生成します。 このキー値は、エンティティが Added 状態になったときにのみ生成されます。 これは、.NET 型にその値を保存する プロパティがない場合にエンティティをアタッチするときに問題になる場合があります。

この制限を回避するには、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 を使用したオプティミスティック同時実行制御

注意

eTag 同時実行制御のサポートが EF Core 5.0 で導入されました。

オプティミスティック同時実行制御を使用するようにエンティティ型を構成するには、UseETagConcurrency を呼び出します。 この呼び出しによって、_etag_etag プロパティが作成され、同時実行制御トークンとして設定されます。

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

同時実行制御エラーを簡単に解決できるようにするには、IsETagConcurrency を使用して、eTag を CLR プロパティにマップします。

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