Condividi tramite


Configurazione del modello con il provider EF Core di Azure Cosmos DB

Contenitori e tipi di entità

In Azure Cosmos DB, i documenti JSON vengono archiviati in contenitori. A differenza delle tabelle nei database relazionali, i contenitori di Azure Cosmos DB possono contenere documenti con forme diverse. Un contenitore non impone uno schema uniforme nei documenti. Tuttavia, diverse opzioni di configurazione vengono definite a livello di contenitore e pertanto influiscono su tutti i documenti contenuti all'interno di esso. Per altre informazioni, vedere la documentazione di Azure Cosmos DB sui contenitori .

Per impostazione predefinita, Entity Framework esegue il mapping di tutti i tipi di entità allo stesso contenitore; si tratta in genere di un buon valore predefinito in termini di prestazioni e prezzi. Il contenitore predefinito è denominato dopo il tipo di contesto .NET (OrderContext in questo caso). Per modificare il nome del contenitore predefinito, usare HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

Per eseguire il mapping di un tipo di entità a un contenitore diverso, usare ToContainer:

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

Prima di eseguire il mapping dei tipi di entità a contenitori diversi, assicurarsi di comprendere le potenziali implicazioni in termini di prestazioni e prezzi (ad esempio, per quanto riguarda la velocità effettiva dedicata e condivisa); per altre informazioni, vedere la documentazione di Azure Cosmos DB.

ID e chiavi

Azure Cosmos DB richiede che tutti i documenti abbiano una id proprietà JSON che li identifica in modo univoco. Analogamente ad altri provider di Entity Framework, il provider Azure Cosmos DB di Entity Framework tenterà di trovare una proprietà denominata Id o <type name>Ide di configurarla come chiave del tipo di entità, mappandola alla id proprietà JSON. Per maggiori informazioni, è possibile configurare qualsiasi proprietà come proprietà chiave usando HasKey. Per maggiori informazioni, consultare la documentazione generale EF sulle chiavi.

Gli sviluppatori che arrivano ad Azure Cosmos DB da altri database a volte prevedono che la proprietà chiave (Id) venga generata automaticamente. Ad esempio, in SQL Server EF configura le proprietà della chiave numerica come colonne IDENTITY, in cui i valori di incremento automatico vengono generati nel database. Al contrario, Azure Cosmos DB non supporta la generazione automatica delle proprietà e pertanto le proprietà chiave devono essere impostate in modo esplicito. L'inserimento di un tipo di entità con una proprietà chiave unset inserirà semplicemente il valore predefinito CLR per tale proprietà (ad esempio 0 per int) e un secondo inserimento avrà esito negativo; EF genera un avviso se si tenta di eseguire questa operazione.

Se si vuole avere un GUID come proprietà della chiave, è possibile configurare EF per generare valori casuali univoci nel client:

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

Chiavi di partizione

Azure Cosmos DB usa il partizionamento per ottenere il ridimensionamento orizzontale; la modellazione corretta e l'attenta selezione della chiave di partizione è fondamentale per ottenere buone prestazioni e ridurre i costi. È consigliabile leggere la documentazione di Azure Cosmos DB sul partizionamento e pianificare in anticipo la strategia di partizionamento.

Per configurare la chiave di partizione con Entity Framework, chiamare HasPartitionKey, passandola una proprietà regolare nel tipo di entità:

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

Qualsiasi proprietà può essere trasformata in una chiave di partizione, purché sia convertita in stringa. Una volta configurata, la proprietà della chiave di partizione deve avere sempre un valore non Null; il tentativo di inserire un nuovo tipo di entità con una proprietà della chiave di partizione non impostata genererà un errore.

Si noti che Azure Cosmos DB consente l'esistenza di due documenti con la stessa id proprietà in un contenitore, purché si trovano in partizioni diverse. Ciò significa che per identificare in modo univoco un documento all'interno di un contenitore, è necessario specificare tutte le proprietà della id chiave di partizione e . Per questo motivo, la nozione interna di Entity Framework della chiave primaria dell'entità contiene entrambi questi elementi per convenzione, a differenza dei database relazionali in cui non esiste alcun concetto di chiave di partizione. Ciò significa, ad esempio, che FindAsync richiede proprietà chiave e chiave di partizione (vedere altri documenti) e una query deve specificarli nella clausola Where per trarre vantaggio da un'efficienza e un'efficienza economica point reads.

Si noti che la chiave di partizione è definita a livello di contenitore. Ciò significa in particolare che non è possibile che più tipi di entità nello stesso contenitore abbiano proprietà diverse della chiave di partizione. Se è necessario definire chiavi di partizione diverse, eseguire il mapping dei tipi di entità pertinenti a contenitori diversi.

Chiavi di partizione gerarchiche

Azure Cosmos DB supporta anche chiavi di partizione gerarchiche per ottimizzare ulteriormente la distribuzione dei dati; per altri dettagli, vedere la documentazione. EF 9.0 ha aggiunto il supporto per le chiavi di partizione gerarchica; per configurare questi elementi, è sufficiente passare fino a 3 proprietà a HasPartitionKey:

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

Con una chiave di partizione gerarchica di questo tipo, le query possono essere inviate facilmente solo a un subset pertinente di sottopartizioni. Ad esempio, se si esegue una query per gli ordini di un tenant specifico, tali query verranno eseguite solo sulle sottopartizioni per tale tenant.

Se non si configura una chiave di partizione con Entity Framework, verrà registrato un avviso all'avvio; EF Core creerà contenitori con la chiave di partizione impostata su __partitionKeye non fornirà alcun valore per esso durante l'inserimento di elementi. Quando non è impostata alcuna chiave di partizione, il contenitore sarà limitato a 20 GB di dati, ovvero l'archiviazione massima per una singola partizione logica. Anche se questo può funzionare per applicazioni di sviluppo/test di piccole dimensioni, è consigliabile distribuire un'applicazione di produzione senza una strategia di chiave di partizione ben configurata.

Dopo aver configurato correttamente le proprietà della chiave di partizione, è possibile specificare i valori nelle query; Per altre informazioni, vedere Esecuzione di query con chiavi di partizione.

Discriminatori

Poiché è possibile eseguire il mapping di più tipi di entità allo stesso contenitore, EF Core aggiunge sempre una $type proprietà discriminatoria a tutti i documenti JSON salvati (questa proprietà è stata chiamata Discriminator prima di EF 9.0). Ciò consente a EF di riconoscere i documenti caricati dal database e di materializzare il tipo .NET corretto. Gli sviluppatori provenienti da database relazionali possono avere familiarità con i discriminatori nel contesto dell'ereditarietà TPH (Table-Per-Hierarchy Ereditarietà). In Azure Cosmos DB, i discriminatori vengono usati non solo negli scenari di mapping dell'ereditarietà, ma anche perché lo stesso contenitore può contenere tipi di documento completamente diversi.

I valori e il nome della proprietà discriminatori possono essere configurati con le API EF standard. Per maggiori informazioni, consultare questi documenti . Se si esegue il mapping di un singolo tipo di entità a un contenitore, si è certi che non si eseguirà mai il mapping di un altro tipo e si vuole eliminare la proprietà discriminatoria, chiamare HasNoDiscriminator:

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

Poiché lo stesso contenitore può contenere tipi di entità diversi e la proprietà JSON id deve essere univoca all'interno di una partizione contenitore, non è possibile avere lo stesso id valore per le entità di tipi diversi nella stessa partizione del contenitore. Confrontarlo con i database relazionali, in cui viene eseguito il mapping di ogni tipo di entità a una tabella diversa e quindi ha uno spazio chiave separato. È quindi responsabilità dell'utente garantire l'univocità id dei documenti inseriti in un contenitore. Se è necessario disporre di tipi di entità diversi con gli stessi valori di chiave primaria, è possibile indicare a EF di inserire automaticamente il discriminatorio nella id proprietà come indicato di seguito:

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

Anche se questo può semplificare l'uso dei id valori, può rendere più difficile l'interoperabilità con le applicazioni esterne che riguardano i documenti, poiché ora devono essere consapevoli del formato concatenato id di Entity Framework, nonché dei valori discriminatori, che sono derivati per impostazione predefinita dai tipi .NET. Si noti che questo è il comportamento predefinito prima di EF 9.0.

Un'opzione aggiuntiva consiste nell'indicare a EF di inserire solo il discriminatore radice, che è il discriminatore del tipo di entità radice della gerarchia, nella proprietà id:

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

Questa operazione è simile, ma consente a Entity Framework di usare letture di punti efficienti in più scenari. Se è necessario inserire un discriminante nella proprietà id, è consigliabile inserire il discriminante radice per ottenere prestazioni migliori.

