Usando linq2sql você poderia fazer:
List<PageInfo> GetHierarchicalPages()
{
var pages = myContext.PageInfos.ToList();
var parentPages = pages.Where(p=>p.ParentId == null).ToList();
foreach(var page in parentPages)
{
BuildTree(
page,
p=> p.Pages = pages.Where(child=>p.pageId == child.ParentId).ToList()
);
}
}
void BuildTree<T>(T parent, Func<T,List<T>> setAndGetChildrenFunc)
{
foreach(var child in setAndGetChildrenFunc(parent))
{
BuildTree(child, setAndGetChildrenFunc);
}
}
Supondo que você defina uma propriedade Pages no PageInfo como:
public partial class PageInfo{
public List<PageInfo> Pages{get;set;}
}
O processamento para obtê-lo em uma hierarquia está acontecendo no lado da aplicação web, o que evita carga extra no servidor sql. Observe também que esse tipo de informação é um candidato perfeito para armazenar em cache.
Você pode fazer a renderização como Rex mencionou. Alternativamente, você pode expandir um pouco essa implementação e torná-la compatível com as interfaces de hierarquia e usar controles asp.net.
Atualização 1: Para a variação de renderização que você pediu em um comentário, você pode:
var sb = new System.IO.StringWriter();
var writer = new HtmlTextWriter(sb);
// rex's rendering code
var html = sb.ToString();