ZetCode

C# parse HTML with AngleSharp

last modified October 5, 2026

This tutorial shows how to parse HTML in C# with the AngleSharp library. AngleSharp is a standards-compliant HTML5 parser that exposes a document as a DOM tree. The same library can also parse SVG, MathML, and XML.

The Document Object Model (DOM) is a tree structure in which every node represents a part of the document. Element nodes and text nodes are the two most common node types. With DOM functions we can create nodes, remove nodes, change their content, and traverse the node hierarchy. Elements, attributes, and text are all nodes, but an element is not the same thing as its tag: the tag is the markup we write, while the element is the parsed DOM object that the parser produces.

Most of the examples below work with a small HTML file called words.html. Create it before running the file and DOM-modification examples.

words.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Words</title>
</head>
<body>

<ul>
    <li>sky</li>
    <li>cup</li>
    <li>water</li>
    <li>cloud</li>
    <li>bear</li>
    <li>wolf</li>
</ul>

</body>
</html>

Parsing an HTML string

In the first example, we parse an HTML document that is stored in a string. AngleSharp parses it in memory, so no file or network access is needed.

Program.cs
using AngleSharp;

var html = @"
<html>
<head>
<title>My Title</title>
</head>
<body>
<h1>Heading I</h1>
<p>Paragraph I</p>
<p>Paragraph II</p>
</body>
</html>";

using var context = BrowsingContext.New(Configuration.Default);
using var doc = await context.OpenAsync(req => req.Content(html));

Console.WriteLine(doc.Title);
Console.WriteLine(doc.Body!.InnerHtml.Trim());
Console.WriteLine(doc.FirstChild!.NodeName.ToLowerInvariant());
Console.WriteLine(doc.LastChild!.NodeName.ToLowerInvariant());

We create a BrowsingContext from the default Configuration. The context is the entry point for parsing and is disposed when the program ends. The document is created with OpenAsync and the req.Content(html) callback supplies the markup directly as a string.

The Title property returns the contents of the title element. doc.Body is the body element, and its InnerHtml property returns the markup between the body tags. FirstChild and LastChild are the first and last nodes of the document; for an HTML document both are the root html element, whose NodeName AngleSharp returns in upper case, so we lower-case it for display.

$ dotnet run
My Title
<h1>Heading I</h1>
<p>Paragraph I</p>
<p>Paragraph II</p>
html
html

Selecting elements with QuerySelectorAll

QuerySelectorAll returns every element that matches a CSS selector. It accepts any valid selector, including groups separated by commas and complex selectors such as ul li.note or a[href].

Program.cs
using AngleSharp;
using AngleSharp.Dom;

var html = @"
<html>
<p>words</p>
<ul>
<li>sky</li>
<li>cup</li>
<li>rock</li>
<li>water</li>
</ul>
</html>";

using var context = BrowsingContext.New(Configuration.Default);
using var doc = await context.OpenAsync(req => req.Content(html));

foreach (var item in doc.QuerySelectorAll("li"))
{
    Console.WriteLine(item.Text());
}

The example selects all li elements. The Text() extension method from the AngleSharp.Dom namespace returns the text content of an element; TextContent returns the same value without collapsing whitespace.

$ dotnet run
sky
cup
rock
water

Selecting one element and reading attributes

QuerySelector returns the first element that matches a selector, or null when nothing matches. Attributes are read with GetAttribute, which also returns null when the attribute is absent.

Program.cs
using AngleSharp;
using AngleSharp.Dom;

var html = @"
<html>
<body>
<ul id=""words"" class=""list"">
<li>sky</li>
<li>cup</li>
<li>rock</li>
</ul>
<a href=""https://example.com/page"" title=""Example"">Example link</a>
</body>
</html>";

using var context = BrowsingContext.New(Configuration.Default);
using var doc = await context.OpenAsync(req => req.Content(html));

var list = doc.QuerySelector("ul#words")!;
Console.WriteLine(list.ClassName);
Console.WriteLine(list.ChildElementCount);

var link = doc.QuerySelector("a")!;
Console.WriteLine(link.GetAttribute("href"));
Console.WriteLine(link.TextContent);

The selector ul#words matches the ul element by its id. ClassName returns the value of the class attribute and ChildElementCount returns the number of child elements. Next we select the anchor, read its href attribute, and print the visible text with TextContent. The null-forgiving operator (!) is used because QuerySelector is declared as returning a nullable reference.

$ dotnet run
list
3
https://example.com/page
Example link

Parsing an HTML file

The next example loads the document from an HTML file instead of a string.

Program.cs
using AngleSharp;
using AngleSharp.Dom;

var html = File.ReadAllText("words.html");

using var context = BrowsingContext.New(Configuration.Default);
using var doc = await context.OpenAsync(req => req.Content(html));

foreach (var li in doc.QuerySelectorAll("li"))
{
    Console.WriteLine(li.InnerHtml);
}

