---
title: "HtmlAgilityPack vs AngleSharp"
url: https://recursiveai.org/onehtml/articles/htmlagilitypack-vs-anglesharp/
description: "HtmlAgilityPack vs AngleSharp compared on parsing, XPath and CSS selectors, frameworks and speed, with the same code run in both, and where onehtml fits."
date: 2026-10-26
author: onehtml team
---

# HtmlAgilityPack vs AngleSharp

HtmlAgilityPack vs AngleSharp compared on parsing, XPath and CSS selectors, frameworks and speed, with the same code run in both, and where onehtml fits.

October 26, 2026 · onehtml team

HtmlAgilityPack vs AngleSharp comes down to queries, trees and targets. HtmlAgilityPack (HAP) is a tolerant parser with XPath built in and builds going back to .NET Framework 3.5. AngleSharp parses to the HTML standard, builds the same tree a browser does, and gives you the browser’s DOM API with CSS selectors, plus add-ons for CSS, JavaScript and XPath. Both are MIT-licensed. Choose HAP to keep XPath queries or support old frameworks. Choose AngleSharp when you want browser-accurate trees and `querySelectorAll`. onehtml is a third option for .NET 10 only: a standards parser with Selectors Level 4, edits that leave the rest of the file untouched, and accessibility checks, but no XPath. This comparison covers HtmlAgilityPack 1.13.1 (released 12 October 2026) and AngleSharp 1.8.6 (25 October 2026). Every fact about them comes from their own pages, read on 26 October 2026, and every sample was compiled and run on .NET 10.

## HtmlAgilityPack and AngleSharp at a glance

|  | HtmlAgilityPack 1.13.1 | AngleSharp 1.8.6 | onehtml 1.0.0 |
|---|---|---|---|
| License | MIT ([NuGet](https://www.nuget.org/packages/HtmlAgilityPack/1.13.1)) | MIT ([NuGet](https://www.nuget.org/packages/AngleSharp/1.8.6)) | MIT OR Apache-2.0 ([NuGet](https://www.nuget.org/packages/RecursiveAI.OneHtml/1.0.0)) |
| Builds in the package | .NET Framework 3.5, 4.0 and 4.5, .NET 8, .NET Standard 2.0, UAP 10.0, portable profiles | .NET 10 and 8, .NET Standard 2.0, .NET Framework 4.6.2 and 4.7.2 | .NET 10 |
| Parser, in the publisher’s words | “very tolerant with ‘real world’ malformed HTML” | “construct a DOM based on the official W3C specification” | “a WHATWG-conformant parser” |
| Queries | XPath: `SelectNodes`, `SelectSingleNode` ([docs](https://html-agility-pack.net/selectors)). CSS through the third-party [Fizzler.Systems.HtmlAgilityPack](https://www.nuget.org/packages/Fizzler.Systems.HtmlAgilityPack/1.2.1), last released April 2020 | CSS: `QuerySelector`, `QuerySelectorAll` ([README](https://github.com/AngleSharp/AngleSharp)). XPath through [AngleSharp.XPath](https://www.nuget.org/packages/AngleSharp.XPath/2.0.10) | Selectors Level 4, including `:has()`. No XPath |
| Object model | Its own, “very similar to what proposes System.Xml” (NuGet) | The W3C DOM, with .NET naming (README) | DOM standard names over immutable snapshots |
| Loading from the web | `HtmlWeb` ([docs](https://html-agility-pack.net/from-web)) | `BrowsingContext` with a loader (README) | None: you pass the bytes |
| CSS and JavaScript | Not documented | AngleSharp.Css and AngleSharp.Js add-ons (README) | CSS cascade and computed styles built in. No script |

The builds row lists the `lib` folders of each published package. HtmlAgilityPack’s home page lists its selector support as “XPATH”, with “CSS *Coming Soon*”, and points to Fizzler for CSS selectors ([html-agility-pack.net](https://html-agility-pack.net/)).

## The same query in both

Both samples read this product listing, `catalog.html`, and print each product’s name and link:

*catalog.html · HTML*

```
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Kitchen &amp; home</title>
</head>
<body>
  <nav class=breadcrumbs><a href="/">Home</a> &rsaquo; <a href="/kitchen/">Kitchen</a></nav>
  <div class="products">
    <div class="product" data-sku="K-100">
      <h2><a href="/p/kettle">Kettle</a></h2>
      <span class="price">$39.00</span>
    </div>
    <div class="product sale" data-sku="T-200">
      <h2><a href="/p/toaster">Toaster</a></h2>
      <span class="price">$24.50</span>
    </div>
    <div class="product" data-sku="M-300">
      <h2><a href='https://partner.example/mixer'>Stand mixer &amp; bowl</a></h2>
      <span class="price">$189.00</span>
    </div>
  </div>
  <h3>Specs</h3>
  <table class="specs"><tr><th>Power</th><td>2200 W</td></tr><tr><th>Capacity</th><td>1.7 l</td></tr></table>
  <a href="/kitchen/?page=2" class="pager">Next page</a>
  <script>track("catalog");</script>
</body>
</html>
```

*Program.cs · HtmlAgilityPack 1.13.1 · .NET 10*

```
using HtmlAgilityPack;
 
var doc = new HtmlDocument();
doc.Load("catalog.html");
 
// XPath; a class test needs the token idiom so "products" doesn't match
string xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' product ')]//h2/a";
foreach (HtmlNode link in doc.DocumentNode.SelectNodes(xpath) ?? Enumerable.Empty<HtmlNode>())
{
    Console.WriteLine($"{HtmlEntity.DeEntitize(link.InnerText)} -> {link.GetAttributeValue("href", "")}");
}
```

*Program.cs · AngleSharp 1.8.6 · .NET 10*

```
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
 
var parser = new HtmlParser();
IDocument doc = parser.ParseDocument(File.ReadAllText("catalog.html"));
 
// CSS selectors, as in a browser's querySelectorAll
foreach (IElement link in doc.QuerySelectorAll("div.product h2 > a"))
{
    Console.WriteLine($"{link.TextContent} -> {link.GetAttribute("href")}");
}
```

*Output · Both programs*

```
Kettle -> /p/kettle
Toaster -> /p/toaster
Stand mixer & bowl -> https://partner.example/mixer
```

The output is the same, but the code shows the everyday differences:

- **Class matching.** `@class='product'` in XPath compares the whole attribute, so it misses `class="product sale"`. `contains(@class, 'product')` also matches the `products` wrapper. The `concat` idiom is the reliable form. A CSS class selector matches one class token.
- **Entities.** HAP’s `InnerText` returns “Stand mixer &amp; bowl” as written, so the sample decodes it with `HtmlEntity.DeEntitize`. AngleSharp’s `TextContent` is already decoded.
- **Empty results.** HAP’s `SelectNodes` returns `null` when nothing matches ([docs](https://html-agility-pack.net/select-nodes)). AngleSharp’s `QuerySelectorAll` returns an empty collection.

## The trees they build from broken HTML

This program parses three snippets with each library and prints each tree as `element(children)` and `"text"`. Comparing the trees, rather than the serialized HTML, matters here. HAP’s `OuterHtml` for an untouched node returns the source text, which hides how the node was parsed.

*Program.cs · .NET 10*

```
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using HtmlAgilityPack;
using OneHtml = RecursiveAI.OneHtml;
 
string[] inputs =
{
    "<p>One<p>Two<div>Three</div>",
    "<b>bold <i>both</b> italic</i>",
    "<table><tr><td>1</td></tr>Total<tr><td>2</td></tr></table>",
};
 
var angleSharp = new HtmlParser();
 
foreach (string html in inputs)
{
    var hap = new HtmlAgilityPack.HtmlDocument();
    hap.LoadHtml(html);
    var angle = angleSharp.ParseDocument(html);
    OneHtml.Document one = OneHtml.Document.Parse(html);
 
    Console.WriteLine($"Input:           {html}");
    Console.WriteLine($"HtmlAgilityPack: {Hap(hap.DocumentNode)}");
    Console.WriteLine($"AngleSharp:      {Angle(angle.Body!)}");
    Console.WriteLine($"onehtml:         {One(one.Root.QuerySelector("body"))}");
    Console.WriteLine();
}
 
// Each prints the children of a node as a tree: element(children) and "text"
static string Hap(HtmlNode n) => string.Join(" ", n.ChildNodes.Select(c =>
    c.NodeType == HtmlNodeType.Text ? $"\"{c.InnerText}\"" : $"{c.Name}({Hap(c)})"));
 
static string Angle(INode n) => string.Join(" ", n.ChildNodes.Select(c =>
    c.NodeType == NodeType.Text ? $"\"{c.TextContent}\"" : $"{c.NodeName.ToLowerInvariant()}({Angle(c)})"));
 
static string One(OneHtml.Node n) => string.Join(" ", n.ChildNodes.Select(c =>
    c.Kind == OneHtml.NodeKind.Text ? $"\"{c.Data}\"" : $"{c.LocalName}({One(c)})"));
```

*Output · Console*

```
Input:           <p>One<p>Two<div>Three</div>
HtmlAgilityPack: p("One") p("Two") div("Three")
AngleSharp:      p("One") p("Two") div("Three")
onehtml:         p("One") p("Two") div("Three")
 
Input:           <b>bold <i>both</b> italic</i>
HtmlAgilityPack: b("bold " i("both")) " italic"
AngleSharp:      b("bold " i("both")) i(" italic")
onehtml:         b("bold " i("both")) i(" italic")
 
Input:           <table><tr><td>1</td></tr>Total<tr><td>2</td></tr></table>
HtmlAgilityPack: table(tr(td("1")) "Total" tr(td("2")))
AngleSharp:      "Total" table(tbody(tr(td("1")) tr(td("2"))))
onehtml:         "Total" table(tbody(tr(td("1")) tr(td("2"))))
```

- **Unclosed paragraphs:** all three agree.
- **Misnested formatting** (`<b><i>…</b></i>`): AngleSharp and onehtml reopen the `i` after the `b` closes, as browsers do, so “italic” is still in an `i` element. HAP leaves it as plain text.
- **Text in a table:** browsers move stray text in front of the table and wrap rows in `tbody`. AngleSharp and onehtml do the same. HAP keeps “Total” inside the table, with no `tbody`. A query written against HAP’s tree, such as the selector `table > tr`, matches nothing in a browser-accurate tree.

If your results have to match what a browser or a JavaScript scraper sees, this is the deciding difference. If you mostly read well-formed pages, it matters less.

## Speed

The onehtml project publishes a [parse benchmark](https://github.com/recursive-ai-oss/onehtml-csharp/blob/v1.0.0/README.md#performance) that includes both libraries. Parsing a 100-page sample of crawled pages from bytes took 292.8 ms per sweep with HtmlAgilityPack 1.13.1, 341.7 ms with AngleSharp 1.8.6 and 125.0 ms with onehtml 1.0.0, on an AMD Ryzen 9 7950X3D with .NET 10, using BenchmarkDotNet in process. The method is in the repository’s [bench README](https://github.com/recursive-ai-oss/onehtml-csharp/blob/v1.0.0/bench/README.md).

## When to choose HtmlAgilityPack or AngleSharp

- **You target .NET Framework:** HtmlAgilityPack ships builds from 3.5 up, AngleSharp from 4.6.2. onehtml doesn’t support .NET Framework.
- **Your code is built on XPath:** HtmlAgilityPack has it built in. AngleSharp has it through AngleSharp.XPath.
- **You want the browser’s tree and CSS selectors:** AngleSharp, or onehtml on .NET 10.
- **You need to run the page’s JavaScript:** AngleSharp with AngleSharp.Js, or a real browser through Playwright.
- **You edit HTML and save it:** see the edit-and-save comparison in the [migration guide](https://recursiveai.org/onehtml/articles/migrate-from-htmlagilitypack/). onehtml changes only the bytes an edit touches.

## Where onehtml fits

onehtml is for .NET 10 projects that want browser-accurate parsing plus what comes after it. It reads bytes with the standard’s encoding sniffing, and its selectors pass 2,537 Web Platform Tests cases, with 43 known failures, all tentative proposals ([Selectors](https://recursiveai.org/onehtml/docs/selectors/)). Edits are spliced into the original bytes, then re-parsed and verified. It computes the CSS cascade, builds the accessibility tree, and checks pages against WCAG 2.2 AA with SARIF output. It is pure C# with no dependencies, works with NativeAOT and trimming, and is licensed MIT OR Apache-2.0.

Against AngleSharp, the project compared trees on 13,857 crawled pages: 12,469 (89.98%) were identical, and every difference in the rest is traced to the HTML, Encoding or DOM standard in the [AngleSharp differential](https://recursiveai.org/onehtml/docs/anglesharp-differential/). The difference you are most likely to see is a page that declares no encoding. onehtml decodes it as windows-1252, as the standard recommends, where AngleSharp assumes UTF-8.

What it leaves out: XPath, fetching, script execution, layout, and every target framework before .NET 10.

## Moving code

- [Migrate from HtmlAgilityPack to onehtml](https://recursiveai.org/onehtml/articles/migrate-from-htmlagilitypack/): XPath to CSS, `InnerText`, editing and saving, with both sides of every sample run.
- [Coming from AngleSharp](https://recursiveai.org/onehtml/cookbook/migrate-from-anglesharp/): where the trees differ and what changes in your code.
- [Parse HTML in C#](https://recursiveai.org/onehtml/articles/parse-html-csharp/): strings, files, URLs and fragments with onehtml.
