Proveedor de Azure Cosmos DB para EF CoreEF Core Azure Cosmos DB Provider

Nota

Este proveedor es nuevo en EF Core 3.0.This provider is new in EF Core 3.0.

Este proveedor de base de datos permite usar Entity Framework Core con Azure Cosmos DB.This database provider allows Entity Framework Core to be used with Azure Cosmos DB. Este proveedor se mantiene como parte del proyecto Entity Framework Core.The provider is maintained as part of the Entity Framework Core Project.

Se recomienda encarecidamente que se familiarice con la documentación sobre Azure Cosmos DB antes de leer esta sección.It is strongly recommended to familiarize yourself with the Azure Cosmos DB documentation before reading this section.

Nota

Este proveedor solo funciona con SQL API de Azure Cosmos DB.This provider only works with the SQL API of Azure Cosmos DB.

InstalarInstall

Instale el paquete NuGet Microsoft.EntityFrameworkCore.Cosmos.Install the Microsoft.EntityFrameworkCore.Cosmos NuGet package.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Primeros pasosGet started

Sugerencia

Puede ver en GitHub un ejemplo de este artículo.You can view this article's sample on GitHub.

Al igual que para otros proveedores, el primer paso es llamar a UseCosmos:Like for other providers the first step is to call UseCosmos:

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

Advertencia

El punto de conexión y la clave se codifican aquí de forma rígida por motivos de simplicidad pero, en una aplicación de producción, se deben almacenar de manera segura.The endpoint and key are hardcoded here for simplicity, but in a production app these should be stored securely.

En este ejemplo, Order es una entidad sencilla con una referencia al tipo owned StreetAddress.In this example Order is a simple entity with a reference to the owned type 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; }
}

La acción de guardar y consultar datos sigue el patrón de EF normal:Saving and quering data follows the normal EF pattern:

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

Llamar a EnsureCreatedAsync es necesario para crear los contenedores necesarios e insertar los datos de inicialización si están presentes en el modelo.Calling EnsureCreatedAsync is necessary to create the required containers and insert the seed data if present in the model. Sin embargo, se debe llamar a EnsureCreatedAsync solo durante la implementación y no durante la operación normal, porque podría provocar problemas de rendimiento.However EnsureCreatedAsync should only be called during deployment, not normal operation, as it may cause performance issues.

Personalización del modelo específico de CosmosCosmos-specific model customization

De manera predeterminada, todos los tipos de entidad están asignados al mismo contenedor, con un nombre que depende del contexto derivado ("OrderContext" en este caso).By default all entity types are mapped to the same container, named after the derived context ("OrderContext" in this case). Para cambiar el nombre de contenedor predeterminado, use HasDefaultContainer:To change the default container name use HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

Para asignar un tipo de entidad a un contenedor distinto, use ToContainer:To map an entity type to a different container use ToContainer:

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

Para identificar el tipo de entidad que un elemento determinado representa, EF Core agrega un valor de discriminador incluso si no hay tipos de entidad derivados.To identify the entity type that a given item represent EF Core adds a discriminator value even if there are no derived entity types. El nombre y el valor del discriminador se pueden modificar.The name and value of the discriminator can be changed.

Si ningún otro tipo de entidad se va a almacenar en el mismo contenedor, se puede quitar el discriminador mediante la llamada a HasNoDiscriminator:If no other entity type will ever be stored in the same container the discriminator can be removed by calling HasNoDiscriminator:

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

Claves de particiónPartition keys

De forma predeterminada, EF Core creará contenedores con la clave de partición establecida en "__partitionKey" sin proporcionarle ningún valor al insertar elementos.By default EF Core will create containers with the partition key set to "__partitionKey" without supplying any value for it when inserting items. Sin embargo, para sacar el máximo partido a las funcionalidades de rendimiento de Azure Cosmos, se debe usar una clave de partición seleccionada cuidadosamente.But to fully leverage the performance capabilities of Azure Cosmos a carefully selected partition key should be used. Se puede configurar mediante la llamada a HasPartitionKey:It can be configured by calling HasPartitionKey:

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

Nota

La propiedad de clave de partición puede ser de cualquier tipo, siempre y cuando se convierta en una cadena.The partition key property can be of any type as long as it is converted to string.

Una vez configurada, la propiedad de clave de partición siempre debe tener un valor distinto de NULL.Once configured the partition key property should always have a non-null value. Al emitir una consulta, se puede agregar una condición para que sea una partición única.When issuing a query a condition can be added to make it single-partition.

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.Where(p => p.PartitionKey == "2").LastAsync();
    Console.Write("Last order will ship to: ");
    Console.WriteLine($"{order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

Entidades insertadasEmbedded entities

En Cosmos, las entidades en propiedad se insertan en el mismo elemento que el propietario.For Cosmos owned entities are embedded in the same item as the owner. Para cambiar el nombre de una propiedad, use ToJsonProperty:To change a property name 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");
    });

Con esta configuración, el pedido del ejemplo anterior se almacena de esta manera:With this configuration the order from the example above is stored like this:

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

Las colecciones de entidades en propiedad también se insertan.Collections of owned entities are embedded as well. En el ejemplo siguiente, usaremos la clase Distributor con una colección de StreetAddress:For the next example we'll use the Distributor class with a collection of StreetAddress:

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

No es necesario que las entidades en propiedad proporcionen valores de clave explícitos que se deban almacenar:The owned entities don't need to provide explicit key values to be stored:

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

Se conservarán de esta manera:They will be persisted in this way:

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

De manera interna, EF Core siempre debe tener valores de clave únicos para todas las entidades sometidas a seguimiento.Internally EF Core always needs to have unique key values for all tracked entities. La clave principal creada de manera predeterminada para las colecciones de tipos en propiedad consta de las propiedades de clave externa que apuntan al propietario y una propiedad int correspondiente al índice de la matriz JSON.The primary key created by default for collections of owned types consists of the foreign key properties pointing to the owner and an int property corresponding to the index in the JSON array. Para recuperar estos valores, se podría usar la API de entrada:To retrieve these values entry API could be used:

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

Sugerencia

Cuando sea necesario, se puede cambiar la clave principal predeterminada para los tipos de entidad en propiedad, pero los valores de clave se deben proporcionar de manera explícita.When necessary the default primary key for the owned entity types can be changed, but then key values should be provided explicitly.

Trabajo con entidades desconectadasWorking with disconnected entities

Cada elemento debe tener un valor id único para la clave de partición específica.Every item needs to have an id value that is unique for the given partition key. De manera predeterminada, EF Core genera el valor mediante la concatenación de los valores de discriminador y de clave principal, con "|" como delimitador.By default EF Core generates the value by concatenating the discriminator and the primary key values, using '|' as a delimiter. Los valores de clave solo se generan cuando una entidad entra en el estado Added.The key values are only generated when an entity enters the Added state. Esto podría suponer un problema al adjuntar entidades si no tienen una propiedad id en el tipo .NET para almacenar el valor.This might pose a problem when attaching entities if they don't have an id property on the .NET type to store the value.

Para evitar esta limitación, se podría crear y establecer el valor id de manera manual o marcar la entidad como agregada y, luego, cambiarla al estado deseado:To work around this limitation one could create and set the id value manually or mark the entity as added first, then changing it to the desired state:

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 es el JSON resultante:This is the resulting 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
}