Provedor do Azure Cosmos DB no EF Core

Este provedor de banco de dados permite que o Entity Framework Core seja usado com o Azure Cosmos DB. O provedor é mantido como parte do Projeto do Entity Framework Core.

É extremamente recomendado familiarizar-se com a documentação do Azure Cosmos DB antes de ler esta seção.

Observação

Esse provedor só funciona com o Azure Cosmos DB for NoSQL.

Instalar

Instale o pacote NuGet Microsoft.EntityFrameworkCore.Cosmos.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Introdução

Dica

Veja o exemplo deste artigo no GitHub.

Assim como para outros provedores, a primeira etapa é chamar UseCosmos:

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

Aviso

Aqui, o ponto de extremidade e a chave são embutidos no código para simplificar, mas em um aplicativo de produção, eles devem ser armazenados com segurança.

Neste exemplo, Order é uma entidade simples com uma referência ao tipo próprioStreetAddress.

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

O processo de salvar e consultar dados segue o padrão de 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();
}

Importante

Será necessário chamar EnsureCreatedAsync para criar os contêineres obrigatórios e inserir os dados de semente (se presentes no modelo). No entanto, EnsureCreatedAsync só deve ser chamado durante a implantação, não durante a operação normal, pois isso pode causar problemas de desempenho.

Opções do Azure Cosmos DB

Também é possível configurar o provedor Azure Cosmos DB com uma única cadeia de conexão e especificar outras opções para personalizar a conexão:

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

Dica

Confira a documentação de Opções do Azure Cosmos DB para obter uma descrição detalhada do efeito de cada opção mencionada acima.

Personalização de modelo específica do Cosmos

Por padrão, todos os tipos de entidade são mapeados para o mesmo contêiner, nomeados segundo o contexto derivado (neste caso, "OrderContext"). Para alterar o nome do contêiner padrão, use HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

Para mapear um tipo de entidade para um contêiner diferente, use ToContainer:

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

Para identificar o tipo de entidade que um determinado item representa, o EF Core adiciona um valor de discriminador mesmo quando não há nenhum tipo de entidade derivada. O nome e o valor do discriminador podem ser alterados.

Se nenhum outro tipo de entidade for armazenado no mesmo contêiner, o discriminador poderá ser removido chamando HasNoDiscriminator:

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

Chaves de partição

Por padrão, o Entity Framework Core criará contêineres com a chave de partição definida como "__partitionKey" sem fornecer nenhum valor para ela ao inserir itens. Mas para aproveitar totalmente as capacidades de desempenho do Azure Cosmos DB, uma chave de partição cuidadosamente selecionada deve ser utilizada. Ela pode ser configurada chamando HasPartitionKey:

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

Observação

A propriedade de chave de partição pode ser de qualquer tipo, contanto que ela seja convertida em cadeia de caracteres.

Uma vez configurada, a propriedade da chave da partição sempre deve ter um valor não nulo. Uma consulta pode ser feita em partição única adicionando uma chamada 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();
}

Geralmente, recomendamos adicionar a chave de partição à chave primária, pois isso reflete melhor a semântica do servidor e permite algumas otimizações, por exemplo, em FindAsync.

Taxa de transferência provisionada

Se você utilizar o EF Core para criar o banco de dados ou contêineres do Azure Cosmos DB, poderá configurar a taxa de transferência provisionada para o banco de dados chamando CosmosModelBuilderExtensions.HasAutoscaleThroughput ou CosmosModelBuilderExtensions.HasManualThroughput. Por exemplo:

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

Para configurar a taxa de transferência provisionada para uma chamada de contêiner CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput ou CosmosEntityTypeBuilderExtensions.HasManualThroughput. Por exemplo:

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

Entidades inseridas

Observação

Os tipos de entidades relacionadas são configurados como propriedade por padrão. Para evitar isso para um tipo de entidade específico, chame ModelBuilder.Entity.

No Azure Cosmos DB, as entidades proprietárias são inseridas no mesmo item que o proprietário. Para alterar o nome de uma propriedade, use 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");
    });

Com essa configuração, a ordem do exemplo acima é armazenada da seguinte forma:

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

Coleções de entidades próprias também são inseridas. Para o próximo exemplo, usaremos a classe Distributor com uma coleção de StreetAddress:

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

As entidades próprias não precisam fornecer valores de chave explícitos para serem armazenadas:

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

Elas serão persistidas da seguinte forma:

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

Internamente, o EF Core sempre precisa ter valores de chave exclusivos para todas as entidades rastreadas. A chave primária criada por padrão para coleções de tipos próprios é composta pelas propriedades de chave estrangeira que apontam para o proprietário e por uma propriedade int correspondente ao índice na matriz JSON. Para recuperar esses valores, a API de entrada pode ser usada:

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

Dica

Quando necessário, a chave primária padrão para os tipos de entidade própria pode ser alterada, mas os valores da chave devem ser fornecidos explicitamente.

Coleções de tipos primitivos

Coleções de tipos primitivos com suporte, como string e int, são descobertas e mapeadas automaticamente. Coleções com suporte são todos os tipos que implementam IReadOnlyList<T> ou IReadOnlyDictionary<TKey,TValue>. Por exemplo, considere este tipo de entidade:

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

A lista e o dicionário podem ser preenchidos e inseridos no banco de dados da maneira normal:

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

Isso resulta no seguinte documento 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
}

Depois, essas coleções podem ser atualizadas novamente da maneira normal:

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

context.SaveChanges();

Limitações:

  • Há suporte apenas para dicionários com chaves de cadeia de caracteres
  • Atualmente, não há suporte para consultar o conteúdo de coleções primitivas. Vote em #16926, #25700 e # 25701 se esses recursos forem importantes para você.

Trabalhando com entidades desconectadas

Cada item precisa ter um valor id exclusivo para a chave de partição especificada. Por padrão, o EF Core gera o valor concatenando os valores do discriminador e da chave primária, usando '|' como delimitador. Os valores da chave são gerados apenas quando a entidade entra no estado Added. Isso poderá ser um problema ao anexar entidades se elas não tiverem uma propriedade id no tipo .NET para armazenar o valor.

Para contornar essa limitação, é possível criar e definir o valor de id manualmente ou marcar a entidade como adicionada primeiro e, em seguida, alterá-la para o estado desejado:

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

Este é o JSON resultante:

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

Simultaneidade otimista com eTags

Para configurar um tipo de entidade para usar simultaneidade otimista, chame UseETagConcurrency. Essa chamada criará uma propriedade _etag no estado de sombra e a definirá como o token de simultaneidade.

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

Para facilitar a resolução de erros de simultaneidade, você pode mapear a eTag para uma propriedade CLR usando IsETagConcurrency.

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