Azure Functions with Entity Framework and Dependency Injection

  • Reading time:6 mins read

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 &quot;$(TargetDir)bin\$(ProjectName).dll&quot; &quot;$(TargetDir)$(ProjectName).dll&quot;" />
</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
    }
  ]
}

Request to test API

Click link to statusQueryGetUri and you should see status Completed.

Process 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:

Subscribe
Notify of

0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments