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) | 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” |
| Queries | XPath: SelectNodes, SelectSingleNode (docs). CSS through the third-party Fizzler.Systems.HtmlAgilityPack, last released April 2020 | CSS: QuerySelector, QuerySelectorAll (README). XPath through AngleSharp.XPath | 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) | 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).
The same query in both
Both samples read this product listing, catalog.html, and print each product’s name and link:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Kitchen & home</title> </head> <body> <nav class=breadcrumbs><a href="/">Home</a> › <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 & 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>
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", "")}"); }
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")}"); }
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 missesclass="product sale".contains(@class, 'product')also matches theproductswrapper. Theconcatidiom is the reliable form. A CSS class selector matches one class token. - Entities. HAP’s
InnerTextreturns “Stand mixer & bowl” as written, so the sample decodes it withHtmlEntity.DeEntitize. AngleSharp’sTextContentis already decoded. - Empty results. HAP’s
SelectNodesreturnsnullwhen nothing matches (docs). AngleSharp’sQuerySelectorAllreturns 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.
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)})"));
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 theiafter thebcloses, as browsers do, so “italic” is still in anielement. 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 notbody. A query written against HAP’s tree, such as the selectortable > 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
- Migrate from HtmlAgilityPack to onehtml: XPath to CSS,
InnerText, editing and saving, with both sides of every sample run. - Coming from AngleSharp: where the trees differ and what changes in your code.
- Parse HTML in C#: strings, files, URLs and fragments with onehtml.