How to easily start using CosmosDB in your C# application in no time with Cosmonaut
I’ve used a lot of document-based NoSQL databases. CosmosDB is by far my favourite, mainly because it’s much more than a simple NoSQL database. Things like the cosmic scale, 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 its pricing, its 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 you should. In addition, everything is internally sealed so unit testing is virtually impossible unless you are willing to do some really hacky stuff.
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 Cosmonautordotnet 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
namedId
- 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, 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 collectionUpdateAsync(TEntity entity)
Updates an existing object in the CosmosDB collectionUpsertAsync(TEntity entity)
Updates an existing object in the CosmosDB collection or Adds it if it is not in the collectionRemoveAsync(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 - 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.
Find more .NET awesomeness on my personal blog http://chapsas.com