Azure Durable Functions Approval process

  • Reading time:5 mins read

Azure Serverless Shopping Cart – Part 3

In the previous part we set up database with Entity Framework Code First approach. In this part we’ll look at Azure Durable Functions Approval process. Set timeout for approval. After 10 mins the status will change automatically. After Cart is saved we’ll wait for 10 minutes to approve the Order by sending request to our function. If the time will pass we’ll set special status to the Order. We’ll also extend Cart table with new properties.

Database migration

We want to track status of the Order. We need to extend Cart model with 2 properties: Status and Email. First lets create enum to handle Status of the Cart. Create the following structure of the folders: in the root add Shared, then in Shared add Models. Create new enum with following values.

namespace ShoppingCart.DurableFunction.Shared.Models
{
    public enum Status
    {
        New,
        Processing,
        Sent,
        Rejected
    }
}

Now we can modify Cart class:

using ShoppingCart.DurableFunction.Shared.Models;
using System.Collections.Generic;

namespace ShoppingCart.DurableFunction.DataAccess.Models
{
    public class Cart
    {
        public int Id { get; set; }

        public ICollection<CartProduct> Products { get; set; } = new List<CartProduct>();

        public Status Status { get; set; }

        public string Email { get; set; }
    }
}

It’s good practice to set maximum length on string properties. We’ll set maximum length for Email. Additionally in the database we would like to save Status as string, not integer. Lets modify AppDbContext. Add those lines at the end of the file.

using Microsoft.EntityFrameworkCore;
using ShoppingCart.DurableFunction.DataAccess.Models;
using ShoppingCart.DurableFunction.Shared.Models;
using System;
 
namespace ShoppingCart.DurableFunction
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
        }
 
        public DbSet<Product> Products { get; set; }
 
        public DbSet<Cart> Carts { get; set; }
 
        public DbSet<CartProduct> CartProducts { get; set; }
 
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>().HasKey(x => x.Id);
 
            modelBuilder.Entity<Cart>().HasKey(x => x.Id);
            modelBuilder.Entity<Cart>().Property(x => x.Status)
                .HasConversion(
                v => v.ToString(),
                v => (Status)Enum.Parse(typeof(Status), v));
            modelBuilder.Entity<Cart>().Property(x => x.Email).IsRequired().HasMaxLength(200);
 
            modelBuilder.Entity<CartProduct>().HasKey(x => x.Id);
            modelBuilder.Entity<CartProduct>().HasOne(x => x.Product).WithMany();
            modelBuilder.Entity<CartProduct>().HasOne(x => x.Cart).WithMany(x => x.Products);
 
            modelBuilder.Entity<Product>().HasData(
                new Product { Name = "A", Price = 1, Quantity = 100, Id = 1 },
                new Product { Name = "B", Price = 1.11M, Quantity = 50, Id = 2 },
                new Product { Name = "C", Price = 12.99M, Quantity = 25, Id = 3 }
                );
        }
    }
}

Now it’s time to add migration.

Add-Migration AddStatusAndEmailToCart

 

We need to add the new property – Email to the CartDTO class.

using System.Collections.Generic;

namespace ShoppingCart.DurableFunction.Models
{
    public class CartDTO
    {
        public IEnumerable<CartProductDTO> Products { get; set; } = new List<CartProductDTO>();

        public string Email { get; set; }
    }
}

Approval Flow

After customer send Order, administrator has 10 minutes to accept or reject the Order. For testing approval process we’ll use Postman. What we need to do is to set timer to 10 minutes and listen for event that set the Status of the Order. Lets modify Process.cs file. First I add two methods to handle different statuses.

[FunctionName("StatusProcessingCart")]
public async Task StatusProcessingCart([ActivityTrigger] int cartId, ILogger log)
{
    Cart cart = _context.Find<Cart>(cartId) ?? throw new ArgumentException($"Cart ({cartId}) not found.");

    cart.Status = Status.Processing;

    _context.Update(cart);
    await _context.SaveChangesAsync();
}

[FunctionName("StatusRejectedCart")]
public async Task StatusRejectedCart([ActivityTrigger] int cartId, ILogger log)
{
    Cart cart = _context.Find<Cart>(cartId) ?? throw new ArgumentException($"Cart ({cartId}) not found.");
    cart.Status = Status.Rejected;

    _context.Update(cart);
    await _context.SaveChangesAsync();
}

Lets use them.

[FunctionName("Process")]
public async Task<CartDTO> RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var input = context.GetInput<CartDTO>();
    int cartId = await context.CallActivityAsync<int>(nameof(SaveCart), input);

    using (var cts = new CancellationTokenSource())
    {
        DateTime timer = context.CurrentUtcDateTime.AddMinutes(10);
        Task timeout = context.CreateTimer(timer, cts.Token);
        Task<bool> orderConfirmed = context.WaitForExternalEvent<bool>(ACCEPT_ORDER);

        if (orderConfirmed == await Task.WhenAny(orderConfirmed, timeout))
        {
            cts.Cancel();

            if (orderConfirmed.Result)
            {
                await context.CallActivityAsync(nameof(StatusProcessingCart), cartId);
            }
            else
            {
                await context.CallActivityAsync(nameof(StatusRejectedCart), cartId);
            }
        }
        else
        {
            await context.CallActivityAsync(nameof(StatusRejectedCart), cartId);
        }
    }

    return input;
}

 

And last thing is to modify SaveCart method to save Email.

[FunctionName("SaveCart")] 
public async Task<int> SaveCart([ActivityTrigger] CartDTO input, ILogger log) 
{ 
    log.LogInformation($"Saving cart."); 
    var cart = new Cart { Status = Status.New, Email = input.Email }; 
    var products = input.Products.Select(x => new CartProduct { Cart = cart, ProductId = x.ProductId, Quantity = x.Quantity }).ToList(); 
    cart.Products = products; 

    _context.Add(cart); 
    await _context.SaveChangesAsync(); 

    return cart.Id; 
}

Testing

Just like before we’ll use Postman. Body of the request change, because we added Email.

{ 
    "products": [ 
        { 
            "productId": 1, 
            "quantity": 2 
        }, 
        { 
            "productId": 2,
            "quantity": 3 
        } 
    ],
    "email": "[email protected]"
}

As response we’ll receive some links including “sendEventPostUri“. Click the link and modify {eventName} to AcceptOrder.

sendEventPostUri

Modify EventName to AcceptOrder

To call the event we need to POST data. Our function is expecting boolean.

Call event

Now you can check in database if the Status changed.

Source code available here

<< Previous part: Azure Functions with Entity Framework and Dependency Injection

 

References:

Subscribe
Notify of

0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments