Working with Azure Cosmos DB in your Azure Functions

willvelida

Will Velida

Posted on February 11, 2020

Working with Azure Cosmos DB in your Azure Functions

Developing Azure Functions that use Azure Cosmos DB as a data-store is quite simple to achieve. We can invoke our Azure Functions with a CosmosDB Trigger, we can use input and output bindings to get data to and from our Cosmos DB collections or we can use Azure Functions support for Dependency Injection to a singleton instance of our Cosmos DB Client for our Functions to use.

In this article, I’m going to discuss the variety of different ways that you can use Cosmos DB within your Azure Function apps, the advantages and disadvantages of each approach and show some code in the process. For the purposes of this article, I’ll be doing the code samples in C#.

To follow along with the code, please check out this repo on GitHub.

Using CosmosDB Triggers

We can use the CosmosDB Trigger to create event-driven functions that use the Cosmos DB Change Feed functionality to monitor changes on containers within our Cosmos DB databases.

With Cosmos DB Triggers, we can use the Change feed to perform actions on the items in our container and store the result of those actions in another container. We can also use the trigger to implement an archiving strategy for our data. For example, we could store hot data in Cosmos DB, use the change feed to listen to new items coming into that container and save it in Azure Storage, then delete the hot data in Cosmos DB.

Let’s use take a look at an example:



using System;
using System.Collections.Generic;
using ChangeFeedDemo.Functions.Helpers;
using ChangeFeedDemo.Functions.Models;
using Microsoft.Azure.Documents;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;

