Showing posts with label REST. Show all posts
Showing posts with label REST. Show all posts

Wednesday, February 5, 2025

Develop simple REST API with PHP and MySQL

 Overview

In this article we will develop is simple REST API with PHP. The database backend is MySQL running in a Docker container and the sample data represents students.

Source code: https://github.com/medhatelmasry/school-api

Getting Started

Start MySQL in a Docker container with:

docker run -d -p 3333:3306 --name maria -e MYSQL_ROOT_PASSWORD=secret mariadb:10.7.3

In a working directory named school-api, create the following sub-folders:

mkdir src
cd src
mkdir Controller
mkdir System
mkdir TableGateways
cd ..
mkdir public

Add a composer.json file in the top directory with just one dependency: the DotEnv library which will allow us to keep our authentication details in a .env file outside our code repository:


Contents of the composer.json file:

{
    "require": {
        "vlucas/phpdotenv": "^2.4"
    },
    "autoload": {
        "psr-4": {
            "Src\\": "src/"
        }
    }
}

We also configured a PSR-4 autoloader which will automatically look for PHP classes in the /src directory.

We can install our dependencies with:

composer install

We now have a /vendor directory, and the DotEnv dependency is installed (we can also use our autoloader to load our classes from /src with no include() calls).

Let’s create a .gitignore file for our project with two lines in it, so the /vendor directory and our local .env file are ignored if our code is pushed to source control. Add this to .gitignore:

vendor/
.env

Next, add a .env file where we’ll put our database connection details:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3333
DB_DATABASE=apidb
DB_USERNAME=root
DB_PASSWORD=secret

Add a bootstrap.php file to load environment variables.

<?php
error_reporting(E_ALL & ~E_DEPRECATED);

require 'vendor/autoload.php';

use Dotenv\Dotenv;

use Src\System\DatabaseConnector;

$dotenv = new DotEnv(__DIR__);
$dotenv->load();

$dbConnection = (new DatabaseConnector())->getConnection();

Configure DB for PHP REST API

We can now create a class to hold our database connection and add the initialization of the connection to our bootstrap.php file. In the src/System/ folder, add a file named DatabaseConnector.php with this code:

<?php
namespace Src\System;
class DatabaseConnector {
    private $dbConnection = null;
    public function __construct() {
        $host = getenv('DB_HOST');
        $port = getenv('DB_PORT');
        $db   = getenv('DB_DATABASE');
        $user = getenv('DB_USERNAME');
        $pass = getenv('DB_PASSWORD');
        try {
            $pdo = new \PDO("mysql:host=$host;port=$port;charset=utf8mb4", $user, $pass);
            $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db`");
        } catch (\PDOException $e) {
            die("DB ERROR: " . $e->getMessage());
        }
        try {
            $this->dbConnection = new \PDO(
                "mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
                $user,
                $pass
            );
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }
    }
         public function getConnection() {
        return $this->dbConnection;
    }
}

Let’s create a dbseed.php file which creates a students table and inserts some records for testing. Code for dbseed.php is shown below:

<?php
require 'bootstrap.php';
$statement = "
    CREATE TABLE IF NOT EXISTS students (
        id INT NOT NULL AUTO_INCREMENT,
        firstname VARCHAR(40) NOT NULL,
        lastname VARCHAR(40) NOT NULL,
        school VARCHAR(40) NOT NULL,
        PRIMARY KEY (id)
    );
    INSERT INTO students
        (firstname, lastname, school)
    VALUES
        ('Mark', 'Fisher','Computing'),
        ('Lisa', 'Fisher','Computing'),
        ('Judy', 'Fisher','Computing'),
        ('Jane', 'Smith','Nursing'),
        ('Mary', 'Smith','Nursing'),
        ('Andy', 'Smith','Nursing'),
        ('Bill', 'Smith','Business'),
        ('Fred', 'Plumber','Business'),
        ('Anna', 'Plumber','Business');
";
try {
    $createTable = $dbConnection->exec($statement);
    echo "Success!\n";
} catch (\PDOException $e) {
    exit($e->getMessage());
}

Run the following command to seed the database with sample data:

php dbseed.php

Add a Gateway Class for students table

In the src/TableGateways/StudentsGateway.php class file, we will implement methods to return all students, return a specific student and add/update/delete a student:

<?php
namespace Src\TableGateways;
class StudentsGateway {
    private $db = null;
    public function __construct($db) {
        $this->db = $db;
    }
    public function findAll() {
        $statement = "
            SELECT 
                id, firstname, lastname, school
            FROM
                students;
        ";
        try {
            $statement = $this->db->query($statement);
            $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
            return $result;
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }
    }
    public function find($id) {
        $statement = "
            SELECT 
                id, firstname, lastname, school
            FROM
                students
            WHERE id = ?;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array($id));
            $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
            return $result;
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function insert(Array $input) {
        $statement = "
            INSERT INTO students 
                (firstname, lastname, school)
            VALUES
                (:firstname, :lastname, :school);
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array(
                'firstname' => $input['firstname'],
                'lastname'  => $input['lastname'],
                'school' => $input['school'] ?? null,
            ));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function update($id, Array $input) {
        $statement = "
            UPDATE students
            SET 
                firstname = :firstname,
                lastname  = :lastname,
                school = :school
            WHERE id = :id;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array(
                'id' => (int) $id,
                'firstname' => $input['firstname'],
                'lastname'  => $input['lastname'],
                'school' => $input['school'] ?? null,
            ));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function delete($id) {
        $statement = "
            DELETE FROM students
            WHERE id = :id;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array('id' => $id));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
}

Implement the PHP REST API

Our REST API will have the following endpoints:

return all recordsGET /students
return a specific recordGET /students/{id}
create a new recordPOST /students
update an existing recordPUT /students/{id}
delete an existing recordDELETE /students/{id}

Create a src/Controller/StudentsController.php to handle the API endpoints with this code:

<?php
namespace Src\Controller;
use Src\TableGateways\StudentsGateway;
class StudentsController {
    private $db;
    private $requestMethod;
    private $id;
    private $studentsGateway;
    public function __construct($db, $requestMethod, $id) {
        $this->db = $db;
        $this->requestMethod = $requestMethod;
        $this->id = $id;
        $this->studentsGateway = new StudentsGateway($db);
    }
    public function processRequest() {
        switch ($this->requestMethod) {
            case 'GET':
                if ($this->id) {
                    $response = $this->getById($this->id);
                } else {
                    $response = $this->getAll();
                };
                break;
            case 'POST':
                $response = $this->createRequest();
                break;
            case 'PUT':
                $response = $this->updateFromRequest($this->id);
                break;
            case 'DELETE':
                $response = $this->deleteById($this->id);
                break;
            default:
                $response = $this->notFoundResponse();
                break;
        }
        header($response['status_code_header']);
        if ($response['body']) {
            echo $response['body'];
        }
    }
    private function getAll() {
        $result = $this->studentsGateway->findAll();
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = json_encode($result);
        return $response;
    }
    private function getById($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = json_encode($result);
        return $response;
    }
    private function createRequest() {
        $input = (array) json_decode(file_get_contents('php://input'), TRUE);
        if (! $this->validate($input)) {
            return $this->unprocessableEntityResponse();
        }
        $this->studentsGateway->insert($input);
        $response['status_code_header'] = 'HTTP/1.1 201 Created';
        $response['body'] = null;
        return $response;
    }
    private function updateFromRequest($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $input = (array) json_decode(file_get_contents('php://input'), TRUE);
        if (! $this->validate($input)) {
            return $this->unprocessableEntityResponse();
        }
        $this->studentsGateway->update($id, $input);
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = null;
        return $response;
    }
    private function deleteById($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $this->studentsGateway->delete($id);
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = null;
        return $response;
    }
    private function validate($input) {
        if (! isset($input['firstname'])) {
            return false;
        }
        if (! isset($input['lastname'])) {
            return false;
        }
        return true;
    }
    private function unprocessableEntityResponse() {
        $response['status_code_header'] = 'HTTP/1.1 422 Unprocessable Entity';
        $response['body'] = json_encode([
            'error' => 'Invalid input'
        ]);
        return $response;
    }
    private function notFoundResponse() {
        $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
        $response['body'] = null;
        return $response;
    }
}

Finally, create a /public/index.php file to serve as the front controller to process requests:

<?php
require "../bootstrap.php";

use Src\Controller\StudentsController;

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");  
 
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode( '/', $uri ); 
 
// all of our endpoints start with /students
// everything else results in a 404 Not Found
if ($uri[1] !== 'students') {
    header("HTTP/1.1 404 Not Found");
    exit();
} 
 
// the id is, of course, optional and must be a number:
$id = null;
if (isset($uri[2])) {
    $id = (int) $uri[2];
} 
 
$requestMethod = $_SERVER["REQUEST_METHOD"]; 
 
// pass the request method and user ID to the StudentsController and process the HTTP request:
$controller = new StudentsController($dbConnection, $requestMethod, $id);
$controller->processRequest();

Start the web server with the following command, where switch -t starts the server in the public folder:

php -S 127.0.0.1:8888 -t public

Test your API with postman with these endpoints:

return all recordsGET http://localhost:8888/students
return a specific recordGET http://localhost:8888/students/{id}
create a new recordPOST http://localhost:8888/students
update an existing recordPUT / http://localhost:8888/students/{id}
delete an existing recordDELETE http://localhost:8888/students/{id}


Sunday, January 2, 2022

Google charts with DataTable .NET Wrapper and API data in ASP.NET 6.0 Razor Pages App

You have data from an API (or any other data source like a database) and wish to display the results in a chart. The library we will use for generating charts is the freely available Google Charts JavaScript-based API. The Google DataTable .NET Wrapper is used to create a lightweight representation of the google.visualization.DataTable object directly in Microsoft.NET. The wrapper allows for the creation of the appropriate JSON which is easily ingested by the Google Chart Tools JavaScript library.

I will show you how to generate six types of charts to display dynamically generated data. The source of data will be an API at https://northwind.vercel.app/api/orders that displays orders. I will work with the ASP.NET Razor Pages template (AKA Web App).

Source code:  https://github.com/medhatelmasry/OrdersChartRazorGoogleWrapper

Companion Video: https://youtu.be/Ie43mv57-5o

The environment I am using is: https://github.com/medhatelmasry/OrdersChartRazorGoogleWrapper

  • .NET version 6.0.100
  • Visual Studio Code

The orders API

We will work with the orders API at https://northwind.vercel.app/api/orders. The data in the API is generated from the well known Northwind sample SQL Server database. If you point your browser to the above address, you will see the following:



Since some properties are not useful in this tutorial, we will ignore orderDate, requiredDate, shippedDate, postalCode and details.

Also, note that shipAddress is represented by a sub JSON address object.

Project setup

Run the following command to create an ASP.NET Core Razor Pages application using .NET 6.0 in a folder named OrdersChartRazorGoogleWrapper:

dotnet new razor -f net6.0 -o OrdersChartRazorGoogleWrapper

Change directory into the new folder and open the project inside VS Code with the following commands:

cd OrdersChartRazorGoogleWrapper 

code .


Install the Google DataTable .NET Wrapper Nuget package:

dotnet add package Google.DataTable.Net.Wrapper

Address & Order model classes

Create a folder named Models. Add to the Models folder two class files, namely: Address.cs and Order.cs

The Address class looks like this:

public class Address {
    [JsonPropertyName("street")]
    public string? Street { get; set; }


    [JsonPropertyName("city")]
    public string? City { get; set; }


    [JsonPropertyName("region")]
    public string? Region { get; set; }


    [JsonPropertyName("country")]
    public string? Country { get; set; }

}

The Order class looks like this:

public class Order {
    [JsonPropertyName("id")]
    public int Id { get; set; }


    [JsonPropertyName("customerId")]
    public string? CustomerId { get; set; }


    [JsonPropertyName("employeeId")]
    public int? EmployeeId { get; set; }


    [JsonPropertyName("shipVia")]
    public int? ShipVia { get; set; }

    
    [JsonPropertyName("freight")]
    public decimal? Freight { get; set; }


    [JsonPropertyName("shipName")]
    public string? ShipName { get; set; }


    [JsonPropertyName("shipAddress")]
    public Address ShipAddress { get; set; } = null!;
}

Reading data

In the Pages folder, add two files ChartData.cshtml and ChartData.cshtml.cs.

Content of ChartData.cshtml is:

@page
@model ChartDataModel

Content of ChartData.cshtml.cs is:

using System.Text.Json;
using Google.DataTable.Net.Wrapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OrdersChartRazorGoogleWrapper.Models;

namespace OrdersChartRazorGoogleWrapper.Pages;

public class ChartDataModel : PageModel {
    private readonly ILogger<ChartDataModel> _logger;

    public ChartDataModel(ILogger<ChartDataModel> logger) {
        _logger = logger;
    }

    public async Task<IActionResult> OnGet() {
        Order[] orders = await GetOrdersAsync();

        var data = orders
          .GroupBy(_ => _.ShipAddress.City)
          .Select(g => new
          {
              Name = g.Key,
              Count = g.Count()
          })
          .OrderByDescending(cp => cp.Count)
          .ToList();


        //let's instantiate the DataTable.
        var dt = new Google.DataTable.Net.Wrapper.DataTable();
        dt.AddColumn(new Column(ColumnType.String, "Name", "Name"));
        dt.AddColumn(new Column(ColumnType.Number, "Count", "Count"));

        foreach (var item in data) {
            Row r = dt.NewRow();
            r.AddCellRange(new Cell[] {
              new Cell(item.Name),
              new Cell(item.Count)
            });
            dt.AddRow(r);
        }

        //Let's create a Json string as expected by the Google Charts API.
        return Content(dt.GetJson());
    }

    private async Task<Order[]> GetOrdersAsync() {
        HttpClient client = new HttpClient();
        var stream = client.GetStreamAsync("https://northwind.vercel.app/api/orders");
        var orders = await JsonSerializer.DeserializeAsync<Order[]>(await stream);

        return orders!;
    }
}


The above code in ChartData.cshtml.cs returns a JSON representation of  Google.DataTable.Net.Wrapper.DataTable. It contains data from the Orders API representing the number of orders by city.

At this stage, let's run our web application and verify that we are indeed able to read data from the Orders API and subsequently generate JSON data. Run your application with:

dotnet watch run

Point your browser to https://localhost:7205/chartdata

NOTE: you will need to adjust the port number to suit your environment.

This is what was revealed in my browser:


We have a sense of assurance that our data is ready to be displayed in a chart.

Charting the data

Let's first generate a simple column-chart. Replace your Pages/Index.cshtml with the following code:

@page
@model IndexModel
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

<div id="column_chart_div"></div>
<script type="text/javascript">

    google.charts.load('current', {
        packages: ['corechart', 'bar']
    });
    google.setOnLoadCallback(drawChart);

    function drawChart() {
        var jsonData = $.ajax({
            url: '/ChartData',
            dataType: "json",
            async: false
        }).responseText;
         // Create our data table out of JSON data loaded from server.
        var data = new google.visualization.DataTable(jsonData);
        var options = { title: 'Orders by city' };
        var chart = new google.visualization.ColumnChart(document.getElementById('column_chart_div'));
        chart.draw(data, options);
    }

</script>

Point your browser to the home page, you should see a column-chart as follows:


If you want to see more types of charts, replace Pages/Index.cshtml with the following code:

@page
@model IndexModel

<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

<div id="column_chart_div"></div>
<div id="line_chart_div"></div>
<div id="pie_chart_div"></div>
<div id="area_chart_div"></div>
<div id="bar_chart_div"></div>
<div id="pie_chart_3d_div"></div>


<script type="text/javascript">
    google.charts.load('current', {
        packages: ['corechart', 'bar']
    });

    google.setOnLoadCallback(drawChart);

    function drawChart() {
        var jsonData = $.ajax({
            url: '/ChartData',
            dataType: "json",
            async: false
        }).responseText;

        PopulationChart(jsonData, "column-chart");
        PopulationChart(jsonData, "line-chart");
        PopulationChart(jsonData, "pie-chart");
        PopulationChart(jsonData, "area-chart");
        PopulationChart(jsonData, "bar-chart");
        PopulationChart(jsonData, "pie-chart-3d");
    }

    function PopulationChart(jsonData, chart_type) {
        // Create our data table out of JSON data loaded from server.
        var data = new google.visualization.DataTable(jsonData);
        var chart;
        var options = { title: 'Orders by city' };

        switch (chart_type) {

            case "line-chart":
                chart = new google.visualization.LineChart(document.getElementById('line_chart_div'));
                break;
            case "pie-chart":
                chart = new google.visualization.PieChart(document.getElementById('pie_chart_div'));
                break;
            case "area-chart":
                chart = new google.visualization.AreaChart(document.getElementById('area_chart_div'));
                break;
            case "bar-chart":
                chart = new google.visualization.BarChart(document.getElementById('bar_chart_div'));
                break;
            case "pie-chart-3d":
                options.is3D = true;
                chart = new google.visualization.PieChart(document.getElementById('pie_chart_3d_div'));
                break;
            default:
                chart = new google.visualization.ColumnChart(document.getElementById('column_chart_div'));
                break;
        }

        chart.draw(data, options);
        return false;
    }

</script>    

You should see six charts on the home page, namely: column, line, pie, area, bar and pie 3D charts.




Conclusion

It is very easy and inexpensive (free) to use Google Charts to generate charts in an ASP.NET Razor application. The .NET Google DataTable wrapper (Google.DataTable.Net.Wrapper) makes it even easier.

Wednesday, December 29, 2021

Generate PDF reports from API data using iText 7 Core library in ASP.NET Razor Pages 6.0

 PDF stands for "Portable Document Format". It is, indeed, the standard for exchanging formatted documents on the internet. PDF documents are read by Adobe Acrobat Reader, most browsers, and even some popular word processors like Microsoft Word. It is a common used-case to generate PDF reports from live data. In this tutorial, I shall show you how you can easily generate a PDF report in an ASP.NET Core Razor Pages app and display data that originates from a Products API. We shall use the iText 7 library to accomplish this task. 

Source Code: https://github.com/medhatelmasry/RazorPdfDemo
Companion Video: https://youtu.be/5KxxRbApoRY

The environment I am using is:

  • Windows 11
  • .NET version 6.0.100
  • Visual Studio Code

The products API

We will work with the products API at https://northwind.vercel.app/api/products. The data in the API is generated from the well known Northwind sample SQL Server database. If you point your browser to the above address, you will see the following:

Project setup

Run the following command to create an ASP.NET Core Razor Pages application using .NET 6.0 in a folder named RazorPdfDemo:

dotnet new razor -f net6.0 -o RazorPdfDemo

Change directory into the new folder and open the project inside VS Code with the following commands:

cd RazorPdfDemo

code .

Add the iText 7 Core package so that we have the ability to generate PDF output.

dotnet add package itext7 --version 7.2.0

Product model class

Each Product JSON object contains the following properties:

Id (int)
SupplierId (int)
CategoryId (int)
QuantityPerUnit (string)
UnitPrice (decimal)
UnitsInStock (short)
ReorderLevel (short)
Discontinued (bool)
Name (string)

We need to create a Product model class that represents the above JSON object. Therefore create a Models folder and add to it a Product class with the following content:

public partial class Product {
  [JsonPropertyName("id")]
  public int Id { get; set; } 
  [JsonPropertyName("supplierId")]
  public int? SupplierId { get; set; }
  [JsonPropertyName("categoryId")]
  public int? CategoryId { get; set; }
  [JsonPropertyName("quantityPerUnit")]
  public string? QuantityPerUnit { get; set; }
  [JsonPropertyName("unitPrice")]
  public decimal? UnitPrice { get; set; }
  [JsonPropertyName("unitsInStock")]
  public short? UnitsInStock { get; set; }
  [JsonPropertyName("unitsOnOrder")]
  public short? UnitsOnOrder { get; set; }
  [JsonPropertyName("reorderLevel")]
  public short? ReorderLevel { get; set; }
  [JsonPropertyName("discontinued")]
  public bool Discontinued { get; set; }
  [JsonPropertyName("name")]
  public string Name { get; set; } = null!;
}

Generating PDF report

In the Pages folder, create two new files named Report.cshtml and Report.cshtml.cs

Add the following C# code to Report.cshtml.cs:

using iText.Kernel.Colors;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas.Draw;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPdfDemo.Models;
using System.Text.Json;

namespace RazorPdfDemo.Pages;

public class ReportModel : PageModel {
  private readonly ILogger<IndexModel> _logger;

  public ReportModel(ILogger<IndexModel> logger) {
    _logger = logger;
  }

  public async Task<IActionResult> OnGet() {
    MemoryStream ms = new MemoryStream();

    PdfWriter writer = new PdfWriter(ms);
    PdfDocument pdfDoc = new PdfDocument(writer);
    Document document = new Document(pdfDoc, PageSize.A4, false);
    writer.SetCloseStream(false);

    Paragraph header = new Paragraph("Northwind Products")
      .SetTextAlignment(TextAlignment.CENTER)
      .SetFontSize(20);

    document.Add(header);

    Paragraph subheader = new Paragraph(DateTime.Now.ToShortDateString())
      .SetTextAlignment(TextAlignment.CENTER)
      .SetFontSize(15);
    document.Add(subheader);

    // empty line
    document.Add(new Paragraph(""));

    // Line separator
    LineSeparator ls = new LineSeparator(new SolidLine());
    document.Add(ls);

    // empty line
    document.Add(new Paragraph(""));

    // Add table containing data
    document.Add(await GetPdfTable());

    // Page Numbers
    int n = pdfDoc.GetNumberOfPages();
    for (int i = 1; i <= n; i++) {
      document.ShowTextAligned(new Paragraph(String
        .Format("Page " + i + " of " + n)),
        559, 806, i, TextAlignment.RIGHT,
        VerticalAlignment.TOP, 0);
    }

    document.Close();
    byte[] byteInfo = ms.ToArray();
    ms.Write(byteInfo, 0, byteInfo.Length);
    ms.Position = 0;

    FileStreamResult fileStreamResult = new FileStreamResult(ms, "application/pdf");

    //Uncomment this to return the file as a download
    //fileStreamResult.FileDownloadName = "NorthwindProducts.pdf";

    return fileStreamResult;
  }

  private async Task<Table> GetPdfTable() {
      // Table
      Table table = new Table(4, false);

      // Headings
      Cell cellProductId = new Cell(1, 1)
         .SetBackgroundColor(ColorConstants.LIGHT_GRAY)
         .SetTextAlignment(TextAlignment.CENTER)
         .Add(new Paragraph("Product ID"));

      Cell cellProductName = new Cell(1, 1)
         .SetBackgroundColor(ColorConstants.LIGHT_GRAY)
         .SetTextAlignment(TextAlignment.LEFT)
         .Add(new Paragraph("Product Name"));

      Cell cellQuantity = new Cell(1, 1)
         .SetBackgroundColor(ColorConstants.LIGHT_GRAY)
         .SetTextAlignment(TextAlignment.CENTER)
         .Add(new Paragraph("Quantity"));

      Cell cellUnitPrice = new Cell(1, 1)
         .SetBackgroundColor(ColorConstants.LIGHT_GRAY)
         .SetTextAlignment(TextAlignment.CENTER)
         .Add(new Paragraph("Unit Price"));

      table.AddCell(cellProductId);
      table.AddCell(cellProductName);
      table.AddCell(cellQuantity);
      table.AddCell(cellUnitPrice);

      Product[] products = await GetProductsAsync();

      foreach (var item in products) {
        Cell cId = new Cell(1, 1)
            .SetTextAlignment(TextAlignment.CENTER)
            .Add(new Paragraph(item.Id.ToString()));

        Cell cName = new Cell(1, 1)
            .SetTextAlignment(TextAlignment.LEFT)
            .Add(new Paragraph(item.Name));

        Cell cQty = new Cell(1, 1)
            .SetTextAlignment(TextAlignment.RIGHT)
            .Add(new Paragraph(item.UnitsInStock.ToString()));

        Cell cPrice = new Cell(1, 1)
            .SetTextAlignment(TextAlignment.RIGHT)
            .Add(new Paragraph(String.Format("{0:C2}", item.UnitPrice)));

        table.AddCell(cId);
        table.AddCell(cName);
        table.AddCell(cQty);
        table.AddCell(cPrice);
      }

      return table;
  }

  private async Task<Product[]> GetProductsAsync() {
    HttpClient client = new HttpClient();
    var stream = client.GetStreamAsync("https://northwind.vercel.app/api/products");
    var products = await JsonSerializer.DeserializeAsync<Product[]>(await stream);
    
    return products!;
  }
}

What does the above code do?
  1. The GetProductsAsync() method makes an HTTP GET request to endpoint https://northwind.vercel.app/api/products and reads products data. It then de-serializes the data into an array of Product objects and subsequently returns the array.
  2. The GetPdfTable() method does the following:
    • The heading of the table is created. There will be four columns with titles: Product IDProduct NameQuantity and Unit Price
    • An array of Product objects is obtained from a call to the GetProductsAsync() method
    • We iterate through each item in the array and add rows of data to the table
  3. The OnGet() method does the following:
    • The first five lines in the OnGet() method sets up all the objects that are needed to generate a PDF document.
    • A header with title "Northwind Products" is placed at the top of the report - center aligned.
    • A sub-header with the current date is placed under the heading - also center aligned.
    • This is followed by an empty line, a solid-line, and another empty line.
    • The table containing product data is then displayed.
    • Paging is added to the top right-side of each page
    • Finally, the report is streamed down to the browser.
Add the following code to Report.cshtml:

@page
@model ReportModel

Let us add a menu item to the navigation of our web app. Open Pages/Shared/_Layout.cshtml and add the following markup code to the bottom of the <ul> .... </ul> navigation block:

<li class="nav-item">
  <a class="nav-link text-dark" asp-area="" asp-page="/Report">Products PDF</a>
</li>

At this stage, let's run our web app and verify that we are indeed able to read data from the Northwind products API and subsequently generate a PDF report. Run your application with:

dotnet watch run

Point your browser to https://localhost:7292

NOTE: you will need to adjust the port number to suit your environment.

This is what the home page looks like:


Click on "Products PDF". You should soon after see the PDF document being generated in your browser:




You can click on the download icon on the top right-side of the report in order to download a copy of the report. 

Alternatively, you can uncomment the following code in the OnGet() method if you want the report to get directly downloaded to your computer:

// fileStreamResult.FileDownloadName = "NorthwindProducts.pdf";

When you run the app again and click on the "Products PDF" link, the report named "NorthwindProducts.pdf" gets immediately downloaded to your computer.

Conclusion

Using the iText 7 library is pretty straight forward when it comes to generating PDF documents. You can learn more at https://kb.itextpdf.com/home/it7kb/ebooks/itext-7-jump-start-tutorial-for-net.

This article is intended to provide you with a good starting point for generating PDF reports from your ASP.NET Razor Pages web apps.

Friday, December 17, 2021

Explore .NET MAUI Blazor Apps with .NET 6.0 & Visual Studio 2022 Version 17.1.0 Preview 1.1

In a previous article, I wrote about .NET MAUI Apps. In this article, I will discuss the Blazor version of .NET MAUI Apps. This is known as .NET MAUI Blazor Apps.

MAUI is not yet officially released. The current bits offer a glimpse into what the final product will look like. 

This is the environment that I am using:

  • Windows 11 Version 21H2
  • Visual Studio 2022 Version 17.1.0 Preview 1.1
  • .NET 6.0.101
Source code for this application can be found at: https://github.com/medhatelmasry/FirstMauiBlazorApp

Setup

You will find installation instructions for .NET MAUI at: https://docs.microsoft.com/en-us/dotnet/maui/get-started/installation

The only workload I installed in Visual Studio 2022 (Preview) is "Mobile development with .NET", as shown below:


It is also worth noting that I do not have any other Android development application (like Android Studio) installed on my computer. The above Visual Studio 2022 workload also installed an Android emulator.

Application

Let's get started exploring what apps we can develop with .NET MAUI. Start Visual Studio 2022 (Preview) and select "Create a new project":


Enter "maui" in the filter field. You will discover that there are three MAUI-related projects that you can create - namely: 
  1. .NET MAUI App
  2. .NET MAUI Blazor App
  3. .NET MAUI Class Library
In this article, I explore the second in the above list - .NET MAUI Blazor App. Select this project type then click Next:




I named my application FirstMauiBlazorApp:




Let's firstly run our app on Windows. From the drop-down-list at the top, make sure you have chosen "Windows Machine".


Click on "Windows Machine" to run the application. Soon after, you should experience the following application running on your desktop:


Stop the application by either closing it or clicking on the red square button on the top of Visual Studio 2022. You will find that the application is installed in your "Apps and Features" on windows. You can, of course, uninstall it if you so desire.

Adding our own page

Add a "Razor Component..." to the Pages folder of your application.


I named my file Toons.razor.

We will modify Toons.razor so that it reads an online API that contains some cartoon characters. If you point your browser to https://apipool.azurewebsites.net/api/toons it will show the following data:

[{"id":1,"lastName":"Flintstone","firstName":"Fred","occupation":"Mining Manager","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/fred.png","votes":0},{"id":2,"lastName":"Rubble","firstName":"Barney","occupation":"Mining Assistant","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/barney.png","votes":0},{"id":3,"lastName":"Rubble","firstName":"Betty","occupation":"Nurse","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/betty.png","votes":0},{"id":4,"lastName":"Flintstone","firstName":"Wilma","occupation":"Teacher","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/wilma.png","votes":0},{"id":5,"lastName":"Rubble","firstName":"Bambam","occupation":"Baby","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/bambam.png","votes":0},{"id":6,"lastName":"Flintstone","firstName":"Pebbles","occupation":"Baby","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/pebbles.png","votes":0},{"id":7,"lastName":"Flintstone","firstName":"Dino","occupation":"Pet","gender":"F","pictureUrl":"https://api4all.azurewebsites.net/images/flintstone/dino.png","votes":0},{"id":8,"lastName":"Mouse","firstName":"Micky","occupation":"Hunter","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/disney/MickyMouse.png","votes":0},{"id":9,"lastName":"Duck","firstName":"Donald","occupation":"Sailor","gender":"M","pictureUrl":"https://api4all.azurewebsites.net/images/disney/DonaldDuck.png","votes":0}]

Each JSON object contains the following properties:

id (int)
lastName (string)
firstName (string)
occupation (string)
gender (string)
pictureUrl (string)
votes (int)

Replace the content of Toons.razor with the following code:

@page "/toons"

@using System.Text.Json
@using System.Text.Json.Serialization

<h1>Toon Characters</h1>

@if (toonList == null) {
  <p><em>Loading...</em></p>
} else {
  <table class="table">
    <tbody>
      @foreach (var item in toonList) {
        <tr>
          <td>@item.FullName</td>
          <td><img src="@item.PictureUrl" style="height: 40px" alt="@item.FirstName @item.LastName"> </td>
        </tr>
      }
    </tbody>
  </table>
}

@code {
  private Toon[] toonList;

  protected override async Task OnInitializedAsync() {
      HttpClient client = new HttpClient();
      var stream = client.GetStreamAsync("https://apipool.azurewebsites.net/api/toons");
      toonList = await JsonSerializer.DeserializeAsync<Toon[]>(await stream);
  }

  public class Toon {
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("lastName")]
    public string LastName { get; set; }

    [JsonPropertyName("firstName")]
    public string FirstName { get; set; }

    [JsonPropertyName("occupation")]
    public string Occupation { get; set; }

    [JsonPropertyName("gender")]
    public string Gender { get; set; }

    [JsonPropertyName("pictureUrl")]
    public string PictureUrl { get; set; }

    [JsonPropertyName("votes")]
    public int Votes { get; set; }

    public string FullName {
      get {
        return string.Format("{0} {1}", this.FirstName, this.LastName);
      }
    }
  } 

Finally, edit the home page, Index.razor, so that it displays cartoon characters. This is done by updating Index.razor so that it looks like this:

@page "/"

<Toons />

Run your application and you will see the following output:


Let us see what this app looks like in an android emulator. To setup an emulator, choose Tools >> Android >> Android Device Manager...


You can configure an Android device of your choice. In my case, even though I configured both Pixel 4 & Pixel 5, I found Pixel 4 to be more cooperative.


You can start the emulator of your choice from within the Android Device Manager.

Choose the android emulator of your choice in the run drop-down-list at the top of Visual Studio 2022:


Run your app in the Android emulator. This is what it should look like:

If you have built some application using Blazor, here is an opportunity for you to migrate some of that functionality into the mobile world.