---
title: "Migrate from AngleSharp to onehtml"
url: https://recursiveai.org/onehtml/articles/migrate-from-anglesharp/
description: "Use onehtml as an AngleSharp alternative on .NET 10. Keep CSS selectors, change null checks and DOM edits, and compare six tested C# migration samples."
date: 2026-11-10
author: onehtml team
---

# Migrate from AngleSharp to onehtml

Use onehtml as an AngleSharp alternative on .NET 10. Keep CSS selectors, change null checks and DOM edits, and compare six tested C# migration samples.

November 10, 2026 · onehtml team

To migrate from AngleSharp to [onehtml](https://recursiveai.org/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](https://www.nuget.org/packages/RecursiveAI.OneHtml/1.6.0) and [AngleSharp’s package targets](https://www.nuget.org/packages/AngleSharp/1.10.1).
- **Separate parsing from browsing.** AngleSharp documents [document and resource loaders](https://github.com/AngleSharp/AngleSharp/blob/devel/docs/tutorials/01-API.md#iloader) and [JavaScript integration through an extension](https://github.com/AngleSharp/AngleSharp/blob/devel/docs/tutorials/03-Examples.md#connecting-javascript-evaluation). 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](https://github.com/recursive-ai-oss/onehtml-csharp/blob/v1.6.0/docs/editing.md) 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](https://recursiveai.org/onehtml/docs/getting-started/) if this is your first use of the package. The [1.6.0 README](https://github.com/recursive-ai-oss/onehtml-csharp/blob/v1.6.0/README.md) describes the library’s scope and runtime requirements.

## AngleSharp to onehtml API map

The read and mutation names below follow AngleSharp’s [published examples](https://github.com/AngleSharp/AngleSharp/blob/devel/docs/tutorials/03-Examples.md). The onehtml replacements are exercised in the programs below; its snapshot and editing model is described in the [core package guide](https://github.com/recursive-ai-oss/onehtml-csharp/blob/v1.6.0/src/OneHtml/README.md).

| 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:

*Terminal*

```
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 `&amp;` 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.

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

```
using System;
using AngleSharp.Html.Parser;
 
const string html = "<main><a class='item' href='/a'>A &amp; 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);
```

*onehtml 1.6.0 · Program.cs · .NET 10*

```
using System;
using RecursiveAI.OneHtml;
 
const string html = "<main><a class='item' href='/a'>A &amp; 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:

*Output*

```
A & B: /a
Empty: 
Missing: (missing)
No aside: True
<a class="item" href="/a">A &amp; B</a>
A &amp; 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](https://github.com/AngleSharp/AngleSharp/blob/devel/docs/tutorials/03-Examples.md#simple-document-manipulation) 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.

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

```
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);
```

*onehtml 1.6.0 · Program.cs · .NET 10*

```
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:

*Output*

```
New & ready
<div class="keep"><a href="/new">New &amp; ready</a></div>
```

onehtml prints:

*Output*

```
Old
New & ready
<DIV class='keep'><a href='/new'>New &amp; 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](https://recursiveai.org/onehtml/docs/editing/), 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](https://github.com/AngleSharp/AngleSharp/blob/devel/docs/tutorials/03-Examples.md#parsing-raw-bytes-from-httpclient) 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](https://recursiveai.org/onehtml/cookbook/detect-encoding/) 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é`:

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

```
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);
```

*onehtml 1.6.0 · Program.cs · .NET 10*

```
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](https://github.com/AngleSharp/AngleSharp/blob/devel/docs/tutorials/03-Examples.md#parsing-a-well-defined-document) 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

1. **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.
2. **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.
3. **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.
4. **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.
5. **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](https://recursiveai.org/onehtml/docs/anglesharp-differential/) 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](https://recursiveai.org/onehtml/docs/selectors/) for supported syntax and selector behavior.
- [Edit HTML without reformatting unrelated markup](https://recursiveai.org/onehtml/cookbook/edit-html-csharp-lossless/) for a focused editing program.
- [Migrate from HtmlAgilityPack](https://recursiveai.org/onehtml/articles/migrate-from-htmlagilitypack/) if you also have XPath-based callers.
- [HtmlAgilityPack vs AngleSharp](https://recursiveai.org/onehtml/articles/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.
