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

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 11, 2020

Use Service Reference in Visual Studio 2019 to consume a REST API with OpenAPI description from a C# console app

In this tutorial I will show you how easy it is to consume a REST API Service using the ‘Service Reference …’ feature in Visual Studio 2019. I will consume a simple service located at https://api4all.azurewebsites.net/

Companion video: https://youtu.be/XqIniGHpcEc

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

Prerequisites:

  1. You need to have Visual Studio 2019 installed on your Windows computer. In my case, I am using: Microsoft Visual Studio Community 2019 Version 16.8.2
  2. The REST API you are consuming needs to have an OpenAPI programming language-agnostic interface description for REST APIs.
Point your browser to https://api4all.azurewebsites.net/. The landing page is an OpenAPI page that has the following heading:

OpenAPI Endpoint







Copy the link to the swagger.json file in the top-left corner and park it in Notepad.exe for later use.

Let us create a simple “Console App (.NET Core)” application in Visual Studio 2019 to consume the REST API service.
Console App (.NET Core)

Once your console app is created, Add >> Service Reference …

Add >> Service Reference
Choose OpenAPI, then click Next.
OpenAPI

Select URL then paste into the textbox the link to the swagger.json file that you parked in Notepad.exe. Click on Finish.
OpenAPI URL

A "Service reference configuration progress" dialog will appear. Click Close when it is done.
Service reference configuration progress

Looking at "Solution Explorer", you will find a folder named OpenAPI with swagger.json contained inside it.
swagger.json


Build (or compile) your application so that the proxy class that gets created is visible in your application's environment.

If you open your obj folder in File Explorer, you will find that a proxy class named swaggerClient.cs was auto-magically created for you.
swaggerClient.cs

Feel free to open this file in a text editor to have a peek at its contents. Alternatively, you can click on ... in your Service Dependencies dialog and select "View generated code".
View generated code


These are the CRUD methods that are provided  by the swaggerClient object:

Method nameHTTP VerbPurpose
StudentsAllAsyncGETretrieve all students
Students2AsyncGETget student by id
StudentsAsyncPOSTadd a student
Students3AsyncPUTupdate student data
Students4AsyncDELETEdelete student by id

It is obvious that the swaggerClient method names are not very intuitive. The above table will help you figure out the translation.

Add the following instance variable, that represents the BASE_URL, to the Program.cs class:

const string BASE_URL = "https://api4all.azurewebsites.net";

Add the following displayStudents() method to Program.cs:

private static async Task displayStudents() {
  using (var httpClient = new HttpClient()) {
    var client = new swaggerClient(BASE_URL, httpClient);
    var items = await client.StudentsAllAsync();
    foreach (var i in items) {
      Console.WriteLine($"{i.StudentId}\t{i.FirstName}\t{i.LastName}\t{i.School}");
    }
  }
}

Replace your Main() method in Program.cs with the following code:

static async Task Main(string[] args) {
   await displayStudents();
}

OpenAPI output

Below are the other methods that complete all other CRUD operations:

private static async Task getStudentById(string id) {
  using (var httpClient = new HttpClient()) {
    var client = new swaggerClient(BASE_URL, httpClient);
    var item = await client.Students2Async(id);
    Console.WriteLine($"{item.StudentId}\t{item.FirstName}\t{item.LastName}\t{item.School}");
  }
}

private static async Task addStudent() {
  using (var httpClient = new HttpClient()) {
    var client = new swaggerClient(BASE_URL, httpClient);
    var student = new Student() {
        StudentId = "A00777776",
        FirstName = "Joe",
        LastName = "Roy",
        School = "Forestry"
    };

    try {
      var response = await client.StudentsAsync(student);
    } catch (ApiException apiEx) {
      // not really error because server returns HTTP Status Code 201
      Console.WriteLine(apiEx.ToString());
    }
  }
}

private static async Task updateStudentById(string id) {
  using (var httpClient = new HttpClient()) {
    var client = new swaggerClient(BASE_URL, httpClient);
    var student = new Student() {
        StudentId = id,
        FirstName = "Pam",
        LastName = "Day",
        School = "Nursing"
    };
    await client.Students3Async(id, student);
  }
}

private static async Task deleteStudentById(string id) {
    using (var httpClient = new HttpClient()) {
      var client = new swaggerClient(BASE_URL, httpClient);
      var item = await client.Students4Async(id);
    }
}

I hope you appreciate how easy and painless it is to consume an OpenAPI REST service using Visual Studio 2019.