迁移应用程序以使用 Azure Cosmos DB .NET SDK v3

适用范围: NoSQL

重要

若要了解 Azure Cosmos DB .NET SDK v3,请参阅发行说明.NET GitHub 存储库、.NET SDK v3 性能提示故障排除指南

本文重点介绍将现有 .NET 应用程序升级到较新 Azure Cosmos DB API for NoSQL .NET SDK v3 的一些注意事项。 Azure Cosmos DB .NET SDK v3 对应于Microsoft.Azure Cosmos DB 命名空间。 如果要从以下任何 Azure Cosmos DB .NET SDK 迁移应用程序,则可以使用此文档中提供的信息:

  • Azure Cosmos DB API for NoSQL .NET Framework SDK v2
  • Azure Cosmos DB API for NoSQL .NET Core SDK v2

本文中的说明还有助于迁移以下外部库,这些库现在是 Azure Cosmos DB API for NoSQL .NET SDK v3 的一部分:

  • .NET 更改源处理器库 2.0
  • .NET 批量执行工具库 1.1 或更高版本

.NET V3 SDK 的新增功能

v3 SDK 包含许多可用性和性能改进,包括:

  • 直观编程模型命名
  • .NET Standard 2.0 **
  • 通过流 API 支持提高了性能
  • 取代 URI 工厂需求的 fluent 层次结构
  • 对更改源处理器库的内置支持
  • 对批量操作的内置支持
  • 用于简化单元测试的 Mockable API
  • 事务性批处理和 Blazor 支持
  • 可插入序列化程序
  • 缩放非分区和自动缩放容器

**该 SDK 面向 .NET Standard 2.0,.NET Standard 2.0 将现有 Azure Cosmos DB .NET Framework 和 .NET Core SDK 统一到单个 .NET SDK 中。 可以在任何实现 .NET Standard 2.0 的平台(包括 .NET Framework 4.6.1+ 和 .NET Core 2.0+ 应用程序)中使用 .NET SDK。

大多数网络、重试逻辑和较低级别的 SDK 在很大程度上保持不变。

Azure Cosmos DB .NET SDK v3 现在为开源。 我们欢迎任何拉取请求,并将通过 GitHub 记录问题并跟踪反馈。我们将努力采用任何可以优化客户体验的功能。

为何要迁移到 .NET v3 SDK

除了大量的可用性和性能改进之外,最新 SDK 中的新功能投资将不会反向移植到较旧版本。 v2 SDK 目前处于维护模式。 如需获得最佳的开发体验,我们建议务必从最新的受支持的 SDK 版本入手。

主名称从 v2 SDK 更改为 v3 SDK

在整个 .NET 3.0 SDK 中应用了以下名称更改,以与 API for NoSQL 的 API 命名约定保持一致:

  • DocumentClient 已重命名为 CosmosClient
  • Collection 已重命名为 Container
  • Document 已重命名为 Item

所有资源对象都是用附加属性重命名的,为清楚起见,其中包含资源名称。

下面是一些主要的类名更改:

.NET v2 SDK .NET v3 SDK
Microsoft.Azure.Documents.Client.DocumentClient Microsoft.Azure.Cosmos.CosmosClient
Microsoft.Azure.Documents.Client.ConnectionPolicy Microsoft.Azure.Cosmos.CosmosClientOptions
Microsoft.Azure.Documents.Client.DocumentClientException Microsoft.Azure.Cosmos.CosmosException
Microsoft.Azure.Documents.Client.Database Microsoft.Azure.Cosmos.DatabaseProperties
Microsoft.Azure.Documents.Client.DocumentCollection Microsoft.Azure.Cosmos.ContainerProperties
Microsoft.Azure.Documents.Client.RequestOptions Microsoft.Azure.Cosmos.ItemRequestOptions
Microsoft.Azure.Documents.Client.FeedOptions Microsoft.Azure.Cosmos.QueryRequestOptions
Microsoft.Azure.Documents.Client.StoredProcedure Microsoft.Azure.Cosmos.StoredProcedureProperties
Microsoft.Azure.Documents.Client.Trigger Microsoft.Azure.Cosmos.TriggerProperties
Microsoft.Azure.Documents.SqlQuerySpec Microsoft.Azure.Cosmos.QueryDefinition

