EF Core Azure Cosmos DB Provider
此数据库提供程序允许将 Entity Framework Core 与 Azure Cosmos DB 一起使用。 该提供程序作为 Entity Framework Core 项目的组成部分进行维护。
在阅读本部分之前,强烈建议先熟悉 Azure Cosmos DB 文档。
注意
此提供程序仅适用于 Azure Cosmos DB for NoSQL。
安装
安装 Microsoft.EntityFrameworkCore.Cosmos NuGet 包。
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
}
还嵌入了从属实体的集合。 对于下一个示例,我们将使用具有 StreetAddress
集合的 Distributor
类:
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 始终需要对所有被跟踪实体提供唯一键值。 默认情况下,为从属类型集合创建的主键包含指向所有者的外键属性和与 JSON 数组中的索引对应的 int
属性。 要检索这些值,可使用以下条目 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();
的限制:
使用断开连接的实体
每个项都需要具有一个对于给定分区键唯一的 id
值。 默认情况下 EF Core 通过使用 '|' 作为分隔符串联鉴别器和主键值来生成值。 仅当实体进入 Added
状态时才生成键值。 如果附加实体在 .NET 类型上没有用于存储值的 id
属性,则这可能会导致问题。
要解决此限制,可以手动创建并设置 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();
为了更轻松地解决并发性错误,可使用 IsETagConcurrency 将 eTag 映射到 CLR 属性。
modelBuilder.Entity<Distributor>()
.Property(d => d.ETag)
.IsETagConcurrency();
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