How to Generate PDF in ASP.NET Core MVC

How to Generate PDF in ASP.NET Core MVC

In this article, I will discuss How to Generate PDFs in ASP.NET Core MVC Application with Examples. Please read our previous article discussing How to Import Excel Data to a Database in an ASP.NET Core MVC Application with Examples.

What is a PDF File?

PDF (Portable Document Format) is a file format developed by Adobe for presenting documents independently of software, hardware, or operating systems. A PDF file encapsulates text, fonts, images, and other elements into a single document that maintains its original layout and formatting across various platforms.

Why Do We Need PDF Files in Web Applications?

PDF files are widely used because of their portability, consistent formatting, and support for security features such as encryption and digital signatures. The following are some of the reasons:

  • Professional Presentation: PDF files maintain a professional appearance, and they are often required for invoices, reports, contracts, and user manual guides.
  • Easy for End-Users to Download/Print: Users often need a fixed-format document, such as an invoice, report, or receipt, that can be easily downloaded, saved, or printed.
  • Preservation of Formatting: PDF ensures that the content appears consistently across different devices and platforms.
  • Security: PDF files can include password protection, encryption, watermarks, and digital signatures for secure and verifiable documentation, safeguarding sensitive information.
  • Offline Access: Users can download and view PDFs offline while maintaining the layout and design.

In web applications, allowing users to download content as a PDF can improve usability and enhance the user experience. For instance, generating downloadable invoices, receipts, or reports from a web application enhances the user experience.

Commonly Used Libraries for Generating PDFs in ASP.NET Core MVC

There are several libraries available for generating PDFs in ASP.NET Core MVC. They are as follows:

  • iTextSharp/iText7: A robust library for creating and manipulating PDF files. It supports advanced features like digital signatures, encryption, and form filling. Feature-rich but requires a commercial license for advanced features.
  • PdfSharp/MigraDoc: A popular open-source solution for generating PDF documents. PdfSharp focuses on drawing PDF documents, while MigraDoc provides high-level document layout capabilities.
  • Rotativa.AspNetCore: Convert MVC views into PDF files.
  • QuestPDF: A modern library that emphasizes simplicity and flexibility. It uses a fluent API to define the document structure and design, making it easier to produce professional-looking PDFs.
  • IronPDF: A commercial library known for rendering HTML to PDF with excellent fidelity. It supports CSS, JavaScript, and complex layouts.

Each of these libraries has its strengths and is suitable for different scenarios. Choosing the right one depends on your specific requirements, such as the complexity of the document layout, licensing considerations, and ease of use.

How to Generate PDF in ASP.NET Core MVC:

Now, we will build a Real-Time Order Management Application using ASP.NET Core MVC with the Entity Framework (EF) Core Code First approach. This application will demonstrate how to fetch order data from the SQL Server database and generate a professional PDF Invoice summary using the iText7 library.

This order management application’s objective is to allow users to view detailed order information, including product details, quantities, prices, tax, customer details, order number, order date, and payment information, and generate a professionally formatted PDF invoice on demand. Let us first see the application’s pictorial representation.

Order Listing Page:

This Page will retrieve all the Orders of a Customer. For the time being and for testing purposes, we are retrieving all customers’ orders.

How to Generate PDF in ASP.NET Core MVC

Order Details Page:

Once the user clicks on the Details button, the Order Details Page will open.

Generating a PDF in ASP.NET Core MVC Applications

Downloaded PDF:

Once the user clicks on the Download PDF button, it should download the Invoice PDF, and the PDF should look as follows:

How to Generate PDFs in ASP.NET Core MVC Application with Examples

Technologies and Frameworks Used to Implement the above Project:
  • ASP.NET Core MVC: For the web application’s architecture, enabling separation of concerns and maintainable code.
  • Entity Framework Core (EF Core): For data access, we will use the EF Core Code-first approach to define the data model and automatically manage the database schema.
  • SQL Server: The relational database used to store invoice data.
  • iText7: A robust PDF generation library for creating professional-quality invoice PDFs.
  • C# Language: The primary language for business logic, controllers, and services.
  • Bootstrap: For responsive, professional-looking views.
Create a New ASP.NET Core MVC Project and Install Packages

Open Visual Studio and create a new ASP.NET Core Web App (Model-View-Controller) project. Name the project InvoiceManagementApp. Then, install the necessary NuGet packages using the Package Manager Console:

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
  • Install-Package iText7
  • Install-Package itext7.bouncy-castle-adapter

Note: iText7 uses BouncyCastle for cryptographic operations (such as encryption or digital signatures). When these features are invoked (even indirectly during PDF generation), the library requires the adapter to interface with BouncyCastle.

Creating Models

Models are the classes that represent the data and business/domain entities in our application. In our example, we will use five models: Product, Customer, Order, Order Item, and Payment.

Customer.cs

Create a file named Customer.cs in the Models folder, and then copy and paste the following code. The Customer entity represents customer details, including name, email, address, and phone. It also establishes a one-to-many relationship with orders (i.e., one customer can have multiple orders).

using System.ComponentModel.DataAnnotations;

namespace InvoiceManagementApp.Models
{
    // Represents customer details.
    public class Customer
    {
        public int CustomerId { get; set; }

        [Required, StringLength(100)]
        public string Name { get; set; }

        [Required, EmailAddress, StringLength(100)]
        public string Email { get; set; }

        [StringLength(250)]
        public string Address { get; set; }

        [StringLength(15)]
        public string Phone { get; set; }

        // Navigation property: one customer can have many orders.
        public ICollection<Order> Orders { get; set; }
    }
}
Product.cs

Create a file named Product.cs in the Models folder, and then copy and paste the following code. The Product entity represents a product that can be included in orders. It defines properties like name, description, and price and is linked to OrderItem through a navigation property.

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace InvoiceManagementApp.Models
{
    // Represents a Product that can be included in orders
    public class Product
    {
        [Key]
        public int ProductId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        [StringLength(500)]
        public string Description { get; set; }

        [Required]
        [Column(TypeName = "decimal(18,2)")]
        public decimal Price { get; set; }

        // Navigation property: one product can be part of many order items.
        public ICollection<OrderItem> OrderItems { get; set; }
    }
}
Order.cs

Create a file named Order.cs in the Models folder, and then copy and paste the following code. The Order entity represents an order (or invoice) with details about the customer, order items, payment, and totals. It serves as the central model connecting Customer, OrderItem, and Payment.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace InvoiceManagementApp.Models
{
    // Represents an order (invoice) with Customer, Payment and Order Item details.
    public class Order
    {
        public int OrderId { get; set; }

        [Required, StringLength(50)]
        public string OrderNumber { get; set; }

        [Required]
        public DateTime OrderDate { get; set; }

        // Foreign key and navigation property for Customer
        public int CustomerId { get; set; }
        public Customer Customer { get; set; }

        // Collection of OrderItems
        // Navigation property: an order can have multiple order items.
        public ICollection<OrderItem> OrderItems { get; set; }

        // Navigation property for Payment (one-to-one relationship)
        public Payment Payment { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal SubTotal { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal TotalTax { get; set; }

        [Column(TypeName = "decimal(18,2)")]
        public decimal GrandTotal { get; set; }
    }
}
OrderItem.cs

Create a file named OrderItem.cs in the Models folder, and then copy and paste the following code. The OrderItem entity represents individual items in an order. This model connects a specific product to an order, tracks quantity, unit price, and tax, and calculates totals for each item.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace InvoiceManagementApp.Models
{
    // Represents an individual item within an order.
    public class OrderItem
    {
        public int OrderItemId { get; set; }

        // Foreign key to the Order.
        public int OrderId { get; set; }
        public Order Order { get; set; }

        // Foreign key to the Product.
        public int ProductId { get; set; }
        public Product Product { get; set; }

        [Required]
        [Range(1, int.MaxValue)]
        public int Quantity { get; set; }

        // Unit price captured at the time of order (may differ from Product.Price).
        [Required]
        [Column(TypeName = "decimal(18,2)")]
        public decimal UnitPrice { get; set; }

        // Tax percentage (e.g., 5, 10, etc.)
        [Required]
        [Column(TypeName = "decimal(5,2)")]
        public decimal TaxPercent { get; set; }

        // Computed properties
        [NotMapped]
        public decimal TotalPrice => Quantity * UnitPrice;

        [NotMapped]
        public decimal TotalTaxAmount => (TotalPrice * TaxPercent) / 100;

        [NotMapped]
        public decimal TotalPriceWithTax => TotalPrice + TotalTaxAmount;
    }
}
Payment.cs

Create a file named Payment.cs in the Models folder, and then copy and paste the following code. The Payment entity represents payment details for a given order, including method, status, amount, and date. It is associated one-to-one with an order.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace InvoiceManagementApp.Models
{
    // Represents payment details for an order.
    public class Payment
    {
        public int PaymentId { get; set; }

        [StringLength(50)]
        public string PaymentMethod { get; set; } // e.g., Credit Card, Cash, Bank Transfer

        public DateTime? PaymentDate { get; set; }

        [StringLength(50)]
        public string PaymentStatus { get; set; }  // e.g., Paid, Pending, Overdue

        [Column(TypeName = "decimal(18,2)")]
        public decimal PaymentAmount { get; set; }

        // Foreign key: one Payment typically belongs to one Order
        public int OrderId { get; set; }
        public Order Order { get; set; }
    }
}
Data Access (DbContext) with Seed Data

The ApplicationDbContext class manages database connections and mappings. It defines DbSet properties for each model to enable CRUD operations. This class also uses OnModelCreating to configure relationships (e.g., one-to-one between Order and Payment, one-to-many between Order and OrderItems). It also seeds initial data for products, customers, orders, order items, and payments.

First, create a folder named Data in the project root directory. Then, inside the Data folder, create a class file named ApplicationDbContext.cs and copy and paste the following code.

using InvoiceManagementApp.Models;
using Microsoft.EntityFrameworkCore;

namespace InvoiceManagementApp.Data
{
    public class ApplicationDbContext : DbContext
    {
        // Constructor accepting DbContextOptions for dependency injection.
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

        // Fluent API configuration and seed data.
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // One-to-one relationship between Order and Payment.
            modelBuilder.Entity<Order>()
                .HasOne(o => o.Payment)
                .WithOne(p => p.Order)
                .HasForeignKey<Payment>(p => p.OrderId);

            // One-to-many relationship between Order and OrderItems.
            modelBuilder.Entity<Order>()
                .HasMany(o => o.OrderItems)
                .WithOne(oi => oi.Order)
                .HasForeignKey(oi => oi.OrderId);

            // Seed data for Products.
            modelBuilder.Entity<Product>().HasData(
                new Product { ProductId = 1, Name = "Laptop", Description = "High performance laptop", Price = 1200.00m },
                new Product { ProductId = 2, Name = "Smartphone", Description = "Latest model smartphone", Price = 800.00m },
                new Product { ProductId = 3, Name = "Headphones", Description = "Noise-cancelling headphones", Price = 150.00m }
            );

            // Seed data for Customers.
            modelBuilder.Entity<Customer>().HasData(
                new Customer { CustomerId = 1, Name = "Alice Johnson", Email = "alice@example.com", Phone = "555-1234", Address = "123 Main St, City A" },
                new Customer { CustomerId = 2, Name = "Bob Smith", Email = "bob@example.com", Phone = "555-5678", Address = "456 Elm St, City B" }
            );

            // Seed data for Orders.
            // For OrderId 1: 
            //   Items: 1 × Laptop (Price 1200, TaxPercent 10) and 2 × Headphones (Price 150, TaxPercent 10).
            //   SubTotal = (1200*1)+(150*2) = 1500, TotalTax = (1200*1*0.10)+(150*2*0.10)=120+30=150, GrandTotal = 1650.
            // For OrderId 2:
            //   Items: 1 × Smartphone (Price 800, TaxPercent 10).
            //   SubTotal = 800, TotalTax = 800*0.10=80, GrandTotal = 880.
            modelBuilder.Entity<Order>().HasData(
                new Order
                {
                    OrderId = 1,
                    OrderNumber = "ORD1001",
                    OrderDate = DateTime.Today.AddDays(-5),
                    CustomerId = 1,
                    SubTotal = 1500.00m,
                    TotalTax = 150.00m,
                    GrandTotal = 1650.00m
                },
                new Order
                {
                    OrderId = 2,
                    OrderNumber = "ORD1002",
                    OrderDate = DateTime.Today.AddDays(-2),
                    CustomerId = 2,
                    SubTotal = 800.00m,
                    TotalTax = 80.00m,
                    GrandTotal = 880.00m
                }
            );

            // Seed data for OrderItems (using TaxPercent instead of Tax amount).
            modelBuilder.Entity<OrderItem>().HasData(
                new OrderItem { OrderItemId = 1, OrderId = 1, ProductId = 1, Quantity = 1, UnitPrice = 1200.00m, TaxPercent = 10.00m },
                new OrderItem { OrderItemId = 2, OrderId = 1, ProductId = 3, Quantity = 2, UnitPrice = 150.00m, TaxPercent = 10.00m },
                new OrderItem { OrderItemId = 3, OrderId = 2, ProductId = 2, Quantity = 1, UnitPrice = 800.00m, TaxPercent = 10.00m }
            );

            // Seed data for Payments with PaymentStatus.
            modelBuilder.Entity<Payment>().HasData(
                new Payment
                {
                    PaymentId = 1,
                    OrderId = 1,
                    PaymentMethod = "Credit Card",
                    PaymentAmount = 1650.00m,
                    PaymentDate = DateTime.Today.AddDays(-4),
                    PaymentStatus = "Paid"
                },
                new Payment
                {
                    PaymentId = 2,
                    OrderId = 2,
                    PaymentMethod = "PayPal",
                    PaymentAmount = 880.00m,
                    PaymentDate = DateTime.Today.AddDays(-1),
                    PaymentStatus = "Paid"
                }
            );
        }

        // DbSets for each entity.
        public DbSet<Product> Products { get; set; }
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }
        public DbSet<Payment> Payments { get; set; }
    }
}
Creating Order Service

