Manage CosmosDB objects (Stored Procedure, Functions, Triggers …) with this NuGet package

In my Toss project, I decided to use CosmosDB as the main data store. Document-oriented Databases are fun to work with as you have to change the way you see the data and processing when compared to a relational database. NoSQL databases are often badly framed as “schemaless”, but there isn’t such thing as schemaless; the schema is just defined elsewhere, i.e., not on the database but on your application code.

There are 2 problems with this approach for the developers.

That’s why I created this package. It’ll help you to manage your database objects and schema evolution along with your application code on your repository and it’ll be applied whenever and wherever you want (on app startup mostly).

Reading the embedded resources

I chose to use embedded JS file in the assembly for object definitions because -

Reading the embedded ressource is fairly simple in C# and .NET Standard.

  1. //read all the migration embbeded in CosmosDB/
  2. Migrationsvarressources=migrationAssembly.GetManifestResourceNames().Where(r=>r.Contains(".CosmosDB.Migrations.")&&r.EndsWith(".js")).OrderBy(r=>r).ToList();
  3. //for each migrationforeach(varmigrationinressources)
  4. {
  5. stringmigrationContent;
  6. using(varstream = migrationAssembly.GetManifestResourceStream(migration)) {
  7. using(varreader = newStreamReader(stream)) {
  8. migrationContent = awaitreader.ReadToEndAsync();
  9. }
  10. }
  11. // do something}

Applying the migrations

I decided to implement the strategy pattern - for each type of objects there is one strategy, so the users will be able to implement their own strategies.

This piece of code goes on the “//do something” from the previous code sample.

  1. var parsedMigration = new ParsedMigrationName(migration);
  2. var strategy = strategies.FirstOrDefault(s => s.Handle(parsedMigration));
  3. if (strategy == null)
  4. {
  5. throw new InvalidOperationException(string.Format("No strategy found for migration '{0}", migration));
  6. }
  7. await client.CreateDatabaseIfNotExistsAsync(parsedMigration.DataBase);
  8. if (parsedMigration.Collection != null)
  9. {
  10. await client.CreateDocumentCollectionIfNotExistsAsync(UriFactory.CreateDatabaseUri(parsedMigration.DataBase.Id), parsedMigration.Collection);
  11. }
  12. await strategy.ApplyMigrationAsync(client, parsedMigration, migrationContent);

Strategy implementation

For each kind of object I want to handle, I have to create an implementation of the strategy. Here is the one for the triggers.

  1. internal class TriggerMigrationStrategy: IMigrationStrategy
  2. {
  3. public async Task ApplyMigrationAsync(IDocumentClient client, ParsedMigrationName migration, string content)
  4. {
  5. var nameSplit = migration.Name.Split('-');
  6. Trigger trigger = new Trigger() {
  7. Body = content, Id = nameSplit[2], TriggerOperation = (TriggerOperation) Enum.Parse(typeof(TriggerOperation), nameSplit[1]), TriggerType = (TriggerType) Enum.Parse(typeof(TriggerType), nameSplit[0])
  8. };
  9. await client.UpsertTriggerAsync(UriFactory.CreateDocumentCollectionUri(migration.DataBase.Id, migration.Collection.Id), trigger);
  10. }
  11. public bool Handle(ParsedMigrationName migration)
  12. {
  13. return migration.Type == "Trigger";
  14. }
  15. }
For the triggers, I need more information than the name, such as - type and operation. So, I created another convention for setting those on the file name.
{Type}-{Operation}-{Name}

Using the library

For using the library, you need to,

I’ll try later to automate the step 2 and 3 when installing the package.

Conclusion

This package was fun to build and design and I think I got something nice. Now, I have many features to do (read here the todo list) but before that, I have to set up the test the suit so the user will be reassured that the package is stable.

This article was published here.