Here’s where Cosmonaut comes into play.

It is a wrapper library around the SQL API of CosmosDB, which allows flexible CRUD (and more) based on your POCO objects. Some might say that the SQL API SDK already does that, but please keep reading and you’ll see exactly why Cosmonaut is the better option out of the two.

Full disclosure: this library is designed and created by me. It’s opensource on Github under the MIT license. Suggestions and feedback are highly appreciated.

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

Once you add the package, integration can be as simple as picking one of the three options.

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 three mandatory settings in it:

DatabaseName

AuthKey

EndpointUrl

There are more things that can be configured, like the ConnectionPolicy or the IndexingPolicy , but if they’re 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

named Implement the ICosmosEntity interface

interface Extend the CosmosEntity class

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, without any problems.

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. This is because the internal LINQtoCosmosSQL provider will take the JsonProperty into account in order to dynamically create the expression.

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 very useful if you want to add Cosmonaut in your existing CosmosDB collection.

The CosmosStore has the following methods for object retrieval and manipulation:

AddAsync(TEntity entity) Adds an object in the CosmosDB collection

Adds an object in the CosmosDB collection UpdateAsync(TEntity entity) Updates an existing object in the CosmosDB collection

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

Updates an existing object in the CosmosDB collection or Adds it if it is not in the collection RemoveAsync(TEntity entity) Removes 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, in order to allow 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 are done with the logic of the query and you are ready to retrieve the data, you have two options.

You can use the LINQ method ToList() but this is a synchronous call that is NOT recommended (yeah, not even by Microsoft).

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-in paging logic to ensure your 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.

Do you like SQL?

Well, Cosmonaut also supports SQL querying and object deserialization. Just use one of the four SQL-related querying methods.

Partition Key

The partition key is one of the most important things you need to understand 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 keyscan can be found 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

In non-shared collections, Cosmonaut will not add a partition key by default. However, by using the [CosmosPartitionKey] attribute, you can specify which property is your partition key. 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 is created with the following collection rules:

This is also not a blog about Indexing itself, so I won’t go in depth but what you need to know is that you cannot query for partially matching strings or ordering using that field, if the index 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. Keep in mind, however, that this comes with the cost of more RUs for this query.

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 as well by just changing the EnableScanInQuery in FeedOptions to ‘true’.

More on indexing can be found here.

Saving money

I get it, RU/s are scary. They directly translate to money and performance. 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 one hour’s 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?

Cosmonaut has built in support for seamless 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

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")] .

The only compromise is that your indexing cannot be very specific because you are sharing collections.

Something that is also enforced is that if you are collection sharing then the id property will automatically become your partition key. There are two reasons that back up this choice:

You cannot add a partition key after the collection is created and it’s a shame to not have at least random partition distribution

You are not guaranteed to have any other common property between your documents

Code

Cosmonaut is open source on Github under the MIT license.

Please consider giving it a try and reporting any issues there. GitHub stars make my heart fly.

Feedback is also hugely appreciated.