I've used a lot of document based NoSQL databases. CosmosDB is by far my favourite, mainly because it's much more than that. Things like it's scale-ability, the different API options, the change feed or even the emulator that Microsoft provides are amazing.

However, unless you are willing to dedicate a lot of time reading about it, the integration experience can be a nightmare for some. Things like it's pricing, it's performance, partition keys and indexing just to name a few, can be too much. On top of that you have to use the SDKs provided by Microsoft, which aren't bad at all but unless you know when to use which method, you will end up having bad performance or paying way more than what you should be.

Here's where Cosmonaut comes into play.
It is wrapper library around the .NET SDK of CosmosDB to allow flexible CRUD and querying based on objects. It adds ORM support, a more user friendly abstraction/interface to work with and features such as skip/take pagination and more.

Installation

Cosmonaut is published on Nuget.
You can install it from the Nuget browser or the command line

Install-Package Cosmonaut  
or  
dotnet add package Cosmonaut  

This package has all the core Cosmonaut functionality but if you want to also have .NET Standard DI support you can add the following package as well.

Install-Package Cosmonaut.Extensions.Microsoft.DependencyInjection  
or  
dotnet add package Cosmonaut.Extensions.Microsoft.DependencyInjection  

Once you add the package, integration can be as simple as adding the following line in your DI service collection.
serviceCollection.AddCosmosStore<YourObject>(cosmosSettings);

Once you do that you can get ICosmosStore<YourObject> from DI and you are ready to roll.
Alternatively, you can manually create a CosmosStore object.

CosmosStoreSettings have only 3 mandatory settings in it.

  • DatabaseName
  • AuthKey
  • EndpointUrl

There are more things that can be configured like the ConnectionPolicy or the IndexingPolicy but if they are not set they will default to the CosmosDB default values.

How to use

By default Cosmonaut will create/need one collection per object. However it also has logic for collection sharing between different objects. We will talk about this later.

For now all you need to know is that there is a single main restriction.

Your objects will NEED to tick one of the following checkboxes

  • Have a property of type string named Id
  • Implement the ICosmosEntity interface
  • Extend the CosmosEntity class
  • Have a property of type string with the attribute [JsonProperty("id")]

This is to ensure that your object can be stored, retrieved and updated in CosmosDB.

If you are planning to do any Select(x => x.Id) queries then you must have the [JsonProperty("id")] attribute OR extend the CosmosEntity class.

The name of the collection created by Cosmonaut (when the collection is missing) is generated in the following way. If the object has the CosmosCollection attribute then you can specify the name of the collection there. If not then a pluralized version of the object's name will be used instead. The attribute is also great if you want to add Cosmonaut in your existing CosmosDB collection.

The CosmosStore has the following methods for object manipulation:

  • AddAsync(TEntity entity) Adds an object in the CosmosDB collection.
  • UpdateAsync(TEntity entity) Updates an existing object in the CosmosDB collection.
  • UpsertAsync(TEntity entity) Updates an existing object in the CosmosDB collection or Adds it if it is not in the collection.
  • RemoveAsync(TEntity entity) Removed an object from the CosmosDB collection.

All of the above also have a Range method which allows the action to happen for a collection of items. RemoveAsync also supports expression removals based on a filter.

The operation responses also contain the ResourceResponse of the Document itself as well in order to allow for the retrieval of low level information.

When it comes to querying...

...you can simply call the .Query() method and have a IQueryable ready to use. Keep in mind that at the query level CosmosDB only supports Where, Select and SelectMany. When you want to return the query you just built to a List or just get the first object you have two options.

You can use the LINQ method ToList() but this is a synchronous call that is not recommended. What you should do instead is to use one of the extension methods that come with Cosmonaut such as:

  • ToListAsync
  • CountAsync
  • FirstOrDefaultAsync
  • FirstAsync
  • SingleOrDefaultAsync
  • SingleAsync
  • MaxAsync
  • MinAsync

These methods will use the built int paging logic to ensure that you application doesn't get locked while Cosmonaut is retrieving documents for you.

As you can tell this gives you pretty much everything you need to get you started.

Partition Key

The partition key is one of the most important things you need to know about in CosmosDB. This blog won't explain exactly what it is and how it works but it will let you know how Cosmonaut works with it. More on partition keys here.

There are a couple of things you need to know about the partition key.

  • Once a collection is created without a partition key, you CANNOT add one.
  • Once a collection is created with a partition key, you CANNOT change it.

Collections in Cosmonaut will not add a partition key by default. However by using the [CosmosPartitionKey] attribute you can specify which property is your partition key property. This will be used to create the collection with the key if the collection isn't created yet.

Indexing

Indexing plays a big role when it comes to querying your document's properties.
By default a cosmosdb collection used to be created with the following collection rules.

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [
        {
            "path": "/*",
            "indexes": [
                {
                    "kind": "Range",
                    "dataType": "Number",
                    "precision": -1
                },
                {
                    "kind": "Hash",
                    "dataType": "String",
                    "precision": 3
                }
            ]
        }
    ],
    "excludedPaths": []
}

This is also not a blog explaining Indexing so i won't go in depth but what you need to know is that you cannot partially query strings if the kind is Hash. You can only exact match them. Cosmonaut allows you to override that at the settings level. Changing the Hash to Range would allow things like StartsWith to match the data you want.

Example: If the String datatype is Hash then exact matches like the following, cosmoStore.Query().FirstOrDefaultAsync(x => x.SomeProperty.Equals($"Nick Chapsas") will return the item if it exists in CosmosDB but cosmoStore.Query().FirstOrDefaultAsync(x => x.SomeProperty.StartsWith($"Nick Ch") will throw an error. Changing the Hash to Range will make the latter work.

However you can also override this at the query level aswell by just changing the EnableScanInQuery in FeedOptions to true.

Update: The default indexing has changed to be Range across both numbers and strings.

More on indexing here.

Pagination

Cosmonaut supports two types of pagination.

  • Page number + Page size
  • ContinuationToken + Page size

Both of there methods work by adding the .WithPagination() method after you used any of the Query methods.

var firstPage = await booksStore.Query().WithPagination(1, 10).OrderBy(x=>x.Name).ToListAsync();  
var secondPage = await booksStore.Query().WithPagination(2, 10).OrderBy(x => x.Name).ToPagedListAsync();  
var thirdPage = await booksStore.Query().WithPagination(secondPage.NextPageToken, 10).OrderBy(x => x.Name).ToPagedListAsync();  
var fourthPage = await thirdPage.GetNextPageAsync();  
var fifthPage = await booksStore.Query().WithPagination(5, 10).OrderBy(x => x.Name).ToListAsync();  

ToListAsync() on a paged query will just return the results. ToPagedListAsync() on the other hand will return a CosmosPagedResults object. This object contains the results but also a boolean indicating whether there are more pages after the one you just got but also the continuation token you need to use to get the next page.

Pagination recommendations

Because page number + page size pagination goes though all the documents until it gets to the requested page, it's potentially slow and expensive.
The recommended approach would be to use the page number + page size approach once for the first page and get the results using the .ToPagedListAsync() method. This method will return the next continuation token and it will also tell you if there are more pages for this query. Then use the continuation token alternative of WithPagination to continue from your last query.

Keep in mind that this approach means that you have to keep state on the client for the next query, but that's what you'd do if you where using previous/next buttons anyway.

Saving money with Collection sharing

I get it. RU/s are scary, but don't worry, Cosmonaut is designed to take that fear away.

You see, the way CosmosDB is charging you is hourly PER collection. However if you change your RU/s in an hour for even a second then you will be charged 1 hours worth of whatever the highest RU/s for that hour was.

This can get out of hand and not every collection needs to be separated from the other. Keep in mind this is a schema-less database so why not share collections.

Well Cosmonaut has built in support for collection sharing.
All you need to do to reliably share collections without messing up your operations are two things.

  • Decorate your object with the SharedCosmosCollection attribute.
  • Implement the ISharedCosmosEntity interface.

You will also need to specify the name of the shared collection that this object will be using like that [SharedCosmosCollection("shared")].

Once you have a shared collection up and running, everything that happens in the CosmosStore of this part of the collection stays in the CosmosStore. This means that if you do a select * from c you will only get the entities in that part of the collection back, not every document in the collection. It also means that if you delete everything from the CosmosStore you only delete part of the shared collection and not everything in the collection.

You can find the full documentation with more info on everything we talked about in this blog here.

Code

Cosmonaut is open source on Github under the MIT license.

It also supports a lot more features so I’d highly recommend you checking the Github README page.

Please consider giving it a try (or a star :D) and reporting any issues there.
Feedback is also hugely appreciated.