Поставщик EF Core для Azure Cosmos DB
Этот поставщик базы данных позволяет использовать Entity Framework Core с Azure Cosmos DB. Работы над этим поставщиком ведутся в рамках проекта Entity Framework Core.
Перед чтением этого раздела мы настоятельно рекомендуем ознакомиться с документацией по Azure Cosmos DB.
Примечание.
Этот поставщик работает только с Azure Cosmos DB для NoSQL.
Установка
Установите пакет 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
следует вызывать только во время развертывания, а не в рабочем режиме, так как это может вызвать проблемы с производительностью.
Параметры Azure Cosmos DB
Также можно настроить поставщик Azure 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));
});
Совет
Подробное описание каждого из указанных выше параметров см. в документации по параметрам 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 DB следует использовать тщательно выбранный ключ секции. Его можно настроить, вызвав 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();
}
Обычно рекомендуется добавить ключ секции в первичный ключ, так как это наилучшим образом отражает семантику сервера и позволяет выполнять некоторые оптимизации, например в 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);
});
Внедренные сущности
Примечание.
Связанные типы сущностей настроены как принадлежащие по умолчанию. Чтобы изменить это для конкретного типа сущности, вызовите ModelBuilder.Entity.
Для Azure Cosmos DB принадлежащие сущности внедряются в тот же элемент, что и владелец. Изменить имя свойства можно с помощью 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
Чтобы настроить тип сущности для использования оптимистического параллелизма, вызовите UseETagConcurrency. Этот вызов создает свойство _etag
в теневом состоянии и устанавливает его в качестве маркера параллелизма.
modelBuilder.Entity<Order>()
.UseETagConcurrency();
Чтобы упростить устранение ошибок параллелизма, можно сопоставить eTag со свойством CLR, используя IsETagConcurrency.
modelBuilder.Entity<Distributor>()
.Property(d => d.ETag)
.IsETagConcurrency();
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по