Penyedia Azure Cosmos DB EF Core

Penyedia database ini memungkinkan Entity Framework Core digunakan dengan Azure Cosmos DB. Penyedia dipertahankan sebagai bagian dari Proyek Entity Framework Core.

Sangat disarankan untuk membiasakan diri Anda dengan dokumentasi Azure Cosmos DB sebelum membaca bagian ini.

Catatan

Penyedia ini hanya berfungsi dengan Azure Cosmos DB untuk NoSQL.

Instal

Instal paket Microsoft.EntityFrameworkCore.Cosmos NuGet.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Mulai

Tip

Anda dapat melihat contoh artikel ini di GitHub.

Untuk penyedia lain, langkah pertama adalah memanggil UseCosmos:

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

Peringatan

Titik akhir dan kunci di sini di-hardcode demi kemudahan, tetapi dalam aplikasi produksi ini harus disimpan dengan aman.

Dalam contoh ini, Order adalah entitas sederhana dengan referensi ke jenis yang dimilikiStreetAddress.

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; }
}

Menyimpan dan mengkueri data mengikuti pola EF konvensional:

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();
}

Penting

Memanggil EnsureCreatedAsync diperlukan untuk membuat kontainer yang diperlukan dan memasukkan seed data jika ada dalam model. Namun EnsureCreatedAsync hanya boleh dipanggil selama penyebaran, bukan operasi normal, karena dapat menyebabkan masalah performa.

Opsi Azure Cosmos DB

Dimungkinkan juga untuk mengonfigurasi penyedia Azure Cosmos DB dengan satu string koneksi dan menentukan opsi lain untuk menyesuaikan koneksi:

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));
        });

Tip

Lihat dokumentasi Opsi Azure Cosmos DB untuk deskripsi mendetail tentang efek dari setiap opsi yang disebutkan di atas.

Kustomisasi model khusus Cosmos

Secara default, semua jenis entitas dipetakan ke kontainer yang sama, diberi nama sesuai dengan konteks turunan ("OrderContext" dalam hal ini). Untuk mengubah nama kontainer default, gunakan HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

Untuk memetakan jenis entitas ke kontainer lain, gunakan ToContainer:

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

Untuk mengidentifikasi jenis entitas yang direpresentasikan oleh item tertentu, EF Core menambahkan nilai diskriminator meskipun tidak ada jenis entitas turunan. Nama dan nilai diskriminator dapat berubah.

Jika tidak ada jenis entitas lain yang akan disimpan dalam kontainer yang sama, diskriminator dapat dihapus dengan memanggil HasNoDiscriminator:

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

Kunci partisi

Secara default, EF Core akan membuat kontainer dengan kunci partisi diatur ke "__partitionKey" tanpa menyediakan nilai apa pun untuknya saat menyisipkan item. Tetapi untuk sepenuhnya memanfaatkan kemampuan performa Azure Cosmos DB, kunci partisi yang dipilih dengan hati-hati harus digunakan. Ini dapat dikonfigurasi dengan memanggil HasPartitionKey:

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

Catatan

Properti kunci partisi dapat berupa jenis apa pun asalkan dikonversi menjadi string.

Setelah dikonfigurasi, properti kunci partisi harus selalu memiliki nilai non-null. Kueri dapat dibuat sebagai partisi tunggal dengan menambahkan panggilan 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();
}

Biasanya penambahan kunci partisi ke kunci utama disarankan karena yang paling mencerminkan semantik server dan memungkinkan beberapa pengoptimalan, misalnya di FindAsync.

Throughput yang disediakan

Jika Anda menggunakan EF Core untuk membuat database atau kontainer Azure Cosmos DB, Anda dapat mengonfigurasi throughput yang disediakan untuk database dengan memanggil CosmosModelBuilderExtensions.HasAutoscaleThroughput atau CosmosModelBuilderExtensions.HasManualThroughput. Contohnya:

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

Untuk mengonfigurasi throughput yang ditentukan untuk panggilan kontainer CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput atau CosmosEntityTypeBuilderExtensions.HasManualThroughput. Contohnya:

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

Entitas yang disematkan

Catatan

Jenis entitas terkait dikonfigurasi sebagai milik secara default. Untuk mencegah hal ini terhadap jenis entitas tertentu, panggil ModelBuilder.Entity.

Untuk Azure Cosmos DB, entitas yang dimiliki disematkan dalam item yang sama dengan pemilik. Untuk mengubah nama properti, gunakan 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");
    });

Dengan konfigurasi ini, urutan dari contoh di atas disimpan seperti ini:

{
    "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
}

Koleksi entitas yang dimiliki juga disematkan. Untuk contoh berikutnya, kita akan menggunakan kelas Distributor dengan koleksi StreetAddress:

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

Entitas yang dimiliki tidak perlu memberikan nilai kunci eksplisit yang akan disimpan:

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();
}

Nilail tersebut akan disimpan dengan cara ini:

{
    "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
}

Secara internal, EF Core selalu perlu memiliki nilai kunci unik untuk semua entitas yang dilacak. Kunci primer yang dibuat secara default untuk koleksi jenis yang dimiliki terdiri dari properti kunci asing yang menunjuk ke pemilik dan properti int yang sesuai dengan indeks dalam array JSON. Untuk mengambil nilai ini, entri API dapat digunakan:

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();
}

Tip

Jika perlu, kunci primer default untuk jenis entitas yang dimiliki dapat diubah, tetapi nilai kunci harus diberikan secara eksplisit.

Koleksi jenis primitif

Kumpulan jenis primitif yang didukung, seperti string dan int, ditemukan dan dipetakan secara otomatis. Koleksi yang didukung adalah semua jenis yang mengimplementasikan IReadOnlyList<T> atau IReadOnlyDictionary<TKey,TValue>. Misalnya, pertimbangkan jenis entitas ini:

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; }
}

Daftar dan kamus dapat diisi dan disisipkan ke dalam database dengan cara konvensional:

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();

Ini menghasilkan dokumen JSON berikut:

{
    "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
}

Koleksi ini kemudian dapat diperbarui, sekali lagi dengan cara konvensional:

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

context.SaveChanges();

Keterbatasan:

  • Hanya kamus dengan kunci string yang akan didukung
  • Mengkueri konten koleksi primitif saat ini tidak didukung. Pilih #16926, #25700, dan #25701 jika fitur ini penting bagi Anda.

Bekerja dengan entitas yang terputus

Setiap item harus memiliki nilai id yang unik untuk kunci partisi yang ditentukan. Secara default, EF Core menghasilkan nilai dengan menggabungkan diskriminator dan nilai kunci primer, menggunakan '|' sebagai pemisah. Nilai kunci hanya dihasilkan saat entitas memasuki status Added. Pelampiran entitas akan bermasalah jika properti id pada jenis .NET untuk menyimpan nilai tidak dimiliki.

Untuk mengatasi batasan ini, Anda dapat membuat dan menetapkan nilai id secara manual atau menandai entitas sebagai ditambahkan terlebih dahulu, lalu mengubahnya ke status yang diinginkan:

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}");
}

Ini adalah JSON yang dihasilkan:

{
    "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
}

Konkurensi optimis dengan eTags

Untuk mengonfigurasi jenis entitas agar menggunakan konkurensi optimis, panggil UseETagConcurrency. Panggilan ini akan membuat properti _etag dalam status bayangan dan menetapkannya sebagai token konkurensi.

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

Untuk mempermudah mengatasi kesalahan konkurensi, Anda dapat memetakan eTag ke properti CLR menggunakan IsETagConcurrency.

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