Fournisseur Azure Cosmos DB EF Core

Ce fournisseur de base de données permet d’utiliser Entity Framework Core avec Azure Cosmos DB. Il est géré dans le cadre du projet Entity Framework Core.

Avant de lire cette section, il est fortement recommandé de vous familiariser avec la documentation d’Azure Cosmos DB.

Remarque

Ce fournisseur fonctionne uniquement avec Azure Cosmos DB for NoSQL.

Installer

Installez le package NuGet Microsoft.EntityFrameworkCore.Cosmos.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Prise en main

Conseil

Vous pouvez afficher cet exemple sur GitHub.

Comme pour les autres fournisseurs, la première étape consiste à appeler UseCosmos :

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

Avertissement

Le point de terminaison et la clé sont codés en dur ici par souci de simplicité, mais dans une application de production, ils doivent être stockés de manière sécurisée.

Dans cet exemple, Order est une entité simple avec une référence au type détenuStreetAddress.

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

L’enregistrement et l’interrogation des données suivent le modèle EF normal :

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

Important

L’appel de EnsureCreatedAsync est nécessaire pour créer les conteneurs requis et insérer les données initiales si elles sont présentes dans le modèle. Toutefois, EnsureCreatedAsync ne doit être appelé qu’au cours du déploiement, pas pendant le fonctionnement normal, car cela peut entraîner des problèmes de performances.

Options d’Azure Cosmos DB

Il est également possible de configurer le fournisseur Azure Cosmos DB avec une seule chaîne de connexion et de spécifier d’autres options pour personnaliser la connexion :

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

Conseil

Consultez la documentation options d’Azure Cosmos DB pour obtenir une description détaillée de l’effet de chaque option mentionnée ci-dessus.

Personnalisation du modèle propre à Cosmos

Par défaut, tous les types d’entité sont mappés au même conteneur, nommé d’après le contexte dérivé ("OrderContext"dans le cas présent). Pour changer le nom de conteneur par défaut, utilisez HasDefaultContainer :

modelBuilder.HasDefaultContainer("Store");

Pour mapper un type d’entité à un autre conteneur, utilisez ToContainer :

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

Pour identifier le type d’entité représenté par un élément donné, EF Core ajoute une valeur de discriminateur même en l’absence de type d’entité dérivé. Le nom et la valeur du discriminateur peuvent être changés.

Si aucun autre type d’entité ne sera jamais stocké dans le même conteneur, le discriminant peut être supprimé en appelant HasNoDiscriminator :

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

Clés de partition

EF Core crée par défaut des conteneurs avec la clé de partition définie sur "__partitionKey" sans fournir de valeur pour celle-ci lors de l’insertion d’éléments. Mais pour tirer pleinement parti des fonctionnalités de performances d’Azure Cosmos DB, une clé de partition soigneusement sélectionnée doit être utilisée. Elle peut être configurée en appelant HasPartitionKey :

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

Remarque

La propriété de clé de partition peut être de n’importe quel type tant qu’elle est convertie en chaîne.

Une fois configurée, la propriété de clé de partition doit toujours avoir une valeur non Null. Une requête peut être effectuée sur une seule partition en ajoutant un appel 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();
}

Il est généralement recommandé d’ajouter la clé de partition à la clé primaire, car cela reflète le mieux la sémantique du serveur et autorise certaines optimisations, par exemple dans FindAsync.

Débit approvisionné

Si vous utilisez EF Core pour créer la base de données ou les conteneurs Azure Cosmos DB, vous pouvez configurer le débit provisionné pour la base de données en appelant CosmosModelBuilderExtensions.HasAutoscaleThroughput ou CosmosModelBuilderExtensions.HasManualThroughput. Par exemple :

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

Pour configurer le débit provisionné pour un appel de conteneur CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput ou CosmosEntityTypeBuilderExtensions.HasManualThroughput. Par exemple :

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

Entités incorporées

Remarque

Les types d’entités associés sont configurés comme appartenant par défaut. Pour empêcher cela pour un appel ModelBuilder.Entity de type d’entité spécifique.

Pour Azure Cosmos DB, les entités détenues sont incorporées dans le même élément que le propriétaire. Pour changer un nom de propriété, utilisez 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");
    });

Avec cette configuration, la commande dans l’exemple ci-dessus est stockée comme suit :

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

Les collections d’entités détenues sont également incorporées. Pour l’exemple suivant, nous allons utiliser la classe Distributor avec la collection StreetAddress :

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

Les entités détenues n’ont pas besoin de fournir des valeurs de clés explicites pour être stockées :

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

Elles sont conservées de cette façon :

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

En interne, EF Core doit toujours avoir des valeurs de clé uniques pour toutes les entités suivies. La clé primaire créée par défaut pour les collections de types détenus se compose des propriétés de clé étrangère qui pointent vers le propriétaire et d’une propriété int correspondant à l’index dans le tableau JSON. Pour récupérer ces valeurs, vous pouvez utiliser l’API d’entrée :

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

Conseil

Quand cela est nécessaire, la clé primaire par défaut pour les types d’entités détenus peut être changée, mais les valeurs de clé doivent être fournies explicitement.

Collection de types primitifs

Les collections de types primitifs pris en charge, telles que string et int, sont découvertes et mappées automatiquement. Les collections prises en charge sont tous les types qui implémentent IReadOnlyList<T> ou IReadOnlyDictionary<TKey,TValue>. Par exemple, prenons le type d'entité :

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

La liste et le dictionnaire peuvent être renseignés et insérés dans la base de données de la manière normale :

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

Cela entraîne le document JSON suivant :

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

Ces collections peuvent ensuite être mises à jour, de nouveau de la manière normale :

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

context.SaveChanges();

Limites :

  • Seuls les dictionnaires avec des clés de chaîne sont pris en charge
  • L’interrogation dans le contenu des collections primitives n’est actuellement pas prise en charge. Votez pour #16926, #25700 et #25701 si ces fonctionnalités sont importantes pour vous.

Utilisation d’entités déconnectées

Chaque élément doit avoir une valeur id unique pour la clé de partition donnée. Par défaut, EF Core génère la valeur en concaténant les valeurs du discriminateur et de la clé primaire, au moyen du caractère « | » en guise de délimiteur. Les valeurs de clé sont générées uniquement quand une entité passe à l’état Added. Cela peut poser un problème lors de l’attachement d’entités si elles n’ont pas de propriété id sur le type .NET pour stocker la valeur.

Pour contourner cette limitation, il est possible de créer et de définir la valeur id manuellement, ou bien de marquer l’entité comme étant ajoutée, puis de lui affecter l’état souhaité :

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

Voici le code JSON résultant :

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

Accès concurrentiel optimiste avec les eTags

Pour configurer un type d’entité afin d’utiliser un appel concurrentiel optimisteUseETagConcurrency. Cet appel crée une propriété _etag dans un état d’ombre et la définit comme jeton d’accès concurrentiel.

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

Pour faciliter la résolution des erreurs d’accès concurrentiel, vous pouvez mapper l’eTag à une propriété CLR à l’aide de IsETagConcurrency.

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