---
title: "Merge PDF files in C#"
url: https://recursiveai.org/onepdf/articles/merge-pdf-csharp/
description: "Merge PDF files in C# with OnePDF: files, byte arrays and ASP.NET Core uploads, chosen pages, and bookmarks, links and form fields kept. MIT or Apache-2.0."
date: 2026-10-26
author: OnePDF team
---

# Merge PDF files in C#

Merge PDF files in C# with OnePDF: files, byte arrays and ASP.NET Core uploads, chosen pages, and bookmarks, links and form fields kept. MIT or Apache-2.0.

October 26, 2026 · OnePDF team

To merge PDF files in C#, open each file with `PdfDocument.Open`, pass them to `PdfDocument.Merge` and save the result. OnePDF copies more than the pages: bookmarks, links between pages, form fields, tagged structure and page labels come along, and each shared font or image is copied once. It is pure C# with no native binaries, licensed MIT OR Apache-2.0. This page merges files on disk, byte arrays and ASP.NET Core uploads, inserts chosen pages, and covers the case that breaks naive merges: two copies of the same form. Every sample was compiled and run against OnePDF 1.11.0 on .NET 10.

## Before you start

*Terminal · NuGet*

```
dotnet new console -o PdfMerge
cd PdfMerge
dotnet add package RecursiveAI.OnePdf --version 1.11.0
```

