Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application

Today, in this article we will discuss how to execute any server-side objects (like Stored Procedure, Triggers, or User-defined Functions) in Cosmos DB using Asp.Net Core Web application. Now, as we all know that we can create or develop stored procedures, triggers, or user-defined functions in a SQL-API based Cosmos DB Database. We can develop these server-side objects using JavaScript language since Cosmos DB provides integration on JavaScript languages and also allow transactional execution of JavaScript. So, first, we will discuss some basic concepts about Server-Side objects in Cosmos DB and then we will discuss how to integrate these using an Asp.Net Core application.
 

Advantages of Server-Side Programming

 
In SQL API Based Cosmos DB database, we can develop or implement three different types of Server-side objects; i.e. User-Defined Functions, Stored Procedures, and Triggers. In Cosmos DB Database, we can create these server-side objects as functions in JavaScript (ECMA-262). We can’t create or change any server-side objects using data-definition languages (DDL) just like RDBMS systems. Using JavaScript for defining Stored Procedure, Triggers, and User-defined functions (UDFs) in Cosmos DB, we can develop any type of rich application which provides the following benefits or advantages,
 
Procedural Logic
 
As one of the high-level languages, JavaScript can provide us with a rich and familiar interface to express business logic. We can perform a sequence of complex operations on the data.
 
Atomic Transactions
 
Azure Cosmos DB always assures that the database operations which are performed within a single stored procedure or a trigger are atomic. This atomic functionality lets an application combine related operations into a single batch so that either all of the operations succeed or none of them succeed.
 
Performance
 
Since Cosmos DB mainly used JSON based data objects which is internally mapped to the JavaScript language type system. This type of mapping provides us several optimizations like the lazy materialization of JSON documents in the buffer pool and making them available on-demand to the executing code.
 
Batching
 
You can group operations like inserts and submit them in bulk. The network traffic latency costs and the store overhead to create separate transactions are reduced significantly. Pre-compilation: Stored procedures, triggers, and UDFs are implicitly precompiled to the byte code format to avoid compilation cost at the time of each script invocation. Due to precompilation, the invocation of stored procedures is fast and has a low footprint.
 
Encapsulation
 
Stored procedures can be used to group logic in one place. Encapsulation adds an abstraction layer on top of the data, which enables you to evolve your applications independently from the data. This layer of abstraction is helpful when the data is schema-less and you don't have to manage to add additional logic directly into your application. The abstraction lets your keep the data secure by streamlining the access from the scripts.
 

Create an Asp.Net Core MVC Application to Store Data in Cosmos DB

 
So, first, we develop an Asp.Net Core MVC Application through which we will perform the normal CRUD based operation in the Cosmos DB.
 
Step 1 - Create Asp.Net Core MVC Application

Now open the Microsoft Visual Studio and Click on Create a New Project

In the Create New Project dialog box, select Asp.Net Core Web Application for C# and then Click on Next Button.
 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
 
In the Configure your new project window, provide the project name and then click on the Create button.
 
In the Create a New ASP.Net Core Web Application dialog, select Web Application (Model-View-Controller) and then click on Create Button.
 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
 
Now, Visual Studio creates a blank MVC application.
 
Now Click on the Solution Explorer and Select the Solution and then Right and choose Add --> New Project options.
 
In the New Project dialog box, select Class Library (.Net Core) Project Template and Click on Next Button.
 
Type the Project name as DataContextLibrary and then click on Ok Button. This project mainly contains all the operation codes related to the Cosmos DB connect and also operations code like Insert data, delete data, etc.
 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
 
Now similar to the above project add another Class Library (.Net Core) project with the name ModelClass which mainly contains all the Model or Entity class files.
 
Step 2 - Add NuGet Packages Related to Cosmos DB
 
In Solution Explorer, Select the DataContextLibrary project and right-click and select Manage Nuget Packages.
 
In the NuGet Package Manager, search Microsoft.Azure.CosmosDB and Install It. 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
 
Visual Studio will download and installs the Azure Cosmos DB Package and its dependencies.
 
Step 3 - Prepare the Asp.Net Core MVC Application
 
Now, add models, data context service, the views, and controllers to the Application. First, we will prepare the application to perform the basic CRUD operation with the help Cosmos DB library in .Net Core.
 
Select the ModelClass Project and add a new class called Product.cs and add the below class file,
  1. using MongoDB.Bson;  
  2. using MongoDB.Bson.Serialization.Attributes;  
  3. using Newtonsoft.Json;  
  4. using System;  
  5. using System.ComponentModel.DataAnnotations;  
  6. using System.ComponentModel.DataAnnotations.Schema;  
  7. using System.Text.Json.Serialization;  
  8.   
  9. namespace ModelClass  
  10. {  
  11.     public class Product  
  12.     {  
  13.         public Product()  
  14.         {  
  15.             CreatedDate = DateTime.Now;  
  16.         }  
  17.   
  18.         [BsonId]  
  19.         [JsonProperty("id")]  
  20.         public string Id { getset; }  
  21.   
  22.         [Key]  
  23.         public int ProductId { getset; }  
  24.   
  25.         [Required(ErrorMessage = "Please Enter Name")]  
  26.         [Column(TypeName = "varchar(50)")]  
  27.         [JsonProperty("name")]  
  28.         public string Name { getset; }  
  29.   
  30.         [Required(ErrorMessage = "Please Enter Description")]  
  31.         [Column(TypeName = "varchar(500)")]  
  32.         [JsonProperty("description")]  
  33.         public string Description { getset; }  
  34.   
  35.         [Required(ErrorMessage ="Please Enter Quantity")]  
  36.         [JsonProperty("quantity")]  
  37.         public int Quantity { getset; }  
  38.   
  39.         [JsonProperty("unitPrice")]  
  40.         public decimal UnitPrice { getset; }  
  41.   
  42.         [JsonProperty("totalStockPrice")]  
  43.         public decimal TotalStockPrice { getset; }  
  44.   
  45.         [JsonProperty("createdOn")]  
  46.         public DateTime CreatedDate { getset; }  
  47.     }  
  48. }  
Now select the DataContextLayer project and Create a New Folder called AppConfig.
 
Now add a new class file named AppConfiguration.cs under the AppConfig folder and add the below code,
  1. using Microsoft.Extensions.Configuration;  
  2. using System;  
  3. using System.Collections.Generic;  
  4. using System.IO;  
  5. using System.Text;  
  6.   
  7. namespace DataContextLayer.AppConfig  
  8. {  
  9.     public static class AppConfiguration  
  10.     {  
  11.         private static IConfiguration currentConfig;  
  12.   
  13.         public static void SetConfig(IConfiguration configuration)  
  14.         {  
  15.             currentConfig = configuration;  
  16.         }  
  17.   
  18.   
  19.         public static string GetConfiguration(string configKey)  
  20.         {  
  21.             try  
  22.             {  
  23.                 string connectionString = currentConfig.GetConnectionString(configKey);  
  24.                 return connectionString;  
  25.             }  
  26.             catch (Exception ex)  
  27.             {  
  28.                 throw (ex);  
  29.             }  
  30.             return "";  
  31.         }  
  32.   
  33.     }  
  34. }  
Now, add an Interface called ICosmosDBSQLDataContext.cs file and add the below code in that file,
  1. using ModelClass;  
  2. using System;  
  3. using System.Collections.Generic;  
  4. using System.Text;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace DataContextLayer.SQL  
  8. {  
  9.     public interface ICosmosDBSQLDataContext  
  10.     {  
  11.         Task<IEnumerable<Product>> GetProductsAsync(string query);  
  12.   
  13.         Task<Product> GetProductByIdAsync(string id);  
  14.   
  15.         Task<IEnumerable<Product>> GetAllProducts();  
  16.   
  17.         Task AddProductAsync(Product item);  
  18.   
  19.         Task UpdateProductAsync(string id, Product item);  
  20.   
  21.         Task DeleteProductAsync(string id);  
  22.     }  
  23. }  
