Compartir a través de


Configuración del modelo con el Proveedor Azure Cosmos DB EF Core

Contenedores y tipos de entidad

En Azure Cosmos DB, los documentos JSON se almacenan en contenedores. A diferencia de las tablas en bases de datos relacionales, los contenedores de Azure Cosmos DB pueden contener documentos con diferentes formas: un contenedor no impone un esquema uniforme a sus documentos. Sin embargo, varias opciones de configuración se definen a nivel de contenedor y, por tanto, afectan a todos los documentos contenidos en él. Para obtener más información, consulte la Documentación de Azure Cosmos DB sobre contenedores.

Por defecto, EF asigna todos los tipos de entidad al mismo contenedor. Esto suele ser un buen valor por defecto en términos de rendimiento y precio. El contenedor predeterminado recibe el nombre del tipo de contexto .NET (OrderContext en este caso). Para cambiar el nombre del contenedor por defecto, use HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

Para asignar un tipo de entidad a un contenedor distinto, use ToContainer:

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

Antes de asignar tipos de entidad a distintos contenedores, asegúrese de que conoce las posibles implicaciones en términos de rendimiento y precios (por ejemplo, en relación con el rendimiento dedicado y compartido);. Consulte la documentación de Azure Cosmos DB para obtener más información.

Identificadores y claves

Azure Cosmos DB requiere que todos los documentos tengan una propiedad JSON id que los identifique de forma exclusiva. Al igual que otros proveedores de EF, el proveedor Azure Cosmos DB de EF intentará encontrar una propiedad denominada Id o <type name>Id, y configurará dicha propiedad como la clave de su tipo de entidad, asignándola a la propiedad JSON id. Puede configurar cualquier propiedad para que sea la propiedad clave mediante HasKey. Para obtener más información, consulte la documentación general de EF sobre claves.

Los desarrolladores que llegan a Azure Cosmos DB desde otras bases de datos a veces esperan que la propiedad clave (Id) se genere automáticamente. Por ejemplo, en SQL Server, EF configura las propiedades clave numéricas como columnas IDENTITY, en las que se generan valores autoincrementados en la base de datos. En cambio, Azure Cosmos DB no admite la generación automática de propiedades, por lo que las propiedades clave deben configurarse explícitamente. Si se inserta un tipo de entidad con una propiedad clave sin establecer, simplemente se insertará el valor predeterminado del CLR para esa propiedad (por ejemplo, 0 para int), y fallará una segunda inserción. EF emite una advertencia si se intenta hacer esto.

Si quiere tener un GUID como propiedad clave, puede configurar EF para que genere valores únicos y aleatorios en el cliente:

modelBuilder.Entity<Session>().Property(b => b.Id).HasValueGenerator<GuidValueGenerator>();

Claves de partición

Azure Cosmos DB usa la partición para lograr el escalado horizontal; el modelado adecuado y la selección cuidadosa de la clave de partición son vitales para lograr un buen rendimiento y mantener los costes bajos. Es muy recomendable leer la Documentación de Azure Cosmos DB sobre particiones y planificar la estrategia de partición con antelación.

Para configurar la clave de partición con EF, llame a HasPartitionKey, pasándole una propiedad normal de su tipo de entidad:

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

Cualquier propiedad puede convertirse en una clave de partición siempre que se convierta en cadena. Una vez configurada, la propiedad de clave de partición debe tener siempre un valor no nulo. Intentar insertar un nuevo tipo de entidad con una propiedad de clave de partición no configurada dará lugar a un error.

Tenga en cuenta que Azure Cosmos DB permite que existan dos documentos con la misma propiedad id en un contenedor, siempre y cuando estén en particiones diferentes. Esto significa que para identificar de forma única un documento dentro de un contenedor, deben proporcionarse las propiedades tanto de id como de clave de partición. Debido a esto, la noción interna de EF de la clave primaria de entidad contiene ambos elementos por convención, a diferencia de, por ejemplo, las bases de datos relacionales, donde no existe el concepto de clave de partición. Esto significa, por ejemplo, FindAsync requiere propiedades tanto de clave como de clave de partición (consulte más documentos), y que una consulta debe especificarlas en su cláusula Where para beneficiarse de la eficacia y rentabilidad de point reads.

Tenga en cuenta que la clave de partición se define a nivel de contenedor. Esto significa, en particular, que no es posible que varios tipos de entidad del mismo contenedor tengan propiedades de clave de partición diferentes. Si necesita definir claves de partición diferentes, asigne los tipos de entidad relevantes a contenedores diferentes.

Claves de partición jerárquicas

Azure Cosmos DB también admite claves de partición jerárquicas para optimizar aún más la distribución de datos. Consulte la documentación para obtener más detalles. EF 9.0 ha añadido soporte para claves de partición jerárquicas; para configurarlas, basta con pasar hasta 3 propiedades a HasPartitionKey:

modelBuilder.Entity<Order>().HasPartitionKey(o => new { e.TenantId, e.UserId, e.SessionId });

Con una clave de partición jerárquica, las consultas se pueden enviar fácilmente solo a un subconjunto relevante de subparticiones. Por ejemplo, si consulta los Pedidos de un inquilino específico, esas consultas solo se ejecutarán en las subparticiones de ese inquilino.

Si no configura una clave de partición con EF, se registrará una advertencia al inicio. El núcleo de EF creará contenedores con la clave de partición establecida en __partitionKey y no proporcionará ningún valor para ella al insertar elementos. Cuando no se establece ninguna clave de partición, el contenedor estará limitado a 20 GB de datos, que es el almacenamiento máximo para una única partición lógica. Mientras que esto puede funcionar para pequeñas aplicaciones de desarrollo/prueba, es altamente desaconsejable desplegar una aplicación de producción sin una estrategia de clave de partición bien configurada.

Una vez que las propiedades de la clave de partición están configuradas correctamente, puede proporcionar valores para ellas en las consultas; consulte Consultar con claves de partición para obtener más información.

Discriminadores

Dado que se pueden asignar varios tipos de entidad al mismo contenedor, EF Core siempre añade una propiedad discriminadora $type a todos los documentos JSON que guarde (esta propiedad se llamaba Discriminator antes de EF 9.0). Esto permite a EF reconocer los documentos que se cargan desde la base de datos y materializar el tipo .NET correcto. Los desarrolladores procedentes de bases de datos relacionales pueden estar familiarizados con los discriminadores en el contexto de la herencia de tablas por jerarquía (TPH). En Azure Cosmos DB, los discriminadores se usan no solo en escenarios de mapeo de herencia, sino también porque el mismo contenedor puede contener tipos de documentos completamente diferentes.

El nombre y los valores de la propiedad del discriminador pueden configurarse con las API estándar de EF; consulte estos documentos para obtener más información. Si está mapeando un único tipo de entidad a un contenedor, está seguro de que nunca mapeará otro y le gustaría deshacerse de la propiedad discriminator, llame a HasNoDiscriminator:

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

Dado que el mismo contenedor puede contener diferentes tipos de entidad, y la propiedad JSON id debe ser única dentro de una partición de contenedor, no puede tener el mismo valor id para las entidades Compare esto con las bases de datos relacionales, donde cada tipo de entidad se asigna a una tabla diferente, y por lo tanto tiene su propio espacio de claves separado. Por lo tanto, es su responsabilidad garantizar la unicidad de los documentos id que inserta en un contenedor. Si necesita tener diferentes tipos de entidad con los mismos valores de clave primaria, puede indicar a EF que inserte automáticamente el discriminador en la propiedad id de la siguiente manera:

modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();

Aunque esto puede facilitar el trabajo con los valores id, puede dificultar la interoperabilidad con aplicaciones externas que trabajen con sus documentos, ya que ahora deben conocer el formato concatenado id de EF, así como los valores del discriminador, que se derivan por defecto de sus tipos .NET. Tenga en cuenta que este era el comportamiento predeterminado antes de EF 9.0.

Una opción adicional es indicar a EF que inserte en la propiedad solo el discriminador raíz, que es el discriminador del tipo de entidad raíz de la jerarquía en la propiedad id:

modelBuilder.Entity<Session>().HasRootDiscriminatorInJsonId();

Esto es similar, pero permite a EF usar lecturas puntuales eficientes en más escenarios. Si necesita insertar un discriminador en la propiedad id, considere insertar el discriminador raíz para obtener un mejor rendimiento.

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 en el modelo Azure Cosmos DB se pueden configurar con un tiempo 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; }
}

El IList y el IDictionary se pueden rellenar y persistir en la base de datos:

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.
  • Se ha agregado compatibilidad para realizar consultas en colecciones primitivas en EF Core 9.0.

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