.NET v3 SDK 上被替换的类

在 3.0 SDK 上,以下类已被替换:

  • Microsoft.Azure.Documents.UriFactory

Microsoft.Azure.Documents.UriFactory 类已被替换为 fluent 设计。

Container container = client.GetContainer(databaseName,containerName);
ItemResponse<SalesOrder> response = await this._container.CreateItemAsync(
        salesOrder,
        new PartitionKey(salesOrder.AccountNumber));

  • Microsoft.Azure.Documents.Document

由于 .NET v3 SDK 允许用户配置自定义序列化引擎,因此不会直接替换 Document 类型。 使用 Newtonsoft.json(默认序列化引擎)时,可使用 JObject 来实现相同的功能。 使用其他序列化引擎时,可以使用其基本 json 文档类型(例如,System.Text.Json 中为 JsonDocument)。 建议使用反映项目架构的 C# 类型,而不是依赖泛型类型。

  • Microsoft.Azure.Documents.Resource

Resource 不能直接替换,如果用于文档,请遵循针对 Document 的指导。

  • Microsoft.Azure.Documents.AccessCondition

IfNoneMatchIfMatch 现已直接在 Microsoft.Azure.Cosmos.ItemRequestOptions 上提供。

对项 ID 生成的更改

.NET v3 SDK 中不再自动填充项 ID。 因此,项 ID 必须具体包括生成的 ID。 查看以下示例:

[JsonProperty(PropertyName = "id")]
public Guid Id { get; set; }

更改了连接模式的默认行为

SDK v3 现在默认为直接 + TCP 连接模式,而以前的 v2 SDK 默认为网关 + HTTPS 连接模式。 此更改增强了性能和可伸缩性。

对 FeedOptions 的更改(v3.0 SDK 中的 QueryRequestOptions)

SDK v2 中的 FeedOptions 类现已在 SDK v3 中被重命名为 QueryRequestOptions,在此类中,多个属性的名称和/或默认值已更改,或者已完全删除。

.NET v2 SDK .NET v3 SDK
FeedOptions.MaxDegreeOfParallelism QueryRequestOptions.MaxConcurrency - 默认值和关联行为保持不变,并行查询执行期间运行客户端的操作将以非并行方式串行执行。
FeedOptions.PartitionKey QueryRequestOptions.PartitionKey - 维护的行为。
FeedOptions.EnableCrossPartitionQuery 删除。 SDK 3.0 中的默认行为是将执行跨分区查询,而不需要专门启用属性。
FeedOptions.PopulateQueryMetrics 删除。 它现在默认处于启用状态,并且是诊断的一部分。
FeedOptions.RequestContinuation 删除。 它现在被提升为查询方法本身。
FeedOptions.JsonSerializerSettings 删除。 有关其他信息,请参阅如何自定义序列化
FeedOptions.PartitionKeyRangeId 删除。 使用 FeedRange 作为查询方法的输入可以获得相同结果。
FeedOptions.DisableRUPerMinuteUsage 删除。

构造客户端

.NET SDK v3 提供取代 SDK v2 URI 工厂需求的 fluent CosmosClientBuilder 类。

Fluent 设计在内部生成 URL,并支持传递单个 Container 对象,而不是 DocumentClientDatabaseNameDocumentCollection

下面的示例创建一个新 CosmosClientBuilder,其中包含强 ConsistencyLevel 和首选位置列表:

CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder(
    accountEndpoint: "https://testcosmos.documents.azure.cn:443/",
    authKeyOrResourceToken: "SuperSecretKey")
.WithConsistencyLevel(ConsistencyLevel.Strong)
.WithApplicationRegion(Regions.ChinaNorth);
CosmosClient client = cosmosClientBuilder.Build();

例外

v2 SDK 在操作过程中使用 DocumentClientException 标记错误,v3 SDK 使用 CosmosException,后者公开了 StatusCodeDiagnostics 和其他与响应相关的信息。 使用 ToString() 时,所有完整信息都会被序列化:

catch (CosmosException ex)
{
    HttpStatusCode statusCode = ex.StatusCode;
    CosmosDiagnostics diagnostics = ex.Diagnostics;
    // store diagnostics optionally with diagnostics.ToString();
    // or log the entire error details with ex.ToString();
}

诊断

v2 SDK 通过 RequestDiagnosticsString 属性提供“仅直接”诊断,而 v3 SDK 则使用所有响应和异常中皆可用的 Diagnostics,后者更加丰富且不局限于直接模式。 它们不仅包括操作在 SDK 上花费的时间,还包括操作接触的区域:

try
{
    ItemResponse<MyItem> response = await container.ReadItemAsync<MyItem>(
                    partitionKey: new PartitionKey("MyPartitionKey"),
                    id: "MyId");
    
    TimeSpan elapsedTime = response.Diagnostics.GetElapsedTime();
    if (elapsedTime > somePreDefinedThreshold)
    {
        // log response.Diagnostics.ToString();
        IReadOnlyList<(string region, Uri uri)> regions = response.Diagnostics.GetContactedRegions();
    }
}
catch (CosmosException cosmosException) {
    string diagnostics = cosmosException.Diagnostics.ToString();
    
    TimeSpan elapsedTime = cosmosException.Diagnostics.GetElapsedTime();
    
    IReadOnlyList<(string region, Uri uri)> regions = cosmosException.Diagnostics.GetContactedRegions();
    
    // log cosmosException.ToString()
}

ConnectionPolicy

ConnectionPolicy 中的某些设置已被重命名或替换为 CosmosClientOptions

.NET v2 SDK .NET v3 SDK
EnableEndpointDiscovery LimitToEndpoint - 该值现在已反转。如果 EnableEndpointDiscovery 设置为 true,则 LimitToEndpoint 应设置为 false。 在使用此设置之前,你需要了解它对客户端有何影响
ConnectionProtocol 删除。 协议与模式相关联,可以是网关 (HTTPS) 或直接 (TCP)。 V3 SDK 不再支持使用 HTTPS 协议的直接模式,建议使用 TCP 协议。
MediaRequestTimeout 删除。 附件不再受支持。
SetCurrentLocation 可以使用 CosmosClientOptions.ApplicationRegion 实现相同的效果。
PreferredLocations 可以使用 CosmosClientOptions.ApplicationPreferredRegions 实现相同的效果。
UserAgentSuffix 可以使用 CosmosClientBuilder.ApplicationName 实现相同的效果。
UseMultipleWriteLocations 删除。 SDK 会自动检测帐户是否支持多个写入终结点。

索引编制策略

根据索引策略,无法配置这些属性。 如果未指定,这些属性现在将始终具有以下值:

属性名称 新值(不可配置)
Kind range
dataType StringNumber

有关包含和排除路径的索引策略示例,请参阅此部分。 由于对查询引擎所作的改进,使得即使使用较旧的 SDK 版本,配置这些属性也不会影响性能。

会话令牌

在需要捕获会话令牌的情况下,由于会话令牌是一个标头,v2 SDK 会将响应的会话令牌作为 ResourceResponse.SessionToken 公开,而 v3 SDK 会将该值公开在任何响应的 Headers.Session 属性中。

时间戳

v2 SDK 通过 Timestamp 属性公开文档的时间戳,因为 Document 不再可用,用户可以将 _ts 系统属性映射到模型中的属性。

OpenAsync

对于使用 OpenAsync() 来预热 v2 SDK 客户端的用例,可使用 CreateAndInitializeAsyncOpenAsync() v3 SDK 客户端。

直接从 v3 SDK 使用更改源处理器 API

v3 SDK 具有对更改源处理器 API 的内置支持,支持使用同一 SDK 来生成应用程序和更改源处理器实现。 以前,必须使用单独的更改源处理器库。

有关详细信息,请参阅如何从更改源处理器库迁移到 Azure Cosmos DB .NET v3 SDK

更改源查询

在 v3 SDK 上执行更改源查询被视为使用更改源拉取模型。 请遵循下表迁移配置:

.NET v2 SDK .NET v3 SDK
ChangeFeedOptions.PartitionKeyRangeId FeedRange - 若要实现并行度,可以使用读取更改源 FeedRange。 这不再是一个必需的参数,你现在可以轻松读取整个容器的更改源
ChangeFeedOptions.PartitionKey FeedRange.FromPartitionKey - 可以使用表示所需分区键的 FeedRange 来读取该分区键值的更改源
ChangeFeedOptions.RequestContinuation ChangeFeedStartFrom.Continuation - 通过保存延续并在创建新的迭代器时使用该延续,可以随时停止和恢复更改源迭代器。
ChangeFeedOptions.StartTime ChangeFeedStartFrom.Time
ChangeFeedOptions.StartFromBeginning ChangeFeedStartFrom.Beginning
ChangeFeedOptions.MaxItemCount ChangeFeedRequestOptions.PageSizeHint - 通过保存延续并在创建新的迭代器时使用该延续,可以随时停止和恢复更改源迭代器。
IDocumentQuery.HasMoreResults response.StatusCode == HttpStatusCode.NotModified - 更改源在概念上是无限的,因此始终可能生成更多的结果。 如果响应包含 HttpStatusCode.NotModified 状态代码,表示此时没有可读取的新更改。 可以使用它来停止和保存延续,或者暂时进入睡眠状态或等待,然后再次调用 ReadNextAsync 来测试新更改。
拆分处理 用户在读取更改源时不再需要处理拆分异常,拆分将以透明方式处理,无需用户交互。

直接从 V3 SDK 使用批量执行工具库

v3 SDK 具有对批量执行工具库的内置支持,支持使用同一 SDK 来生成应用程序和执行批量操作。 以前,需要使用单独的批量执行工具库。

有关详细信息,请参阅如何从批量执行工具库迁移到 Azure Cosmos DB .NET V3 SDK 中的批量操作支持

自定义序列化

.NET V2 SDK 允许在用于反序列化结果文档的操作级别设置 RequestOptions 中的 JsonSerializerSettings:

// .NET V2 SDK
var result = await container.ReplaceDocumentAsync(document, new RequestOptions { JsonSerializerSettings = customSerializerSettings })

.NET SDK v3 提供了一个序列化程序接口,用于完全自定义序列化引擎;或者提供多个通用序列化选项,作为客户端构造的一部分。

通过使用流 API,可以在操作级别自定义序列化:

// .NET V3 SDK
using(Response response = await this.container.ReplaceItemStreamAsync(stream, "itemId", new PartitionKey("itemPartitionKey"))
{

    using(Stream stream = response.ContentStream)
    {
        using (StreamReader streamReader = new StreamReader(stream))
        {
            // Read the stream and do dynamic deserialization based on type with a custom Serializer
        }
    }
}

代码片段比较

以下代码片段显示了 .NET v2 和 v3 SDK 在资源创建方式上的差异:

数据库操作

创建数据库

// Create database with no shared provisioned throughput
DatabaseResponse databaseResponse = await client.CreateDatabaseIfNotExistsAsync(DatabaseName);
Database database = databaseResponse;
DatabaseProperties databaseProperties = databaseResponse;

// Create a database with a shared manual provisioned throughput
string databaseIdManual = new string(DatabaseName + "_SharedManualThroughput");
database = await client.CreateDatabaseIfNotExistsAsync(databaseIdManual, ThroughputProperties.CreateManualThroughput(400));

// Create a database with shared autoscale provisioned throughput
string databaseIdAutoscale = new string(DatabaseName + "_SharedAutoscaleThroughput");
database = await client.CreateDatabaseIfNotExistsAsync(databaseIdAutoscale, ThroughputProperties.CreateAutoscaleThroughput(4000));

按 ID 读取数据库

// Read a database
Console.WriteLine($"{Environment.NewLine} Read database resource: {DatabaseName}");
database = client.GetDatabase(DatabaseName);
Console.WriteLine($"{Environment.NewLine} database { database.Id.ToString()}");

// Read all databases
string findQueryText = "SELECT * FROM c";
using (FeedIterator<DatabaseProperties> feedIterator = client.GetDatabaseQueryIterator<DatabaseProperties>(findQueryText))
{
    while (feedIterator.HasMoreResults)
    {
        FeedResponse<DatabaseProperties> databaseResponses = await feedIterator.ReadNextAsync();
        foreach (DatabaseProperties _database in databaseResponses)
        {
            Console.WriteLine($"{ Environment.NewLine} database {_database.Id.ToString()}");
        }
    }
}

删除数据库

// Delete a database
await client.GetDatabase(DatabaseName).DeleteAsync();
Console.WriteLine($"{ Environment.NewLine} database {DatabaseName} deleted.");

// Delete all databases in an account
string deleteQueryText = "SELECT * FROM c";
using (FeedIterator<DatabaseProperties> feedIterator = client.GetDatabaseQueryIterator<DatabaseProperties>(deleteQueryText))
{
    while (feedIterator.HasMoreResults)
    {
        FeedResponse<DatabaseProperties> databaseResponses = await feedIterator.ReadNextAsync();
        foreach (DatabaseProperties _database in databaseResponses)
        {
            await client.GetDatabase(_database.Id).DeleteAsync();
            Console.WriteLine($"{ Environment.NewLine} database {_database.Id} deleted");
        }
    }
}

容器操作

创建容器(自动缩放 + 生存时间(包含到期时间))

private static async Task CreateManualThroughputContainer(Database database)
{
    // Set throughput to the minimum value of 400 RU/s manually configured throughput
    string containerIdManual = ContainerName + "_Manual";
    ContainerResponse container = await database.CreateContainerIfNotExistsAsync(
        id: containerIdManual,
        partitionKeyPath: partitionKeyPath,
        throughput: 400);
}

// Create container with autoscale
private static async Task CreateAutoscaleThroughputContainer(Database database)
{
    string autoscaleContainerId = ContainerName + "_Autoscale";
    ContainerProperties containerProperties = new ContainerProperties(autoscaleContainerId, partitionKeyPath);

    Container container = await database.CreateContainerIfNotExistsAsync(
        containerProperties: containerProperties,
        throughputProperties: ThroughputProperties.CreateAutoscaleThroughput(autoscaleMaxThroughput: 4000);
}

// Create a container with TTL Expiration
private static async Task CreateContainerWithTtlExpiration(Database database)
{
    string containerIdManualwithTTL = ContainerName + "_ManualTTL";

    ContainerProperties properties = new ContainerProperties
        (id: containerIdManualwithTTL,
        partitionKeyPath: partitionKeyPath);

    properties.DefaultTimeToLive = (int)TimeSpan.FromDays(1).TotalSeconds; //expire in 1 day

    ContainerResponse containerResponse = await database.CreateContainerIfNotExistsAsync(containerProperties: properties);
    ContainerProperties returnedProperties = containerResponse;
}

读取容器属性

private static async Task ReadContainerProperties(Database database)
{
    string containerIdManual = ContainerName + "_Manual";
    Container container = database.GetContainer(containerIdManual);
    ContainerProperties containerProperties = await container.ReadContainerAsync();
}

删除容器

private static async Task DeleteContainers(Database database)
{
    string containerIdManual = ContainerName + "_Manual";

    // Delete a container
    await database.GetContainer(containerIdManual).DeleteContainerAsync();

    // Delete all CosmosContainer resources for a database
    using (FeedIterator<ContainerProperties> feedIterator = database.GetContainerQueryIterator<ContainerProperties>())
    {
        while (feedIterator.HasMoreResults)
        {
            foreach (ContainerProperties _container in await feedIterator.ReadNextAsync())
            {
                await database.GetContainer(_container.Id).DeleteContainerAsync();
                Console.WriteLine($"{Environment.NewLine}  deleted container {_container.Id}");
            }
        }
    }
}

项和查询操作

创建项

private static async Task CreateItemAsync(Container container)
{
    // Create a SalesOrder POCO object
    SalesOrder salesOrder1 = GetSalesOrderSample("Account1", "SalesOrder1");
    ItemResponse<SalesOrder> response = await container.CreateItemAsync(salesOrder1,
        new PartitionKey(salesOrder1.AccountNumber));
}

private static async Task RunBasicOperationsOnDynamicObjects(Container container)
{
    // Dynamic Object
    dynamic salesOrder = new
    {
        id = "SalesOrder5",
        AccountNumber = "Account1",
        PurchaseOrderNumber = "PO18009186470",
        OrderDate = DateTime.UtcNow,
        Total = 5.95,
    };
    Console.WriteLine("\nCreating item");
    ItemResponse<dynamic> response = await container.CreateItemAsync<dynamic>(
        salesOrder, new PartitionKey(salesOrder.AccountNumber));
    dynamic createdSalesOrder = response.Resource;
}

读取容器中的所有项

private static async Task ReadAllItems(Container container)
{
    // Read all items in a container
    List<SalesOrder> allSalesForAccount1 = new List<SalesOrder>();

    using (FeedIterator<SalesOrder> resultSet = container.GetItemQueryIterator<SalesOrder>(
        queryDefinition: null,
        requestOptions: new QueryRequestOptions()
        {
            PartitionKey = new PartitionKey("Account1"),
            MaxItemCount = 5
        }))
    {
        while (resultSet.HasMoreResults)
        {
            FeedResponse<SalesOrder> response = await resultSet.ReadNextAsync();
            SalesOrder salesOrder = response.First();
            Console.WriteLine($"\n1.3.1 Account Number: {salesOrder.AccountNumber}; Id: {salesOrder.Id}");
            allSalesForAccount1.AddRange(response);
        }
    }
}

查询项

对 SqlQuerySpec(在 v3.0 SDK 中为 QueryDefinition)的更改

SDK v2 中的 SqlQuerySpec 类现已在 SDK v3 中重命名为 QueryDefinition

SqlParameterCollectionSqlParameter 已删除。 现已使用 QueryDefinition.WithParameter 通过生成器模型将参数添加到 QueryDefinition。 用户可以使用 QueryDefinition.GetQueryParameters 访问参数

private static async Task QueryItems(Container container)
{
    // Query for items by a property other than Id
    QueryDefinition queryDefinition = new QueryDefinition(
        "select * from sales s where s.AccountNumber = @AccountInput")
        .WithParameter("@AccountInput", "Account1");

    List<SalesOrder> allSalesForAccount1 = new List<SalesOrder>();
    using (FeedIterator<SalesOrder> resultSet = container.GetItemQueryIterator<SalesOrder>(
        queryDefinition,
        requestOptions: new QueryRequestOptions()
        {
            PartitionKey = new PartitionKey("Account1"),
            MaxItemCount = 1
        }))
    {
        while (resultSet.HasMoreResults)
        {
            FeedResponse<SalesOrder> response = await resultSet.ReadNextAsync();
            SalesOrder sale = response.First();
            Console.WriteLine($"\n Account Number: {sale.AccountNumber}; Id: {sale.Id};");
            allSalesForAccount1.AddRange(response);
        }
    }
}

删除项

private static async Task DeleteItemAsync(Container container)
{
    ItemResponse<SalesOrder> response = await container.DeleteItemAsync<SalesOrder>(
        partitionKey: new PartitionKey("Account1"), id: "SalesOrder3");
}

更改源查询

private static async Task QueryChangeFeedAsync(Container container)
{
    FeedIterator<SalesOrder> iterator = container.GetChangeFeedIterator<SalesOrder>(ChangeFeedStartFrom.Beginning(), ChangeFeedMode.Incremental);

    string continuation = null;
    while (iterator.HasMoreResults)
    {
        FeedResponse<SalesOrder> response = await iteratorForTheEntireContainer.ReadNextAsync();
    
        if (response.StatusCode == HttpStatusCode.NotModified)
        {
            // No new changes
            continuation = response.ContinuationToken;
            break;
        }
        else 
        {
            // Process the documents in response
        }
    }
}

后续步骤