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.HasAutoscaleThroughputCosmosModelBuilderExtensions.HasManualThroughput 为数据库配置预配的吞吐量。 例如:

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

要为容器配置预配吞吐量,请调用 CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughputCosmosEntityTypeBuilderExtensions.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();
}

提示

必要时,可更改从属实体类型的默认主键,但应显式提供键值。

基元类型的集合

将自动发现和映射支持的基元类型(如 stringint)的集合。 支持的集合是所有实现 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 状态时才生成键值。 如果附加实体在 .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();