Proveedor de Azure Cosmos DB para EF Core
Este proveedor de base de datos permite usar Entity Framework Core con Azure Cosmos DB. Este proveedor se mantiene como parte del proyecto Entity Framework Core.
Se recomienda encarecidamente que se familiarice con la documentación sobre Azure Cosmos DB antes de leer esta sección.
Nota
Este proveedor solo funciona con SQL API de Azure Cosmos DB.
Instalar
Instale el paquete NuGet Microsoft.EntityFrameworkCore.Cosmos.
dotnet add package Microsoft.EntityFrameworkCore.Cosmos
Introducción
Sugerencia
Puede ver en GitHub un ejemplo de este artículo.
Al igual que para otros proveedores, el primer paso es llamar a 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.
En este ejemplo, Order es una entidad sencilla con una referencia al OrderStreetAddress.
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:
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. 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.
Opciones de Cosmos
El proveedor de Cosmos DB también se puede configurar con una sola cadena de conexión y especificar otras opciones para personalizar la conexión:
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));
});
Nota
La mayoría de estas opciones se incluyeron por primera vez en EF Core 5.0.
Sugerencia
Consulte la documentación de las opciones de Azure Cosmos DB para ver una descripción detallada del efecto que tiene cada opción arriba mencionada.
Personalización del modelo específico de Cosmos
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). Para cambiar el nombre de contenedor predeterminado, use HasDefaultContainer:
modelBuilder.HasDefaultContainer("Store");
Para asignar un tipo de entidad a un contenedor distinto, 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. El nombre y el valor del discriminador se pueden modificar.
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:
modelBuilder.Entity<Order>()
.HasNoDiscriminator();
Claves de partición
De forma predeterminada, EF Core creará contenedores con la clave de partición establecida en "__partitionKey" sin proporcionarle ningún valor al insertar elementos. 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. Se puede configurar mediante la llamada a 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.
Una vez configurada, la propiedad de clave de partición siempre debe tener un valor distinto de NULL. Una consulta se puede convertir en partición única agregando una llamada 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();
}
Nota
WithPartitionKey se incluyó por primera vez en EF Core 5.0.
Con carácter general, se recomienda agregar la clave de partición a la clave principal, ya que es lo que mejor refleja la semántica de servidor y permite algunas optimizaciones, por ejemplo, en FindAsync.
Entidades insertadas
En Cosmos, las entidades en propiedad se insertan en el mismo elemento que el propietario. Para cambiar el nombre de una propiedad, 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:
{
"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. En el ejemplo siguiente, usaremos la clase Distributor con una colección de StreetAddress:
public class Distributor
{
public int Id { get; set; }
public string ETag { 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:
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:
{
"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. 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. Para recuperar estos valores, se podría usar la API de entrada:
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.
Trabajo con entidades desconectadas
Cada elemento debe tener un valor id único para la clave de partición específica. De manera predeterminada, EF Core genera el valor mediante la concatenación de los valores de discriminador y de clave principal, con "|" como delimitador. Los valores de clave solo se generan cuando una entidad entra en el estado Added. Esto podría suponer un problema al adjuntar entidades si no tienen una propiedad en el tipo .NET para almacenar el valor.
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:
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:
{
"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
}
Simultaneidad optimista con mecanismos ETag
Nota
La compatibilidad con la simultaneidad de ETag se incluyó por primera vez en EF Core 5.0.
Si necesita configurar un tipo de entidad para que use la simultaneidad optimista, llame a UseETagConcurrency. Esta llamada creará una propiedad _etag en _etag y la establecerá como el token de simultaneidad.
modelBuilder.Entity<Order>()
.UseETagConcurrency();
Para facilitar la resolución de errores de simultaneidad, puede asignar el ETag a una propiedad de CLR mediante IsETagConcurrency.
modelBuilder.Entity<Distributor>()
.Property(d => d.ETag)
.IsETagConcurrency();