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 Azure Cosmos DB para NoSQL.
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. Consulte Conexión y autenticación para ver distintas formas de conectarse a Azure Cosmos DB.
En este ejemplo, Order
es una entidad sencilla con una referencia al tipo owned 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:
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.
Conexión y autenticación
El proveedor de Azure Cosmos DB para EF Core tiene varias sobrecargas del método UseCosmos. Estas sobrecargas admiten las distintas formas en que se puede realizar una conexión a la base de datos y las distintas formas de asegurarse de que la conexión sea segura.
Importante
Asegúrese de comprender el acceso seguro a los datos de Azure Cosmos DB para entender las implicaciones de seguridad y los procedimientos recomendados para usar cada sobrecarga del método UseCosmos
.
Mecanismo de conexión | Sobrecarga de UseCosmos | Información adicional |
---|---|---|
Punto de conexión y clave de cuenta | UseCosmos<DbContext>(accountEndpoint, accountKey, databaseName) |
Claves principal o secundaria |
Punto de conexión y token de cuenta | UseCosmos<DbContext>(accountEndpoint, tokenCredential, databaseName) |
Tokens de recursos |
Cadena de conexión | UseCosmos<DbContext>(connectionString, databaseName) |
Trabajo con claves de cuenta y cadenas de conexión |
Consultas
Consultas LINQ
Las consultas LINQ de EF Core se pueden ejecutar en Azure Cosmos DB de la misma manera que para otros proveedores de bases de datos. Por ejemplo:
var stringResults = await context.Triangles.Where(
e => e.Name.Length > 4
&& e.Name.Trim().ToLower() != "obtuse"
&& e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
.ToListAsync();
Nota:
El proveedor de Azure Cosmos DB no traduce el mismo conjunto de consultas LINQ que otros proveedores. Para más información, consulte Limitaciones.
Consultas SQL
Las consultas también se pueden escribir directamente en SQL. Por ejemplo:
var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
@"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
.ToListAsync();
Esta consulta da como resultado la ejecución de la consulta siguiente:
SELECT c
FROM (
SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c
Al igual que para las consultas FromSql
relacionales, el SQL escrito a mano puede componerse aún más utilizando operadores LINQ. Por ejemplo:
var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
@"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
.Where(e => e.InsertedOn <= DateTime.UtcNow)
.Select(e => e.Angle1).Distinct()
.ToListAsync();
Esta combinación de SQL y LINQ se traduce a:
SELECT DISTINCT c["Angle1"]
FROM (
SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c
WHERE (c["InsertedOn"] <= GetCurrentDateTime())
Opciones de Azure Cosmos DB
También se puede configurar el proveedor de Azure Cosmos DB con una única 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));
});
Sugerencia
El código anterior muestra posibles opciones. No se pretende que se utilicen todas al mismo tiempo. 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 DB, 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();
}
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
.
Rendimiento aprovisionado
Si usa EF Core para crear la base de datos o los contenedores de Azure Cosmos DB, puede configurar el rendimiento aprovisionado para la base de datos mediante una llamada a CosmosModelBuilderExtensions.HasAutoscaleThroughput o CosmosModelBuilderExtensions.HasManualThroughput. Por ejemplo:
modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);
Para configurar el rendimiento aprovisionado para un contenedor, llame a CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput o CosmosEntityTypeBuilderExtensions.HasManualThroughput. Por ejemplo:
modelBuilder.Entity<Family>(
entityTypeBuilder =>
{
entityTypeBuilder.HasManualThroughput(5000);
entityTypeBuilder.HasAutoscaleThroughput(3000);
});
Período de vida
Los tipos de entidad del modelo de Azure Cosmos DB ahora se pueden configurar con un período de vida predeterminado. Por ejemplo:
modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);
O bien, para el almacén analítico:
modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);
El período de vida de las entidades individuales se puede establecer mediante una propiedad asignada a "ttl" en el documento JSON. Por ejemplo:
modelBuilder.Entity<Village>()
.HasDefaultTimeToLive(3600)
.Property(e => e.TimeToLive)
.ToJsonProperty("ttl");
Nota:
Un período de vida predeterminado debe configurarse en el tipo de entidad para que "ttl" tenga algún efecto. Consulte Período de vida (TTL) en Azure Cosmos DB para más información.
La propiedad de período de vida se establece antes de guardar la entidad. Por ejemplo:
var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();
La propiedad de período de vida puede ser una propiedad reemplazada para evitar contaminar la entidad de dominio con las preocupaciones de la base de datos. Por ejemplo:
modelBuilder.Entity<Hamlet>()
.HasDefaultTimeToLive(3600)
.Property<int>("TimeToLive")
.ToJsonProperty("ttl");
A continuación, la propiedad de período de vida reemplazada se establece mediante el acceso a la entidad con seguimiento. Por ejemplo:
var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();
Entidades insertadas
Nota:
Los tipos de entidad relacionados se configuran como propiedad de forma predeterminada. Para evitar esto en un tipo de entidad específico, llame a ModelBuilder.Entity.
En Azure Cosmos DB, 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.
Colecciones de tipos primitivos
Las colecciones de tipos primitivos admitidos, como string
y int
, se detectan y asignan automáticamente. Las colecciones admitidas son todos los tipos que implementen IReadOnlyList<T> o IReadOnlyDictionary<TKey,TValue>. Por ejemplo, considere este tipo de entidad:
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; }
}
Tanto la lista como el diccionario se pueden rellenar e insertar en la base de datos de la manera 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);
await context.SaveChangesAsync();
Esto da como resultado el siguiente 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
}
Después, estas colecciones se pueden actualizar, también de la manera habitual:
book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";
await context.SaveChangesAsync();
Limitaciones:
- Solo se admiten diccionarios con claves de cadena
- Actualmente no se admite la consulta en el contenido de colecciones primitivas. Vote por nº 16926, nº 25700 y nº 25701 si considera importantes estas características.
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 id
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
Si necesita configurar un tipo de entidad para que use la simultaneidad optimista, llame a UseETagConcurrency. Esta llamada creará una propiedad _etag
en estado de propiedad reemplazada 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();