File.ReadAllText reads the file into a string, which is then passed to the parser. We select all li elements and print their InnerHtml. For an element that contains only text, InnerHtml and TextContent are equal; InnerHtml would additionally include any nested markup.

$ dotnet run
sky
cup
water
cloud
bear
wolf

Modifying the document

A parsed document is a live DOM tree, so we can add and remove elements before saving it again. The following example removes the first list item, appends a new one, adds a paragraph, and writes the result to a second file.

Program.cs
using AngleSharp;
using AngleSharp.Dom;

var html = File.ReadAllText("words.html");

using var context = BrowsingContext.New(Configuration.Default);
using var doc = await context.OpenAsync(req => req.Content(html));

var li = doc.CreateElement("li");
li.TextContent = "smile";

var ul = doc.QuerySelector("ul")!;
ul.FirstElementChild!.Remove();
ul.AppendChild(li);

var p = doc.CreateElement("p");
p.TextContent = "New paragraph";
doc.Body!.AppendChild(p);

File.WriteAllText("words2.html", doc.ToHtml());

Console.WriteLine(string.Join(", ", doc.QuerySelectorAll("li").Select(e => e.TextContent)));
Console.WriteLine(doc.QuerySelector("p")!.TextContent);

CreateElement creates a detached element and TextContent sets its text. We locate the ul element, remove its first child with Remove, and append the new item with AppendChild. A paragraph is created in the same way and added to the end of the body with Body.AppendChild.

ToHtml serializes the in-memory tree back to HTML markup, which is written to words2.html with File.WriteAllText. Finally we print the text of the remaining list items and of the new paragraph.

$ dotnet run
cup, water, cloud, bear, wolf, smile
New paragraph

Parsing an HTML table

Because the selectors are standard CSS selectors, tabular data can be read row by row. In this example we parse a small table and print each row as a line of text.

Program.cs
using AngleSharp;
using AngleSharp.Dom;

var html = @"
<table>
<tr><th>Name</th><th>Price</th></tr>
<tr><td>Apple</td><td>1.20</td></tr>
<tr><td>Banana</td><td>0.80</td></tr>
<tr><td>Cherry</td><td>3.50</td></tr>
</table>";

using var context = BrowsingContext.New(Configuration.Default);
using var doc = await context.OpenAsync(req => req.Content(html));

foreach (var row in doc.QuerySelectorAll("tr"))
{
    var cells = row.QuerySelectorAll("td, th").Select(c => c.TextContent.Trim());
    Console.WriteLine(string.Join(" | ", cells));
}

The outer query selects every tr element. Inside each row, the selector group td, th collects both header and data cells, and TextContent reads the value of each cell. For real-world tables, remember that rows can also contain cells of other types, and that the first row usually holds the header.

$ dotnet run
Name | Price
Apple | 1.20
Banana | 0.80
Cherry | 3.50

Loading an HTML page from the web

To fetch and parse a page from a URL, enable the default network loader with WithDefaultLoader. The parser then resolves the address, downloads the page, and builds the DOM tree.

Program.cs
using AngleSharp;

var config = Configuration.Default.WithDefaultLoader();
using var context = BrowsingContext.New(config);

using var doc = await context.OpenAsync("https://example.com");

Console.WriteLine(doc.Title);

foreach (var par in doc.QuerySelectorAll("p"))
{
    Console.WriteLine(par.TextContent.Trim());
}

The example reads the page title and the text of every paragraph. The exact output depends on the live page, and it can change over time.

$ dotnet run
Example Domain
This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.

Parsing several pages asynchronously

OpenAsync returns a Task<IDocument>, so several pages can be downloaded and parsed concurrently. This is much faster than parsing them one after another.

Program.cs
using AngleSharp;

var urls = new[]
{
    "https://example.com",
    "https://example.org",
    "https://www.rust-lang.org",
    "https://go.dev",
    "https://fsharp.org",
    "https://clojure.org",
    "https://www.perl.org",
    "https://www.gnu.org",
};

var config = Configuration.Default.WithDefaultLoader();
using var context = BrowsingContext.New(config);

var tasks = urls.Select(url => context.OpenAsync(url)).ToList();
await Task.WhenAll(tasks);

foreach (var task in tasks)
{
    using var doc = await task;
    Console.WriteLine(doc.Title);
}

We start one task per URL, wait for all of them with Task.WhenAll, and then print the title of each document. Disposing a document releases the resources held by that tree.

$ dotnet run
Example Domain
Example Domain
Rust Programming Language
The Go Programming Language
fsharp.org
Clojure
The Perl Programming Language - www.perl.org
The GNU Operating System and the Free Software Movement

Source

AngleSharp - documentation

AngleSharp - GitHub project

Document.querySelectorAll() - MDN Web Docs

In this article we have parsed, queried, and modified HTML documents in C# with the AngleSharp library.

Author

Jan Bodnar writes programming tutorials and educational content.

List all C# tutorials.