namespace ChangeFeedDemo.Functions
{
    public static class ChangeFeedListener
    {
        [FunctionName("ChangeFeedListener")]
        public static void Run([CosmosDBTrigger(
            databaseName: Constants.CosmosDBName,
            collectionName: Constants.CosmosCollectionName,
            ConnectionStringSetting = Constants.CosmosConnectionString,
            LeaseCollectionName = "LeaseCollection",
            CreateLeaseCollectionIfNotExists = true)]IReadOnlyList<Document> input, ILogger log)
        {
            if (input != null && input.Count > 0)
            {
                log.LogInformation("Documents modified " + input.Count);
                log.LogInformation("First document Id " + input[0].Id);
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

This is the simplest example of using a CosmosDB Trigger in a Function. We’re listening to a collection in a database, managing the lease of the Change Feed by specifying a collection for it and creating that collection if it doesn’t exist in our Binding.

Then when the Change Feed detects a change in the monitored container, we’re just logging how many documents were modified in that container, and what the id of the first document was. By default, the Change Feed polls the specified container every 5 seconds, but if you need to change that value, you can specify a different value in the binding.

If you wanted to take a further look at you can work with the Change Feed in Azure Functions, I wrote a more detailed post here.

Using Input Bindings

We can bind our Azure Functions to containers in Cosmos DB which will allow the Function to read data from that container when the function gets executed.

Let’s say we have a Serverless API that uses HTTP Triggers to invoke our Function, we could get a document stored in Azure Cosmos DB using an input binding.

Here’s a way that we could do it:



using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using GIBDemo.Core.Helpers;
using System.Collections.Generic;
using GIBDemo.Core.Models;

namespace GIBDemo.Triggers.Functions
{
    public static class ReadProductTrigger
    {
        [FunctionName(nameof(ReadProductTrigger))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "ReadProduct/{id}")] HttpRequest req,
            [CosmosDB(
                databaseName: Constants.COSMOS_DB_DATABASE_NAME,
                collectionName: Constants.COSMOS_DB_CONTAINER_NAME,
                ConnectionStringSetting = Constants.COSMOS_DB_CONNECTION_STRING,
                SqlQuery ="SELECT * FROM c WHERE c.id={id} ORDER BY c._ts DESC")] IEnumerable<Product> productItem,
            ILogger log,
            string id)
        {
            if (productItem == null)
            {
                return new NotFoundResult();
            }

            return new OkObjectResult(productItem);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

We can specify the SQL query that we want to run in our Cosmos DB input binding and return the result as a IEnumerable of our Product class. If we don’t get a result, we can return Not Found, otherwise in 16 lines of code, we can return the product item to our client.

Using Output Bindings

With output binds, we can connect to containers in Cosmos and write data to those containers.

Using our Serverless API as an example, this time we can use a Cosmos DB output binding to connect to our container to insert our new product items into.

Here’s an example:



using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using GIBDemo.Core.Helpers;
using GIBDemo.Core.Models;

namespace GIBDemo.Triggers.Functions
{
    public static class InsertProductTrigger
    {
        [FunctionName(nameof(InsertProductTrigger))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "InsertProductTrigger")] HttpRequest req,
            [CosmosDB(
                databaseName: Constants.COSMOS_DB_DATABASE_NAME,
                collectionName: Constants.COSMOS_DB_CONTAINER_NAME,
                ConnectionStringSetting = Constants.COSMOS_DB_CONNECTION_STRING)] IAsyncCollector<object> products,
            ILogger log)
        {
            try
            {
                string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

                var input = JsonConvert.DeserializeObject<Product>(requestBody);

                var product = new Product
                {
                    ProductName = input.ProductName,
                    ProductType = input.ProductType,
                    Price = input.Price,
                    Manufacturer = input.Manufacturer
                };

                await products.AddAsync(product);

                return new OkObjectResult(product);
            }
            catch (Exception ex)
            {
                log.LogError($"Couldn't insert item. Exception thrown: {ex.Message}");
                return new StatusCodeResult(StatusCodes.Status500InternalServerError);
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

All we are doing in our output binding is specifying which container we want to insert our item into, which database it lives in and the connection string to our Cosmos DB account.

As you can see, it’s pretty easy to build some simple applications using the Cosmos DB bindings, but it does come at a cost.

Disadvantages of Bindings

As of the time of me writing this, Azure Functions triggers and bindings only support the SQL API. If we want to use another Cosmos DB API in our Azure Functions, we’ll have to create a static client or as we’ll do next, create a Singleton instance of the client for the API that we’re using.

By default, the Cosmos DB bindings use version 2 of the .NET SDK. Meaning that if you want to use new V3 features such as Transaction Batches in your functions, then you’ll have to use Dependency Injection to create a v3 compatible client.

Using Dependency Injection

Support for dependency injection came with v2 of Azure Functions. It’s built on .NET Core Dependency Injection features. There are three service lifetimes that Dependency Injection in Functions provide:

  • Transient: These are created for each request of the service.
  • Scoped: This scope matches the execution lifetime of a function. They’re created once per execution.
  • Singleton: These lifetime of these match the lifetime of the host and is reused across all executions of functions on that instance. So instead of having a client for that connects to each binding, we can have a shared service for each function in our function app. This is the scope that we will use for our CosmosClient.

To implement dependency injection in our function, we’ll need to register our CosmosClient service in our Startup.cs file:



using GIBDemo.Core.Helpers;
using GIBDemo.DI.Helpers;
using Microsoft.Azure.Cosmos.Fluent;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

[assembly: FunctionsStartup(typeof(Startup))]
namespace GIBDemo.DI.Helpers
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.AddFilter(level => true);
            });

            var config = (IConfiguration)builder.Services.First(d => d.ServiceType == typeof(IConfiguration)).ImplementationInstance;

            builder.Services.AddSingleton((s) =>
            {
                CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder(config[Constants.COSMOS_DB_CONNECTION_STRING]);

                return cosmosClientBuilder.WithConnectionModeDirect()
                    .WithApplicationRegion("Australia East")
                    .WithBulkExecution(true)
                    .Build();
            });
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Here, I’m adding a Singleton instance of a CosmosClientBuilder, passing through my connection setting for my CosmosDB account. I then use the fluent API to add attributes such as setting the application region and enabling bulk execution.

Now that I have my Singleton instance, I can inject it into our Function:



using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Azure.Cosmos;
using GIBDemo.DI.Helpers;
using GIBDemo.Core.Models;
using GIBDemo.Core.Helpers;

namespace GIBDemo.DI.Functions
{
    public class InsertProduct
    {
        private readonly ILogger _logger;
        private readonly IConfiguration _config;
        private CosmosClient _cosmosClient;

        private Database _database;
        private Container _container;

        public InsertProduct(
            ILogger<InsertProduct> logger,
            IConfiguration config,
            CosmosClient cosmosClient
            )
        {
            _logger = logger;
            _config = config;
            _cosmosClient = cosmosClient;

            _database = _cosmosClient.GetDatabase(_config[Constants.COSMOS_DB_DATABASE_NAME]);
            _container = _database.GetContainer(_config[Constants.COSMOS_DB_CONTAINER_NAME]);
        }

        [FunctionName(nameof(InsertProduct))]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "InsertProduct")] HttpRequest req)
        {
            IActionResult returnValue = null;

            try
            {
                string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

                var input = JsonConvert.DeserializeObject<Product>(requestBody);

                var product = new Product
                {
                    ProductId = Guid.NewGuid().ToString(),
                    ProductName = input.ProductName,
                    ProductType = input.ProductType,
                    Manufacturer = input.Manufacturer,
                    Price = input.Price
                };

                ItemResponse<Product> item = await _container.CreateItemAsync(
                    product,
                    new PartitionKey(product.ProductType));

                _logger.LogInformation("Item inserted");
                _logger.LogInformation($"This query cost: {item.RequestCharge} RU/s");
                returnValue = new OkObjectResult(product);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Could not insert item. Exception thrown: {ex.Message}");
                returnValue = new StatusCodeResult(StatusCodes.Status500InternalServerError);
            }

            return returnValue;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Here I’m using constructor injection to make my dependencies available to my function. Notice here that I’m not using a static class for my function anymore, since this is required for constructor injection.

If you want to understand how dependency injection works in Azure Functions, this bit of documentation has a pretty straightforward guide.

Conclusion

I hope that after reading this, you have a good idea of what the implications are of using Bindings and Dependency Injection for working with Cosmos DB in your Functions.

My preference is to use a Singleton instance over the bindings, but like I said earlier, it depends on your use case.

I hope you enjoyed this article and found it useful. As always, if you have any questions please let me know in the comments!

💖 💪 🙅 🚩
willvelida
Will Velida

Posted on February 11, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related