How to easily start using CosmosDB in your C# application in no time with Cosmonaut
Nick Chapsas
Posted on May 7, 2018
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 SQL API of CosmosDB to allow flexible CRUD (and more) based on objects. Some might say that the SQL API SDK already does that but please keep readying and you'll see what I mean.
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 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
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.
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.
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 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 is 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.
More on indexing here.
Saving money
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")]
.
The only compromise is that your indexing cannot be very specific because you are sharing collections.
Something that i also enabled 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.
Also a star on the GitHub repo makes me feel warm inside.
Feedback is also hugely appreciated.
Posted on May 7, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
May 7, 2018