Sdílet prostřednictvím


Poskytovatel Azure Cosmos DB pro EF Core

Tento poskytovatel databáze umožňuje použít Entity Framework Core s Azure Cosmos DB. Tento poskytovatel je součástí projektu Entity Framework Core.

Před přečtením této části důrazně doporučujeme seznámit se s dokumentací ke službě Azure Cosmos DB.

Poznámka

Tento poskytovatel funguje jenom se službou Azure Cosmos DB for NoSQL.

Instalace

Nainstalujte si balíček Microsoft.EntityFrameworkCore.Cosmos NuGet.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Začínáme

Tip

Ukázku pro tento článek najdete na GitHubu.

Stejně jako u jiných poskytovatelů je prvním krokem volání UseCosmos:

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

Upozorňující

Koncový bod a klíč jsou zde kvůli jednoduchosti pevně zakódovány, ale v produkční aplikaci by měly být bezpečně uloženy.

V tomto příkladu je Order jednoduchá entita s odkazem na vlastněný typStreetAddress.

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

Ukládání a dotazování dat se řídí běžným vzorem 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();
}

Důležité

Volání EnsureCreatedAsync je nezbytné k vytvoření požadovaných kontejnerů a k vložení počátečních dat, pokud se v modelu nacházejí. EnsureCreatedAsync by se však mělo volat jen během nasazování a nikoli při běžném provozu, protože může způsobit problémy s výkonem.

Možnosti služby Azure Cosmos DB

Je také možné nakonfigurovat poskytovatele služby Azure Cosmos DB s jedním připojovací řetězec a zadat další možnosti pro přizpůsobení připojení:

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

Podrobný popis účinku jednotlivých výše uvedených možností najdete v dokumentaci k možnostem Azure Cosmos DB.

Přizpůsobení modelu specifická pro Cosmos

Standardně jsou všechny typy entit namapovány na stejný kontejner pojmenovaný po odvozeném kontextu (v tomto případě "OrderContext"). Pokud chcete změnit výchozí název kontejneru, použijte HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

K namapování typu entity na jiný kontejner použijte ToContainer:

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

Kvůli identifikaci typu entity, kterou daná položka reprezentuje, přidává EF Core hodnotu diskriminátoru, i když neexistují žádné odvozené typy entit. Název a hodnotu tohoto diskriminátoru lze změnit.

Pokud ve stejném kontejneru nebude nikdy uložen žádný jiný typ entity, může být diskriminátor odebrán voláním HasNo Discriminatoror:

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

Klíče oddílu

Ve výchozím nastavení EF Core vytvoří kontejnery s klíčem oddílu nastaveným na "__partitionKey" bez zadání jakékoli hodnoty pro ni při vkládání položek. Pokud ale chcete plně využít možnosti výkonu služby Azure Cosmos DB, měli byste použít pečlivě vybraný klíč oddílu. Lze ho nakonfigurovat voláním HasPartitionKey:

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

Poznámka

Vlastnost klíče oddílu může být libovolného typu, pokud je převedena na řetězec.

Po nakonfigurování by vlastnost klíče oddílu měla mít vždy jinou hodnotu než null. Přidáním volání WithPartitionKey lze dotaz vytvořit s jedním oddílem.

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

Obecně se doporučuje přidat klíč oddílu do primárního klíče, protože to nejlépe odráží sémantiku serveru a umožňuje některé optimalizace, například u FindAsync.

Zřízená propustnost

Pokud k vytvoření databáze nebo kontejnerů Azure Cosmos DB použijete EF Core, můžete nakonfigurovat zřízenou propustnost pro databázi voláním CosmosModelBuilderExtensions.HasAutoscaleThroughput nebo CosmosModelBuilderExtensions.HasManualThroughput. Příklad:

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

Pokud chcete pro kontejner nakonfigurovat zřízenou propustnost, volejte CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput nebo CosmosEntityTypeBuilderExtensions.HasManualThroughput. Příklad:

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

Vložené entity

Poznámka

Související typy entit jsou ve výchozím nastavení nakonfigurované jako vlastněné. Voláním ModelBuilder.Entity tomu můžete u konkrétního typu entity zabránit.

Ve službě Azure Cosmos DB jsou vlastněné entity vložené do stejné položky jako vlastník. Ke změně názvu vlastnosti použijte 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");
    });

Při této konfiguraci se objednávka z předchozího příkladu uloží takto:

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

Kolekce vlastněných entit jsou také vložené. V dalším příkladu použijeme třídu Distributor s kolekcí StreetAddress:

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

Vlastněné entity nemusí poskytovat explicitní hodnoty klíče, které se mají uložit:

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

Budou zachovány takto:

{
    "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 musí mít interně pro všechny sledované entity vždy jedinečné klíčové hodnoty. Primární klíč standardně vytvořený pro kolekce vlastněných typů sestává z vlastností cizího klíče odkazujících na vlastníka a z vlastnosti int odpovídající indexu v poli JSON. K načtení položky s těmito hodnotami lze použít rozhraní 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();
}

Tip

V případě potřeby lze výchozí primární klíč pro typy vlastněných entit změnit, ale hodnoty klíčů by měly být zadány explicitně.

Kolekce primitivních typů

Kolekce podporovaných primitivních typů, například string a int, jsou zjišťovány a mapovány automaticky. Podporované kolekce jsou všechny typy, které implementují IReadOnlyList<T> nebo IReadOnlyDictionary<TKey,TValue>. Představte si například tento typ entity:

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

Jak seznam, tak slovník lze naplnit a vložit do databáze běžným způsobem:

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

Výsledkem je následující dokument 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
}

Tyto kolekce je pak možné aktualizovat, a to opět běžným způsobem:

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

context.SaveChanges();

Omezení:

  • Podporují se jen slovníky s řetězcovými klíči.
  • Dotazování na obsah primitivních kolekcí se v současné době nepodporuje. Pokud jsou pro vás tyto funkce důležité, hlasujte pro #16926, #25700 a #25701.

Práce s odpojenými entitami

Každá položka musí mít hodnotu id, která je pro daný klíč oddílu jedinečná. EF Core standardně generuje tuto hodnotu tak, že zřetězí diskriminátor a hodnoty primárního klíče, přičemž jako oddělovač použije znak „|“. Hodnoty klíče se generují jen v případě, kdy entita přejde do stavu Added. To může představovat problém při připojování entit, pokud u typu .NET nemají vlastnost id k uložení hodnoty.

Toto omezení obejdete tak, že hodnotu id vytvoříte a nastavíte ručně, nebo entitu nejprve označíte jako přidanou a pak ji změníte do požadovaného stavu:

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

Zde je výsledný 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
}

Optimistické uzamykání pomocí eTags

Pokud chcete nějaký typ entity nakonfigurovat tak, aby se používalo optimistické uzamykání, volejte UseETagConcurrency. Toto volání vytvoří vlastnost _etag ve stínovém stavu a nastaví ji jako token uzamykání.

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

Pokud chcete usnadnit řešení chyb uzamykání, můžete pomocí IsETagConcurrency namapovat eTag na některou vlastnost CLR.

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