Azure Serverless Shopping Cart – Part 2
In the previous part we setup our local environment. In this part we’ll replace default code. We’ll use Entity Framework Code First to create the database. We will connect Azure Functions with Entity Framework and Dependency Injection.
Prerequisites
To work with Entity Framework we need to install some NuGet packages. Run following commands:
- We’ll use MSSQL database so,
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 3.1.5
- We’ll use migrations so,
Install-Package Microsoft.EntityFrameworkCore.Design -Version 3.1.5 Install-Package Microsoft.EntityFrameworkCore.Tools -Version 3.1.5
Version 3.1.5 is the newest version at this moment.
Create Entities
I made new folder DataAccess
. In this folder I made one more folder Models
. Here we’ll keep all classes related to database. Let’s create the entities.
Product will represent an item that User can buy. We’ll just use some basic properties. Quantity is the product’s amount in a warehouse.
namespace ShoppingCart.DurableFunction.DataAccess.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public int Quantity { get; set; } } }
Cart represent the list of Products to buy by User.
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>(); } }
CartProduct is associative entity. It matches a Cart with a Product. Contains also Quantity that User wants to buy.
namespace ShoppingCart.DurableFunction.DataAccess.Models { public class CartProduct { public int Id { get; set; } public int CartId { get; set; } public Cart Cart { get; set; } public int ProductId { get; set; } public Product Product { get; set; } public int Quantity { get; set; } } }
Create DbContext
Now it’s time to create the DbContext. We’ll provide relationships between tables. For testing purpose we add some Products.
using Microsoft.EntityFrameworkCore; using ShoppingCart.DurableFunction.DataAccess.Models; namespace ShoppingCart.DurableFunction.DataAccess { 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<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 } ); } } }
Add Migration:
Before we can add migration we need to tell the tool how to create DbContext. We have to setup a DbContextOptions
object for AppDbContext
class. In other words during command line execution it doesn’t know where to execute or check the migrations on.
In order to workaround this we’ll have to implement a IDesignTimeDbContextFactory<AppDbContext>
. We’ll not have to reference it anywhere, the tooling will just check for the existence and initiate the class.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; using System.IO; namespace ShoppingCart.DurableFunction.DataAccess { public class AppContextFactory : IDesignTimeDbContextFactory<AppDbContext> { public AppDbContext CreateDbContext(string[] args) { IConfiguration config = new ConfigurationBuilder() .SetBasePath(Path.Combine(Directory.GetCurrentDirectory())) .AddJsonFile("local.settings.json") .Build(); var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>(); optionsBuilder.UseSqlServer(config.GetSection("Values").GetValue<string>("SqlConnectionString")); return new AppDbContext(optionsBuilder.Options); } } }
Lets add SqlConnectionString to settings. Open local.settings.json file and add the value. Your settings file should look like
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "SqlConnectionString": "Data Source=(LocalDB)\\MSSQLLocalDB;Integrated Security=true;Database=ShoppingCart" } }
Last thing we have to do is get project .dll in the right spot. Entity Framework migration expects the .dll to be at the root of the build target. We need to add post-build event to copy the .dll to the root. In .csproj add this lines:
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Exec Command="copy "$(TargetDir)bin\$(ProjectName).dll" "$(TargetDir)$(ProjectName).dll"" /> </Target>
Finally, we can add migration
Add-Migration Initial
Add Startup.cs
We’ll use the same way as it’s done in ASP.NET Core. We need to create Startup class in the root and specify FunctionsStartup configuration.
using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; [assembly: FunctionsStartup(typeof(ShoppingCart.DurableFunction.Startup))] namespace ShoppingCart.DurableFunction { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { string connectionString = Environment.GetEnvironmentVariable("SqlConnectionString"); builder.Services.AddDbContext<AppDbContext>( options => options.UseSqlServer(connectionString)); } } }
Inject DbContext
We change the Process file default template. First we have to get rid off a static keyword and create the constructor.
private readonly AppDbContext _context; public Process(AppDbContext context) { _context = context; }
Now we will modify default implementation. We’ll change it to use our database. First we’ll add DTOs. At root I made the new folder Models and add following files. We’ll use these classes as API input.
namespace ShoppingCart.DurableFunction.Models { public class CartProductDTO { public int ProductId { get; set; } public int Quantity { get; set; } } }
using System.Collections.Generic; namespace ShoppingCart.DurableFunction.Models { public class CartDTO { public IEnumerable<CartProductDTO> Products { get; set; } = new List<CartProductDTO>(); } }
Orchestrator Client
Let’s start with – Process_HttpStart.
[FunctionName("Order")] public async Task<HttpResponseMessage> HttpStart( [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestMessage req, [DurableClient] IDurableOrchestrationClient starter, ILogger log) { var body = await req.Content.ReadAsAsync<CartDTO>(default(CancellationToken)); string instanceId = await starter.StartNewAsync("Process", null, body); log.LogInformation($"Started orchestration with ID = '{instanceId}'."); return starter.CreateCheckStatusResponse(req, instanceId); }
We changed the code. It reads the requests body and parse it to CartDTO model. Then the model is passed to Orchestrator Function.
Orchestrator Function
The method reads the input and pass it to Activity Function.
[FunctionName("Process")] public async Task<CartDTO> RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context) { var input = context.GetInput<CartDTO>(); int cartId = await context.CallActivityAsync<int>(nameof(SaveCart), input); return input; }
Activity Function
At this level we finally use the database. We save the Cart with associated Products.
[FunctionName("SaveCart")] public async Task<int> SaveCart([ActivityTrigger] CartDTO input, ILogger log) { log.LogInformation($"Saving cart."); var cart = new Cart(); 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; }
Test
At the end lets test our work. We need to call the API with the URL and pass body as JSON. I’m using Postman.
Request body:
{ "products": [ { "productId": 1, "quantity": 2 }, { "productId": 2, "quantity": 3 } ] }
Click link to statusQueryGetUri and you should see status Completed.
Good job! We connected our app with database by Entity Framework and Dependency Injection. We changed default template to our implementation.
Source code available here
<< Previous part: Setup Local Azure Environment
>> Next part: Azure Durable Functions – Approval process
References:
- https://medium.com/hitachisolutions-braintrust/azure-functions-v2-dependency-injection-using-net-core-fccd93b80c0
- https://markheath.net/post/ef-core-di-azure-functions
- https://blog.rasmustc.com/azure-functions-dependency-injection/
- https://medium.com/@therealjordanlee/dependency-injection-in-azure-functions-v3-7148d0574dfc