The Order Service encapsulates business logic for fetching orders and generating invoice PDFs. It uses ApplicationDbContext to retrieve data (e.g., fetching all orders or a specific order with details). The Order Service organizes the data into a meaningful structure that the controller or views can consume. It generates PDF invoices using iText7 and returns the PDF as a byte array.

First, create a folder named Services in the project root directory. Then, create a class named OrderService.cs within the Services folder and copy and paste the following code.

using InvoiceManagementApp.Models;
using iText.Kernel.Pdf;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using InvoiceManagementApp.Data;
using iText.IO.Font.Constants;
using iText.Kernel.Font;
using iText.Kernel.Colors;
using iText.Kernel.Geom;
using iText.Kernel.Pdf.Canvas.Draw;
using iText.Layout.Borders;

namespace InvoiceManagementApp.Services
{
    public class OrderService
    {
        private readonly ApplicationDbContext _context;

        // Constructor injection for AppDbContext.
        public OrderService(ApplicationDbContext context)
        {
            _context = context;
        }

        // Retrieves a specific order by ID including customer, order items (with product details), and payment.
        public async Task<Order?> GetOrderByIdAsync(int id)
        {
            return await _context.Orders
                .AsNoTracking()
                .Include(o => o.Customer)
                .Include(o => o.OrderItems)
                    .ThenInclude(oi => oi.Product)
                .Include(o => o.Payment)
                .FirstOrDefaultAsync(o => o.OrderId == id);
        }

        // Retrieves all orders with related customer information.
        public async Task<List<Order>> GetAllOrdersAsync()
        {
            return await _context.Orders
                .AsNoTracking()
                .Include(o => o.Customer)
                .ToListAsync();
        }

        // Generates a professional PDF invoice for the specified order using iText7.
        // orderId: The ID of the order to generate PDF for.
        // returns: Byte array representing the generated PDF file.
        public async Task<byte[]> GenerateOrderPdfAsync(int orderId)
        {
            var order = await GetOrderByIdAsync(orderId);
            if (order == null)
            {
                throw new Exception("Order not found.");
            }

            using (MemoryStream ms = new MemoryStream())
            {
                PdfWriter writer = new PdfWriter(ms);
                PdfDocument pdf = new PdfDocument(writer);
                Document document = new Document(pdf, PageSize.A4);
                document.SetMargins(20, 20, 20, 20);

                // Create fonts.
                PdfFont boldFont = PdfFontFactory.CreateFont(StandardFonts.HELVETICA_BOLD);
                PdfFont regularFont = PdfFontFactory.CreateFont(StandardFonts.HELVETICA);

                // --------------------------------------------------------------------
                // Invoice Header: Company Name, Company Address & Invoice Info.
                Table headerTable = new Table(2).UseAllAvailableWidth();

                // Left cell: Company Name and Address (hard coded) in smaller font.
                Cell leftCell = new Cell()
                    .Add(new Paragraph("My ECommerce App")
                        .SetFont(boldFont)
                        .SetFontSize(26))
                    .Add(new Paragraph("1234 Business Rd, Suite 100\nCityville, ST 12345")
                        .SetFont(boldFont)
                        .SetFontSize(12))
                    .SetBorder(Border.NO_BORDER);
                headerTable.AddCell(leftCell);

                // Right cell: Invoice Number and Date.
                Cell rightCell = new Cell()
                    .Add(new Paragraph($"Invoice #{order.OrderNumber}\nDate: {order.OrderDate:yyyy-MM-dd}")
                        .SetFont(boldFont)
                        .SetTextAlignment(TextAlignment.RIGHT))
                    .SetBorder(Border.NO_BORDER);
                headerTable.AddCell(rightCell);
                document.Add(headerTable);

                // Add a horizontal line after the header.
                document.Add(new LineSeparator(new SolidLine()).SetMarginTop(10).SetMarginBottom(10));

                // --------------------------------------------------------------------
                // Customer & Payment Details (2-column layout).
                Table detailsTable = new Table(2).UseAllAvailableWidth();

                // Left: Customer Details.
                Cell customerCell = new Cell()
                    .Add(new Paragraph("Bill To:")
                        .SetFont(boldFont)
                        .SetFontSize(14))
                    .Add(new Paragraph(order.Customer.Name).SetFont(regularFont))
                    .Add(new Paragraph(order.Customer.Address).SetFont(regularFont))
                    .Add(new Paragraph($"Email: {order.Customer.Email}").SetFont(regularFont))
                    .Add(new Paragraph($"Phone: {order.Customer.Phone}").SetFont(regularFont))
                    .SetBorder(Border.NO_BORDER);
                detailsTable.AddCell(customerCell);

                // Right: Payment Details.
                Cell paymentCell = new Cell()
                    .Add(new Paragraph("Payment Details:")
                        .SetFont(boldFont)
                        .SetFontSize(14))
                    .SetTextAlignment(TextAlignment.RIGHT);
                if (order.Payment != null)
                {
                    paymentCell.Add(new Paragraph($"Method: {order.Payment.PaymentMethod}").SetFont(regularFont));
                    paymentCell.Add(new Paragraph($"Status: {order.Payment.PaymentStatus}").SetFont(regularFont));
                    paymentCell.Add(new Paragraph($"Date: {order.Payment.PaymentDate:yyyy-MM-dd}").SetFont(regularFont));
                }
                else
                {
                    paymentCell.Add(new Paragraph("Payment not recorded.").SetFont(regularFont));
                }
                paymentCell.SetBorder(Border.NO_BORDER);
                detailsTable.AddCell(paymentCell);
                document.Add(detailsTable);
                //document.Add(new Paragraph("\n")); // Extra spacing

                // --------------------------------------------------------------------
                // Items Table.
                Table itemsTable = new Table(6).UseAllAvailableWidth();
                // Define header cells with DARK_GRAY background and white font.
                itemsTable.AddHeaderCell(new Cell().Add(new Paragraph("Description")
                    .SetFont(boldFont)
                    .SetFontColor(ColorConstants.WHITE))
                    .SetBackgroundColor(ColorConstants.DARK_GRAY));
                itemsTable.AddHeaderCell(new Cell().Add(new Paragraph("Quantity")
                    .SetFont(boldFont)
                    .SetFontColor(ColorConstants.WHITE))
                    .SetBackgroundColor(ColorConstants.DARK_GRAY)
                    .SetTextAlignment(TextAlignment.RIGHT));
                itemsTable.AddHeaderCell(new Cell().Add(new Paragraph("Unit Price")
                    .SetFont(boldFont)
                    .SetFontColor(ColorConstants.WHITE))
                    .SetBackgroundColor(ColorConstants.DARK_GRAY)
                    .SetTextAlignment(TextAlignment.RIGHT));
                itemsTable.AddHeaderCell(new Cell().Add(new Paragraph("Tax %")
                    .SetFont(boldFont)
                    .SetFontColor(ColorConstants.WHITE))
                    .SetBackgroundColor(ColorConstants.DARK_GRAY)
                    .SetTextAlignment(TextAlignment.RIGHT));
                itemsTable.AddHeaderCell(new Cell().Add(new Paragraph("Tax Amount")
                    .SetFont(boldFont)
                    .SetFontColor(ColorConstants.WHITE))
                    .SetBackgroundColor(ColorConstants.DARK_GRAY)
                    .SetTextAlignment(TextAlignment.RIGHT));
                itemsTable.AddHeaderCell(new Cell().Add(new Paragraph("Total")
                    .SetFont(boldFont)
                    .SetFontColor(ColorConstants.WHITE))
                    .SetBackgroundColor(ColorConstants.DARK_GRAY)
                    .SetTextAlignment(TextAlignment.RIGHT));

                // Add each order item.
                foreach (var item in order.OrderItems)
                {
                    //decimal subTotal = item.UnitPrice * item.Quantity;
                    //decimal taxAmount = subTotal * (item.TaxPercent / 100);
                    //decimal total = subTotal + taxAmount;

                    itemsTable.AddCell(new Cell().Add(new Paragraph(item.Product.Name + " - " + item.Product.Description)
                        .SetFont(regularFont)));
                    itemsTable.AddCell(new Cell().Add(new Paragraph(item.Quantity.ToString())
                        .SetFont(regularFont)).SetTextAlignment(TextAlignment.RIGHT));
                    itemsTable.AddCell(new Cell().Add(new Paragraph(item.UnitPrice.ToString("C"))
                        .SetFont(regularFont)).SetTextAlignment(TextAlignment.RIGHT));
                    itemsTable.AddCell(new Cell().Add(new Paragraph(item.TaxPercent.ToString("F2") + "%")
                        .SetFont(regularFont)).SetTextAlignment(TextAlignment.RIGHT));
                    itemsTable.AddCell(new Cell().Add(new Paragraph(item.TotalTaxAmount.ToString("C"))
                        .SetFont(regularFont)).SetTextAlignment(TextAlignment.RIGHT));
                    itemsTable.AddCell(new Cell().Add(new Paragraph(item.TotalPriceWithTax.ToString("C"))
                        .SetFont(regularFont)).SetTextAlignment(TextAlignment.RIGHT));
                }
                document.Add(itemsTable.SetMarginTop(20));

                // --------------------------------------------------------------------
                // Totals Section.
                Table totalsTable = new Table(2).UseAllAvailableWidth();
                totalsTable.SetHorizontalAlignment(HorizontalAlignment.RIGHT);
                totalsTable.AddCell(new Cell()
                    .Add(new Paragraph("Subtotal:").SetFont(boldFont))
                    .SetBorder(Border.NO_BORDER)
                    .SetTextAlignment(TextAlignment.RIGHT));
                totalsTable.AddCell(new Cell()
                    .Add(new Paragraph(order.SubTotal.ToString("C")).SetFont(regularFont))
                    .SetBorder(Border.NO_BORDER)
                    .SetTextAlignment(TextAlignment.RIGHT));
                totalsTable.AddCell(new Cell()
                    .Add(new Paragraph("Total Tax:").SetFont(boldFont))
                    .SetBorder(Border.NO_BORDER)
                    .SetTextAlignment(TextAlignment.RIGHT));
                totalsTable.AddCell(new Cell()
                    .Add(new Paragraph(order.TotalTax.ToString("C")).SetFont(regularFont))
                    .SetBorder(Border.NO_BORDER)
                    .SetTextAlignment(TextAlignment.RIGHT));
                totalsTable.AddCell(new Cell()
                    .Add(new Paragraph("Grand Total:").SetFont(boldFont))
                    .SetBorder(Border.NO_BORDER)
                    .SetTextAlignment(TextAlignment.RIGHT));
                totalsTable.AddCell(new Cell()
                    .Add(new Paragraph(order.GrandTotal.ToString("C")).SetFont(regularFont))
                    .SetBorder(Border.NO_BORDER)
                    .SetTextAlignment(TextAlignment.RIGHT));
                document.Add(totalsTable.SetMarginTop(20));

                // --------------------------------------------------------------------
                // Footer.
                document.Add(new Paragraph("\n")); // spacing
                document.Add(new LineSeparator(new SolidLine()));
                document.Add(new Paragraph("Thank you for your business!")
                    .SetTextAlignment(TextAlignment.CENTER)
                    .SetFont(regularFont)
                    .SetFontSize(12)
                    .SetMarginTop(10));

                document.Close();
                return ms.ToArray();
            }
        }
    }
}
OrdersController

The Orders Controller handles incoming HTTP requests, calls the appropriate methods in Order Service, and returns views or files as responses. For example, it serves the list of orders on the Index view, shows details of a specific order in the Details view, and triggers PDF generation for download through the GeneratePdf action. So, create an empty MVC Controller named OrdersController within the Controllers folder and then copy and paste the following code:

using InvoiceManagementApp.Services;
using Microsoft.AspNetCore.Mvc;

namespace InvoiceManagementApp.Controllers
{
    public class OrdersController : Controller
    {
        private readonly OrderService _orderService;

        // Dependency injection of OrderService.
        public OrdersController(OrderService orderService)
        {
            _orderService = orderService;
        }

        // Displays a list of all orders.
        public async Task<IActionResult> Index()
        {
            var orders = await _orderService.GetAllOrdersAsync();
            return View(orders);
        }

        // Displays detailed information about a specific order.
        public async Task<IActionResult> Details(int id)
        {
            var order = await _orderService.GetOrderByIdAsync(id);
            if (order == null)
            {
                return NotFound();
            }
            return View(order);
        }

        // Generates and returns the PDF for a given order.
        public async Task<IActionResult> GeneratePdf(int id)
        {
            try
            {
                var pdfBytes = await _orderService.GenerateOrderPdfAsync(id);

                return File(pdfBytes, "application/pdf", $"Order_{id}.pdf");
            }
            catch (Exception)
            {
                TempData["Error"] = "An error occurred while generating the PDF.";
                return RedirectToAction("Details", new { id });
            }
        }
    }
}
Creating Views
Index View

