This tutorials demonstrates how to create a Azure Durable functions to create an order processing workflow for confirming the order details placed by an e-commerce application and send confirmation or cancellation mail to the customer according to the status of the payment. Orchestrator function calls the CheckPaymentStatus
activity function to check the payment status. If payment status is Completed
, the orchestrator function calls a SendOrderToVendorQueue
to send the order details to the vendors queue in a storage account. After the order details are sent to the vendor-orders
queue the workflow executes a SendConfirmationMail
activity function to send an order confirmation mail to the customer. If the payment status is not Completed
the SendCancellationMail
function will be executed and it send a mail to the customer indicating the cancellation of order due to payment failure.
In this tutorial we are using Azure SQL database
, SendGrid
and Storage account Queue
services for the execution of the workflow.
- Azure Subscription
- .NET Core 3.1 SDK
- Visual Studio 2019 (Azure Development must be enabled during installation)
- Postman (REST api testing tool)
- SQL Server Management Studio
-
Open Azure Portal by navigating to https://portal.azure.com and login to the Azure account.
-
Create a new resource group with the name
AzureFunctionsGroup
. -
Create an
SQL Database
service instance by selectingCreate a resource > Databases > SQL Database
. -
Provide the following values to create the database and logical server.
Parameter Value Basic Resource Group AzureFunctionsGroup Location East US Database name eshopdb Server Name eshop-server Admin user labuser Password Password@123 Compute + Stroage General Purpose (Serverless) Networking Connectivity Method Public endpoint Allow Azure services and resources to access this server Yes Add current client IP address Yes Additional Settings Use existing data None Collation Do not modify. Leave the default value -
After the database server is created, go to the overview page and copy the
Server name
value. OpenSQL Server Management Studio
and connect using the Server name you have copied and the admin user name and password which you have used at the time of creating service. -
Open a new query window in
SQL Server Management Studio
and run the following query to create the payments table.USE eshopdb GO CREATE TABLE [dbo].[Payments]( [Id] [int] IDENTITY(1,1) NOT NULL, [OrderId] [int] NOT NULL, [PaymentMode] [varchar](25) NOT NULL, [Amount] [numeric](10, 2) NOT NULL, [PaymentStatus] [varchar](20) NOT NULL, CONSTRAINT [PK_Payments] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] GO
-
After the
Payments
table is created run the following queries to insert some sample records in to the table.INSERT INTO [dbo].[Payments] VALUES ( 101, 'Cash', 5000, 'Completed') INSERT INTO [dbo].[Payments] VALUES ( 102, 'Cash', 12300, 'Completed') INSERT INTO [dbo].[Payments] VALUES ( 103, 'CC', 7600, 'Pending') INSERT INTO [dbo].[Payments] VALUES ( 104, 'DC', 3200, 'Completed') INSERT INTO [dbo].[Payments] VALUES ( 105, 'Online', 4300, 'Pending') GO
-
Go to Azure Portal and copy the database connection string from the
Connection Strings
section of your database service and save into a text file. We will be using this while creating the Durable function. Replace{your_password}
with the password value you have used while creating the Database instance.
-
Open Azure Portal and create a new storge account service by choosing
Create a resource > Storage > Storage account
. -
Provide the following values to create the storage account.
Parameter Value Resource group AzureFunctionsGroup Storage account name eshopstorage Location East US Performance Standard Account Kind Storage V2 Replication LRS Access Tier Hot Connectivity Method Public endpoint -
Once the storage account is created, open the storage account and navigate to the Queues section.
-
Click on
+ Queue
to create a new queue. Provide the queue name asvendor-orders
and click OK.
-
Open Azure Portal and click on
Create a resource
. -
Search for
SendGrid
and click onCreate
to create a newSendGrid
service. -
Provide the following values to create the account
Parameter Value Resource group AzureFunctionsGroup Location East US Name EshopSendgrid Password Password@123 Pricing Tier Free First name <Your first name> Last name <Your last name> Email <Your email id> Company <Your company name> Website <Your company website> -
Once the SendGrid account is created, Click on the
Manage
button from the overview page. This will open the SendGrid account console page in new tab. -
On the left pane, select
Settings
and click on theAPI Keys
to create a new API key for your application. -
Click in the
Create API Key
button and provide a name for the API key. ChooseFull Access
from theAPI Key Permissions
list and click onCreate and View
. -
Copy the API key in to a text file which we need to configure in out Durable functions application.
-
Open Azure Portal and click on
create a resource
and search forFunction App
. From the list chooseFunction App
and click Create. -
Provide the following details to create Function App:
Parameter Value Resource group AzureFunctionsGroup Region East US Name eshoporderprocesssor Publish Code Runtime stack .NET Core Version 3.1 Hosting Storage account Select the storage account created above Operating System Windows Plan Consumption Plan -
Click on create to create the Function App.
-
Open Visual Studio 2019 and select
Azure Functions
template to create a Azure Functions app project. Specify the Project name asOrderProcessingWorkflow
and select a project location. -
In the Trigger selection page, choose
Empty
and select Functions runtime asAzure Functions V3
. You can also selectStorage Emulator
as the storage account value. -
Once the project is created, open the solution explorer and right click on the project and choose
Add > New Azure Function
. In the dialog box, specify the function name asOrderProcessingFn
and click Add. -
From the Trigger selection dialog box choose
Durable Functions Orchestration
and click OK. -
The function will be generated with some default code. You can remove all the methods from the
OrderProcessingFn
class. -
In this tutorial, we are going to process and order reqeust sent from the e-commerce application. The application will send an Http POST reqeust with the order data to start the workflow. We need to create a model class to store the order informations. Create a new folder
Models
and add a new class file in to the folder. Name the class file asOrder.cs
. Add the following code to theOrder.cs
file.using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Text; namespace OrderProcessingWorkflow.Models { public class Order { [JsonProperty("id")] public int Id { get; set; } [JsonProperty("customerName")] public string CustomerName { get; set; } [JsonProperty("amount")] public double Amount { get; set; } [JsonProperty("orderDate")] public DateTime OrderDate { get; set; } [JsonProperty("deliveryDate")] public DateTime DeliveryDate { get; set; } [JsonProperty("email")] public string Email { get; set; } } }
-
Now, we are creating our Durable functions starter method. The starter method will be an
HttpTrigger
function. Copy the below code to theOrderProcessingFn
class.[FunctionName("OrderProcessor_Starter")] public static async Task<HttpResponseMessage> HttpStart( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req, [DurableClient] IDurableOrchestrationClient starter, ILogger log) { Order order = await req.Content.ReadAsAsync<Order>(); string instanceId = await starter.StartNewAsync<Order>("OrderProcessorWorkflowFn", order); log.LogInformation($"Started orchestration with ID = '{instanceId}'."); return starter.CreateCheckStatusResponse(req, instanceId); }
-
Add an Durable function orchestrator method to the class. Copy the below code below the Starter method.
[FunctionName("OrderProcessorWorkflowFn")] public static async Task<string> RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context) { // Will update the method soon }
-
Next, we need to create and activity function
CheckPaymentStatus
that checks the payment status of the current order received in the starter method. It uses thepayments
table in theeshopdb
batabase. To connect to the SQL database install theSystem.Data.SqlClient
nuget package by running the following command.Install-Package System.Data.SqlClient -Version 4.8.1
-
Open the
local.settings.json
file and add a new setting that provides the connection string value for theAzure SQL database
. Add a new setting with the keysqldb_connection
and the value as the the connection string you have copied and stored in the text file. -
Import the namespace
System.Data.SqlClient
to theOrderProcessingFn
class and copy the following code below the orchestrator function.[FunctionName("CheckPaymentStatus")] public static async Task<bool> CheckPaymentStatus([ActivityTrigger] int orderId, ILogger log) { var str = Environment.GetEnvironmentVariable("sqldb_connection"); using (SqlConnection connection = new SqlConnection(str)) { connection.Open(); var sql = $"select PaymentStatus from Payments where OrderId={orderId}"; using (SqlCommand command = connection.CreateCommand()) { command.CommandText = sql; object status = await command.ExecuteScalarAsync(); if (status != null) { string statusText = status.ToString(); if (statusText == "Completed") return true; else return false; } else { return false; } } } }
-
Inside the Orchestrator method add the following code that calls the
CheckPaymentStatus
activity function and returns true if payment is completed and false if not completed. If payment is completed we will add the order details to thevendor-orders
queue and send a confirmation mail to the customer. If payment is not completed we will send a cancellation mail to the customer. We can read the order object from thecontext
object using theGetInput()
method.Order order = context.GetInput<Order>(); var paymentCompleted = await context.CallActivityAsync<bool>("CheckPaymentStatus", order.Id); if (paymentCompleted) { // Send order to vendor-orders queue // Send confirmation mail to customer } else { // Send cancellation mail to customer }
-
To send the order details to the
vendor-details
queue in the storage account you need to install theMicrosoft.Azure.WebJobs.Extensions.Storage
nuget package to the application. Add the following code below theCheckPaymentStatus
activity function.[FunctionName("SendOrderToVendorQueue")] [return: Queue("vendor-orders", Connection = "AzureWebJobsStorage")] public static string SendOrderToVendorQueue([ActivityTrigger] Order order, ILogger log) { return JsonConvert.SerializeObject(order); }
[!TIP] Import the
Newtonsoft.Json
naemspace in the import section to use the JsonConvert class.[!NOTE] We assume that the
vendor-orders
queue is created in the default storage account for the Azure function. If you are using a different storage account, you need to create a key-value pair inlocal.settings.json
file with the connection string name and value. -
After the order is sent to the
vendor-orders
queue, you need to send a confirmation mail to customer. To send the mail from the application, we can use theSendGrid
service. Install theMicrosoft.Azure.WebJobs.Extensions.SendGrid
nuget package to the application. Openlocal.settings.json
file and add a new setting with the keysendgrid_key
and the sendgrid key which you have copied in text file as the value."sendgrid_key": "<YOUR_SENDGRID_SERVICE_KEY_HERE"
-
Add the following code below the
SendOrderToVendorQueue
function. This will add theSendConfirmationMail
andSendCancellationMail
functions to the class.[FunctionName("SendConfirmationMail")] public static async Task<bool> SendConfirmationMail([ActivityTrigger] Order order, ILogger log) { try { var authKey = Environment.GetEnvironmentVariable("sendgrid_key"); SendGridClient client = new SendGridClient(authKey); var from = new EmailAddress("[email protected]", "byteSTREAM Admin"); var subject = $"Your Order confirmed with order Id {order.Id}"; var to = new EmailAddress(order.Email, order.CustomerName); var htmlContent = $"Hi {order.CustomerName},<br/>" + $"Your order with Id {order.Id} for Rs {order.Amount}/- is confirmed by the seller. Your order will be " + $"delivered on {order.DeliveryDate.ToShortDateString()}. "; var message = MailHelper.CreateSingleEmail(from, to, subject, "", htmlContent); var response = await client.SendEmailAsync(message); return true; } catch (Exception ex) { log.LogInformation(ex.Message); return false; } } [FunctionName("SendCancellationMail")] public static async Task<bool> SendCancellationMail([ActivityTrigger] Order order, ILogger log) { try { var authKey = Environment.GetEnvironmentVariable("sendgrid_key"); SendGridClient client = new SendGridClient(authKey); var from = new EmailAddress("[email protected]", "byteSTREAM Admin"); var subject = $"Order cancelled. Order Id: {order.Id}"; var to = new EmailAddress(order.Email, order.CustomerName); var htmlContent = $"Hi {order.CustomerName},<br/>" + $"Your order with Id {order.Id} for Rs {order.Amount}/- is cancelled because the payment is " + $"not completed. You can try to place the order after sometime."; var message = MailHelper.CreateSingleEmail(from, to, subject, "", htmlContent); var response = await client.SendEmailAsync(message); return true; } catch (Exception ex) { log.LogInformation(ex.Message); return false; } }
-
Update the Orchestrator function to call the newly created activity methods
[FunctionName("OrderProcessorWorkflowFn")] public static async Task<string> RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context) { Order order = context.GetInput<Order>(); var paymentCompleted = await context.CallActivityAsync<bool>("CheckPaymentStatus", order.Id); if (paymentCompleted) { await context.CallActivityAsync("SendOrderToVendorQueue", order); await context.CallActivityAsync<bool>("SendConfirmationMail", order); return $"Order confirmation mail sent to {order.Email}"; } else { await context.CallActivityAsync<bool>("SendCancellationMail", order); return $"Order is not completed. Cancellation mail sent to {order.Email}"; } }
-
Press F5 to run and test the function locally.
[!TIP] While running locally, you will be using your storage emulator to store the messages in the queue. Ensure the
vendor-queues
queue is created in the storage emulator.
- To publish the application to Azure Function App, right click on the project and select
Publish
. - In the publish window, select
Azure > Azure Function App (Windows)
and click Next. - Choose the Azure subscription, Resource group and the Function app which you have created previously. Click Finish.
- In the Publish window, click on
Manage Azure App Service Settings
link. - In the dialog box you can see the list of app service settings with local and remote values. Find the keys
sqldb_connection
andsendgrid_key
and click on theInsert value from Local
button to copy the values from local to remote. Click OK. - Click on the publish button to publish the application to Azure Function App.