Now add a class file called CosmosDBSQLDataContext.cs to implement the above interface class.
  1. using DataContextLayer.AppConfig;  
  2. using Microsoft.Azure.Cosmos;  
  3. using ModelClass;  
  4. using Newtonsoft.Json;  
  5. using System;  
  6. using System.Collections.Generic;  
  7. using System.Linq;  
  8. using System.Text;  
  9. using System.Threading.Tasks;  
  10.   
  11. namespace DataContextLayer.SQL.Implementations  
  12. {  
  13.     public class CosmosDBSQLDataContext : ICosmosDBSQLDataContext  
  14.     {  
  15.         private Container _container;  
  16.   
  17.         public string containerName { getset; }  
  18.   
  19.         public CosmosDBSQLDataContext(CosmosClient dbClient, string databaseName, string containerName)  
  20.         {  
  21.             this._container = dbClient.GetContainer(databaseName, containerName);  
  22.         } 
  23.   
  24.         public async Task AddProductAsync(Product item)  
  25.         {  
  26.             await this._container.CreateItemAsync<Product>(item, new PartitionKey(item.Id));  
  27.         }  
  28.   
  29.         public async Task UpdateProductAsync(string id, Product item)  
  30.         {  
  31.             await this._container.UpsertItemAsync<Product>(item, new PartitionKey(id));  
  32.         }  
  33.   
  34.         public async Task DeleteProductAsync(string id)  
  35.         {  
  36.             await this._container.DeleteItemAsync<Product>(id, new PartitionKey(id));  
  37.         }  
  38.   
  39.         public async Task<Product> GetProductByIdAsync(string id)  
  40.         {  
  41.             try  
  42.             {  
  43.                 ItemResponse<Product> response = await this._container.ReadItemAsync<Product>(id, new  
  44.                 PartitionKey(id));  
  45.                 return response.Resource;  
  46.             }  
  47.             catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)  
  48.             {  
  49.                 return null;  
  50.             }  
  51.         }  
  52.   
  53.         public async Task<IEnumerable<Product>> GetProductsAsync(string queryString)  
  54.         {  
  55.             var query = this._container.GetItemQueryIterator<Product>(new QueryDefinition(queryString));  
  56.             List<Product> results = new List<Product>();  
  57.             while (query.HasMoreResults)  
  58.             {  
  59.                 var response = await query.ReadNextAsync();  
  60.                 results.AddRange(response.ToList());  
  61.             }  
  62.             return results;  
  63.         }  
  64.   
  65.         public async Task<IEnumerable<Product>> GetAllProducts()  
  66.         {  
  67.             var query = this._container.GetItemQueryIterator<Product>();  
  68.             List<Product> results = new List<Product>();  
  69.             while (query.HasMoreResults)  
  70.             {  
  71.                 var response = await query.ReadNextAsync();  
  72.                 results.AddRange(response.ToList());  
  73.             }  
  74.             return results;  
  75.         }  
  76.   
  77.     }  
  78. }  
Now select the view folder under Asp.net Core MVC project and add the below views –
  • Add a List Product View (Index.cshtml)
  • Add a Create New Product View (Create.cshtml)
  • Add an Edit Product View (Edit.cshtml)
  • Add a Delete Product View (Delete.cshtml)
  • Add a Display Product details View (Details.cshtml)
