Technologies


Extend your application from Docker-Exercise7_WebApp+Db. Add a new functionality that allows the generation of PDF documents, so you can print your TODO-Lists.

Add a new button to your web application that starts the PDF generation for your Todo List. When pressing the button, send a message to a container that runs a message-oriented middleware. I recommend using a RabbitMQ image from Docker Hub.

Add another container, with a simple application that you build yourself using C#. The container should connect to the message-oriented middleware and receive messages. The message contains all todo items, that should be shown on the PDF. Save the generated file to a path inside the container and add a bind mount to your host machine, so you can access the generated PDFs.

For generating the PDFs you can use any NuGet package that you like, e.g. QuestPDF. To use the free Community License for QuestPdf you have to activate it once in code:

QuestPDF.Settings.License = LicenseType.Community;

# Assignment

docker-compose.yaml:

version: "3.8"
services:
  web:
    build:
      context: ./ToDoApp/
      dockerfile: ./ToDoApp/Dockerfile
    environment:
      - ConnectionStrings__DefaultConnection=Server=db;Database=${MYSQL_DATABASE};User=${MYSQL_USER};Password=${MYSQL_PASSWORD};
    ports:
      - "6500:8080"
      - "6501:8081"
    depends_on:
      - db
      - rabbitmq
  db:
    image: mysql:8.0.21
    environment:
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
  pdf-generator:
    build:
      context: ./ToDoApp/
      dockerfile: ./PdfGenerator_Service/Dockerfile
    volumes:
      - ./pdf-export:/app/pdf
    depends_on:
      - rabbitmq
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"
volumes:
  db-data:
 

PdfGen>Program.cs

using QuestPDF.Fluent;  
using QuestPDF.Helpers;  
using RabbitMQ.Client;  
using RabbitMQ.Client.Events;  
using System.Text;  
using Newtonsoft.Json;  
using QuestPDF.Infrastructure;  
using Model.Entities;  
  
// Set QuestPDF license  
QuestPDF.Settings.License = LicenseType.Community;  
  
var tries = 0;  
// Retry connection to RabbitMQ  
while (true)  
{  
    try  
    {  
        tries++;  
        Console.WriteLine("RabbitMQ is online!");  
        break; // Stop retrying once connected  
    }  
    catch  
    {  
        Console.WriteLine($"RabbitMQ not ready yet. Retrying... ({tries})");  
        await Task.Delay(5000); // Wait before retrying  
    }  
}  
  
// Connect to RabbitMQ and declare queue  
var factory = new ConnectionFactory() { HostName = "rabbitmq" };  
using var connection = factory.CreateConnection();  
using var channel = connection.CreateModel();  
  
// Declare a queue for PDF generation requests  
channel.QueueDeclare(queue: "pdf_generation", durable: false, exclusive: false, autoDelete: false, arguments: null);  
  
// Set up consumer to listen for messages  
var consumer = new EventingBasicConsumer(channel);  
consumer.Received += (model, ea) => ReceiveMessage(model, ea);  
  
// Start consuming messages  
channel.BasicConsume(queue: "pdf_generation", autoAck: true, consumer: consumer);  
 
Console.WriteLine("Waiting for messages...");  
while (true)  
{  
    await Task.Delay(10000); // Keep the process alive  
}  
  
// Handle received messages  
void ReceiveMessage(object model, BasicDeliverEventArgs e)  
{  
    Console.WriteLine("Message received!");  
    var body = e.Body.ToArray();  
    var message = Encoding.UTF8.GetString(body);  
    var todoItems = JsonConvert.DeserializeObject<List<ToDoItem>>(message);  
  
    // Log received items  
    Console.WriteLine("Items received:");  
    foreach (var item in todoItems)  
    {  
        Console.WriteLine($"--Title: {item.Title}, IsDone: {item.IsDone}");  
    }  
    // Generate the PDF  
    GeneratePDF(todoItems);  
}  
  
// Generate PDF for the to-do list  
void GeneratePDF(List<ToDoItem> items)  
{  
    Console.WriteLine("Generating PDF...");  
    var filePath = $"/app/pdf/export{DateTime.Now:yyyyMMdd_hhmmss}.pdf";  
  
    try  
    {  
        // Create the PDF document  
        var document = Document.Create(document =>  
        {  
            document.Page(page =>  
            {  
                page.Margin(20);  
                page.Size(PageSizes.A4);  
                page.DefaultTextStyle(x => x.FontSize(12));  
  
                // Header  
                page.Header()  
                    .Text("To-Do List")  
                    .FontSize(18)  
                    .Bold()  
                    .AlignCenter();  
  
                // Content: List of To-Do items  
                page.Content()  
                    .Column(column =>  
                    {  
                        column.Spacing(5);  
                        column.Item().Text("To-Do Items:");  
                        foreach (var item in items)  
                        {  
                            column.Item().Text($"- {item.Title} (Done: {(item.IsDone ? "Yes" : "No")})");  
                        }  
                    });  
  
                // Footer: Date generated  
                page.Footer()  
                    .AlignCenter()  
                    .Text($"Generated on {DateTime.Now:yyyy-MM-dd HH:mm}");  
            });  
        });  
  
        // Save the PDF  
        document.GeneratePdf(filePath);  
    }  
    catch (Exception e)  
    {  
        Console.WriteLine(e.Message);  
        Console.WriteLine("Error generating PDF");  
    }  
  
    Console.WriteLine($"PDF generated at {filePath}");  
}

UI>Home.razor:

@page "/"
@using Model.Entities
@using RabbitMQ.Client
@using Newtonsoft.Json
@using System.Text
@inject TodoContext context
@inject HttpClient httpClient
@rendermode InteractiveServer
 
<PageTitle>ToDoList</PageTitle>
 
<h1>ToDo List</h1>
 
<!-- Button to delete all items -->
<button @onclick=DeleteAllItems>Delete Everything</button>
 
<!-- Input field for adding new items -->
<input type="text" @bind="newItemTitle"/>
<button @onclick="AddItem">Add</button>
 
<br>
<br>
 
<!-- Table displaying ToDo items -->
<table>
    <thead>
    <tr>
        <th>Id</th>
        <th>Title</th>
        <th>Is Done</th>
        <th>Action</th>
    </tr>
    </thead>
    <tbody>
    @foreach (var item in context.ToDoItems)
    {
        <tr>
            <td>@item.Id</td>
            <td>@item.Title</td>
            <td>
                <input type="checkbox" @bind="item.IsDone">
            </td>
            <td>
                <!-- Button to delete individual item -->
                <button @onclick="() => DeleteItem(item.Id)">Delete</button>
            </td>
        </tr>
    }
    </tbody>
</table>
 
<br>
<br>
 
<!-- Button to save changes to the database -->
<button @onclick="() => context.SaveChanges()">Save Changes</button>
 
@if (ActivatePDFExport)
{
    <!-- Button to export data to PDF if RabbitMQ is online -->
    <button @onclick="ExportToPdf">Export to PDF</button>
}
else
{
    <p>Export to PDF is disabled until RabbitMQ is online.</p>
}
 
@code {
    private string newItemTitle { get; set; }  // New item title
    private bool ActivatePDFExport { get; set; }  // Flag to check if PDF export is available
 
    // Check if RabbitMQ is online
    protected override async Task OnParametersSetAsync()
    {
        ActivatePDFExport = await RabbitMqAlive();
    }
 
    // Add a new item to the list
    private void AddItem()
    {
        if (string.IsNullOrWhiteSpace(newItemTitle)) return;
        var newItem = new ToDoItem { Title = newItemTitle, IsDone = false };
        context.ToDoItems.Add(newItem);
        context.SaveChanges();
        newItemTitle = string.Empty;  // Clear the input field
    }
 
    // Delete a single item by id
    private void DeleteItem(int id)
    {
        var item = context.ToDoItems.FirstOrDefault(i => i.Id == id);
        if (item == null) return;
        context.ToDoItems.Remove(item);
        context.SaveChanges();
    }
 
    // Delete all items
    private void DeleteAllItems()
    {
        context.ToDoItems.RemoveRange(context.ToDoItems);
        context.SaveChanges();
    }
    
    // Check if RabbitMQ is available
    private async Task<bool> RabbitMqAlive()
    {
        try
        {
            var response = await httpClient.GetAsync("http://rabbitmq:15672/api/health/checks/alarms");
            return true;
        }
        catch { return false; }
    }
 
    // Export data to PDF
    private async Task ExportToPdf()
    {
        if (!await RabbitMqAlive()) return;  // Ensure RabbitMQ is available
        var todoItems = context.ToDoItems.ToList();  // Get all to-do items
        SendToRabbitMq(todoItems);  // Send data to RabbitMQ for PDF generation
    }
    
    // Send data to RabbitMQ for PDF generation
    private static async Task SendToRabbitMq(List<ToDoItem> todoItems)
    {
        var factory = new ConnectionFactory() { HostName = "rabbitmq" };
        using var connection = factory.CreateConnection();
        using var channel = connection.CreateModel();
 
        // Declare the queue
        channel.QueueDeclare(queue: "pdf_generation", durable: false, exclusive: false, autoDelete: false, arguments: null);
 
        // Serialize the items and send to the queue
        var message = JsonConvert.SerializeObject(todoItems);
        var body = Encoding.UTF8.GetBytes(message);
 
        channel.BasicPublish(exchange: "", routingKey: "pdf_generation", basicProperties: null, body: body);
    }
}

RabbitMq UI:

Export to .pdf: