You can write your own custom Azure Function triggers.
I didn't know that this was a thing until very recently. This is amazing and it opens insane opportunities for some really cool ideas I have.
The thing I always wanted to do however was to create a Cosmonaut based trigger binding for Azure functions that has the same context as the CosmosStore. Quick recap for those who didn't know. Cosmonaut is a CosmosDB ORM for the SQL API which has a different unit of work than the official CosmosDB SDK. With the original SDK you get access to the Database Account level which is awesome because it gives you access to everything in the CosmosDB account. However in most applications that you are going to use the SQL API you don't need all that. All you need is for your object to have a place to live and be retrieved from. For that reason Cosmonaut's context is the
CosmosStore can be either a single collection or part of a shared collection. However, deleting everything or retrieving everything from a
CosmosStore in a shared collection won't return any other object type that co-exists in this shared collection but only the one you are specifying.
Read more about Cosmonaut here.
The Cosmos DB Change Feed and Azure Functions
One of Cosmos DB's best features is the Change Feed. Here is how it's described on the Cosmos DB documentation site.
Change feed support in Azure Cosmos DB works by listening to an Azure Cosmos DB container for any changes. It then outputs the sorted list of documents that were changed in the order in which they were modified. The changes are persisted, can be processed asynchronously and incrementally, and the output can be distributed across one or more consumers for parallel processing.
This means that with the change feed and the change feed processor library we can listen to document being added or being changed in our database and do something with this data as it happens. This is HUGE. I covered one of the usecases in the previous blog, which I highly recommend reading.
So as you probably know (well you did click to read a blog about the topic) Azure Functions support 2 types of special paramaters.
What I want to focus on is the first type. A trigger defines how you want an Azure Function to be invoked. For example the Cosmos DB trigger is using the change feed processor behind the scenes. This means that it will be invoked every time a document is created or updated in a collection.
The code for it looks like this:
As you can see it is using the
CosmosDBTrigger attribute alongside some configuration in order to specify where you want the document changes to come from. This is awesome and I love it. It allows for some really easy and painless implementations of every day use cases. (There are also other trigger types like service bus listening, HTTP triggers etc).
However the problem I have with that is that if my collection is using Cosmonaut in the application level, then the trigger won't give me POCO objects but documents, which I have to serialise and filter on my own.
That's where the CosmosStoreTrigger comes into place.
After realising that the code for those bindings and triggers is open source (God I love the new Microsoft) I rushed into it and started experimenting with creating a trigger that could use the same DTOs as the application that my consumers use and provide a simple way of consuming the change feed with POCO objects in a narrower context instead of the full document in the collection level.
Here what I came up with.
This code might look the same as the original CosmosDB trigger code but it works really differently.
First and foremost we no longer consume a
Document class but a
Llama class which is my POCO class. Deserialization happens behind the scenes so you don't have to deal with it. In this scenario our llamas live in a shared collection with my alpacas. They have different properties and different contexts. This means that if an
Alpaca object is added in the
stable collection I won't get notified in my
We also no longer have to specify collection name. This is coming by the POCO class' attributes. This simplifies everything because it means that you can move your contracts in a shared class and share it between your Azure Functions project and your app project. You can still override the collection name if you want to just like in Cosmonaut.
(All the configuration that the original Azure Function trigger has for Cosmos DB related to the leases is still there)
The leases are now created in a way that allows for automatic lease collection sharing and they look like this.
The format used is
This is more than enough to differentiate two or more Azure Functions running at the same time against the same collection.
In this scenario I have two Azure Functions running in parallel agains the same collection called
stable. This collections has Alpacas and Llamas. The image above showcases the complete separation on the two instances even though the host collection is shared. Each instance is notified only for the object that it's responsible for.
Sadly, there is a little bit of necessary ceremony that needs to take place before this can run. Because the object is now inferred from the generic type that the user provided in the
IReadOnlyList, I can no longer hide then startup configuration inside my function and I have to leave that to the user to do. This means that you need to create a Startup class with a single line, registering the trigger.
Here is my class:
There are two problems I currently have with this.
Firstly, Azure Functions are using reflection to find all the user defined
IWebJobsStartup implemented classes in order to configure it's service DI container. The problem with this is that because the Azure Function is a .NET Core 2.1 app and the Azure Function SDK is a .NET Standard library, the startup class won't be detected unless it is moved in a separate .NET Standard library that your Azure Function will use.
The second problem is that you cannot register the same trigger binding in the same function even if the generic type is different. This means that if I want to listen on changes for two different objects I have to do that in separate Azure Functions. In my opinion this should be addressed by the Azure Functions team, but for now I'll just have to deal with it.
Code and package
The Cosmonaut Azure Function trigger is published in Nuget and it's currently in preview so feel free to download it and try it out.
Install-Package Cosmonaut.WebJobs.Extensions -Version 1.0.0-preview1 dotnet add package Cosmonaut.WebJobs.Extensions --version 1.0.0-preview1