Now click on the Controller folder and add a new Controller called ProductsController.cs file and add the below code in that file,
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5. using DataContextLayer;  
  6. using DataContextLayer.SQL;  
  7. using Microsoft.AspNetCore.Http;  
  8. using Microsoft.AspNetCore.Mvc;  
  9. using ModelClass;  
  10. using MongoDB.Bson;  
  11. using MongoDB.Driver;  
  12.   
  13.   
  14.   
  15. namespace CosmosDb_Demo_Crud_ServerObjects.Controllers  
  16. {  
  17.     public class ProductsController : Controller  
  18.     {  
  19.         private readonly ICosmosDBSQLDataContext _cosmosDBService;  
  20.   
  21.         public ProductsController(ICosmosDBSQLDataContext cosmosDBService)  
  22.         {  
  23.             _cosmosDBService = cosmosDBService;  
  24.         }  
  25.   
  26.         [ActionName("Index")]  
  27.         public async Task<IActionResult> Index()  
  28.         {  
  29.             return View(await _cosmosDBService.GetProductsAsync("SELECT * FROM c"));  
  30.         }  
  31.   
  32.         [ActionName("Create")]  
  33.         public ActionResult Create()  
  34.         {  
  35.             return View();  
  36.         }  
  37.   
  38.         [HttpPost]  
  39.         [ActionName("Create")]  
  40.         [ValidateAntiForgeryToken]  
  41.         public async Task<ActionResult> CreateAsync(Product item)  
  42.         {  
  43.             if (ModelState.IsValid)  
  44.             {  
  45.                 item.Id = Guid.NewGuid().ToString();  
  46.                 await _cosmosDBService.AddProductAsync(item);  
  47.                 return RedirectToAction("Index");  
  48.             }  
  49.             return View(item);  
  50.         }  
  51.   
  52.         [HttpPost]  
  53.         [ActionName("Edit")]  
  54.         [ValidateAntiForgeryToken]  
  55.         public async Task<ActionResult> EditAsync(Product item)  
  56.         {  
  57.             if (ModelState.IsValid)  
  58.             {  
  59.                 await _cosmosDBService.UpdateProductAsync(item.Id, item);  
  60.                 return RedirectToAction("Index");  
  61.             }  
  62.             return View(item);  
  63.         }  
  64.   
  65.         [ActionName("Edit")]  
  66.         public async Task<ActionResult> EditAsync(string id)  
  67.         {  
  68.             if (id == null)  
  69.             {  
  70.                 return BadRequest();  
  71.             }  
  72.             Product item = await _cosmosDBService.GetProductByIdAsync(id);  
  73.             if (item == null)  
  74.             {  
  75.                 return NotFound();  
  76.             }  
  77.             return View(item);  
  78.         }  
  79.   
  80.         [ActionName("Delete")]  
  81.         public async Task<ActionResult> DeleteAsync(string id)  
  82.         {  
  83.             if (id == null)  
  84.             {  
  85.                 return BadRequest();  
  86.             }  
  87.             Product item = await _cosmosDBService.GetProductByIdAsync(id);  
  88.             if (item == null)  
  89.             {  
  90.                 return NotFound();  
  91.             }  
  92.             return View(item);  
  93.         }  
  94.   
  95.         [HttpPost]  
  96.         [ActionName("Delete")]  
  97.         [ValidateAntiForgeryToken]  
  98.         public async Task<ActionResult> DeleteConfirmedAsync( string id)  
  99.         {  
  100.             await _cosmosDBService.DeleteProductAsync(id);  
  101.             return RedirectToAction("Index");  
  102.         }  
  103.   
  104.         [ActionName("Details")]  
  105.         public async Task<ActionResult> DetailsAsync(string id)  
  106.         {  
  107.             return View(await _cosmosDBService.GetProductByIdAsync(id));  
  108.         }  
  109.           
  110.     }  
  111. }  
Now open the appsettings.json file and add the below configuration key,
  1. "ConnectionStrings": {  
  2.     "CosmosDBAccount""https://xxxxxx.documents.azure.com:443/",  
  3.     "ConnectionKey""xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyy==",  
  4.     "DatabaseName""SampleDB",  
  5.     "ContainerName""Products"  
  6.   }  
