Wednesday, June 19, 2019

Build a Web API web app using Serverless Azure Functions with Entity Framework Migrations and Dependency Injection

One of the exciting developments from the Microsoft Build 2019 conference was support for dependency injection in Azure Functions. Combining Entity Framework Core and Dependency Injection with Azure Functions provides a very compelling opportunity to create data driven server-less solutions.

Source code for this tutorial can be found at: https://github.com/medhatelmasry/FunctionStudentsAPI

Let us create a very simple Azure Function that acts as a Web API endpoint.
Start Visual Studio 2019 and choose "Create a new project".

In the "Create a new project" dialog, select "Azure Functions" then click Next.

I named my project FunctionStudentsAPI. Give your project a name then click on Create.

Ensure that you have selected "Azure Functions V2 (.NET Core)" from the dropdown list at the top. Choose the "Http trigger" template and set Authorization level to Anonymous, then click on Create.

Let us see what the app does. Hit CTRL F5 on the keyboard. An output terminal window will launch that looks like this:

Copy and paste the URL into a browser.

The message in your browser suggests that you should pass a name query string. I appended the following to the URL: ?name=Superman. I got the following result:

We will need to add some Nuget packages. Execute the following dotnet commands from a terminal window:
dotnet add package Microsoft.Azure.Functions.Extensions   >> for Dependency Injection
dotnet add package Microsoft.EntityFrameworkCore   >> for Dependency Injection
dotnet add package Microsoft.EntityFrameworkCore.Design   >> for Entity Framework
dotnet add package Microsoft.EntityFrameworkCore.SqlServer  >> for Entity Framework Design Time Migration
dotnet add package Microsoft.EntityFrameworkCore.Tools   >> for Entity Framework Design Time Migration
dotnet add package Microsoft.Extensions.Http    >> for Http support
Let us make a few minor enhancements to our application.

1) Add a simple Student.cs class file to your project with the following content:
public class Student {
  public string StudentId { get; set; }
  [Required]
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
  [Required]
  public string School { get; set; }
}

2) Change the C# class file name Function1.cs to FunctionStudents.cs.

3) Open FunctionStudents.cs in the editor and change the name of the class from Function1 to FunctionStudents.

4) Change the signature of the FunctionStudents class so that it does not have the static keyword. Therefore, the signature of the class will look like this:
public class FunctionStudents

5) Add an Entity Framework DbContext class. In our case, we will create a class file named SchoolDbContext.cs with the following content:
public class SchoolDbContext : DbContext {
    public DbSet<Student> Students { get; set; }

    public SchoolDbContext(DbContextOptions<SchoolDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)     {
        base.OnModelCreating(builder);

        builder.Entity<Student>().HasData(
          new {
              StudentId = Guid.NewGuid().ToString(),
              FirstName = "Jane",
              LastName = "Smith",
              School = "Medicine"
          }, new {
              StudentId = Guid.NewGuid().ToString(),
              FirstName = "John",
              LastName = "Fisher",
              School = "Engineering"
          }, new {
              StudentId = Guid.NewGuid().ToString(),
              FirstName = "Pamela",
              LastName = "Baker",
              School = "Food Science"
          }, new {
              StudentId = Guid.NewGuid().ToString(),
              FirstName = "Peter",
              LastName = "Taylor",
              School = "Mining"
          }
        );
    }
}

6) To register a service like SchoolDbContext, create a class file named Startup.cs that implements FunctionStartup. This class will look like this:
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;

[assembly: FunctionsStartup(typeof(FunctionStudentsAPI.Startup))]
namespace FunctionStudentsAPI {
  public class Startup : FunctionsStartup {
    public override void Configure(IFunctionsHostBuilder builder) {
      var connStr = Environment.GetEnvironmentVariable("CSTRING");
      builder.Services.AddDbContext<SchoolDbContext>(
        option => option.UseSqlServer(connStr));

      builder.Services.AddHttpClient();
    }
  }
}
7) Inject the objects that are needed by your function class. Open FunctionStudents.cs in the editor and add the following instance variables and constructor at the top of the class:
private readonly HttpClient _client;
private readonly SchoolDbContext _context;

public FunctionStudents(IHttpClientFactory httpClientFactory, 
  SchoolDbContext context) {
  _client = httpClientFactory.CreateClient();
  _context = context;
}
8) We want to use the design-time DbContext creation. Since we are not using ASP.NET directly here, but implementing the Azure Functions Configure method, Entity Framework will not automatically discover the desired DbContext. So we need to implement an IDesignTimeDbContextFactory to drive the tooling. Add a C# class file named SchoolContextFactory.cs and add to it the following content:
public class SchoolContextFactory : IDesignTimeDbContextFactory<SchoolDbContext> {
  public SchoolDbContext CreateDbContext(string[] args) {
    var optionsBuilder = new DbContextOptionsBuilder<SchoolDbContext>();
    optionsBuilder.UseSqlServer(Environment.GetEnvironmentVariable("CSTRING"));

    return new SchoolDbContext(optionsBuilder.Options);
  }
}
9) Entity Framework migrations expects the main .dll file to be in the root project directory. Therefore, we shall add a post-build event to copy the .dll file to the appropriate place. Add the following to the .csproj file:
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
  <Exec Command="copy /Y &quot;$(TargetDir)bin\$(ProjectName).dll&quot; &quot;$(TargetDir)$(ProjectName).dll&quot;" />
</Target>

10) Go ahead and create a SQL Database Server in Azure. Copy the connection string into the following so that you can use it to set an environment variable in your computer's environment:
SET CSTRING=Server=tcp:XXXX.database.windows.net,1433;Initial Catalog=StudentsDB;Persist Security Info=False;User ID=YYYY;Password=ZZZZ;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;

Where:  
XXXX is the name of your SQL Azure database server
YYYY is your database username
ZZZZ is your database password

Note: I called the database StudentsDB. You can call it whatever you like.

Open a terminal window at the root of your project and paste the environment variable. The syntax will differ if your do it in Linux or Mac.

Also, in Visual Studio, open launchSettings.json under the Properties node. Add the connection string as an environment variable so that the content looks like this:
{
  "profiles": {
    "FunctionStudentsAPI": {
      "commandName": "Project",
      "environmentVariables": {
        "CSTRING": "PUT THE CONECTION STRING HERE"
      }
    }
  }
}

11) The next step is to apply Entity Framework migrations. Execute the following command from the root of your project:
dotnet ef migrations add m1
This produces a Migrations folder in your project.
If you open the numbered file that ends in _m1.cs, it looks like this:
using Microsoft.EntityFrameworkCore.Migrations;

namespace FunctionStudentsAPI.Migrations {
  public partial class m1 : Migration {
    protected override void Up(MigrationBuilder migrationBuilder) {
      migrationBuilder.CreateTable(
        name: "Students",
        columns: table => new {
            StudentId = table.Column<string>(nullable: false),
            FirstName = table.Column<string>(nullable: false),
            LastName = table.Column<string>(nullable: false),
            School = table.Column<string>(nullable: false)
        }, constraints: table => {
            table.PrimaryKey("PK_Students", x => x.StudentId);
        });

      migrationBuilder.InsertData(
        table: "Students",
        columns: new[] { "StudentId", "FirstName", "LastName", "School" },
        values: new object[,] {
            { "695fef8f-6702-4a9c-a1f4-f49c798462c5", "Jane", "Smith", "Medicine" },
            { "b5e2814c-a56b-4bfc-a7cc-a1676763e43c", "John", "Fisher", "Engineering" },
            { "e80785c9-7a07-41a1-a932-c8c9a03e405a", "Pamela", "Baker", "Food Science" },
            { "215bcf8e-fd72-486d-842e-bb754c1d2ea5", "Peter", "Taylor", "Mining" }
        });
    }

    protected override void Down(MigrationBuilder migrationBuilder) {
      migrationBuilder.DropTable(
        name: "Students");
    }
  }
}

12) The next step is to create the database and tables. Execute the following command in the same terminal window as above:
dotnet ef database update
If all goes well, you will receive a message that looks like this:
Applying migration '20190620023500_m1'.
Done.
I went back in the Azure portal and found the database was created with the Students table:

13) Let us now create an endpoint in our Azure function that returns all the students as an API.

Add the following method to the Azure functions file named FunctionStudents.cs:
[FunctionName("GetStudents")]
public IActionResult GetStudents(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "students")] HttpRequest req,
    ILogger log) 
{
    log.LogInformation("C# HTTP GET/posts trigger function processed a request.");

    var studentsArray = _context.Students.OrderBy(s => s.School).ToArray();

    return new OkObjectResult(studentsArray);
}

All that is left for us to do is test out our application and make sure it returns our students API. Run the application. You will see the following output in a terminal window:
Copy the second URL (I.E. http://localhost:7071/api/students) and paste it into a browser. The result will look like this:

Take Away

Creating an API with Azure Functions is much more cheaper than doing it with an ASP.NET Core Web application because you pay a fraction of a cent for every request and the app does not need to be constantly running. Also, it is very easy to do.

No comments:

Post a Comment