Provisioning velocità effettiva

Se si usa EF Core per creare il database o i contenitori di Azure Cosmos DB, è possibile configurare la velocità effettiva con provisioning per il database chiamando CosmosModelBuilderExtensions.HasAutoscaleThroughput o CosmosModelBuilderExtensions.HasManualThroughput. Ad esempio:

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

Per configurare la velocità effettiva con provisioning per un contenitore chiamare CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput o CosmosEntityTypeBuilderExtensions.HasManualThroughput. Ad esempio:

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

Time-To-Live

I tipi di entità nel modello di Azure Cosmos DB possono essere configurati con una durata predefinita. Ad esempio:

modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);

In alternativa, per l'archivio analitico:

modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);

La durata per le singole entità può essere impostata usando una proprietà mappata a "ttl" nel documento JSON. Ad esempio:

modelBuilder.Entity<Village>()
    .HasDefaultTimeToLive(3600)
    .Property(e => e.TimeToLive)
    .ToJsonProperty("ttl");

Nota

Per ottenere alcun effetto, è necessario configurare un valore predefinito per il tipo di entità "ttl". Per maggiori informazioni, consultare la sezione Time to Live (TTL) in Azure Cosmos DB.

La proprietà time-to-live viene quindi impostata prima del salvataggio dell'entità. Ad esempio:

var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();

La proprietà time-to-live può essere una proprietà shadow per evitare di inquinare l'entità di dominio con problemi del database. Ad esempio:

modelBuilder.Entity<Hamlet>()
    .HasDefaultTimeToLive(3600)
    .Property<int>("TimeToLive")
    .ToJsonProperty("ttl");

La proprietà shadow time-to-live viene quindi impostata accedendo all'entità rilevata. Ad esempio:

var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();

Entità incorporate

Nota

I tipi di entità correlati sono configurati come di proprietà per impostazione predefinita. Per evitare questo problema per un di tipo di entità specifica, chiamare ModelBuilder.Entity.

Per Azure Cosmos DB, le entità con proprietario sono incorporate nello stesso elemento del proprietario. Per modificare il nome di una proprietà, usare 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 questa configurazione, l'ordine dell'esempio precedente viene archiviato come segue:

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

Anche le raccolte di entità di proprietà sono incorporate. Per l'esempio successivo verrà usata la classe Distributor con una raccolta di StreetAddress:

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

Non è necessario che le entità di proprietà forniscano valori di chiave espliciti da archiviare:

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

Verranno salvati in modo persistente in questo modo:

{
    "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, EF Core deve avere sempre valori di chiave univoca per tutte le entità rilevate. La chiave primaria creata per impostazione predefinita per le raccolte di tipi di proprietà è costituita dalle proprietà di chiave esterna che puntano al proprietario e da una proprietà int corrispondente all'indice nella matrice JSON. Per recuperare questi valori, è possibile usare l'API Entry:

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

Suggerimento

Quando necessario, è possibile modificare la chiave primaria predefinita per i tipi di entità di proprietà, ma i valori di chiave devono poi essere specificati in modo esplicito.

Raccolte di tipi primitivi

Le raccolte di tipi primitivi supportati, ad esempio string e int, vengono individuate e mappate automaticamente. Le raccolte supportate sono tutti i tipi che implementano IReadOnlyList<T> o IReadOnlyDictionary<TKey,TValue>. Si consideri ad esempio questo tipo di entità:

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

IList e IDictionary possono essere popolati e salvati in modo permanente nel database:

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

Si ottiene come risultato il documento JSON seguente:

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

Queste raccolte possono quindi essere aggiornate, sempre in modo normale:

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

await context.SaveChangesAsync();

Limitazioni:

  • Sono supportati solo i dizionari con chiavi stringa.
  • Il supporto per l'esecuzione di query in raccolte primitive è stato aggiunto in EF Core 9.0.

Concorrenza ottimistica con ETag

Per configurare un tipo di entità per l'uso della concorrenza ottimistica, chiamare UseETagConcurrency. Questa chiamata creerà una proprietà _etag con stato shadow e la imposterà come token di concorrenza.

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

Per semplificare la risoluzione degli errori di concorrenza, è possibile eseguire il mapping dell'ETag a una proprietà CLR usando IsETagConcurrency.

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