§HOW-TO GUIDE · C# · .NET 8 & 10

HTML to PDF in Azure Functions with C#

An HTTP-triggered function converts HTML with SelectPdf.Universal and returns the PDF. It runs on a Dedicated (App Service) plan of Basic or above, or on Elastic Premium, on Linux or Windows.

Compiled and run: Azure Functions host image (.NET 10 isolated), x64

Video walkthrough · 4:06 · Watch on YouTube · Short · 0:53
§STEP BY STEP

Build it step by step.

  1. 1Create the project

    A function app is a .NET isolated worker project. The project file references the Functions worker packages and SelectPdf: the engine package matches the machine that builds the app, so it runs locally on Windows and is published for Linux with -r linux-x64. local.settings.json is for func start (Azure Functions Core Tools) and is not published.

    PdfFunctions.csproj
    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <AzureFunctionsVersion>v4</AzureFunctionsVersion>
        <OutputType>Exe</OutputType>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    
      <!-- Azure Functions v4, .NET isolated worker -->
      <ItemGroup>
        <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
        <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.1.0" />
        <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
      </ItemGroup>
    
      <!-- the engine for the machine that builds the app; publishing with -r linux-x64 picks Linux,
           for a Linux function app. Fonts: a Linux function app has none for CJK and other scripts. -->
      <PropertyGroup>
        <_SelectPdfArch>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant())</_SelectPdfArch>
        <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('Windows'))">win-$(_SelectPdfArch)</SelectPdfRid>
        <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux-$(_SelectPdfArch)</SelectPdfRid>
        <SelectPdfRid Condition="$([MSBuild]::IsOSPlatform('OSX'))">osx-$(_SelectPdfArch)</SelectPdfRid>
        <SelectPdfRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</SelectPdfRid>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="SelectPdf.Universal" Version="26.5.0" />
        <PackageReference Include="SelectPdf.Universal.Native.$(SelectPdfRid)" Version="26.5.0" />
        <PackageReference Include="SelectPdf.Universal.Fonts" Version="26.5.0" />
      </ItemGroup>
    
      <ItemGroup>
        <None Update="host.json" CopyToOutputDirectory="PreserveNewest" />
        <None Update="local.settings.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="Never" />
      </ItemGroup>
    
    </Project>
    host.json
    {
      "version": "2.0"
    }
    local.settings.json
    {
      "IsEncrypted": false,
      "Values": {
        "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
        "AzureWebJobsStorage": "UseDevelopmentStorage=true"
      }
    }
  2. 2Start the isolated worker

    Program.cs · lines 1–11
    using System.Net;
    using System.Web;
    using Microsoft.Azure.Functions.Worker;
    using Microsoft.Azure.Functions.Worker.Http;
    using Microsoft.Extensions.Hosting;
    using SelectPdf.Universal;
    
    new HostBuilder()
        .ConfigureFunctionsWorkerDefaults()
        .Build()
        .Run();

    The .NET isolated worker hosts the functions. Nothing SelectPdf-specific happens here.

  3. 3Declare the HTTP trigger

    Program.cs · lines 13–18
    public static class InvoiceFunction
    {
        // GET /api/Invoice?number=1042 returns the invoice as a PDF
        [Function("Invoice")]
        public static async Task<HttpResponseData> Invoice(
            [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)

    [Function("Invoice")] with an HttpTrigger answers GET /api/Invoice. AuthorizationLevel.Function requires a function key in the code query parameter or the x-functions-key header.

  4. 4Build the HTML

    Program.cs · lines 20–24
            string number = HttpUtility.ParseQueryString(req.Url.Query)["number"] ?? "1";
            string html = $"""
                <h1>Invoice #{HttpUtility.HtmlEncode(number)}</h1>
                <p>Billed to Contoso Ltd.</p>
                """;

    The invoice number comes from the query string. HtmlUtility.HtmlEncode keeps user input from becoming markup.

  5. 5Convert and return the PDF

    Program.cs · lines 26–35
            HtmlToPdf converter = new HtmlToPdf();
            PdfDocument doc = await converter.ConvertHtmlStringAsync(html);
            byte[] pdf = doc.Save();
            doc.Close();
    
            HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
            response.Headers.Add("Content-Type", "application/pdf");
            await response.Body.WriteAsync(pdf);
            return response;
        }

    ConvertHtmlStringAsync renders the HTML; Save() returns the bytes, which go into the response body with the application/pdf content type.

  6. 6Run it

    terminal
    func start
    
    # in a second terminal (the local host does not ask for function keys):
    curl "http://localhost:7071/api/Invoice?number=1042" -o invoice.pdf

    The request returns invoice.pdf. Without a license key, every page carries a trial watermark; set GlobalProperties.LicenseKey before the first conversion to remove it.

§COMPLETE PROGRAM

The complete Program.cs.

This is the exact file our samples pipeline compiled and ran on Azure Functions host image (.NET 10 isolated), x64 to produce the output on this page.

Program.cs
using System.Net;
using System.Web;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Hosting;
using SelectPdf.Universal;

new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .Build()
    .Run();

public static class InvoiceFunction
{
    // GET /api/Invoice?number=1042 returns the invoice as a PDF
    [Function("Invoice")]
    public static async Task<HttpResponseData> Invoice(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
    {
        string number = HttpUtility.ParseQueryString(req.Url.Query)["number"] ?? "1";
        string html = $"""
            <h1>Invoice #{HttpUtility.HtmlEncode(number)}</h1>
            <p>Billed to Contoso Ltd.</p>
            """;

        HtmlToPdf converter = new HtmlToPdf();
        PdfDocument doc = await converter.ConvertHtmlStringAsync(html);
        byte[] pdf = doc.Save();
        doc.Close();

        HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "application/pdf");
        await response.Body.WriteAsync(pdf);
        return response;
    }
}

Open the project on GitHub → this program and the variations below, ready to dotnet run

§DEPLOY

Deploy it.

Create a Linux function app on an Elastic Premium or Dedicated plan, then deploy the published app as a zip. Replace the names in angle brackets with your own.

terminal
az login
az group create --name selectpdf-rg --location westeurope
az storage account create --resource-group selectpdf-rg --name <storageaccount> --sku Standard_LRS
az functionapp plan create --resource-group selectpdf-rg --name selectpdf-fnplan --sku P1v3 --is-linux
az functionapp create --resource-group selectpdf-rg --name <function-app> --plan selectpdf-fnplan \
    --storage-account <storageaccount> --functions-version 4 \
    --runtime dotnet-isolated --runtime-version 10 --os-type Linux
az functionapp config set --resource-group selectpdf-rg --name <function-app> --always-on true

dotnet publish -c Release -r linux-x64 --self-contained false -o publish
cd publish && zip -r ../fn.zip . && cd ..
az functionapp deployment source config-zip --resource-group selectpdf-rg --name <function-app> --src fn.zip

# call it with a function key from the portal (Functions > Invoice > Function Keys)
curl "https://<function-app>.azurewebsites.net/api/Invoice?number=1042&code=<function-key>" -o invoice.pdf

These steps, with the Windows variants, are in the documentation: selectpdf.com/pdf-library/html/Azure-Functions.htm.

§OUTPUT

The PDF it produces.

First page of invoice.pdf, the PDF this sample produces
invoice.pdf, page 1

Download invoice.pdf ↓ the file this sample produced, unedited

§TROUBLESHOOTING

Common problems and fixes.

The function fails on a Consumption plan

Windows Consumption and Flex Consumption cannot run the engine: the package does not fit the plan's disk space or runs from a read-only mount. Use Elastic Premium (EP1 and up) or a Dedicated plan of Basic or above.

Chinese, Japanese or Korean text shows as boxes on Linux

The Linux image has no fonts for these scripts: reference SelectPdf.Universal.Fonts, as the project file above does.

The first call is slow

Turn on Always On (a Dedicated plan setting, in the deployment steps) so the app stays loaded between calls.

§GOOD TO KNOW

Good to know.

  • Supported plans: Dedicated (App Service) Basic or above and Elastic Premium (EP1 and up), on Windows and Linux. Consumption and Flex Consumption are not supported. Source
  • On Linux, reference SelectPdf.Universal.Native.linux-x64 and SelectPdf.Universal.Fonts; on Windows, SelectPdf.Universal.Native.win-x64. Source
  • A function app on a Dedicated plan does not run from the package, so the engine starts from a writable folder. Do not set WEBSITE_RUN_FROM_PACKAGE. Source
?FAQ

Questions, answered.

Short answers about this guide. The SelectPdf.Universal documentation covers every class and option.

Yes, on a Dedicated (App Service) plan of Basic or above or on Elastic Premium, on Linux or Windows, with the .NET isolated worker. Consumption and Flex Consumption plans are not supported.
SelectPdf.Universal, SelectPdf.Universal.Native.linux-x64 and SelectPdf.Universal.Fonts. A Windows function app uses SelectPdf.Universal.Native.win-x64 instead of the Linux packages.
Publish with dotnet publish -c Release -r linux-x64, zip the output, and deploy it with az functionapp deployment source config-zip. Do not turn on Run From Package.
Function apps run on the same machines as App Service web apps: on P1v3 a warm instance converts selectpdf.com in under 2 seconds; Basic and Standard plans take several times longer.