Поставщик EF Core для Azure Cosmos DB

Этот поставщик базы данных позволяет использовать Entity Framework Core с Azure Cosmos DB. Работы над этим поставщиком ведутся в рамках проекта Entity Framework Core.

Перед чтением этого раздела мы настоятельно рекомендуем ознакомиться с документацией по Azure Cosmos DB.

Примечание

Этот поставщик работает только с API SQL Azure Cosmos DB.

Установка

Установите пакет NuGet Microsoft.EntityFrameworkCore.Cosmos.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Начало работы

Совет

Вы можете скачать используемый в этой статье пример из репозитория GitHub.

Как и для других поставщиков, сначала нужно вызвать UseCosmos.

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

Предупреждение

Для простоты конечная точка и ключ здесь заданы в коде, но в рабочем приложении необходимо обеспечить их безопасное хранение.

В этом примере Order представляет собой простую сущность, ссылающуюся на 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; }
}

Сохранение данных и запрос на их получение выполняется, как обычно в EF:

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

Важно!

Путем вызова EnsureCreatedAsync вы создадите требуемые контейнеры и вставите начальные данные, если они есть в модели. Но EnsureCreatedAsync следует вызывать только во время развертывания, а не в рабочем режиме, так как это может вызвать проблемы с производительностью.

Параметры Cosmos

Можно также настроить поставщик Cosmos DB с помощью одной строки подключения и указать другие параметры для настройки подключения:

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

Примечание

Большинство этих параметров появились в EF Core 5.0.

Совет

Подробное описание каждого из указанных выше параметров см. в документации по параметрам Azure Cosmos DB.

Настройка моделей для Cosmos

По умолчанию все типы сущностей сопоставляются с одним и тем же контейнером с именем на основе производного контекста (в нашем примере "OrderContext"). Изменить имя контейнера по умолчанию можно с помощью HasDefaultContainer.

modelBuilder.HasDefaultContainer("Store");

Сопоставить тип сущности с другим контейнером можно с помощью ToContainer.

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

Чтобы определить тип сущности элемента, EF Core добавляет значение дискриминатора, даже если у элемента нет производных типов сущности. Имя и значение дискриминатора можно изменить.

Если в том же контейнере не будут храниться другие типы сущностей, дискриминатор можно удалить, вызвав HasNoDiscriminator.

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

Ключи секции

По умолчанию EF Core создаст контейнеры с ключом секции, равным "__partitionKey", не предоставляя для него значения при вставке элементов. Но чтобы полностью использовать возможности производительности Azure Cosmos, следует использовать тщательно выбранный ключ секции. Его можно настроить, вызвав HasPartitionKey.

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

Примечание

Свойство ключа секции может иметь любой тип, если он преобразован в строку.

После настройки свойство ключа секции всегда должно иметь значение, отличное от NULL. Запрос можно ограничить одной секцией, добавив вызов 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();
}

Примечание

WithPartitionKey появился в EF Core 5.0.

Обычно рекомендуется добавить ключ секции в первичный ключ, так как это наилучшим образом отражает семантику сервера и позволяет выполнять некоторые оптимизации, например в FindAsync.

Подготовленная пропускная способность

Если вы используете EF Core для создания базы данных или контейнеров Azure Cosmos DB, можно настроить подготовленную пропускную способность для базы данных с помощью вызова CosmosModelBuilderExtensions.HasAutoscaleThroughput или CosmosModelBuilderExtensions.HasManualThroughput. Пример:

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

Чтобы настроить подготовленную пропускную способность для контейнера, вызовите CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput или CosmosEntityTypeBuilderExtensions.HasManualThroughput. Пример:

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

Внедренные сущности

Примечание

Начиная с EF Core 6.0 связанные типы сущностей по умолчанию настраиваются как принадлежащие. Чтобы изменить это для конкретного типа сущности, вызовите ModelBuilder.Entity.

В Cosmos зависимые сущности внедряются в тот же элемент, что и владелец. Изменить имя свойства можно с помощью 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");
    });

В такой конфигурации порядок из приведенного выше примера сохраняется так:

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

Также внедряются коллекции зависимых сущностей. В следующем примере мы будем использовать класс Distributor с коллекцией StreetAddress:

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

Зависимым сущностям не нужно предоставлять значения ключа явным образом для хранения:

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

Они будут сохранены так:

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

В EF Core всегда должны присутствовать уникальные значения ключа для всех отслеживаемых сущностей. Первичный ключ, создаваемый по умолчанию для коллекций зависимых типов, состоит из свойств внешнего ключа, указывающих на владельца, и свойства int, соответствующего индексу в массиве JSON. Чтобы получить запись этих значений, можно использовать API:

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

Совет

При необходимости можно изменить первичный ключ по умолчанию для принадлежащих типов сущностей, а затем указать значения ключа явным образом.

Коллекции примитивных типов

Обнаружение и сопоставление коллекций поддерживаемых примитивных типов, таких как string и int, происходит автоматически. Все типы, реализующие IReadOnlyList<T> или IReadOnlyDictionary<TKey,TValue>, являются поддерживаемыми коллекциями. Например, рассмотрим следующий тип сущности:

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

И список, и словарь могут быть заполнены и вставлены в базу данных обычным способом:

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);
context.SaveChanges();

В результате будет получен следующий документ 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
}

Затем эти коллекции можно обновить обычным способом:

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

context.SaveChanges();

Ограничения

  • Поддерживаются только словари со строковыми ключами.
  • Запросы к содержимому примитивных коллекций в настоящее время не поддерживаются. Проголосуйте за проблемы 16926, 25700 и 25701, если эти функции важны для вас.

Работа с отключенными сущностями

Каждый элемент должен иметь значение id, уникальное для определенного ключа секции. По умолчанию EF Core создает значение, объединяя дискриминатор и значения первичного ключа с помощью символа | в качестве разделителя. Значения ключа создаются, только если сущность принимает состояние Added. Это может вызвать проблему при присоединении сущностей, если у них нет свойства id для типа .NET, чтобы хранить значение.

Чтобы обойти это ограничение, можно создать и задать значение id вручную или пометить сущность как добавленную первой, а затем придать ей нужное состояние:

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

В результате мы получим такой файл 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
}

Оптимистический параллелизм с тегами ETag

Примечание

Поддержка параллелизма eTag появилась в EF Core 5.0.

Чтобы настроить тип сущности для использования оптимистического параллелизма, вызовите UseETagConcurrency. Этот вызов создает свойство _etag в теневом состоянии и устанавливает его в качестве маркера параллелизма.

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

Чтобы упростить устранение ошибок параллелизма, можно сопоставить eTag со свойством CLR, используя IsETagConcurrency.

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