Can Cosmos SDK3 Container be a singleton?

Viewed 3398

I would like to reduce load on Azure Cosmos DB SQL-API, which is called from a .NET Core Web API with dependency injection.

In App Insights, I have noticed that every call to the Web API results in GetDatabase and GetCollection calls to Cosmos which can take 5s to run when Cosmos is under heavy load.

I have made CosmosClient a singleton (e.g advice here - https://docs.microsoft.com/en-us/azure/cosmos-db/performance-tips-dotnet-sdk-v3-sql)

However I could not find any advice for whether the Database or Container objects could also be singletons so these are created for each request to the Web API.

I check for the existence of the database and collection (e.g. following advice here - https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.cosmosclient.getdatabase?view=azure-dotnet#remarks and https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.cosmosclient.getcontainer?view=azure-dotnet#remarks)

This means that for every request to the Web API, the following code is run

var databaseResponse = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(
    this.databaseConfiguration.DatabaseName,
    throughput: this.databaseConfiguration.DatabaseLevelThroughput);
var database = databaseResponse.Database;
var containerResponse = await database.CreateContainerIfNotExistsAsync(containerId, partitionKey);
var container = containerResponse.Container;

Can I make Database and Container singletons and add them to the DI to be injected like CosmosClient in order to reduce the number of calls to GetDatabase and GetCollection seen in App Insights?

3 Answers

According to the latest Microsoft documentation, you create a CosmosClient Service singleton, which owns the Containers you will be working with. In affect making the Containers singletons as well.

First, make your interface contract:

public interface ICosmosDbService
{
    // identify the database CRUD operations you need
}

Second, define your Service based on the contract:

public class CosmosDbService : ICosmosDbService
{
    private Container _container;

    public CosmosDbService(CosmosClient dbClient, string databaseName, string containerName)
    {
        this._container = dbClient.GetContainer(databaseName, containerName);
    }
    
    // your database CRUD operations go here using the Container field(s)
}

Third, create a method in your Startup class to return a CosmosClient:

private static async Task<CosmosDbService> InitializeCosmosClientAsync(IConfigurationSection cosmosConfig)
{
    var databaseName = cosmosConfig.GetSection("DatabaseName").Value;
    var containerName = cosmosConfig.GetSection("ContainerName").Value;
    var account = cosmosConfig.GetSection("Account").Value;
    var key = cosmosConfig.GetSection("Key").Value;
    var client = new Microsoft.Azure.Cosmos.CosmosClient(account, key);
    var database = await client.CreateDatabaseIfNotExistsAsync(databaseName);
    await database.Database.CreateContainerIfNotExistsAsync(containerName, "/id");
    return new CosmosDbService(client, databaseName, containerName);
}

Finally, add your CosmosClient to the ServiceCollection:

public void ConfigureServices(IServiceCollection services)
{
    var cosmosConfig = this.Configuration.GetSection("CosmosDb");
    var cosmosClient = InitializeCosmosClientAsync(cosmosConfig).GetAwaiter().GetResult();
    services.AddSingleton<ICosmosDbService>(cosmosClient);
}

Now your CosmosClient has only been created once (with all the Containers), and it will be reused each time you get it through Dependency Injection.

CreateDatabaseIfNotExistsAsync should only be called once as it is just a setup step for DB configuration.

You'd better create a DbService to persist the container object. And inject the DbService into each services instead of the DB client

You don't need to call CreateIfNotExistsAsync every time, if you know that they are available, you can use CosmosClient.GetContainer(dbName, containerName) which is a lightweight proxy class.

Unless you are expecting the database and containers to be deleted dynamically at some point?

Related