OnePDF targets .NET 10 (`net10.0`). The samples merge `chapter-1.pdf` and `chapter-2.pdf`, two-page files with a bookmark per part and a link from page 1 to page 2, with the six-page [IRS Form W-9](https://www.irs.gov/pub/irs-pdf/fw9.pdf), which has 23 form fields. To make the chapter files, run this once with `RecursiveAI.OnePdf.Layout` added, or use any PDFs of your own:

*MakeChapters.cs · OnePdf.Layout 1.11.0*

```
using OnePdf.Layout;
 
// Two short chapters, each with bookmarks and a link to its second page.
foreach (int n in new[] { 1, 2 })
{
    LayoutDocument.Compose(doc =>
    {
        doc.Title($"Chapter {n}").Language("en-US");
        doc.Page(page =>
        {
            page.Size(PageSize.Letter);
            page.Margin(72);
            page.Content().Column(col =>
            {
                col.Spacing(12);
                col.Item().Bookmark($"Chapter {n}, part 1").Heading(1).Text($"Chapter {n}, part 1").FontSize(20).Bold();
                col.Item().Text($"The opening of chapter {n}.");
                col.Item().LinkToAnchor($"part2-{n}").Text("Skip to part 2");
                col.Item().PageBreak();
                col.Item().Bookmark($"Chapter {n}, part 2").Anchor($"part2-{n}").Heading(1).Text($"Chapter {n}, part 2").FontSize(20).Bold();
                col.Item().Text($"The end of chapter {n}.");
            });
        });
    }).Save($"chapter-{n}.pdf");
}
```

## Merge PDF files

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
 
string[] files = ["chapter-1.pdf", "chapter-2.pdf", "fw9.pdf"];
 
List<PdfDocument> sources = files.Select(f => PdfDocument.Open(f)).ToList();
try
{
    using PdfDocument merged = PdfDocument.Merge(sources);
    merged.SetInfo("Title", "Vendor handbook");   // Merge copies no metadata
    merged.Save("merged.pdf");
 
    Console.WriteLine($"{merged.PageCount} pages, {merged.Form?.TerminalFields.Count() ?? 0} form fields");
    foreach (PdfOutlineItem bookmark in merged.Outline)
        Console.WriteLine($"bookmark: {bookmark.Title}");
}
finally
{
    foreach (PdfDocument source in sources)
        source.Dispose();
}
```

*Output · Console*

```
10 pages, 23 form fields
bookmark: Chapter 1, part 1
bookmark: Chapter 1, part 2
bookmark: Chapter 2, part 1
bookmark: Chapter 2, part 2
```

`merged.pdf` has the ten pages in order, the four bookmarks, and the W-9’s fillable fields, which sit on its first page, now page 5. The “Skip to part 2” links still work: they now lead to pages 2 and 4 of the merged file.

- `Merge` builds a new document in memory that doesn’t refer to its sources, so you can dispose the sources as soon as it returns, as the `finally` block does.
- It writes no title, author or other metadata, so set what you need with `SetInfo`.
- `PdfDocument.Open` accepts a path, a `Stream` or a `ReadOnlyMemory<byte>`. Sources don’t need `AllowEditing`: merging only reads them.

## What a merge keeps

- **Bookmarks** that lead to copied pages, appended in source order.
- **Links** between copied pages, with the named destinations they use. A name already taken by a different place gets a `_2` suffix. A link to a page you left out keeps its rectangle but loses its target.
- **Form fields** with the field tree above them. Appearances are generated and stored for each copied text and choice field, so values display the same in the merged file.
- **Tagged structure** (for accessibility), **page labels** and **optional content** of the copied pages.
- **Resources once:** each font or image the sources share is copied once, however many pages use it.

Not carried: the sources’ metadata, signatures (a signature signs the bytes of the original file), document-level JavaScript and permissions. An XFA form description is dropped, because it would no longer describe the fields. The full rules are in [page operations](https://recursiveai.org/onepdf/docs/page-operations/).

## Merge PDF byte arrays or streams

When the PDFs come from a database, a queue or an HTTP call, open them from memory and save the result to a `MemoryStream`:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
 
byte[] merged = MergePdfs([File.ReadAllBytes("chapter-1.pdf"), File.ReadAllBytes("chapter-2.pdf")]);
File.WriteAllBytes("merged-from-bytes.pdf", merged);
Console.WriteLine($"{merged.Length:N0} bytes");
 
// Merge PDFs held in memory (uploads, database blobs, HTTP responses) and get bytes back.
static byte[] MergePdfs(IEnumerable<byte[]> pdfs)
{
    var sources = new List<PdfDocument>();
    try
    {
        foreach (byte[] pdf in pdfs)
            sources.Add(PdfDocument.Open(pdf));
 
        using PdfDocument merged = PdfDocument.Merge(sources);
        using var output = new MemoryStream();
        merged.Save(output);
        return output.ToArray();
    }
    finally
    {
        foreach (PdfDocument source in sources)
            source.Dispose();
    }
}
```

*Output · Console*

```
28,590 bytes
```

## Merge uploaded PDFs in ASP.NET Core

A web endpoint that takes several uploads and returns one PDF. It uses `TryOpen`, which returns an error instead of throwing when an upload isn’t a PDF it can read. Create the project with `dotnet new web` and add the package as above:

*Program.cs · OnePDF 1.11.0 · ASP.NET Core 10*

```
using OnePdf;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
// POST two or more PDFs as multipart/form-data (field "files"); get one merged PDF back.
app.MapPost("/pdf/merge", async (IFormFileCollection files) =>
{
    var sources = new List<PdfDocument>();
    try
    {
        foreach (IFormFile file in files)
        {
            using var buffer = new MemoryStream();
            await file.CopyToAsync(buffer);
            if (!PdfDocument.TryOpen(buffer.ToArray(), out PdfDocument? doc, out PdfException? error))
                return Results.BadRequest($"{file.FileName}: {error?.Message}");
            sources.Add(doc);
        }
 
        using PdfDocument merged = PdfDocument.Merge(sources);
        var output = new MemoryStream();
        merged.Save(output);
        return Results.File(output.ToArray(), "application/pdf", "merged.pdf");
    }
    finally
    {
        foreach (PdfDocument source in sources)
            source.Dispose();
    }
}).DisableAntiforgery();
 
app.Run();
```

*Terminal · curl*

```
# APP_URL is the address that dotnet run prints
curl -F files=@chapter-1.pdf -F files=@fw9.pdf -o merged.pdf "$APP_URL/pdf/merge"
```

That returns an eight-page PDF. If one of the uploads isn’t a PDF, the response is a 400 that names the file, for example `"bad.txt: The file has no usable cross-reference data, trailer or document catalog."` OnePDF is built for files you didn’t write: limits on decoded size, nesting and object count are on by default, and it never runs a document’s scripts. Damaged files are repaired where possible (see [repair a damaged PDF](https://recursiveai.org/onepdf/cookbook/repair-damaged-pdf/)).

## Append or insert chosen pages

To add pages to an existing document instead of building a new one, open the target with `AllowEditing` and call `ImportPages` with zero-based page indexes, and optionally the position to insert at:

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
 
using PdfDocument report = PdfDocument.Open("chapter-1.pdf", new PdfOpenOptions { AllowEditing = true });
using PdfDocument w9 = PdfDocument.Open("fw9.pdf");
using PdfDocument other = PdfDocument.Open("chapter-2.pdf");
 
report.ImportPages(w9, [0]);                     // append page 1 of the W-9
report.ImportPages(other, [1], insertAt: 1);     // insert page 2 of chapter 2 after the first page
report.Save("chapter-1-plus.pdf");
 
Console.WriteLine($"{report.PageCount} pages");
for (int i = 0; i < report.PageCount; i++)
    Console.WriteLine($"page {i + 1}: {report.Pages[i].ExtractText().Text.Split('\n')[0]}");
```

*Output · Console*

```
4 pages
page 1: Chapter 1, part 1
page 2: Chapter 2, part 2
page 3: Chapter 1, part 2
page 4: Form W-9
```

Pages added by `ImportPages` bring the same things a merge does. Take `doc.Pages` again after a page operation, since page objects read before it are out of date. To go the other way, see [split a PDF](https://recursiveai.org/onepdf/cookbook/split-pdf/). To delete, rotate or reorder pages, see [rotate, delete and reorder pages](https://recursiveai.org/onepdf/cookbook/rotate-delete-pages/).

## Merge filled copies of the same form

A PDF form field has one value however many times it appears. iText’s knowledge base puts it plainly: if you merge identical forms, “you have multiple visualizations of the same field, but *each visualization will show the same value*”, so fields have to be renamed ([merging forms](https://kb.itextpdf.com/itext/how-to-merge-forms-from-different-files-into-one-p)). OnePDF renames them for you: a top-level field name already used in the merged document gets a `_2`, `_3` suffix.

*Program.cs · OnePDF 1.11.0 · .NET 10*

```
using OnePdf;
 
// Fill two copies of the same form, then merge them.
string[] people = ["Lovelace, Ada", "Hopper, Grace"];
var copies = new List<PdfDocument>();
foreach (string name in people)
{
    var copy = PdfDocument.Open("sf2823.pdf", new PdfOpenOptions { AllowEditing = true });
    copy.Form!.SetValue("Name", name);
    copies.Add(copy);
}
 
using (PdfDocument both = PdfDocument.Merge(copies))
{
    both.Save("two-forms.pdf");
    foreach (PdfFormField field in both.Form!.TerminalFields.Where(f => f.FullName.StartsWith("Name")))
        Console.WriteLine($"{field.FullName} = {field.Value}");
}
foreach (PdfDocument copy in copies)
    copy.Dispose();
```

*Output · Console*

```
Name = Lovelace, Ada
Name_2 = Hopper, Grace
```

The form is [OPM’s SF 2823](https://www.opm.gov/forms/pdf_fill/sf2823.pdf), three pages with 61 fields. Each copy in `two-forms.pdf` keeps its own value and stays fillable. If the merged file is for printing or archiving, flatten each copy before merging instead, so the values become page content and no fields are left: see [flatten a PDF form in C#](https://recursiveai.org/onepdf/articles/flatten-pdf-csharp/). Filling is covered in [fill PDF form fields in C#](https://recursiveai.org/onepdf/articles/fill-pdf-form-csharp/).

## Encrypted PDFs

Open an encrypted source with its password (`new PdfOpenOptions { Password = "…" }`) and merge it like any other. Its pages arrive as plain content, so the merged file is unencrypted unless you save it with `PdfSaveOptions.Encryption = PdfEncryptionAction.Set`. See [decrypt or encrypt a PDF](https://recursiveai.org/onepdf/cookbook/decrypt-pdf/).

## The same merge with iText or PDFsharp

iText’s API reference describes [`PdfMerger`](https://api.itextpdf.com/iText/dotnet/9.8.0/classi_text_1_1_kernel_1_1_utils_1_1_pdf_merger.html) as a helper that copies tags and outlines by default. For form fields, iText’s knowledge base says to copy pages with `CopyPagesTo` and a `PdfPageFormCopier`, “or the fields won’t be copied”, and its example calls `InitializeOutlines` on the target first ([how to merge documents correctly](https://kb.itextpdf.com/itext/how-to-merge-documents-correctly)). Written from those pages and run on the same three files with iText 9.8.0 (AGPL or commercial):

*Program.cs · iText 9.8.0*

```
using iText.Forms;
using iText.Kernel.Pdf;
 
string[] files = ["chapter-1.pdf", "chapter-2.pdf", "fw9.pdf"];
 
using PdfDocument merged = new PdfDocument(new PdfWriter("merged-itext-forms.pdf"));
merged.InitializeOutlines();                       // copy bookmarks too
foreach (string file in files)
{
    using PdfDocument source = new PdfDocument(new PdfReader(file));
    source.CopyPagesTo(1, source.GetNumberOfPages(), merged, new PdfPageFormCopier());
}
Console.WriteLine($"{merged.GetNumberOfPages()} pages");
```

The result has the ten pages, the four bookmarks, working links and the 23 fields. With a plain `PdfMerger.Merge` loop instead, the bookmarks and links came across but the form fields did not, in line with iText’s note. iText 9.8.0 under the AGPL also writes “iText® Core 9.8.0 (AGPL version)” into the file’s producer: see [iText license cost and AGPL terms](https://recursiveai.org/onepdf/articles/itext-license/).

PDFsharp merges by opening each file for import and adding its pages to another document ([reading existing files](https://docs.pdfsharp.net/PDFsharp/Topics/File-IO/Reading.html)). Its [FAQ](https://docs.pdfsharp.net/PDFsharp/Overview/FAQ.html) notes that “PDFsharp does not yet write object streams”, and that processing files that contain them “can lead to increased file size after saving the file”. OnePDF reads and writes object streams. The PDFsharp version of the merge, side by side with OnePDF’s, is in [migrating from PDFsharp](https://recursiveai.org/onepdf/docs/migrating-from-pdfsharp/). PDFsharp 6.5.0 is AGPL or commercial: see [PDFsharp license changes](https://recursiveai.org/onepdf/articles/pdfsharp-license/).

## Check the merged file

Two command-line checks catch most problems: `qpdf --check merged.pdf` reports structural errors, and `pdfinfo merged.pdf` shows the page count, the title you set and whether a form is present. All the files on this page pass `qpdf --check` with “No syntax or stream encoding errors found”.

## Where next

- [Merge PDFs](https://recursiveai.org/onepdf/cookbook/merge-pdfs/) and [split a PDF](https://recursiveai.org/onepdf/cookbook/split-pdf/), the cookbook recipes.
- [Page operations](https://recursiveai.org/onepdf/docs/page-operations/): every rule for copied, deleted and reordered pages.
- [Create a PDF in C#](https://recursiveai.org/onepdf/articles/create-pdf-csharp/) for a cover page or report to merge in.
- [Choosing a C# PDF library](https://recursiveai.org/onepdf/articles/csharp-pdf-library/) for licenses and features compared.
