Migrate your application to use the Azure Cosmos DB .NET SDK v3

APPLIES TO: SQL API

Important

To learn about the Azure Cosmos DB .NET SDK v3, see the Release notes, the .NET GitHub repository, .NET SDK v3 Performance Tips, and the Troubleshooting guide.

This article highlights some of the considerations of upgrading your existing .NET application to the newer Azure Cosmos DB .NET SDK v3 for Core (SQL) API. Azure Cosmos DB .NET SDK v3 corresponds to the Microsoft.Azure.Cosmos namespace. You can use the information provided in this doc if you are migrating your application from any of the following Azure Cosmos DB .NET SDKs:

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

The instructions in this article also help you to migrate the following external libraries that are now part of the Azure Cosmos DB .NET SDK v3 for Core (SQL) API:

  • .NET change feed processor library 2.0
  • .NET bulk executor library 1.1 or greater

What's new in the .NET V3 SDK

The v3 SDK contains many usability and performance improvements, including:

  • Intuitive programming model naming
  • .NET Standard 2.0 **
  • Increased performance through stream API support
  • Fluent hierarchy that replaces the need for URI factory
  • Built-in support for change feed processor library
  • Built-in support for bulk operations
  • Mockable APIs for easier unit testing
  • Transactional batch and Blazor support
  • Pluggable serializers
  • Scale non-partitioned and autoscale containers

** The SDK targets .NET Standard 2.0 that unifies the existing Azure Cosmos DB .NET Framework and .NET Core SDKs into a single .NET SDK. You can use the .NET SDK in any platform that implements .NET Standard 2.0, including your .NET Framework 4.6.1+ and .NET Core 2.0+ applications.

Most of the networking, retry logic, and lower levels of the SDK remain largely unchanged.

The Azure Cosmos DB .NET SDK v3 is now open source. We welcome any pull requests and will be logging issues and tracking feedback on GitHub. We'll work on taking on any features that will improve customer experience.

Why migrate to the .NET v3 SDK

In addition to the numerous usability and performance improvements, new feature investments made in the latest SDK will not be back ported to older versions. The v2 SDK is currently in maintenance mode. For the best development experience, we recommend always starting with the latest supported version of SDK.

Major name changes from v2 SDK to v3 SDK

The following name changes have been applied throughout the .NET 3.0 SDK to align with the API naming conventions for the Core (SQL) API:

  • DocumentClient is renamed to CosmosClient
  • Collection is renamed to Container
  • Document is renamed to Item

All the resource objects are renamed with additional properties, which, includes the resource name for clarity.

The following are some of the main class name changes:

.NET v2 SDK .NET v3 SDK
Microsoft.Azure.Documents.Client.DocumentClient Microsoft.Azure.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

Classes replaced on .NET v3 SDK

The following classes have been replaced on the 3.0 SDK:

  • Microsoft.Azure.Documents.UriFactory

  • Microsoft.Azure.Documents.Document

  • Microsoft.Azure.Documents.Resource

The Microsoft.Azure.Documents.UriFactory class has been replaced by the fluent design. The fluent design builds URLs internally and allows a single Container object to be passed around instead of a DocumentClient, DatabaseName, and DocumentCollection.

Changes to item ID generation

Item ID is no longer auto populated in the .NET v3 SDK. Therefore, the Item ID must specifically include a generated ID. View the following example:

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

Changed default behavior for connection mode

The SDK v3 now defaults to Direct + TCP connection modes compared to the previous v2 SDK, which defaulted to Gateway + HTTPS connections modes. This change provides enhanced performance and scalability.

Changes to FeedOptions (QueryRequestOptions in v3.0 SDK)

The FeedOptions class in SDK v2 has now been renamed to QueryRequestOptions in the SDK v3 and within the class, several properties have had changes in name and/or default value or been removed completely.

FeedOptions.MaxDegreeOfParallelism has been renamed to QueryRequestOptions.MaxConcurrency and default value and associated behavior remains the same, operations run client side during parallel query execution will be executed serially with no-parallelism.