In the above configuration setting, CosmosDBAccount and ConnectionKey are available in the Connection string section of the Cosmos DB Accounts in the Azure Portal.
 
Also, we provide the Database Name and Container Name as a key.
 
Now if the mentioned database exists, it will connect it otherwise application needs to create the database at the time of first time loading.
 
For that purpose, open the Startup.cs file and then add the below method which checks the existence of the database and if the database does not exist, it will create the database.
  1. private static async Task<CosmosDBSQLDataContext> InitializeCosmosClientInstanceAsync(IConfigurationSection configurationSection)  
  2.         {  
  3.             string databaseName = configurationSection.GetSection("DatabaseName").Value;  
  4.             string containerName = configurationSection.GetSection("ContainerName").Value;  
  5.             string account = configurationSection.GetSection("CosmosDBAccount").Value;  
  6.             string key = configurationSection.GetSection("ConnectionKey").Value;  
  7.             Microsoft.Azure.Cosmos.CosmosClient client = new Microsoft.Azure.Cosmos.CosmosClient(account, key);  
  8.             CosmosDBSQLDataContext cosmosDbService = new CosmosDBSQLDataContext(client, databaseName, containerName);  
  9.             Microsoft.Azure.Cosmos.DatabaseResponse database = await  
  10.             client.CreateDatabaseIfNotExistsAsync(databaseName);  
  11.             await database.Database.CreateContainerIfNotExistsAsync(containerName, "/id");  
  12.             return cosmosDbService;  
  13.         }  
Now inject the ICosmosDBSQLDataContext in the ConfigureServices().
  1. services.AddSingleton(_ => Configuration);  
  2. services.AddSingleton<ICosmosDBSQLDataContext>(InitializeCosmosClientInstanceAsync(Configuration.GetSection("ConnectionStrings")).GetAwaiter().GetResult());  
Now run the application and it will load the Product list UI.
 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
 
Now click on the Add button to add a new Product,
 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
After clicking on the Save Button, the newly added record will be displayed in the Product List view.
 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
Step 4 - Define UDFs in Cosmos DB and fetch the value
 
Now, open the Data Explorer in Azure Portal and create a User Defined Function (UDFs) which will convert the product rate in USD and return in the select statement.
  1. function getPriceInUSD(price, rate) {  
  2.     if (typeof price != 'number')  
  3.         throw new Error("Parameter must be numeric");  
  4.     if (price < 0)  
  5.         throw new Error("Parameter must be non-negative");  
  6.   
  7.     var finalPrice = price / rate;  
  8.     return finalPrice;  
  9. }  
Now in the ProductsController, make the below changes in the Index() –
  1. [ActionName("Index")]  
  2.         public async Task<IActionResult> Index()  
  3.         {  
  4.             return View(await _cosmosDBService.GetProductsAsync("SELECT c.id, c.ProductId, c.name, c.description, c.quantity, c.unitPrice,c.createdOn, udf.getPriceInUSD(c.unitPrice, 70) rateInUSD FROM c"));  
  5.         }  
Now to display the Price in USD, we need to add the below property in the Product.cs file –
  1. [JsonProperty("rateInUSD")]  
  2. public decimal RateInUSD { getset; }  
Add this property in the Index.cshtml file and then execute the projects.
 
Implement Stored Procedure, UDFs And Trigger In Cosmos DB Using ASP.NET Core Web Application
Step 5 - Create a Stored Procedure to Save the Data into Product Collection
 
Still now we implement the CRUD operation without using any server-side objects. Now, we will insert the data into the Product collection using the Stored Procedure. For that purpose, open the Data Explorer in Azure Portal and create a Stored Procedure that will insert the product data into the collections.
  1. function spInsertProductData(items) {  
  2.     var collection = getContext().getCollection();  
  3.     var collectionLinkInfo = collection.getSelfLink();  
  4.   
  5.     var count = 0;  
  6.     if (!items) throw new Error("The array is undefined or null.");  
  7.   
  8.     var numItems = items.length;  
  9.     if (numItems == 0) {  
  10.         getContext().getResponse().setBody(0);  
  11.         return;  
  12.     }  
  13.   
  14.     tryCreate(items[count], callback);  
  15.   
  16.     function tryCreate(item, callback) {  
  17.         var options = { disableAutomaticIdGeneration: false };  
  18.         var isAccepted = collection.createDocument(collectionLinkInfo, item, options, callback);  
  19.         if (!isAccepted) getContext().getResponse().setBody(count);  
  20.     }  
  21.   
  22.     function callback(err, item, options) {  
  23.         if (err) throw err;  
  24.         count++;  
  25.         if (count >= numItems) {  
  26.             getContext().getResponse().setBody(count);  
  27.         } else {  
  28.             tryCreate(items[count], callback);  
  29.         }  
  30.     }  
  31. }  
Now open the CosmosDBSQLDataContext.cs file and make the below changes in the AddProductAsync().
  1. public async Task AddProductAsync(Product item)  
  2.         {  
  3.             try  
  4.             {  
  5.                 string item1 = JsonConvert.SerializeObject(item);  
  6.                 List<Product> products = new List<Product>();  
  7.                 products.Add(item);  
  8.                 await this._container.Scripts.ExecuteStoredProcedureAsync<string>("spInsertProductData"new PartitionKey(item.Id.ToString()), new[] { products });  
  9.             }  
  10.             catch (Exception ex)  
  11.             {  
  12.                 throw ex;  
  13.             }  
  14.         }  
Now execute the application and try to insert a new product.
 
Step 6 - Create a Pre-Trigger to calculate and save the Stock Price value
 
Now, we need to create a trigger to manipulate the inserted data to perform either any special type of data validation or any business operation. As we know that Trigger can be two types – PreTrigger and Post Trigger. Now, in this example, we will create a Pre-trigger that will calculate the stock price and save it into the collections. For that purpose, open the Data Explorer in Azure Portal and create a Trigger which will calculate the stock price.
  1. function validateProductData() {  
  2.     var context = getContext();  
  3.     var request = context.getRequest();  
  4.   
  5.     // item to be created in the current operation  
  6.     var itemToCreate = request.getBody();  
  7.   
  8.     // validate properties  
  9.     if (!("timestamp" in itemToCreate)) {  
  10.         var ts = new Date();  
  11.         itemToCreate["timestamp"] = ts.getTime();  
  12.     }  
  13.   
  14.     //update Total Stock Amount  
  15.     var quantity = 0;  
  16.     var price = 0;  
  17.     var stockPrice = 0;  
  18.     if (("quantity" in itemToCreate)) {  
  19.         quantity = itemToCreate["quantity"];  
  20.     }  
  21.   
  22.     if (("unitPrice" in itemToCreate)) {  
  23.         price = itemToCreate["unitPrice"];  
  24.     }  
  25.   
  26.     if (quantity > 0 && price > 0) {  
  27.         stockPrice = quantity * price;  
  28.     }  
  29.     itemToCreate["totalStockPrice"] = stockPrice;  
  30.   
  31.     // update the item that will be created  
  32.     request.setBody(itemToCreate);  
  33. }  
Now open the CosmosDBSQLDataContext.cs file and make the below changes in the AddProductAsync().
  1. public async Task AddProductAsync(Product item)  
  2.         {  
  3.             await this._container.CreateItemAsync<Product>(item, new PartitionKey(item.Id), new ItemRequestOptions { PreTriggers = new List<string> { "validateProductData" } });  
  4.   
  5.         }  
Now execute the application and try to insert a new product and check either Stock Price is calculated or not for the newly added document.
 

Conclusion

 
In this article, we discuss how to store data using the Asp.Net Core application in Cosmos DB. Also, we discussed the steps related to the implementation of Server-Side Objects in Cosmos DB. Any suggestions or feedback or query related to this article are most welcome.