INode Interface and ToTree Methods - lobodava/artisan-orm GitHub Wiki
Hierarchical data — folder trees, organisation charts, comment threads, BOMs — is naturally stored in SQL as a flat table with Id / ParentId (or via hierarchyid). Artisan.Orm has built-in support for reading such tables and re-assembling them into a tree of objects in one step.
Background reading. This page is the API reference. For the long-form story of the Id-ParentId +
hierarchyidcombined approach — including the schema design, the recursive CTE, and the trade-offs — see the re-published article: Combination of Id-ParentId and HierarchyId Approaches to Hierarchical Data.
public interface INode<T> where T : class
{
int Id { get; set; }
int? ParentId { get; set; }
IList<T>? Children { get; set; }
}Any class that exposes these three members can be turned into a tree by Artisan.Orm. ParentId == null marks a root.
public class Folder : INode<Folder>
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Name { get; set; } = null!;
public IList<Folder>? Children { get; set; }
}create procedure dbo.GetFolderTree
@RootFolderId int
as
begin
set nocount on;
with cte as
(
select Id, ParentId, [Name], 0 as Lvl
from dbo.Folders
where Id = @RootFolderId
union all
select f.Id, f.ParentId, f.[Name], cte.Lvl + 1
from dbo.Folders f
inner join cte on cte.Id = f.ParentId
)
select Id, ParentId, [Name]
from cte
order by Lvl, ParentId, Id; -- pre-order traversal
end;public Folder? GetFolderTree(int rootFolderId)
{
return GetByCommand(cmd =>
{
cmd.UseProcedure("dbo.GetFolderTree");
cmd.AddIntParam("@RootFolderId", rootFolderId);
// hierarchicallySorted: true unlocks the faster single-pass build.
return cmd.ReadToTree<Folder>(hierarchicallySorted: true);
});
}The result is a fully-linked tree where every node's Children is populated.
| Method | Returns | Use when |
|---|---|---|
ReadToTree<T> |
T? (root) |
The query produces a single root node. |
ReadToTreeList<T> |
IList<T> |
The query produces a forest — multiple roots. |
Both come in three layers — base / command / reader — and have async siblings:
// RepositoryBase level
public T? ReadToTree<T> (string sql, bool hierarchicallySorted = false, ...);
public IList<T> ReadToTreeList<T>(string sql, bool hierarchicallySorted = false, ...);
public Task<T?> ReadToTreeAsync<T>(...);
public Task<IList<T>> ReadToTreeListAsync<T>(...);
// SqlCommand level — when you need to set parameters via Action<SqlCommand>
public T? ReadToTree<T>(this SqlCommand cmd, bool hierarchicallySorted = false);
// SqlDataReader level — for reading a tree from a multi-result-set reader
public T? ReadToTree<T>(this SqlDataReader dr, bool getNextResult = true,
bool hierarchicallySorted = false);<T> must satisfy where T : class, INode<T>.
-
false(default): the builder collects all nodes into a dictionary keyed byId, then a second pass walks the dictionary linking each node to its parent'sChildren. Two passes, allocates aDictionary<int, T>. Works with any row order. -
true: the builder assumes rows arrive in pre-order traversal (parent before its children, siblings consecutive). It walks the list once as a stack — no dictionary, single pass. Faster, but the SQL must guarantee the order.
The pre-order requirement is satisfied by:
- a recursive CTE with
order by Lvl, ParentId, Id(as shown in the example above), -
hierarchyid-based queries withorder by Hid, - path-string columns with
order by Path.
If the order isn't guaranteed, leave hierarchicallySorted at false.
ReadToTree<T> returns the first root found (or null for an empty result set). If your query may yield multiple top-level nodes — e.g., "all folders for several users" — use ReadToTreeList<T>:
// Returns every Folder whose ParentId is null, each with its full subtree.
IList<Folder> roots = repo.ReadToTreeList<Folder>("dbo.GetAllUserFolders");If you already have a flat IList<T> from somewhere — say, you read it once with ReadToList and want to build the tree separately — call the same algorithm directly:
IList<Folder> flat = repo.ReadToList<Folder>("dbo.GetAllFolders");
Folder? root = flat.ToTree(hierarchicallySorted: false);
// or
IList<Folder> forest = flat.ToTreeList(hierarchicallySorted: false);This is helpful when you want to apply C# filters / transformations on the flat list first, then build the tree from the survivors.
For very deep or very wide hierarchies, the SQL Server hierarchyid type pairs nicely with INode<T>:
- Keep
Id/ParentIdas the primary application contract (works in any DB engine, easy to debug). - Add a
hierarchyidcolumn for fast subtree queries (<descendant_of>,<>, range scans on a hierarchical index). -
order bythehierarchyidcolumn to get pre-order rows that satisfyhierarchicallySorted: true.
The full design — schema, indexing strategy, and how the two representations stay in sync — is covered in the re-published article: Combination of Id-ParentId and HierarchyId Approaches to Hierarchical Data.
Tests/DAL/Folders/Repository.cs exercises the full set — GetFolderTree, FindFoldersWithParentTree, list-to-tree conversion utilities — against the seeded dbo.Folders table. Worth a read when you adopt this for a new entity.
See also:
- Read Methods Understanding
-
Mappers —
INode<T>types still need a[MapperFor]mapper forCreateObject