To migrate from AngleSharp to onehtml, replace HtmlParser.ParseDocument with Document.Parse and query through doc.Root. Keep CSS selectors, change missing-element checks from null to IsNull, and replace DOM mutation with edits that return a new document. onehtml is an AngleSharp alternative for .NET 10 applications that need HTML queries and source-preserving edits, not a drop-in replacement for a browsing context or scripting setup.
Every C# program below was run on .NET 10 with AngleSharp 1.10.1 and RecursiveAI.OneHtml 1.6.0, using the same input on both sides. The migration changes how you hold nodes and write output; it does not require changing CSS selectors into XPath.
Before you migrate
- Check your target framework. onehtml 1.6.0 targets
net10.0. If your application must stay on an earlier framework, this migration is not an option. See the onehtml package and AngleSharp’s package targets. - Separate parsing from browsing. AngleSharp documents document and resource loaders and JavaScript integration through an extension. onehtml does not fetch pages, execute scripts, or perform layout. Supply the HTML yourself. If your code relies on those services, plan their replacement before changing the parser.
- Decide what output you need. onehtml can serialize the parsed tree or write source bytes after edits. Its editing contract preserves bytes outside changed regions and checks the resulting tree by parsing it again. That is useful when an HTML patch should leave unrelated markup alone.
Start with the onehtml getting-started guide if this is your first use of the package. The 1.6.0 README describes the library’s scope and runtime requirements.
AngleSharp to onehtml API map
The read and mutation names below follow AngleSharp’s published examples. The onehtml replacements are exercised in the programs below; its snapshot and editing model is described in the core package guide.
| Task | AngleSharp 1.10.1 | onehtml 1.6.0 |
|---|---|---|
| Parse an HTML string | new HtmlParser().ParseDocument(html) | Document.Parse(html) |
| Find one element | doc.QuerySelector(css) | doc.Root.QuerySelector(css) |
| Find all matches | doc.QuerySelectorAll(css) | doc.Root.QuerySelectorAll(css) |
| Check a missing element | element is null | node.IsNull |
| Read text or an attribute | TextContent, GetAttribute(name) | TextContent, GetAttribute(name) |
| Serialize an element | element.OuterHtml | doc.SerializeNode(node) |
| Serialize its children | element.InnerHtml | doc.SerializeChildren(node) |
| Change an attribute | element.SetAttribute(name, value) | Edit.SetAttribute(node, name, value), then doc.Apply(edits) |
| Replace an element’s text | element.TextContent = text | Edit.ReplaceText(node, text), then doc.Apply(edits) |
Do not carry over IElement assumptions. A onehtml Node is a value-type handle into one immutable document. It is not an object whose properties you assign to change the tree.
Run the examples with pinned packages
Create separate console projects so each side has a clear dependency. These commands use the .NET 10 SDK:
dotnet new console -n Angle --framework net10.0 dotnet add Angle package AngleSharp --version 1.10.1 dotnet new console -n One --framework net10.0 dotnet add One package RecursiveAI.OneHtml --version 1.6.0
For each pair, replace Angle/Program.cs and One/Program.cs with the corresponding programs. Run dotnet run --project Angle and dotnet run --project One. Each program is complete and uses only the input shown.
Keep CSS selectors; change missing-node checks
Start with the extraction code. Both programs use main > a.item, decode & in text, and distinguish an empty href from a missing attribute. The final two lines serialize one link and its children rather than reading its decoded text.
using System; using AngleSharp.Html.Parser; const string html = "<main><a class='item' href='/a'>A & B</a>" + "<a class='item' href=''>Empty</a><a class='item'>Missing</a></main>"; using var doc = new HtmlParser().ParseDocument(html); foreach (var link in doc.QuerySelectorAll("main > a.item")) { string? href = link.GetAttribute("href"); Console.WriteLine($"{link.TextContent}: {href ?? "(missing)"}"); } var missing = doc.QuerySelector("aside"); Console.WriteLine($"No aside: {missing is null}"); Console.WriteLine(doc.QuerySelector("a")!.OuterHtml); Console.WriteLine(doc.QuerySelector("a")!.InnerHtml);
using System; using RecursiveAI.OneHtml; const string html = "<main><a class='item' href='/a'>A & B</a>" + "<a class='item' href=''>Empty</a><a class='item'>Missing</a></main>"; var doc = Document.Parse(html); foreach (var link in doc.Root.QuerySelectorAll("main > a.item")) { string? href = link.GetAttribute("href"); Console.WriteLine($"{link.TextContent}: {href ?? "(missing)"}"); } var missing = doc.Root.QuerySelector("aside"); Console.WriteLine($"No aside: {missing.IsNull}"); Console.WriteLine(doc.SerializeNode(doc.Root.QuerySelector("a"))); Console.WriteLine(doc.SerializeChildren(doc.Root.QuerySelector("a")));
Both print:
A & B: /a Empty: Missing: (missing) No aside: True <a class="item" href="/a">A & B</a> A & B
The empty attribute prints an empty value. The absent attribute returns null and prints (missing). A missing element is different: AngleSharp returns null, while onehtml returns a node whose IsNull is true. Check it before reading node members. Replace null-conditional element access with an explicit IsNull branch, not a reference comparison.
TextContent is decoded tree text. Serialization escapes the ampersand again because the output is HTML. Do not add an HTML-decoding step to the text in this example. Also, TextContent is not a promise of visible text: it does not mean “what a browser painted.”
OuterHtml and InnerHtml are not properties on a onehtml node. Use the document’s SerializeNode and SerializeChildren methods. Keep the distinction between serialized markup and original source bytes.
Replace DOM mutation with an edit transaction
AngleSharp’s DOM manipulation example changes the existing tree. In onehtml, build edits against the original snapshot, apply them together, check the result, then query the returned document. Here both programs update a link’s destination and text.
using System; using AngleSharp.Html.Parser; const string html = "<DIV class='keep'><a href='/old'>Old</a></DIV>"; using var doc = new HtmlParser().ParseDocument(html); var link = doc.QuerySelector("a")!; link.SetAttribute("href", "/new"); link.TextContent = "New & ready"; Console.WriteLine(link.TextContent); Console.WriteLine(doc.QuerySelector("div")!.OuterHtml);
using System; using System.Text; using RecursiveAI.OneHtml; using RecursiveAI.OneHtml.Editing; const string html = "<DIV class='keep'><a href='/old'>Old</a></DIV>"; var doc = Document.Parse(html); var link = doc.Root.QuerySelector("a"); var result = doc.Apply(new[] { Edit.SetAttribute(link, "href", "/new"), Edit.ReplaceText(link, "New & ready") }); if (!result.Succeeded) throw new InvalidOperationException(result.Failure); var changed = result.Document!; Console.WriteLine(link.TextContent); Console.WriteLine(changed.Root.QuerySelector("a").TextContent); Console.WriteLine(Encoding.UTF8.GetString(changed.ToLosslessBytes()));
AngleSharp prints:
New & ready <div class="keep"><a href="/new">New & ready</a></div>
onehtml prints:
Old New & ready <DIV class='keep'><a href='/new'>New & ready</a></DIV>
The onehtml handle named link still reads Old. It belongs to the old snapshot. The query on changed reads New & ready. Requery the new document before your next editing step; do not pass handles from the old document into an edit on the new one.
The output operations are deliberate. AngleSharp’s OuterHtml serializes the selected element. onehtml’s ToLosslessBytes() writes the edited source: this input keeps its uppercase DIV tags and single quotes. The UTF-8 decode in the sample is valid because its input was a C# string; do not use that decode blindly for documents parsed from bytes in another encoding.
Per the editing contract, all edits apply or none do. A failed transaction has no new document. Keep the Succeeded check even if your first test input always succeeds. When a local patch cannot represent the requested tree, an edit can widen to an enclosing region or fail; source preservation is not a promise that every edit touches only one attribute.
Make byte encodings explicit
String inputs are already decoded. Byte inputs need a separate review. AngleSharp’s raw-byte parsing guide says that, without an explicit encoding, it detects a byte order mark and starts with UTF-8, allowing an HTML declaration to restart decoding. onehtml’s encoding guide documents a windows-1252 fallback when nothing else decides the encoding.
For UTF-8 bytes with no declaration, make that knowledge explicit. Both programs print café:
using System; using System.Text; using AngleSharp.Html.Parser; ReadOnlyMemory<byte> bytes = Encoding.UTF8.GetBytes("<p>café</p>"); var parser = new HtmlParser(); using var doc = parser.ParseDocument(bytes, Encoding.UTF8); Console.WriteLine(doc.QuerySelector("p")!.TextContent);
using System; using System.Text; using RecursiveAI.OneHtml; using RecursiveAI.OneHtml.Encodings; ReadOnlyMemory<byte> bytes = Encoding.UTF8.GetBytes("<p>café</p>"); var doc = Document.Parse(bytes, new ParseOptions { DefaultEncoding = WhatwgEncoding.Utf8 }); Console.WriteLine(doc.Root.QuerySelector("p").TextContent);
DefaultEncoding is a fallback, not a command to ignore a byte order mark or declared charset. For HTTP input, preserve the response bytes and supply the Content-Type header through ParseOptions.ContentType. If you decode incorrectly before calling Document.Parse(string), the parser cannot recover the lost characters.
Move fetching and scripting out of the parser
If your current entry point is BrowsingContext.OpenAsync, first ask what it does in your application. AngleSharp’s virtual-request example supplies a string; that parsing step maps to Document.Parse. A configured loader or scripting service is separate work, not another name for parsing.
- HTTP and files: keep downloads, cookies, redirects, retries, and file reads in your application. Pass the resulting bytes to onehtml.
- Relative links: keep the final page URL alongside the parsed document. Reading
GetAttribute("href")gives an attribute value, not a fetch operation. Resolve relative URLs against the correct base URL in your application, accounting for a page’s base element when needed. - Script-built content: supply HTML captured after the required script execution, or keep the existing execution layer. onehtml treats scripts as inert text; installing it does not replace a JavaScript engine.
If those services are central to your application, migrate extraction as a separate step rather than removing the whole browsing setup at once.
Check behavior before switching production code
- Save representative inputs. Include malformed tables, omitted end tags, templates, SVG, missing attributes, and the byte encodings your application receives. Keep the input bytes and headers when decoding matters.
- Compare extracted values. Run both packages on those inputs. Check selector match counts, text, and attribute values before comparing serialized strings. Different output formatting alone does not establish a different tree.
- Review writes separately. Replace property assignments with edits, handle failure, and use the returned snapshot. Check both the intended tree change and which source regions changed.
- Keep parser configuration in the test. A scripting flag can affect parsing even when no JavaScript runs. Do not assume defaults from an old browsing context match a new parser call.
- Remove the old dependency last. Switch callers once the extraction results and edit behavior meet your application’s expectations.
Keeping CSS selectors is not a guarantee that every saved page produces an identical tree. The published AngleSharp differential notes describe tested versions, inputs, and known differences. Use those notes to choose fixtures, not as a substitute for running your application’s inputs on the versions you install.
Where to go next
- CSS selectors in onehtml for supported syntax and selector behavior.
- Edit HTML without reformatting unrelated markup for a focused editing program.
- Migrate from HtmlAgilityPack if you also have XPath-based callers.
- HtmlAgilityPack vs AngleSharp if you are still choosing rather than migrating.
Package versions and public documentation checked November 10, 2026. The examples use AngleSharp 1.10.1 and onehtml 1.6.0; they do not claim compatibility with every extension or selector in an existing application.