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.
Hi David,
Regardless of whether I run my own code or examples, I get an error (Azure Functions only):
How can I set this parameter?
Hi Daniel, thank you for posting this. I have fixed this bug, and the latest version will have resolve this issue.
https://www.nuget.org/packages/IEvangelist.Azure.CosmosRepository/2.1.3 Thanks again, and by all means let me know how it works for you. Take care
Hi David,
Now it works like a charm
Thx, take care.
@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?
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....
Hi David,
Why one method has return type
and another
?
This is actively being discussed here: https://github.com/IEvangelist/azure-cosmos-dotnet-repository/issues/3. This was fixed in version 2.0.0 https://twitter.com/davidpine7/status/1314616874517368832?s=20
P.S. code syntax formatter is incapable of handling generics, malforms the body instead. Do you know who to report to this issue?
I’m not actually sure, but I did notice that too.
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...
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.
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...
Hi Rich, thank you for you positive feedback. I saw your pull request and suggested a slight modification. The
<code>
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.