Showing posts with label Code 1'st. Show all posts
Showing posts with label Code 1'st. Show all posts

Wednesday, March 16, 2022

Build & publish Azure Functions app that uses a budget SQLite database & .NET 6.0

In this tutorial I will build a Web API application using Azure Functions & SQLite. Although it is not typical to use SQLite with Azure Functions, this is a decent option if you want to have a cheap storage solution. I will later deploy the SQLite enabled Azure Function. This tutorial was done on a Windows 11 computer with VS Code.

Source code: https://github.com/medhatelmasry/SQLiteFunction.git

Prerequisites

Create a folder named SQLiteFunction on your hard drive at the location where your project will reside. 

Under the Functions tab in VS Code, create a new Azure Functions project.


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


You will then be asked to choose the .NET runtime, choose .NET 6:


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 HttpApi.


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


Choose Anonymous for AccessRights.

When asked about 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.

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:


NOTE: You can start your function app with the terminal command: func start

Copy and paste the URL into a browser or hit CTRL Click on the link. 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 and 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 project:

dotnet add package Microsoft.Azure.Functions.Extensions
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.EntityFrameworkCore.SQLite.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools

Let us make a few minor enhancements to our application.

1) Our SQLite database will be named school.db. Add the following to the project's .csproj file so that the SQLite database is copied to the output directory when the app gets built:

<ItemGroup>
 <None Update="school.db">
   <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 </None>  
</ItemGroup>
2) Change the signature of the HttpAPI class so that it does not have the static keyword. Therefore, the signature of the class will look like this:

public class HttpApi

3) Create a Models folder and add to it a simple Student.cs class file with the following content:

using System.ComponentModel.DataAnnotations;

namespace SQLiteFunction.Models;

public class Student {
    public int StudentId { get; set; }
    [Required]
    public string FirstName { get; set; }
    [Required]
    public string LastName { get; set; }
    [Required]
    public string School { get; set; }
}

4) We will deploy our Azure Functions app to a Windows server on Azure. When the school.db SQLite database file is published to Azure, it will reside in directory d:\home\site\wwwroot. Therefore, we shall create a helper class that will locate for us the SQLite database file in both development and deployment environments. Create a file named Utils.cs in the Models folder and add to it the following code:

using System;

namespace SQLiteFunction.Models;

public class Utils
{
    public static string GetSQLiteConnectionString()
    {
        var home = Environment.GetEnvironmentVariable("HOME") ?? "";
        Console.WriteLine($"home: {home}");
        if (!string.IsNullOrEmpty(home))
        {
            home = System.IO.Path.Combine(home, "site", "wwwroot");
        }
        var databasePath = System.IO.Path.Combine(home, "school.db");
        var connStr = $"Data Source={databasePath}";

        return connStr;
    }
}

The above helper class provides us with a static method Utils.GetSQLiteConnectionString() that returns the fully qualified location of the SQLite database file named school.db.

5) Add an Entity Framework DbContext class to the Models folder. In our case, we will add a class file named ApplicationDbContext.cs with the following content:

using Microsoft.EntityFrameworkCore;
using System;

namespace SQLiteFunction.Models;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext() { }
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
    public DbSet<Student> Students { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
             optionsBuilder.UseSqlite(Utils.GetSQLiteConnectionString());
        }
    }

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

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

The above context class seeds some sample data pertaining to four students.

6) To register a service like ApplicationDbContext, create a class file named Startup.cs in the root of your application. The Startup class implements FunctionStartup. This class will look like this:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using SQLiteFunction.Models;

[assembly: FunctionsStartup(typeof(SQLiteFunction.StartUp))]
namespace SQLiteFunction
{
    public class StartUp : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlite(Utils.GetSQLiteConnectionString());
            });
        }

        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            base.ConfigureAppConfiguration(builder);
        }

    }
}

7) Inject the ApplicationDbContext class that is needed by your function class. Open HttpApi.cs in the editor and add the following instance variables and constructor at the top of the class:

private readonly ApplicationDbContext _context;
 
public HttpApi(ApplicationDbContext context) {
   _context = context;
}

8) The next step is to apply Entity Framework migrations. Open a terminal window in the root of your application and execute the following EF migration command inside the same terminal window:

dotnet-ef migrations add m1 -o Data/Migrations

This produces a Data/Migrations folder in your project.




9) 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 '20220314204919_m1'.
Done.

10) 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 HttpApi.cs:

[FunctionName("students")]
public IActionResult GetStudents(
   [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "students")] HttpRequest req,
   ILogger log) {
   
   log.LogInformation("C# HTTP GET trigger function processed api/students request.");

   var studentsArray = _context.Students.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 data. Run the application by hitting CTRL F5 on the keyboard. You will see the following output in a VS Code terminal window:


Copy and paste the /api/students endpoint into a browser. Alternatively, you can simply hit CTRL Click on the link. The result will look like this:

Deployment to Azure

Click on the "Deploy to Function App ..." icon.



Select your subscription.


Choose "+ Create new Function App in Azure ...".


Enter a globally unique name for your function app then hit Enter.


Select .NET 6 for runtime stack:


Choose a preferred data center.


The deployment process starts. Be patient as it could take 3-4 minutes. When deployment is complete you will see the following message:


If you click on the "View output" you will see the two endpoints. 


Although the first endpoint works, the second does not. To fix this problem, login into the azure portal https://portal.azure.com. In the filter field, enter func then choose "Function App".


Click on the function that was created.


Select Configuration on the left navigation.


Click on WEBSITE_RUN_FROM_PACKAGE.


Change the value from 1 to 0 then click on OK.


Remember to Save at the top.


Back in VS Code, publish your functions app again.


Click on Deploy to confirm.


This time, deployment will not take as long as the last time. Once deployment is completed, try the /api/students on the deployed endpoint and you should find it working to your satisfaction.


Conclusion

It is easy to create Azure Functions with the SQLite. Also, creating an API with Azure Functions is much more cheaper than doing it with an ASP.NET Core Web API application because you pay a fraction of a cent for every request and the app does not need to be constantly running. It is also worth mentioning that using SQLite makes it even cheaper as you do not need to pay extra for hosting your relational database.

Saturday, February 27, 2021

Build REST API using EF with Azure Functions v3 & .NET Core 3.1

In this tutorial I will demonstrate how to build a REST API application using Azure Functions v3. The application we will build together uses Entity Framework Core Migrations, Dependency Injection and .NET Core 3.1. We will use the light-weight VS Code editor so that you can go through this tutorial on Windows 10, Mac or Linux.

Source code: https://github.com/medhatelmasry/AzureFunctionsEF.git

Install the appropriate Azure CLI for your operating system from https://docs.microsoft.com/en-us/cli/azure/install-azure-cli.

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.


In your working directory, create the following folder structure:


Inside the AzureFunctionsEF directory, execute the following terminal window commands:

dotnet new sln
dotnet new classlib -f netcoreapp3.1 -o DataLayer
dotnet sln add DataLayer/DataLayer.csproj

Start Visual Studio Code.  Under the Functions tab, select your Azure subscription. You will then be able to create a new Azure Functions project.


Choose the AzureFunctionsEF/Functions folder.


Select C#.
Select HttpTrigger.


Name your Azure function HttpWebAPI.


Give your function app the namespace Snoopy.Function.



Select Anonymous for access rights.



Finally, select "Open in current window".

Let us see what the app does. Enter the following in the terminal window inside the AzureFunctionsEF directory:

cd Functions
func start

The following will appear:


Copy the URL (http://localhost:7071/api/HttpWebAPI) and paste it in the address line of your browser. You will see a response like this:


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:


Hit CTRL C to terminate the running app.

Back in the terminal window inside the AzureFunctionsEF directory, execute the following commands:

dotnet sln add Functions/Functions.csproj
dotnet add Functions/Functions.csproj reference DataLayer/DataLayer.csproj


DataLayer class library project

We will work on the DataLayer project by adding a Student class, an Entity Framework database context class, a connection string, and a class that is capable of reading configurations settings.

Add the following packages to the DataLayer project by executing the following commands in a terminal window inside the DataLayer folder:

dotnet add package Microsoft.EntityFrameworkCore -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.Tools -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.Design -v 3.1.9
dotnet add package Microsoft.EntityFrameworkCore.SqlServer -v 3.1.9
dotnet add package Microsoft.Extensions.Configuration.Json -v 3.1.9
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables -v 3.1.9 

Open the DataLayer folder in Visual Studio Code.

Delete DataLayer/Class1.cs.

Add a Models folder. Inside the Models folder, add a C# Student class file with the following code:

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; }
}

Also, in the Models folder, add another C#  Config class file with the following code:

public static class Config {
    private static IConfiguration configuration;
    static Config() {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
            .AddEnvironmentVariables();

        configuration = builder.Build();
    }

    public static string Get(string name) {
        string appSettings = configuration[name];
        return appSettings;
    }
}

Now let us add a settings file named local.settings.json right inside the root DataLayer folder with the following content:

{
  "DefaultConnection":"Server=(localdb)\\mssqllocaldb;Database=SchoolDB;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Since we are using Entity Framework, we need to add a database context class. Add the following ApplicationDbContext.cs class to the DataLayer root directory:

public class ApplicationDbContext : DbContext {
  public DbSet<Student> Students { get; set; }

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

  public ApplicationDbContext(DbContextOptions options) : base(options) { }

  public ApplicationDbContext() : base() { }

  protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlServer(Config.Get("DefaultConnection"));

  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"
      }
    );
  }
}

Now that we have out models and database context classes in place, let us go ahead and run Entity Framework Migrations. In a terminal window inside the DataLayer root folder, execute the following commands:

dotnet-ef migrations add M1 -o Data/Migrations
dotnet-ef database update

At this point, if all goes well, the database would be created and seeded with sample data.


Azure Functions Project

Add the following package to the Functions project by executing the following command in a terminal window inside the Functions folder:

dotnet add package Microsoft.Extensions.Http -v 3.1.10

Open the Functions folder in Visual Studio Code.

Add the following connection string to local.settings.json.

"DefaultConnection":"Server=(localdb)\\mssqllocaldb;Database=SchoolDB;Trusted_Connection=True;MultipleActiveResultSets=true",

We will use dependency injection to access the database context and HttpClient objects. Therefore, create a Startup.cs file in the root Functions folder and add to it this code:

[assembly: WebJobsStartup(typeof(StartUp))]
namespace Functions {
  public class StartUp : IWebJobsStartup {
    public void Configure(IWebJobsBuilder builder) {
      builder.Services.AddDbContext<ApplicationDbContext>(options1 => {
        options1.UseSqlServer(
          Config.Get("DefaultConnection"),
          builder =>
          {
            builder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null);
            builder.CommandTimeout(10);
          }
        );
      });

      builder.Services.AddHttpClient();
    }
  }
}


Delete the static keyword from the class declaration of HttpWebAPI class:

public static class HttpWebAPI

We will use dependency injection inside our Functions class to access the database context and HttpClient objects. Therefore, add these instance variables and constructor to HttpWebAPI.cs:

private readonly HttpClient _client;
private readonly ApplicationDbContext _context;

public HttpWebAPI(IHttpClientFactory httpClientFactory,
    ApplicationDbContext context) {
    _client = httpClientFactory.CreateClient();
    _context = context;
}

Add the following method to the HttpWebAPI.cs class:

[FunctionName("GetStudents")]
public IActionResult GetStudents(
[HttpTrigger(AuthorizationLevel.Anonymous, "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 find out is whether or not our API works. Hit CTRL F5 inside the Visual Studio Code.


CTRL Click with your mouse on http://localhost:7071/api/students. It should open up a browser window with the following data:


It is left up to you to complete this tutorial with POST, PUT and DELETE functionality.

Monday, October 14, 2019

Produce & Consume a .NET Core 3.0 gRPC DB Driven Service

gRPC is a method of web communication like WCF. .NET Remoting, SOAP, etc. It is a standard created by Google and is widely used. It relies on known configuration shared between client and server. Contracts are called protocol buffers. 

gRPC was introduces to the world of .NET with .NET Core 3.0. In this tutorial we shall build a gRPC service that talks with LocalDB using Entity Framework.

Source Code: https://github.com/medhatelmasry/GrpcWorld.git

Video:  https://youtu.be/Xh47x_C-aMM

Let’s build a gRPC server then client.
You need .NET Core 3.0
Visual Studio 2019 >> Create New Project >> gRPC Service

Project Name: GrpcServer
Solution Name: GrpcRocks


Similar to Web API, this service sits and listens for calls.

The proto file defines the contract between the server and the client.

greet.proto

syntax = "proto3";
option csharp_namespace = "GrpcServer";
package Greet;
// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}
If you navigate to the definition of GreeterBase, you will find a generated file named GreetGrpc.cs where the abstract class is defined. This file is regenerated with every build. If you hit CTRL F5, this is what you will see:
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: E:\play\gRPC\GrpcDemo\GrpcServer
When you point your browser to https://localhost:5001, this is what appears:

Communication with gRPC endpoints must be made through a gRPC client. 
To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909 
This tells us that we need to create a client.

Add a console App to the solution.

Name the project GrpcClient.

Add the following three packages to GrpcClient project:
dotnet add package Google.Protobuf
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Tools
Copy the Protos folder from the server project to the client project.

Edit the GrpcClient.csproj file.

Change : <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
To: <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />

Rebuild the CLIENT app.

Replace the Main() method in Program.cs with this code:
static async Task Main(string[] args) {
  var input = new HelloRequest { Name = "Jane Bond" };
  var channel = GrpcChannel.ForAddress("https://localhost:5001");
  var client = new Greeter.GreeterClient(channel);
  var reply = await client.SayHelloAsync(input);
  Console.WriteLine(reply.Message);
  Console.ReadLine();
}
Right-click on solution >> Properties.

Select “Multiple startup projects”.

Choose Start for both projects, but make GrpcServer start before GrpcClient.


CTRL F5. This is the output:

Student Database Model

Add the following packages to the GrpcService project:
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Grpc.Tools
Add this connection string to the appsettings.json file:
"ConnectionStrings": {
  "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=School;Trusted_Connection=True;MultipleActiveResultSets=true"
},
Create Data & Models folders.

Add the following Student class to the Models folder:
 public class Student {
  public int StudentId { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string School { get; set; }
}
Add the following database context class named SchoolDbContext to the Data folder:
public class SchoolDbContext  : DbContext {
  public SchoolDbContext (DbContextOptions options) : base(options) { }
  protected override void OnModelCreating(ModelBuilder builder) {
    builder.Entity<Student>().HasData(
      GetStudents()
    );
  }
  public DbSet<Student> Students { get; set; }
  private static List<Student> GetStudents() {
    List<Student> students = new List<Student>() {
      new Student() {    // 1
        StudentId=11,
        FirstName="Ann",
        LastName="Fox",
        School="Nursing",                
      },
      new Student() {    // 2
        StudentId=22,
        FirstName="Sam",
        LastName="Doe",
        School="Mining",                
      },
      new Student() {    // 3
        StudentId=33,
        FirstName="Sue",
        LastName="Cox",
        School="Business",                
      },
      new Student() {    // 4
        StudentId=44,
        FirstName="Tim",
        LastName="Lee",
        School="Computing",                
      },
      new Student() {    // 5
        StudentId=55,
        FirstName="Jan",
        LastName="Ray",
        School="Computing",                
      },
    };
    return students;
  }
}
We will need access to the Configuration object in Startup class. Therefore add this instance variable and constructor to Startup.cs:
private IConfiguration _configuration { get; }
public Startup(IConfiguration configuration) {
  _configuration = configuration;
}
Add the following code to ConfigureServices() method in the Startup.cs file:
services.AddDbContext<SchoolDbContext>(options =>
  options.UseSqlServer(_configuration.GetConnectionString("DefaultConnection")));

Rebuild the SERVER app.

Now we can add migrations. Execute the following commands from within a terminal window in the GrpcServer folder:
dotnet-ef migrations add InitialCreate –o Data/Migrations
dotnet-ef database update
Check the database to make sure that tables and data were created:

Students Service

Let us setup our own gRPC service. Comment out the content of the Main() method on the client side Program.cs.

Inside the Protos folder in GrpcServer project, add a new file named students.proto.


The content of students.proto is:
syntax = "proto3";
option csharp_namespace = "GrpcServer.Protos";
// The student service definition.
service RemoteStudent {
  rpc GetStudentInfo (StudentLookupModel) returns (StudentModel);
}
// The request message 
message StudentLookupModel {
  int32 studentId = 1;
}
// The response message 
message StudentModel {
  string studentId = 1;  
  string firstName = 2;
  string lastName = 3;
  string school = 4;
}
Right-click on students.proto >> Properties.

Set “Build Action” value to “Protobuf compiler”. Note: if you do not see this option then you are missing the Grpc.Tools package.

Set “gRPC Stub Classes” value to “Server only”.

Rebuild the SERVER app.

Add a class named StudentsService into the Services folder. Contents of StudentsService.cs are:
public class StudentsService : RemoteStudent.RemoteStudentBase {
  private readonly ILogger<StudentsService> _logger;
  private readonly SchoolDbContext _context;
  public StudentsService(ILogger<StudentsService> logger, SchoolDbContext context) {
    _logger = logger;
    _context = context;
  }

  public override Task<StudentModel> GetStudentInfo(StudentLookupModel request, ServerCallContext context)
  {
    StudentModel output = new StudentModel();
    var student = _context.Students.Find(request.StudentId);
    _logger.LogInformation("Sending Student response");
    if (student != null) {
      output.StudentId = student.StudentId;
      output.FirstName = student.FirstName;
      output.LastName = student.LastName;
      output.School = student.School;
    }
    return Task.FromResult(output);
  }
}
On the server-side GrpcServer project, in Startup.cs file’s Configure() method, add the following endpoint mapping:
endpoints.MapGrpcService<StudentsService>();

Rebuild the SERVER app.

Copy students.proto from GrpcServer project to GrpcClient project.

In the GrpcClient.csproj file, change Server to Client for Protos\students.proto.

Rebuild the SERVER & CLIENT apps.

Except for the Console.ReadLine() statement, comment previous code in Main() method on client-side Program.cs and, instead, add this code:
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var studentClient = new RemoteStudent.RemoteStudentClient(channel);
var studentInput = new StudentLookupModel { StudentId = 33 };
var studentReply = await studentClient.GetStudentInfoAsync(studentInput);
Console.WriteLine($"{studentReply.FirstName} {studentReply.LastName}");
Run application. You should see this result: