Skip to footer content
MIGRATION GUIDES

How to Migrate from Rotativa to IronPDF in C#

Migrating from Rotativa to IronPDF addresses critical security vulnerabilities while modernizing your PDF generation workflow. This guide provides a complete, step-by-step migration path that eliminates the abandoned wkhtmltopdf dependency, enables support for modern CSS and JavaScript, and provides cross-platform compatibility beyond ASP.NET MVC.

Why Migrate from Rotativa to IronPDF

Understanding Rotativa

Rotativa has long been a popular choice among developers for generating PDFs in C#. It leverages the wkhtmltopdf tool to convert HTML content into PDF format. Rotativa is an open-source library specifically designed for ASP.NET MVC applications. However, while it has attracted a significant audience, Rotativa's reliance on an outdated technology stack presents challenges that might not be immediately evident to every developer.

At its core, Rotativa provides a simple way to integrate PDF generation into ASP.NET MVC projects, taking advantage of wkhtmltopdf for its backend functionalities.

Critical Security Advisory

Rotativa wraps wkhtmltopdf, which has CRITICAL UNPATCHED SECURITY VULNERABILITIES.

AttributeValue
CVE IDCVE-2022-35583
SeverityCRITICAL (9.8/10)
Attack VectorNetwork
StatusWILL NEVER BE PATCHED
AffectedALL Rotativa versions

wkhtmltopdf was officially abandoned in December 2022. The maintainers explicitly stated they will NOT fix security vulnerabilities. Every application using Rotativa is permanently exposed.

How the Attack Works

<!-- Attacker submits this content via your MVC model -->
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
<img src="http://internal-database:5432/admin" />
<!-- Attacker submits this content via your MVC model -->
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
<img src="http://internal-database:5432/admin" />
HTML

Impact:

  • Access AWS/Azure/GCP cloud metadata endpoints
  • Steal internal API data and credentials
  • Port scan internal networks
  • Exfiltrate sensitive configuration

The Technology Crisis

Rotativa wraps wkhtmltopdf, which uses:

  • Qt WebKit 4.8 (from 2012)
  • No Flexbox support
  • No CSS Grid support
  • Broken JavaScript execution
  • No ES6+ support

Rotativa vs IronPDF Comparison

FeatureRotativaIronPDF
Project CompatibilityASP.NET MVC OnlyAny .NET Project Type (MVC, Razor Pages, Blazor, etc.)
MaintenanceAbandonedActively Maintained
SecurityVulnerable due to wkhtmltopdf dependencies (CVE-2022-35583)Regular updates and security patches
HTML RenderingOutdated WebKitModern Chromium
CSS3PartialFull support
Flexbox/GridNot supportedFull support
JavaScriptUnreliableFull ES6+
Razor PagesNot supportedFull support
BlazorNot supportedFull support
PDF ManipulationNot availableFull
Digital SignaturesNot availableFull
PDF/A ComplianceNot availableFull
Async/AwaitSynchronous onlyFull async
Open SourceYes, MIT LicenseNo, Commercial License

For teams planning .NET 10 and C# 14 adoption through 2025 and 2026, IronPDF provides modern Chromium rendering and cross-platform support that Rotativa cannot offer.


Before You Start

Prerequisites

  1. .NET Environment: .NET Framework 4.6.2+ or .NET Core 3.1+ / .NET 5/6/7/8/9+
  2. NuGet Access: Ability to install NuGet packages
  3. IronPDF License: Obtain your license key from ironpdf.com

NuGet Package Changes

# Remove Rotativa
dotnet remove package Rotativa
dotnet remove package Rotativa.AspNetCore

# Install IronPDF
dotnet add package IronPdf
# Remove Rotativa
dotnet remove package Rotativa
dotnet remove package Rotativa.AspNetCore

# Install IronPDF
dotnet add package IronPdf
SHELL

Remove wkhtmltopdf Binaries

Delete these files from your project:

  • wkhtmltopdf.exe
  • wkhtmltox.dll
  • Any Rotativa/ folders

These are the source of CVE-2022-35583. IronPDF needs no native binaries.

License Configuration

// Add in Program.cs or Startup.cs
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
// Add in Program.cs or Startup.cs
IronPdf.License.LicenseKey = "YOUR-LICENSE-KEY";
$vbLabelText   $csharpLabel

Complete API Reference

Namespace Changes

// Before: Rotativa
using Rotativa;
using Rotativa.Options;
using Rotativa.AspNetCore;

// After: IronPDF
using IronPdf;
using IronPdf.Rendering;
// Before: Rotativa
using Rotativa;
using Rotativa.Options;
using Rotativa.AspNetCore;

// After: IronPDF
using IronPdf;
using IronPdf.Rendering;
$vbLabelText   $csharpLabel

Core Class Mappings

Rotativa ClassIronPDF EquivalentNotes
ViewAsPdfChromePdfRendererRender HTML
ActionAsPdfChromePdfRenderer.RenderUrlAsPdf()Render URL
UrlAsPdfChromePdfRenderer.RenderUrlAsPdf()Render URL
Orientation enumPdfPaperOrientation enumOrientation
Size enumPdfPaperSize enumPaper size

Page Placeholder Conversion

Rotativa PlaceholderIronPDF Placeholder
[page]{page}
[topage]{total-pages}
[date]{date}
[time]{time}
[title]{html-title}
[sitepage]{url}

Code Migration Examples

Example 1: HTML to PDF Conversion

Before (Rotativa):

// NuGet: Install-Package Rotativa.Core
using Microsoft.AspNetCore.Mvc;
using Rotativa.AspNetCore;
using System.Threading.Tasks;

namespace RotativaExample
{
    public class PdfController : Controller
    {
        public async Task<IActionResult> GeneratePdf()
        {
            var htmlContent = "<h1>Hello World</h1><p>This is a PDF document.</p>";

            // Rotativa requires returning a ViewAsPdf result from MVC controller
            return new ViewAsPdf()
            {
                ViewName = "PdfView",
                PageSize = Rotativa.AspNetCore.Options.Size.A4
            };
        }
    }
}
// NuGet: Install-Package Rotativa.Core
using Microsoft.AspNetCore.Mvc;
using Rotativa.AspNetCore;
using System.Threading.Tasks;

namespace RotativaExample
{
    public class PdfController : Controller
    {
        public async Task<IActionResult> GeneratePdf()
        {
            var htmlContent = "<h1>Hello World</h1><p>This is a PDF document.</p>";

            // Rotativa requires returning a ViewAsPdf result from MVC controller
            return new ViewAsPdf()
            {
                ViewName = "PdfView",
                PageSize = Rotativa.AspNetCore.Options.Size.A4
            };
        }
    }
}
$vbLabelText   $csharpLabel

After (IronPDF):

// NuGet: Install-Package IronPdf
using IronPdf;
using System;

namespace IronPdfExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var renderer = new ChromePdfRenderer();
            var htmlContent = "<h1>Hello World</h1><p>This is a PDF document.</p>";

            var pdf = renderer.RenderHtmlAsPdf(htmlContent);
            pdf.SaveAs("output.pdf");

            Console.WriteLine("PDF generated successfully!");
        }
    }
}
// NuGet: Install-Package IronPdf
using IronPdf;
using System;

namespace IronPdfExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var renderer = new ChromePdfRenderer();
            var htmlContent = "<h1>Hello World</h1><p>This is a PDF document.</p>";

            var pdf = renderer.RenderHtmlAsPdf(htmlContent);
            pdf.SaveAs("output.pdf");

            Console.WriteLine("PDF generated successfully!");
        }
    }
}
$vbLabelText   $csharpLabel

This example demonstrates the fundamental architectural difference. Rotativa requires returning a ViewAsPdf result from an MVC controller action, tying you to the ASP.NET MVC framework. The pattern only works within the MVC request pipeline and requires a Razor view to render.

IronPDF works anywhere: console applications, web APIs, Blazor, Razor Pages, or any .NET project type. You call RenderHtmlAsPdf() with an HTML string and save the result. No MVC controller required, no view dependency. See the HTML to PDF documentation for comprehensive examples.

Example 2: URL to PDF Conversion

Before (Rotativa):

// NuGet: Install-Package Rotativa.Core
using Microsoft.AspNetCore.Mvc;
using Rotativa.AspNetCore;
using System.Threading.Tasks;

namespace RotativaExample
{
    public class UrlPdfController : Controller
    {
        public async Task<IActionResult> ConvertUrlToPdf()
        {
            // Rotativa works within MVC framework and returns ActionResult
            return new UrlAsPdf("https://www.example.com")
            {
                FileName = "webpage.pdf",
                PageSize = Rotativa.AspNetCore.Options.Size.A4,
                PageOrientation = Rotativa.AspNetCore.Options.Orientation.Portrait
            };
        }
    }
}
// NuGet: Install-Package Rotativa.Core
using Microsoft.AspNetCore.Mvc;
using Rotativa.AspNetCore;
using System.Threading.Tasks;

namespace RotativaExample
{
    public class UrlPdfController : Controller
    {
        public async Task<IActionResult> ConvertUrlToPdf()
        {
            // Rotativa works within MVC framework and returns ActionResult
            return new UrlAsPdf("https://www.example.com")
            {
                FileName = "webpage.pdf",
                PageSize = Rotativa.AspNetCore.Options.Size.A4,
                PageOrientation = Rotativa.AspNetCore.Options.Orientation.Portrait
            };
        }
    }
}
$vbLabelText   $csharpLabel

After (IronPDF):

// NuGet: Install-Package IronPdf
using IronPdf;
using System;

namespace IronPdfExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var renderer = new ChromePdfRenderer();

            var pdf = renderer.RenderUrlAsPdf("https://www.example.com");
            pdf.SaveAs("webpage.pdf");

            Console.WriteLine("URL converted to PDF successfully!");
        }
    }
}
// NuGet: Install-Package IronPdf
using IronPdf;
using System;

namespace IronPdfExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var renderer = new ChromePdfRenderer();

            var pdf = renderer.RenderUrlAsPdf("https://www.example.com");
            pdf.SaveAs("webpage.pdf");

            Console.WriteLine("URL converted to PDF successfully!");
        }
    }
}
$vbLabelText   $csharpLabel

Rotativa's UrlAsPdf class requires returning an ActionResult from an MVC controller. IronPDF's RenderUrlAsPdf() method can be called from any context and returns a PdfDocument object directly. The URL rendering uses modern Chromium instead of wkhtmltopdf's vulnerable and outdated WebKit engine. Learn more in our tutorials.

Example 3: Headers and Footers with Page Numbers

Before (Rotativa):

// NuGet: Install-Package Rotativa.Core
using Microsoft.AspNetCore.Mvc;
using Rotativa.AspNetCore;
using Rotativa.AspNetCore.Options;
using System.Threading.Tasks;

namespace RotativaExample
{
    public class HeaderFooterController : Controller
    {
        public async Task<IActionResult> GeneratePdfWithHeaderFooter()
        {
            return new ViewAsPdf("Report")
            {
                PageSize = Size.A4,
                PageMargins = new Margins(20, 10, 20, 10),
                CustomSwitches = "--header-center \"Page Header\" --footer-center \"Page [page] of [toPage]\""
            };
        }
    }
}
// NuGet: Install-Package Rotativa.Core
using Microsoft.AspNetCore.Mvc;
using Rotativa.AspNetCore;
using Rotativa.AspNetCore.Options;
using System.Threading.Tasks;

namespace RotativaExample
{
    public class HeaderFooterController : Controller
    {
        public async Task<IActionResult> GeneratePdfWithHeaderFooter()
        {
            return new ViewAsPdf("Report")
            {
                PageSize = Size.A4,
                PageMargins = new Margins(20, 10, 20, 10),
                CustomSwitches = "--header-center \"Page Header\" --footer-center \"Page [page] of [toPage]\""
            };
        }
    }
}
$vbLabelText   $csharpLabel

After (IronPDF):

// NuGet: Install-Package IronPdf
using IronPdf;
using IronPdf.Rendering;
using System;

namespace IronPdfExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var renderer = new ChromePdfRenderer();

            renderer.RenderingOptions.TextHeader = new TextHeaderFooter()
            {
                CenterText = "Page Header",
                DrawDividerLine = true
            };

            renderer.RenderingOptions.TextFooter = new TextHeaderFooter()
            {
                CenterText = "Page {page} of {total-pages}",
                DrawDividerLine = true
            };

            var htmlContent = "<h1>Report Title</h1><p>Report content goes here.</p>";
            var pdf = renderer.RenderHtmlAsPdf(htmlContent);
            pdf.SaveAs("report.pdf");

            Console.WriteLine("PDF with headers and footers created successfully!");
        }
    }
}
// NuGet: Install-Package IronPdf
using IronPdf;
using IronPdf.Rendering;
using System;

namespace IronPdfExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var renderer = new ChromePdfRenderer();

            renderer.RenderingOptions.TextHeader = new TextHeaderFooter()
            {
                CenterText = "Page Header",
                DrawDividerLine = true
            };

            renderer.RenderingOptions.TextFooter = new TextHeaderFooter()
            {
                CenterText = "Page {page} of {total-pages}",
                DrawDividerLine = true
            };

            var htmlContent = "<h1>Report Title</h1><p>Report content goes here.</p>";
            var pdf = renderer.RenderHtmlAsPdf(htmlContent);
            pdf.SaveAs("report.pdf");

            Console.WriteLine("PDF with headers and footers created successfully!");
        }
    }
}
$vbLabelText   $csharpLabel

Rotativa uses CustomSwitches to pass command-line arguments to wkhtmltopdf, including header and footer configuration with placeholders like [page] and [toPage]. This string-based approach is error-prone and hard to validate at compile time.

IronPDF uses strongly-typed TextHeaderFooter objects with properties like CenterText and DrawDividerLine. The placeholder syntax changes from [page] to {page} and from [toPage] to {total-pages}. Typed properties provide IntelliSense, compile-time checking, and no risk of typos.


The MVC-Only Architecture Problem

Rotativa was designed for ASP.NET MVC 5 and earlier:

// ❌ Rotativa - Only works with classic MVC pattern
public class InvoiceController : Controller
{
    public ActionResult InvoicePdf(int id)
    {
        var model = GetInvoice(id);
        return new ViewAsPdf("Invoice", model);  // Tied to MVC Views
    }
}

// Problems:
// - No Razor Pages support
// - No Blazor support
// - No minimal APIs support
// - No ASP.NET Core native integration
// ❌ Rotativa - Only works with classic MVC pattern
public class InvoiceController : Controller
{
    public ActionResult InvoicePdf(int id)
    {
        var model = GetInvoice(id);
        return new ViewAsPdf("Invoice", model);  // Tied to MVC Views
    }
}

// Problems:
// - No Razor Pages support
// - No Blazor support
// - No minimal APIs support
// - No ASP.NET Core native integration
$vbLabelText   $csharpLabel

IronPDF separates view rendering from PDF generation, which is actually more flexible—you can render any HTML, not just MVC views.


Async Pattern Migration

Rotativa blocks the thread; IronPDF supports full async/await:

// ❌ Rotativa - Blocks the thread
public ActionResult GeneratePdf()
{
    return new ViewAsPdf("Report");
    // This blocks the request thread until PDF is complete
    // Poor scalability under load
}

// ✅ IronPDF - Full async support
public async Task<IActionResult> GeneratePdf()
{
    var renderer = new ChromePdfRenderer();
    var pdf = await renderer.RenderHtmlAsPdfAsync(html);
    return File(pdf.BinaryData, "application/pdf");
    // Non-blocking, better scalability
}
// ❌ Rotativa - Blocks the thread
public ActionResult GeneratePdf()
{
    return new ViewAsPdf("Report");
    // This blocks the request thread until PDF is complete
    // Poor scalability under load
}

// ✅ IronPDF - Full async support
public async Task<IActionResult> GeneratePdf()
{
    var renderer = new ChromePdfRenderer();
    var pdf = await renderer.RenderHtmlAsPdfAsync(html);
    return File(pdf.BinaryData, "application/pdf");
    // Non-blocking, better scalability
}
$vbLabelText   $csharpLabel

New Capabilities After Migration

After migrating to IronPDF, you gain capabilities that Rotativa cannot provide:

PDF Merging

var merged = PdfDocument.Merge(pdf1, pdf2, pdf3);
merged.SaveAs("complete.pdf");
var merged = PdfDocument.Merge(pdf1, pdf2, pdf3);
merged.SaveAs("complete.pdf");
$vbLabelText   $csharpLabel

Digital Signatures

var signature = new PdfSignature("certificate.pfx", "password");
pdf.Sign(signature);
var signature = new PdfSignature("certificate.pfx", "password");
pdf.Sign(signature);
$vbLabelText   $csharpLabel

Password Protection

pdf.SecuritySettings.UserPassword = "secret";
pdf.SecuritySettings.UserPassword = "secret";
$vbLabelText   $csharpLabel

Watermarks

pdf.ApplyWatermark("<h1 style='color:red; opacity:0.3;'>DRAFT</h1>");
pdf.ApplyWatermark("<h1 style='color:red; opacity:0.3;'>DRAFT</h1>");
$vbLabelText   $csharpLabel

PDF/A Archival Compliance

pdf.SaveAsPdfA("archive.pdf", PdfAVersions.PdfA3b);
pdf.SaveAsPdfA("archive.pdf", PdfAVersions.PdfA3b);
$vbLabelText   $csharpLabel

Modern CSS Support

// This now works (broke in Rotativa)
var html = @"
    <div style='display: flex; justify-content: space-between;'>
        <div>Left</div>
        <div>Right</div>
    </div>
    <div style='display: grid; grid-template-columns: 1fr 1fr 1fr;'>
        <div>Col 1</div><div>Col 2</div><div>Col 3</div>
    </div>";
var pdf = renderer.RenderHtmlAsPdf(html);  // Works!
// This now works (broke in Rotativa)
var html = @"
    <div style='display: flex; justify-content: space-between;'>
        <div>Left</div>
        <div>Right</div>
    </div>
    <div style='display: grid; grid-template-columns: 1fr 1fr 1fr;'>
        <div>Col 1</div><div>Col 2</div><div>Col 3</div>
    </div>";
var pdf = renderer.RenderHtmlAsPdf(html);  // Works!
$vbLabelText   $csharpLabel

Migration Checklist

Pre-Migration

  • Identify all Rotativa usage in codebase
  • Document CustomSwitches used for conversion to RenderingOptions
  • Note header/footer placeholder syntax for conversion ([page]{page})
  • Obtain IronPDF license key from ironpdf.com

Package Changes

  • Remove Rotativa and Rotativa.AspNetCore NuGet packages
  • Delete wkhtmltopdf binaries (wkhtmltopdf.exe, wkhtmltox.dll)
  • Install IronPdf NuGet package

Code Changes

  • Update namespace imports (using Rotativa;using IronPdf;)
  • Replace ViewAsPdf with ChromePdfRenderer + RenderHtmlAsPdf()
  • Replace UrlAsPdf with RenderUrlAsPdf()
  • Convert CustomSwitches to RenderingOptions properties
  • Update placeholder syntax ([page]{page}, [topage]{total-pages})
  • Replace PageMargins with individual MarginTop/MarginBottom/MarginLeft/MarginRight
  • Change to async pattern where appropriate
  • Add license initialization at application startup

Post-Migration

  • Verify all PDF generation works correctly
  • Compare PDF output quality (Chromium renders more accurately)
  • Verify CSS rendering improvements (Flexbox/Grid now work)
  • Test JavaScript execution (now reliable with Chromium)
  • Verify security scan passes (no more CVE-2022-35583 flags)
  • Update Docker configurations to remove wkhtmltopdf installation

Curtis Chau
Technical Writer

Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.

...

Read More