Recursive AI

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.

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.1AngleSharp 1.8.6onehtml 1.0.0
LicenseMIT (NuGet)MIT (NuGet)MIT OR Apache-2.0 (NuGet)
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”
QueriesXPath: SelectNodes, SelectSingleNode (docs). CSS through the third-party Fizzler.Systems.HtmlAgilityPack, last released April 2020CSS: QuerySelector, QuerySelectorAll (README). XPath through AngleSharp.XPathSelectors Level 4, including :has(). No XPath
Object modelIts 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 webHtmlWeb (docs)BrowsingContext with a loader (README)None: you pass the bytes
CSS and JavaScriptNot documentedAngleSharp.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).

The same query in both

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

catalog.htmlHTML
<!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.csHtmlAgilityPack 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.csAngleSharp 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")}");
}
OutputBoth 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). 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)})"));
OutputConsole
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 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.

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. 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). 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. 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