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.

14 comments

Discussion is closed. Login to edit/delete existing comments.

  • Rich Mercer 0

    This looks great. I’ve built something similar for my project, but one thing I ran into is that Get(string id) doesn’t enforce the type. For example, if there is a Foo with an ID of abc123, you can call Bar myBar = _defaultRepository.Get(“abc123”) and get back the data for Foo. Is this handled in this project? It really makes a mess when Foo and Bar have overlapping properties e.g. if they both have a Name property, it will populate those properties and you end up with a Frankenobject which makes a real mess when you Upsert it. 🙂

    • David PineMicrosoft employee 0

      Hi Rich, thank you for you positive feedback. I saw your pull request and suggested a slight modification. The

      GetAsync(Expression<Func<TItem, bool>> predicate)

      handles the type checking. There is an option that you can toggle on too for this SDK, that allows you to have a container per type – which will further help to ensure that there is less object typing contention. The option is ContainerPerItemType.

  • Archie Manukian 0

    Looking at this solution it seems like whichever solutions are being developed to implement Repository pattern on top of Azure Cosmos NET SDK are look in core alike. As well as Rich Mercer, for the project in our company, I have created a library on top of Azure Cosmos .NET SDK that implements Repository pattern. In addition to CRUD. (BTW, while using the object model seems fine, there is one of the limitation that makes almost impossible to renaming of the properties or object itself as every property is mapped to the property name in resulting document, but that is one of the limitations that we can live with or can have a workaround).
    To solve the problem that Rich Mercer mentioned as enforcing the type, I have a solution by introducing internal object that expand the model that we preserve in DB with property _type that is set to the name of the Type. So, that way every filtering on Get also include the parameter _type=’TypeNameOfObject’.
    That way you will be protected from querying the “wrong type” of the object.

    • David PineMicrosoft employee 0

      Hi Archie, thank you for posting these thoughts – they are much appreciated. Rich did bring up a great point, and I have been contemplating how alternatives to that concern.

  • Alexander Batishchev 0

    Hi David,
    Why one method has return type

    Task<TItem>

    and another

    ValueTask<IEnumerable<TItem>>

    ?

  • Thiago Silva 0

    @David Pine – how would you differentiate this from just using the EFCore provider for Cosmos DB? Seems like DbSet is a repository pattern implementation , so I am just trying to figure when to reach for one vs. the other?

    • David PineMicrosoft employee 0

      Hi @Thiago Silva, it’s different in that it is more lightweight in my opinion. I know someone who is using it over the EFCore provider and this is what they said about it, “it’s smaller, faster, and simpler by a wide margin”. This was their opinion too. The choice is yours and again this is just another tool. If you’re more familiar with EFCore already and happy with it, by all means use that one. If you want to give this one a try, then I encourage you to do so – but I am a bit biased 🙂

      • Thiago Silva 0

        👍

  • Daniel Jabłoński 0

    Hi David,

    Regardless of whether I run my own code or examples, I get an error (Azure Functions only):

    Microsoft.Azure.CosmosRepository: Cosmos Client options are required. (Parameter 'cosmosClientOptions').

    How can I set this parameter?

      • Daniel Jabłoński 0

        Hi David,

        Now it works like a charm 🙂

        Thx, take care.

Feedback usabilla icon