EF Core Azure Cosmos DB-Anbieter

Dieser Datenbankanbieter ermöglicht die Verwendung von Entity Framework Core mit Azure Cosmos DB. Dieser Anbieter wird nicht im Rahmen des Entity Framework Core-Projekts verwaltet.

Es wird dringend empfohlen, sich vor dem Lesen dieses Abschnitts mit der Azure Cosmos DB-Dokumentation vertraut zu machen.

Hinweis

Dieser Anbieter funktioniert nur mit Azure Cosmos DB for NoSQL.

Installieren

Installieren Sie das NuGet-Paket „Microsoft.EntityFrameworkCore.Cosmos“.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Erste Schritte

Tipp

Das in diesem Artikel verwendete Beispiel finden Sie auf GitHub.

Wie bei anderen Anbietern besteht der erste Schritt darin, UseCosmos aufzurufen:

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

Warnung

Der Endpunkt und der Schlüssel sind aus Gründen der Einfachheit hier hartcodiert, aber in einer Produktions-App sollten sie sicher gespeichert werden.

In diesem Beispiel ist Order eine einfache Entität mit einem Verweis auf den nicht eigenständigen Typ 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; }
}

Das Speichern und Abfragen von Daten folgt dem normalen EF-Muster:

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

Wichtig

Der Aufruf von EnsureCreatedAsync ist erforderlich, um die erforderlichen Container zu erstellen und die Seeddaten einzufügen, wenn Sie im Modell vorhanden sind. EnsureCreatedAsync sollte jedoch nur während der Bereitstellung und nicht im normalen Betrieb aufgerufen werden, da dies zu Leistungsproblemen führen kann.

Azure Cosmos DB-Optionen

Es ist auch möglich, den Azure Cosmos DB-Anbieter mit einer einzelnen Verbindungszeichenfolge zu konfigurieren und andere Optionen zum Anpassen der Verbindung anzugeben:

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

Tipp

In der Dokumentation zu Azure Cosmos DB-Optionen finden Sie eine ausführliche Beschreibung der Auswirkungen der einzelnen oben genannten Optionen.

Cosmos-spezifische Modellanpassung

Standardmäßig werden alle Entitätstypen demselben Container zugeordnet, der nach dem abgeleiteten Kontext benannt wird (in diesem Fall "OrderContext"). Verwenden Sie zum Ändern des Standardcontainernamens HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

Um einen Entitätstyp einem anderen Container zuzuordnen, verwenden Sie ToContainer:

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

Um den Entitätstyp zu identifizieren, den ein bestimmtes Element darstellt, fügt EF Core einen Diskriminatorwert auch dann hinzu, wenn keine abgeleiteten Entitätstypen vorhanden sind. Der Name und der Wert des Diskriminators können geändert werden.

Wenn kein anderer Entitätstyp jemals im gleichen Container gespeichert wird, kann der Diskriminator durch Aufruf von HasNoDiscriminator entfernt werden:

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

Partitionsschlüssel

Standardmäßig erstellt EF Core Container, bei denen der Partitionsschlüssel auf "__partitionKey" festgelegt ist, ohne beim Einfügen von Elementen einen Wert dafür anzugeben. Um die Leistungsfunktionen von Azure Cosmos DB vollständig nutzen zu können, sollten Sie jedoch einen sorgfältig ausgewählten Partitionsschlüssel verwenden. Dieser kann durch Aufrufen von HasPartitionKey konfiguriert werden:

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

Hinweis

Als Eigenschaft des Partitionsschlüssels kann ein beliebiger Typ verwendet werden, sofern dieser in eine Zeichenfolge konvertiert wird.

Nach der Konfiguration muss die Partitionsschlüsseleigenschaft immer einen Wert aufweisen, der nicht NULL ist. Eine Abfrage kann durch Hinzufügen eines WithPartitionKey-Aufrufs einer einzelnen Partition zugewiesen werden.

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

Im Allgemeinen wird empfohlen, den Partitionsschlüssel dem Primärschlüssel hinzuzufügen, da dies die Serversemantik am besten widerspiegelt und einige Optimierungen ermöglicht, z. B. in FindAsync.

Bereitgestellter Durchsatz

Wenn Sie EF Core zum Erstellen der Azure Cosmos DB-Instanz oder von -Containern verwenden, können Sie einen bereitgestellten Durchsatz für die Datenbank durch Aufrufen von CosmosModelBuilderExtensions.HasAutoscaleThroughput oder CosmosModelBuilderExtensions.HasManualThroughput konfigurieren. Zum Beispiel:

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

Um den bereitgestellten Durchsatz für einen Container zu konfigurieren, rufen Sie CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput oder CosmosEntityTypeBuilderExtensions.HasManualThroughput auf. Zum Beispiel:

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

Eingebettete Entitäten

Hinweis

Verknüpfte Entitätstypen sind standardmäßig als nicht eigenständig konfiguriert. Rufen Sie ModelBuilder.Entity auf, um diese für einen bestimmten Entitätstypen zu verhindern.

Nicht eigenständige Azure Cosmos DB-Entitäten sind in das gleiche Element wie der Besitzer eingebettet. Verwenden Sie zum Ändern eines Eigenschaftennamens 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");
    });

Mit dieser Konfiguration wird die Reihenfolge des obigen Beispiels wie folgt gespeichert:

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

Sammlungen von nicht eigenständigen Entitäten werden ebenfalls eingebettet. Im nächsten Beispiel verwenden wir die Distributor-Klasse mit einer Sammlung von StreetAddress:

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

Die nicht eigenständigen Entitäten müssen keine expliziten Schlüsselwerte angeben, die gespeichert werden sollen:

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

Sie werden auf diese Weise persistent gespeichert:

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

Intern muss EF Core für alle nachverfolgten Entitäten immer eindeutige Schlüsselwerte aufweisen. Der Primärschlüssel, der standardmäßig für Sammlungen nicht eigenständiger Typen erstellt wird, besteht aus den Fremdschlüsseleigenschaften, die auf den Besitzer verweisen, und einer int-Eigenschaft, die dem Index im JSON-Array entspricht. Zum Abrufen dieser Werte kann die Eingabe-API verwendet werden:

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

Tipp

Bei Bedarf kann der standardmäßige Primärschlüssel für die nicht eigenständigen Entitätstypen geändert werden. Dann sollten jedoch Schlüsselwerte explizit angegeben werden.

Eine Sammlung primitiver Typen

Sammlungen unterstützter primitiver Typen, z. B. string und int, werden automatisch ermittelt und zugeordnet. Unterstützte Sammlungen sind alle Typen, die IReadOnlyList<T> oder IReadOnlyDictionary<TKey,TValue> implementieren. Betrachten Sie beispielsweise die folgenden Entitätstypen:

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

Sowohl die Liste als auch das Wörterbuch können auf normale Weise aufgefüllt und in die Datenbank eingefügt werden:

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

Dies ergibt folgendes JSON-Dokument:

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

Diese Sammlungen können dann wieder auf die übliche Weise aktualisiert werden:

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

context.SaveChanges();

Einschränkungen:

  • Nur Wörterbücher mit Zeichenfolgenschlüsseln werden unterstützt.
  • Das Abfragen des Inhalts primitiver Sammlungen wird derzeit nicht unterstützt. Entscheiden Sie sich für #16926, #25700 sowie #25701, wenn diese Features für Sie wichtig sind.

Arbeiten mit getrennten Entitäten

Jedes Element muss über einen id-Wert verfügen, der für den angegebenen Partitionsschlüssel eindeutig ist. Standardmäßig generiert EF Core den Wert durch Verkettung der Diskriminatorwerte und der Primärschlüsselwerte, wobei „|“ als Trennzeichen verwendet wird. Die Schlüsselwerte werden nur generiert, wenn eine Entität in den Zustand Added wechselt. Dies kann ein Problem darstellen, wenn Sie Entitäten anfügen, wenn diese nicht über eine id-Eigenschaft für den .NET-Typ verfügen, um den Wert zu speichern.

Um diese Einschränkung zu umgehen, können Sie den id-Wert manuell erstellen und festlegen oder die Entität zuerst als hinzugefügt markieren und dann in den gewünschten Zustand ändern:

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

Dieser JSON-Code ergibt sich:

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

Optimistische Nebenläufigkeit mit ETags

Rufen Sie UseETagConcurrency auf, um einen Entitätstyp so zu konfigurieren, dass die optimistische Nebenläufigkeit verwendet wird. Dieser Aufruf erstellt eine _etag-Eigenschaft als Schatteneigenschaft, und diese Eigenschaft wird als Nebenläufigkeitstoken festgelegt.

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

Wenn Sie das Auflösen von Nebenläufigkeitsfehlern vereinfachen möchten, können Sie das ETag mithilfe von IsETagConcurrency einer CLR-Eigenschaft zuordnen.

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