Create a view named Index.cshtml within the Views/Orders directory and then copy and paste the following code. The Index View displays a list of orders in a tabular format. It uses Bootstrap for styling and provides a link to the details view for each order.

@model IEnumerable<Order>
@{
    ViewData["Title"] = "Order List";
}

<!-- Bootstrap CSS for professional styling -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" />

<div class="container">
    <h2>Order List</h2>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Order Number</th>
                <th>Order Date</th>
                <th>Customer</th>
                <th>Grand Total</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var order in Model)
            {
                <tr>
                    <td>@order.OrderNumber</td>
                    <td>@order.OrderDate.ToString("yyyy-MM-dd")</td>
                    <td>@order.Customer?.Name</td>
                    <td>@order.GrandTotal.ToString("C")</td>
                    <td>
                        <a asp-controller="Orders" asp-action="Details" asp-route-id="@order.OrderId" class="btn btn-info btn-sm">Details</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>
Details.cshtml

Create a view named Details.cshtml within the Views/Orders directory and then copy and paste the following code. The Details View shows detailed information about a specific order, including order items, customer information, payment details, and calculated totals. It also includes a button to download the PDF invoice.

@model Order
@{
    ViewData["Title"] = "Order Details";
}

<div class="container mt-4">
    <div class="card shadow-sm">
        <div class="card-header bg-primary text-white">
            <h3 class="mb-0">Order Details - @Model.OrderNumber</h3>
        </div>
        <div class="card-body">
            <div class="row mb-3">
                <!-- Order Information -->
                <div class="col-md-6">
                    <h5 class="mb-2 text-muted">Order Information</h5>
                    <p><strong>Order Date:</strong> @Model.OrderDate.ToString("yyyy-MM-dd")</p>
                    <p><strong>SubTotal:</strong> @Model.SubTotal.ToString("C")</p>
                    <p><strong>Total Tax:</strong> @Model.TotalTax.ToString("C")</p>
                    <p><strong>Grand Total:</strong> @Model.GrandTotal.ToString("C")</p>
                </div>
                <!-- Customer Information -->
                <div class="col-md-6">
                    <h5 class="mb-2 text-muted">Customer Details</h5>
                    <p><strong>Name:</strong> @Model.Customer?.Name</p>
                    <p><strong>Email:</strong> @Model.Customer?.Email</p>
                    <p><strong>Phone:</strong> @Model.Customer?.Phone</p>
                    <p><strong>Address:</strong> @Model.Customer?.Address</p>
                </div>
            </div>
            <hr />
            <!-- Order Items Table -->
            <h5 class="mb-2">Order Items</h5>
            <div class="table-responsive">
                <table class="table table-striped table-hover">
                    <thead class="thead-dark">
                        <tr>
                            <th>Product</th>
                            <th>Description</th>
                            <th class="text-center">Quantity</th>
                            <th class="text-right">Unit Price</th>
                            <th class="text-center">Tax %</th>
                            <th class="text-right">Tax Amount</th>
                            <th class="text-right">Total</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach (var item in Model.OrderItems)
                        {
                            var subTotal = item.UnitPrice * item.Quantity;
                            var taxAmount = subTotal * (item.TaxPercent / 100);
                            var total = subTotal + taxAmount;
                            <tr>
                                <td>@item.Product.Name</td>
                                <td>@item.Product.Description</td>
                                <td class="text-center">@item.Quantity</td>
                                <td class="text-right">@item.UnitPrice.ToString("C")</td>
                                <td class="text-center">@item.TaxPercent.ToString("F2") + "%"</td>
                                <td class="text-right">@taxAmount.ToString("C")</td>
                                <td class="text-right">@total.ToString("C")</td>
                            </tr>
                        }
                    </tbody>
                </table>
            </div>
            <hr />
            <!-- Payment Details -->
            <h5 class="mb-2">Payment Details</h5>
            @if (Model.Payment != null)
            {
                <div class="row">
                    <div class="col-md-6">
                        <p><strong>Payment Method:</strong> @Model.Payment.PaymentMethod</p>
                        <p><strong>Payment Date:</strong> @Model.Payment.PaymentDate?.ToString("yyyy-MM-dd")</p>
                    </div>
                    <div class="col-md-6">
                        <p><strong>Amount Paid:</strong> @Model.Payment.PaymentAmount.ToString("C")</p>
                        <p><strong>Payment Status:</strong> @Model.Payment.PaymentStatus</p>
                    </div>
                </div>
            }
            else
            {
                <div class="alert alert-warning" role="alert">
                    Payment not recorded.
                </div>
            }
            <!-- Action Buttons -->
            <div class="mt-4">
                <a asp-action="GeneratePdf" asp-route-id="@Model.OrderId" class="btn btn-success mr-2">
                    <i class="fas fa-file-pdf"></i> Download PDF
                </a>
                <a asp-action="Index" class="btn btn-secondary">
                    <i class="fas fa-arrow-left"></i> Back to List
                </a>
            </div>
        </div>
    </div>
</div>
Modifying _Layout.cshtml

Modify the _Layout.cshtml view as follows. The _Layout View defines the common layout for all pages, including header, navigation, footer, and site-wide styles or scripts.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - InvoiceManagementApp</title>

    <!-- Using Bootstrap 4.5.2 from CDN -->
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />

    <!-- Site-specific styles -->
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/InvoiceManagementApp.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">InvoiceManagementApp</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse"
                        aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>

    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <!-- Footer appears after the main content -->
    <footer class="border-top footer text-muted mt-3">
        <div class="container">
            &copy; 2025 - InvoiceManagementApp - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>

    <!-- jQuery, Popper.js, and Bootstrap Bundle (includes Popper) -->
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>

    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Modifying AppSettings.json File:

Please modify the appsettings.json file as follows. This file stores configuration details like database connection strings. In this example, we provide a connection string for SQL Server (DefaultConnection) and set logging levels. The connection string is consumed by ApplicationDbContext through dependency injection, enabling the app to connect to the database.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=InvoiceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Modifying Program Class File:

Please modify the Program.cs class file as follows. The Program class is the application’s entry point, configuring the web host, middleware pipeline, services, and routing.

using InvoiceManagementApp.Data;
using InvoiceManagementApp.Services;
using Microsoft.EntityFrameworkCore;

namespace InvoiceManagementApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            // Configure EF Core to use SQL Server.
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
            );

            //Register the OrderService
            builder.Services.AddScoped<OrderService>();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Orders}/{action=Index}/{id?}");

            app.Run();
        }
    }
}
Migrate and Create the Database

Open the Visual Studio Package Manager Console and run the following commands to create the database and tables:

  • Add-Migration Mig1
  • Update-Database

This will create the InvoiceDB database with the required tables, as shown in the below image:

Generate PDF in ASP.NET Core MVC

Now, run the application and test the functionalities, and it should work as expected.

In the next article, I will discuss how to Generate a Password-Protected PDF in an ASP.NET Core MVC Application with examples. In this article, I explain how to Generate a PDF in an ASP.NET Core MVC Application with Examples. I hope you enjoy this article on generating a PDF in ASP.NET Core MVC applications.

Leave a Reply

Your email address will not be published. Required fields are marked *