Thursday, June 27, 2019

Build Serverless Azure Functions Web API app with EF Migrations and Dependency Injection using VS Code on Mac

This tutorial was done on a Mac running macOS Mojave 10.14.5.

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

Support for dependency injection in Azure Functions was announced at Build 2019. This opens up new opportunities for building better architected C# applications with Serverless Azure Functions. In this tutorial I will demonstrate how to build a Web API application using Azure Functions. The application we will build together will use Entity Framework Core Migrations and Dependency Injection. We will use the light-weight VS Code editor for Mac.

You need to install the Azure Functions extension for Visual Studio Code before proceeding with this tutorial. Once the extension is installed, you will find it among your extensions.




Also, install Azure Functions Core Tools with the following npm command:

sudo npm i -g azure-functions-core-tools --unsafe-perm true

Create a folder on your hard drive for the location where your project will reside. Under the Functions tab, select your Azure subscription. You will then be able to create a new Azure Functions project.


Choose the location on your hard drive that you had previously designated as your workspace folder for this project. You will next be asked to select a programming language. Choose C#.


You will be asked to choose a template for your project's first function. Note that you can have more than one function in your project. Choose HttpTrigger.


Give your function a name. I named my function HttpWebApi. Hit Enter.


Hit Enter after you give your function a name. Give your class a namespace. The namespace I used is Snoopy.Function. I then hit Enter.


Choose Anonymous for AccessRights then hit Enter.


When asked how you would like to open your project, choose "Open in current window".


If a popup window appears asking if you wish to restore unresolved dependencies, click the Restore button.


Alternatively, you can go to a terminal window in the project directory and type "dotnet restore".

Let us see what the app does. Hit CTRL F5 on the keyboard. The built-in VS Code terminal window will eventually display a URL that uses port number 7071:


Copy and paste the URL into a browser or hit Command Click on the URL. You will see the following output in your 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 in the root directory of your application:

dotnet add package Microsoft.Azure.Functions.Extensions
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.Extensions.Http

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:
namespace Snoopy.Function {
  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 signature of the HttpWebAPI class so that it does not have the static keyword. Therefore, the signature of the class will look like this:

public class HttpWebAPI

3) Add an Entity Framework DbContext class. In our case, we will add a class file named SchoolDbContext.cs with the following content:

namespace Snoopy.Function {
  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"
        }
      );
    }
  }
}

4) To register a service like SchoolDbContext, create a class file named Startup.cs that implements FunctionStartup. This class will look like this: 

[assembly: FunctionsStartup(typeof(Snoopy.Function.Startup))]
namespace Snoopy.Function {
  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();
    }
  }
}
5) Inject the objects that are needed by your function class. Open HttpWebAPI.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 HttpWebApi(IHttpClientFactory httpClientFactory, 
  SchoolDbContext context) {
  _client = httpClientFactory.CreateClient();
  _context = context;
}

6) 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. Therefore, we need to implement an IDesignTimeDbContextFactory to drive the tooling. Create a C# class file named SchoolContextFactory.cs and add to it the following content:
 
namespace Snoopy.Function {
  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);
    }
  }
}

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

8) The EF utility used by .NET Core 3.x had changed. We want to make sure that we are using .NET Core 2.2. Therefore, add the following global.json file in the root of your application so that we specify the SDK version:

{
   "sdk": {
      "version": "2.2.300"
   }
}

9) Go ahead and create a SQL Database Server in Azure. Copy the connection string into a plain text editor (like TextEdit) so that you can later use it to set an environment variable in your Mac's environment: 

export CSTRING="Server=tcp:XXXX.database.windows.net,1433;Initial Catalog=SchoolDB;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.

10) The next step is to apply Entity Framework migrations. Open a terminal window in the root of your application. Paste the environment variable setting that you saved in a text editor in the previous step into the terminal window. Thereafter, execute the following command inside the same terminal window:

dotnet build
dotnet ef migrations add m1

This produces a Migrations folder in your project.


If you inspect contents of the numbered file that ends in _m1.cs, it looks like this:
using Microsoft.EntityFrameworkCore.Migrations;

using Microsoft.EntityFrameworkCore.Migrations;

namespace FunctionsWebAPI.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[,] {
          { "4ceab280-96ba-4c2a-911b-5af687a641a4", "Jane", "Smith", "Medicine" },
          { "8ebb5891-b7ca-48f8-bd74-88de7513c6d0", "John", "Fisher", "Engineering" },
          { "4f00688f-1c03-4255-bcb0-025b9221c0d7", "Pamela", "Baker", "Food Science" },
          { "e5dd769d-55d1-49a0-9f79-d8ae3cf9c474", "Peter", "Taylor", "Mining" }
        });
    }

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

11) 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 '20190626012048_m1'.
Done.

12) 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 HttpWebApi.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. Before we can run the application, we need to set the CSTRING environment variable in the application's local environment. This is done by adding the CSTRING  environment variable to the local.settings.json file as shown below:

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "CSTRING": "Server=tcp:XXXX.database.windows.net,1433;Initial Catalog=SchoolDB;Persist Security Info=False;User ID=YYYY;Password=ZZZZ;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
    }
}

Run the application by hitting CTRL F5 on the keyboard. You will see the following output in a VS Code terminal window:



Copy the students URL (I.E. http://localhost:7071/api/students) and paste it into a browser. Alternatively, you can simply hit Command Click on the link. The result will look like this:

Conclusion

It is easy to create Azure Functions with the very flexible VS Code editor. Also, 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. 

No comments:

Post a Comment