Azure Cosmos DB Repository .NET SDK v.1.0.4

David Pine

I’m excited to share the Azure Cosmos DB Repository .NET SDK. It’s an unofficial SDK that wraps the official Azure Cosmos .NET SDK. The Repository Pattern is a useful abstraction between your application’s data and the business logic layer. This has been a passion project of mine for a long time, dating four years back to “Document DB”!

Just another tool 🔧


This is not a replacement of the existing Azure Cosmos DB .NET SDK. Instead, it’s another tool in your developer toolbox.

The repository SDK is currently being used in production for The .NET Docs Show as part of .NET Live TV, and is open-source on GitHub:

https://github.com/IEvangelist/DotNetDocs.Show

While implementing the Repository Pattern, the SDK simplifies the consumption of Azure Cosmos DB by abstracting away some of the underlying complexities of the existing .NET SDK. There are always tradeoffs that you must consider. The repository SDK has a much smaller API surface area and is easier to get started with, whereas the proper SDK has many more capabilities and a much larger API surface area to learn.

The repository SDK exposes all the common data access operations (CRUD) you’d expect to find in a modern, generic Repository Pattern-based interface.

  • Create
  • Read
  • Update
  • Delete
public interface IRepository<TItem> where TItem : Item
{
    // Create
    ValueTask<TItem> CreateAsync(TItem value);
    Task<TItem[]> CreateAsync(IEnumerable values);

    // Read
    ValueTask<TItem> GetAsync(string id);
    ValueTask<IEnumerable<TItem>> GetAsync(
        Expression<Func<TItem, bool>> predicate);

    // Update
    ValueTask<TItem> UpdateAsync(TItem value);

    // Delete
    ValueTask DeleteAsync(TItem value);
    ValueTask DeleteAsync(string id);
}

You may have noticed the generic type constraint of Item. The Item object is a required base class, which automatically assigns a globally unique identifier (GUID) and manages partitioning. By targeting .NET Standard 2.0, the SDK can be consumed by any supported .NET implementation that aligns with .NET Standard 2.0. The SDK follows the common nomenclature and naming conventions established by .NET and supports configuration and dependency injection.

Get started

All you need to get started is an Azure Cosmos DB resource, connection string, and the Azure Cosmos DB Repository .NET SDK. Add Cosmos repository to your IServiceCollection instance passing the app’s IConfiguration:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCosmosRepository(Configuration);
}

An overload exposes the configuration options to manually configure your app:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCosmosRepository(Configuration,
        options =>
        {
            options.CosmosConnectionString = "< Your connection string >";
        });
}

Configuration

The only required configuration is the Cosmos DB connection string. Optionally, consumers can specify a database and container identifier. If these identifiers are not set, default values are used.

public class RepositoryOptions
{
    public string CosmosConnectionString { get; set; }

    public string DatabaseId { get; set; } = "database";
    public string ContainerId { get; set; } = "container";

    public bool OptimizeBandwidth { get; set; } = true;
    public bool ContainerPerItemType { get; set; } = false;
}

When OptimizeBandwidth is true (its default value), the repository SDK reduces networking and CPU load by not sending the resource back over the network and serializing it to the client. This is specific to writes, such as create, update, and delete. For more information, see Optimizing bandwidth in the Azure Cosmos DB .NET SDK.

There is much debate with how to structure your database and corresponding containers. Many developers with relational database design experience might prefer to have a single container per item type, while others understand that Azure Cosmos DB will handle things correctly regardless. By default, the ContainerPerItemType option is false and all items are persisted into the same container. However, when it is true, each distinct subclass of Item gets its own container named by the class itself. For example, an item defined as:

public class Foo : Item 
{
    // Omitted for brevity...
}

would be stored in a container named “Foo”. This could be useful for administrative purposes from the Azure portal and the Azure Cosmos DB resource Data Explorer.

Databases and containers do not have to exist prior to persisting to them. Again, only the connection string to the existing Azure Cosmos DB resource is required – databases and containers will be automatically created if they do not already exist.

Well-known keys

Depending on the .NET configuration provider your app is using, there are several well-known keys that map to the repository options that configure your usage of the repository SDK. When using environment variables, such as those in Azure App Service configuration or Azure Key Vault secrets, the following keys map to the RepositoryOptions instance:

Key Data type Default value
RepositoryOptions__CosmosConnectionString string null
RepositoryOptions__DatabaseId string "database"
RepositoryOptions__ContainerId string "container"
RepositoryOptions__OptimizeBandwidth boolean true
RepositoryOptions__ContainerPerItemType boolean false

If you’re using the JSON configuration provider, you map to the following JSON:

{
    "RepositoryOptions": {
        "CosmosConnectionString": "< your connection string >",
        "DatabaseId": "database",
        "ContainerId": "container",
        "OptimizeBandwidth": true,
        "ContainerPerItemType": false
}

Item objects

To use the IRespository<TItem>, you must first have at least one object that you want to persist. Any item that you want to store must be a subclass of the Item type. As an example, imagine the two following C# classes:

using System.Collections.Generic;

public class GameResult : Item
{
    public Name { get; set; }
    public GameOutcome Outcome { get; set; }
    public TimeSpan Duration { get; set; }
    public IEnumerable<Player> Players { get; set; }
}

public enum GameOutcome
{
    NotCompleted = 0,
    Tie,
    PlayerOneWins,
    PlayerTwoWins
}

Next, a simple Player object:

public class Player
{
    public Username { get; set; }
    public PlayerPosition Position { get; set; }
}

public enum PlayerPosition
{
    One, Two
}

Use the repository

The repository SDK comes dependency injection ready, meaning once it’s added to the service collection – you can request it to be injected where you need it. This is where the magic of generic dependency injection comes into fruition. Any subclass of Item gets its own corresponding IRepository<TItem> instance. Imagine you have a consuming service that is represented as a game engine class:

public class GameEngineService
{
    readonly IRepository<GameResult> _gameResultRepository;

    public GameEngineService(
        IRepository<GameResult> gameResultRepository) =>
        _gameResultRepository = gameResultRepository;
}

The same is true when using IRepository<TItem> instances in ASP.NET Core controllers, or background services – where dependency injection is common form.

Create

To create items, use either of the provided CreateAsync overloads. Consider the following code, which persists a single game result.

public async ValueTask<GameResult> CreateSingleGameResultExampleAsync()
{
    GameResult gameResult =
        await _gameResultRepository.CreateAsync(new GameResult
        {
            Name = "Chess",
            Outcome = GameOutcome.PlayerTwoWins,
            Duration = TimeSpan.FromMinutes(20),
            Players = new[]
            {
                new Player
                {
                    Username = "davidpine",
                    Position = PlayerPosition.Two
                },
                new Player
                {
                    Username = "bradygaster",
                    Position = PlayerPosition.One
                }
            }
        });

    return gameResult; // gameResult.Id = "7F8D7CA9-1434-4A0C-841A-94D59BF22121"
}

Read

Much like the create functionality, there are two overloads for reading items. Given the previous example, imagine the gameResult had an Id of “7F8D7CA9-1434-4A0C-841A-94D59BF22121” – you could read it by passing the identifier to the GetAsync function.

public async ValueTask ReadSingleGameResultExampleAsync()
{
    GameResult gameResult =
        await _gameResultRepository.GetAsync(
            "7F8D7CA9-1434-4A0C-841A-94D59BF22121");

    // Interact with result object
}

The other overload exposes the ability to express a predicate, much like Linq to SQL.

public async ValueTask ReadMultipleGameResultsExampleAsync()
{
    IEnumerable<GameResult> tiedGameResults =
        await _gameResultRepository.GetAsync(
            game => game.Outcome == GameOutcome.Tie);

    // Interact with tied result objects
}

In the preceding code example, the game repository would read all of the game result instances where the outcome of the game was a tie.

Update

To update an item, it must first be read into memory so that the Id member is hydrated. Mutations of the item occur, then you call UpdateAsync to persist all changes:

public async ValueTask UpdateGameResultsExampleAsync()
{
    GameResult gameResult =
        await _gameResultRepository.GetAsync(
            "7F8D7CA9-1434-4A0C-841A-94D59BF22121");

    gameResult.Outcome = GameOutcome.Tied;
    _ = await _gameResultRepository.UpdateAsync(gameResult);
}

In this case, the game result instance was the same as the one that was read out – it can be discarded.

Delete

The delete operations boast two overloads of DeleteAsync. One method takes the TItem instance to delete and the other takes the Id of the item to delete.

public async ValueTask DeleteSingleGameResultExampleAsync()
{
    await _gameResultRepository.DeleteAsync(
        "7F8D7CA9-1434-4A0C-841A-94D59BF22121");
}

Conclusion

As a reminder, the Azure Cosmos DB Repository .NET SDK is unofficial, but hopefully one day it will be adopted by the engineering team – and supported in an official capacity. For now, enjoy it as an open-source project from a passionate developer at Microsoft 🤘🏼.

Feel free to give it a star, fork it, post issues against it, or provide pull requests to improve it. I look forward to collaborating with you all and hope that you find value in this!

Scaling and partitioning


As long as your scale needs fit on a single physical partition (configured at 50 GB and 10 K RU/sec), partition keys do not matter. However, if you need more this SDK is not currently ideal.