FeedOptions.EnableCrossPartitionQuery has been removed and the default behavior in SDK 3.0 is that cross-partition queries will be executed without the need to enable the property specifically.

FeedOptions.PopulateQueryMetrics is enabled by default with the results being present in the diagnostics property of the response.

FeedOptions.RequestContinuation has now been promoted to the query methods themselves.

The following properties have been removed:

  • FeedOptions.DisableRUPerMinuteUsage

  • FeedOptions.EnableCrossPartitionQuery

  • FeedOptions.JsonSerializerSettings

  • FeedOptions.PartitionKeyRangeId

  • FeedOptions.PopulateQueryMetrics

Constructing a client

The .NET SDK v3 provides a fluent CosmosClientBuilder class that replaces the need for the SDK v2 URI Factory.

The following example creates a new CosmosClientBuilder with a strong ConsistencyLevel and a list of preferred locations:

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

Exceptions

Where the v2 SDK used DocumentClientException to signal errors during operations, the v3 SDK uses CosmosClientException, which exposes the StatusCode, Diagnostics, and other response-related information. All the complete information is serialized when ToString() is used:

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

Diagnostics

Where the v2 SDK had Direct-only diagnostics available through the ResponseDiagnosticsString property, the v3 SDK uses Diagnostics available in all responses and exceptions, which are richer and not restricted to Direct mode. They include not only the time spent on the SDK for the operation, but also the regions the operation contacted:

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

Some settings in ConnectionPolicy have been renamed or replaced:

.NET v2 SDK .NET v3 SDK
EnableEndpointRediscovery LimitToEndpoint - The value is now inverted, if EnableEndpointRediscovery was being set to true, LimitToEndpoint should be set to false. Before using this setting, you need to understand how it affects the client.
ConnectionProtocol Removed. Protocol is tied to the Mode, either it's Gateway (HTTPS) or Direct (TCP). Direct mode with HTTPS protocol is no longer supported on V3 SDK and the recommendation is to use TCP protocol.
MediaRequestTimeout Removed. Attachments are no longer supported.

Indexing policy

In the indexing policy, it is not possible to configure these properties. When not specified, these properties will now always have the following values:

Property Name New Value (not configurable)
Kind range
dataType String and Number

See this section for indexing policy examples for including and excluding paths. Due to improvements in the query engine, configuring these properties, even if using an older SDK version, has no impact on performance.

Session token

Where the v2 SDK exposed the session token of a response as ResourceResponse.SessionToken for cases where capturing the session token was required, because the session token is a header, the v3 SDK exposes that value in the Headers.Session property of any response.

Timestamp

Where the v2 SDK exposed the timestamp of a document through the Timestamp property, because Document is no longer available, users can map the _ts system property to a property in their model.

OpenAsync

For use cases where OpenAsync() was being used to warm up the v2 SDK client, CreateAndInitializeAsync can be used to both create and warm-up a v3 SDK client.

Using the change feed processor APIs directly from the v3 SDK

The v3 SDK has built-in support for the Change Feed Processor APIs, allowing you use the same SDK for building your application and change feed processor implementation. Previously, you had to use a separate change feed processor library.

For more information, see how to migrate from the change feed processor library to the Azure Cosmos DB .NET v3 SDK

Using the bulk executor library directly from the V3 SDK

The v3 SDK has built-in support for the bulk executor library, allowing you to use the same SDK for building your application and performing bulk operations. Previously, you were required to use a separate bulk executor library.

For more information, see how to migrate from the bulk executor library to bulk support in Azure Cosmos DB .NET V3 SDK

Code snippet comparisons

The following code snippet shows the differences in how resources are created between the .NET v2 and v3 SDKs:

Database operations

Create a database

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

Read a database by 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

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

Container operations

Create a container (Autoscale + Time to live with expiration)

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

Read container properties

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

Delete a container

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

Item and query operations

Create an item

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

Read all the items in a container

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

Query items

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

Delete an item

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

Next steps