Mesch.RealTree 0.0.4

There is a newer version of this package available.
See the version list below for details.
dotnet add package Mesch.RealTree --version 0.0.4
                    
NuGet\Install-Package Mesch.RealTree -Version 0.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mesch.RealTree" Version="0.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mesch.RealTree" Version="0.0.4" />
                    
Directory.Packages.props
<PackageReference Include="Mesch.RealTree" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mesch.RealTree --version 0.0.4
                    
#r "nuget: Mesch.RealTree, 0.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mesch.RealTree@0.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mesch.RealTree&version=0.0.4
                    
Install as a Cake Addin
#tool nuget:?package=Mesch.RealTree&version=0.0.4
                    
Install as a Cake Tool

Mesch.RealTree

A tree hierarchy management abstraction with plugin architecture and eventing.

Overview

RealTree provides a flexible tree implementation where:

  • Containers can hold both other containers and items
  • Items can hold containers but not other items
  • Operations support middleware-style actions and fire-and-forget events
  • Validation prevents cyclic references automatically

Core Concepts

Node Types

IRealTreeNode       // Base interface for all nodes
├── IRealTreeContainer  // Can contain containers and items
├── IRealTreeItem      // Can contain containers only
└── IRealTree          // Root node with global operations (extends IRealTreeNode)

Operations Service

All tree modifications go through IRealTreeOperations which provides:

  • Middleware-style action pipelines (can intercept/modify operations)
  • Event notifications (fire-and-forget, executed after operations)
  • Bulk operations for performance
  • Copy operations with recursive traversal

Quick Start

// Create tree
var factory = new RealTreeFactory();
var tree = factory.CreateTree(name: "MyTree");
var operations = new RealTreeOperations(factory);

// Add nodes
var container = await operations.AddContainerAsync(tree, null, "Documents");
var item = await operations.AddItemAsync(container, null, "Report.pdf");

// Navigate
var found = tree.FindByPath("/Documents/Report.pdf");
Console.WriteLine(found?.Name); // "Report.pdf"

API Reference

IRealTreeOperations Methods

All tree modifications go through IRealTreeOperations. Every method supports middleware actions and events.

Add Operations
Method Parameters Returns Triggers
AddContainerAsync (IRealTreeNode parent, Guid? id, string name) IRealTreeContainer AddContainerAction, ContainerAddedEvent
AddContainerAsync (IRealTreeNode parent, IRealTreeContainer container) IRealTreeContainer AddContainerAction, ContainerAddedEvent
AddItemAsync (IRealTreeContainer parent, Guid? id, string name) IRealTreeItem AddItemAction, ItemAddedEvent
AddItemAsync (IRealTreeContainer parent, IRealTreeItem item) IRealTreeItem AddItemAction, ItemAddedEvent
Remove Operations
Method Parameters Returns Triggers
RemoveAsync (IRealTreeNode node) Task RemoveContainerAction or RemoveItemAction, corresponding events
RemoveAllContainersAsync (IRealTreeNode parent) Task RemoveContainerAction, ContainerRemovedEvent (per container)
RemoveAllItemsAsync (IRealTreeContainer parent) Task RemoveItemAction, ItemRemovedEvent (per item)
Update/Move Operations
Method Parameters Returns Triggers
UpdateAsync (IRealTreeNode node, string? newName, Dictionary<string, object>? newMetadata) Task UpdateAction, NodeUpdatedEvent
MoveAsync (IRealTreeNode node, IRealTreeNode newParent) Task MoveAction, NodeMovedEvent
Bulk Operations
Method Parameters Returns Triggers
BulkAddContainersAsync (IRealTreeNode parent, IEnumerable<IRealTreeContainer> containers) Task BulkAddContainerAction, BulkContainersAddedEvent
BulkAddItemsAsync (IRealTreeContainer parent, IEnumerable<IRealTreeItem> items) Task BulkAddItemAction, BulkItemsAddedEvent
BulkRemoveAsync (IEnumerable<IRealTreeNode> nodes) Task BulkRemoveAction, BulkNodesRemovedEvent
Copy Operations
Method Parameters Returns Triggers
CopyContainerAsync (IRealTreeContainer source, IRealTreeNode destination, Guid? newId, string? newName) IRealTreeContainer None (uses Add operations internally)
CopyItemAsync (IRealTreeItem source, IRealTreeContainer destination, Guid? newId, string? newName) IRealTreeItem None (uses Add operations internally)
Query Operations
Method Parameters Returns Triggers
ListContainerAsync (IRealTreeContainer container, bool includeContainers, bool includeItems, bool recursive) IReadOnlyList<IRealTreeNode> ListContainerAction, ContainerListedEvent
ShowItemAsync (IRealTreeItem item) Task ShowItemAction, ItemShownEvent

Notes:

  • All methods have optional triggerActions, triggerEvents, and cancellationToken parameters (not shown above)
  • Middleware actions execute before the operation; events execute after
  • Actions can cancel operations by throwing exceptions or not calling next()

Node Registration Methods

Register middleware and events on nodes to intercept operations:

On IRealTreeContainer, IRealTreeItem, and IRealTree

Actions (Middleware):

  • RegisterAddContainerAction(AddContainerDelegate handler)
  • RegisterRemoveContainerAction(RemoveContainerDelegate handler)
  • RegisterAddItemAction(AddItemDelegate handler)
  • RegisterRemoveItemAction(RemoveItemDelegate handler)
  • RegisterUpdateAction(UpdateNodeDelegate handler)
  • RegisterMoveAction(MoveNodeDelegate handler)
  • RegisterBulkAddContainerAction(BulkAddContainerDelegate handler)
  • RegisterBulkAddItemAction(BulkAddItemDelegate handler)
  • RegisterBulkRemoveAction(BulkRemoveContainerDelegate handler)
  • RegisterListContainerAction(ListContainerDelegate handler)

Events:

  • RegisterContainerAddedEvent(ContainerAddedEventDelegate handler)
  • RegisterContainerRemovedEvent(ContainerRemovedEventDelegate handler)
  • RegisterItemAddedEvent(ItemAddedEventDelegate handler)
  • RegisterItemRemovedEvent(ItemRemovedEventDelegate handler)
  • RegisterNodeUpdatedEvent(NodeUpdatedEventDelegate handler)
  • RegisterNodeMovedEvent(NodeMovedEventDelegate handler)
  • RegisterBulkContainersAddedEvent(BulkContainersAddedEventDelegate handler)
  • RegisterBulkItemsAddedEvent(BulkItemsAddedEventDelegate handler)
  • RegisterBulkNodesRemovedEvent(BulkNodesRemovedEventDelegate handler)
  • RegisterContainerListedEvent(ContainerListedEventDelegate handler)
On IRealTreeItem only

Actions (Middleware):

  • RegisterShowItemAction(ShowItemDelegate handler)

Events:

  • RegisterItemShownEvent(ItemShownEventDelegate handler)

Deregistration: Replace Register with Deregister (e.g., DeregisterAddContainerAction, DeregisterShowItemAction)

On IRealTree:

  • FindByPath(string path) - Returns node at path (e.g., "/folder/item")
  • FindById(Guid id) - Returns node with matching ID

On IRealTreeNode:

  • .Parent - Parent node
  • .Path - Full path from root
  • .Tree - Root tree reference

On IRealTreeContainer:

  • .Containers - Read-only list of child containers
  • .Items - Read-only list of child items
  • .Children - Read-only list of all children (containers + items)

Middleware Actions

Actions execute before the operation and can intercept, modify, or cancel it:

container.RegisterAddContainerAction(async (context, next) =>
{
    // Pre-operation logic
    // Parent is IRealTreeNode, use ParentAsContainer helper for container-specific access
    var parentName = context.ParentAsContainer?.Name ?? context.Parent.Name;
    Console.WriteLine($"Adding {context.Container.Name} to {parentName}");

    // Validate
    if (context.Container.Name.StartsWith("temp"))
        throw new InvalidOperationException("Temp folders not allowed");

    // Continue pipeline
    await next();

    // Post-operation logic
    Console.WriteLine("Container added successfully");
});

Available Middleware Actions Reference

Middleware actions form a pipeline that executes before operations. Each action must call await next() to continue the pipeline, or throw an exception to cancel the operation.

Modification Actions
Action Delegate Type Signature
RegisterAddContainerAction AddContainerDelegate Task (AddContainerContext context, Func<Task> next)
RegisterAddItemAction AddItemDelegate Task (AddItemContext context, Func<Task> next)
RegisterRemoveContainerAction RemoveContainerDelegate Task (RemoveContainerContext context, Func<Task> next)
RegisterRemoveItemAction RemoveItemDelegate Task (RemoveItemContext context, Func<Task> next)
RegisterUpdateAction UpdateNodeDelegate Task (UpdateContext context, Func<Task> next)
RegisterMoveAction MoveNodeDelegate Task (MoveContext context, Func<Task> next)

Context Properties: Same as event contexts (see Event Handling section below)

Bulk Operation Actions
Action Delegate Type Signature
RegisterBulkAddContainerAction BulkAddContainerDelegate Task (BulkAddContainerContext context, Func<Task> next)
RegisterBulkAddItemAction BulkAddItemDelegate Task (BulkAddItemContext context, Func<Task> next)
RegisterBulkRemoveAction BulkRemoveContainerDelegate Task (BulkRemoveContext context, Func<Task> next)
Query Actions
Action Delegate Type Signature
RegisterListContainerAction ListContainerDelegate Task (ListContainerContext context, Func<Task> next)
RegisterShowItemAction ShowItemDelegate Task (ShowItemContext context, Func<Task> next)

Context Properties:

  • ListContainerContext: .Container, .IncludeContainers, .IncludeItems, .Recursive, .ListingMetadata, .Tree, .CancellationToken
  • ShowItemContext: .Item, .ShowMetadata, .Tree, .CancellationToken

Important Notes:

  • Actions can run before or after the operation by placing code before/after the await next() call
  • Throwing an exception cancels the operation and propagates to the caller
  • Actions execute in order from the node up through its parent hierarchy
  • Forgetting to call await next() will prevent the operation from completing

Event Handling

Events fire after operations complete and run in parallel:

container.RegisterContainerAddedEvent(async context =>
{
    // Log to database
    await LogActivity($"Container {context.Container.Name} added");
});

container.RegisterContainerAddedEvent(async context =>
{
    // Send notification (runs in parallel with logging)
    await NotifyUsers($"New folder: {context.Container.Name}");
});

Available Events Reference

All events are fire-and-forget notifications that execute after operations complete. They run in parallel and exceptions are logged but don't propagate.

Modification Events
Event Delegate Type Signature
RegisterContainerAddedEvent ContainerAddedEventDelegate Task (AddContainerContext context)
RegisterItemAddedEvent ItemAddedEventDelegate Task (AddItemContext context)
RegisterContainerRemovedEvent ContainerRemovedEventDelegate Task (RemoveContainerContext context)
RegisterItemRemovedEvent ItemRemovedEventDelegate Task (RemoveItemContext context)
RegisterNodeUpdatedEvent NodeUpdatedEventDelegate Task (UpdateContext context)
RegisterNodeMovedEvent NodeMovedEventDelegate Task (MoveContext context)

Context Properties:

  • AddContainerContext: .Container (IRealTreeContainer), .Parent (IRealTreeNode), .Tree, .CancellationToken
  • AddItemContext: .Item (IRealTreeItem), .Parent (IRealTreeContainer), .Tree, .CancellationToken
  • RemoveContainerContext: .Container (IRealTreeContainer), .Parent (IRealTreeContainer), .Tree, .CancellationToken
  • RemoveItemContext: .Item (IRealTreeItem), .Parent (IRealTreeContainer), .Tree, .CancellationToken
  • UpdateContext: .Node, .OldName, .NewName, .OldMetadata, .NewMetadata, .Tree, .CancellationToken
  • MoveContext: .Node, .OldParent, .NewParent, .Tree, .CancellationToken
Bulk Operation Events
Event Delegate Type Signature
RegisterBulkContainersAddedEvent BulkContainersAddedEventDelegate Task (BulkAddContainerContext context)
RegisterBulkItemsAddedEvent BulkItemsAddedEventDelegate Task (BulkAddItemContext context)
RegisterBulkNodesRemovedEvent BulkNodesRemovedEventDelegate Task (BulkRemoveContext context)

Context Properties:

  • BulkAddContainerContext: .Containers (IReadOnlyList), .Parent (IRealTreeNode), .Tree, .CancellationToken
  • BulkAddItemContext: .Items (IReadOnlyList), .Parent (IRealTreeContainer), .Tree, .CancellationToken
  • BulkRemoveContext: .Nodes (IReadOnlyList), .Parent (IRealTreeNode), .Tree, .CancellationToken
Query Events
Event Delegate Type Signature
RegisterContainerListedEvent ContainerListedEventDelegate Task (ListContainerContext context, IReadOnlyList<IRealTreeNode> result, CancellationToken cancellationToken)
RegisterItemShownEvent ItemShownEventDelegate Task (ShowItemContext context, CancellationToken cancellationToken)

Context Properties:

  • ListContainerContext: .Container, .IncludeContainers, .IncludeItems, .Recursive, .ListingMetadata, .Tree, .CancellationToken
  • ShowItemContext: .Item, .ShowMetadata, .Tree, .CancellationToken

Note: Events can be registered at multiple levels:

  • On specific nodes (container/item) - fires for operations on that subtree
  • On the tree root - fires for all operations globally

Example:

// Local event - only fires for this container's operations
myContainer.RegisterItemAddedEvent(async ctx =>
    await LogLocal($"Item added: {ctx.Item.Name}"));

// Global event - fires for all item additions in the entire tree
tree.RegisterItemAddedEvent(async ctx =>
    await LogGlobal($"Item added anywhere: {ctx.Item.Name}"));

Bulk Operations

Efficient operations for multiple nodes:

var containers = new List<IRealTreeContainer>
{
    factory.CreateContainer(name: "Folder1"),
    factory.CreateContainer(name: "Folder2"),
    factory.CreateContainer(name: "Folder3")
};

await operations.BulkAddContainersAsync(tree, containers);

Query Operations

List Container Contents

// List all direct children
var children = await operations.ListContainerAsync(container);

// List only containers
var containers = await operations.ListContainerAsync(container, includeContainers: true, includeItems: false);

// List recursively (all descendants)
var allDescendants = await operations.ListContainerAsync(container, recursive: true);

// Middleware can filter or modify results
container.RegisterListContainerAction(async (ctx, next) =>
{
    // Only show items with specific metadata
    ctx.ListingMetadata["filterBy"] = "status";
    await next();
});

// Event notification after listing
container.RegisterContainerListedEvent(async (ctx, result, ct) =>
{
    Console.WriteLine($"Listed {result.Count} items from {ctx.Container.Name}");
});

Show Item

// Show/display an item - triggers middleware for custom rendering
await operations.ShowItemAsync(item);

// Register middleware to control how items are displayed
item.RegisterShowItemAction(async (ctx, next) =>
{
    // Validate access or prepare data
    if (!HasPermission(currentUser, ctx.Item))
        throw new UnauthorizedAccessException();

    // Add rendering metadata
    ctx.ShowMetadata["format"] = "detailed";
    ctx.ShowMetadata["includeHistory"] = true;

    await next();
});

// Another middleware that uses the metadata to render
item.RegisterShowItemAction(async (ctx, next) =>
{
    var format = ctx.ShowMetadata.GetValueOrDefault("format", "summary");

    if (format == "detailed")
    {
        Console.WriteLine($"Item: {ctx.Item.Name}");
        Console.WriteLine($"Path: {ctx.Item.Path}");
        Console.WriteLine($"ID: {ctx.Item.Id}");
    }
    else
    {
        Console.WriteLine(ctx.Item.Name);
    }

    await next();
});

// Event notification after item is shown
item.RegisterItemShownEvent(async (ctx, ct) =>
{
    await LogItemAccess(ctx.Item.Id, currentUser.Id);
});

Common Patterns

Hierarchical Permissions

// Register at tree level - applies to all operations
tree.RegisterAddContainerAction(async (context, next) =>
{
    var user = GetCurrentUser();
    var parent = context.Parent;
    
    if (!HasPermission(user, parent, "create"))
        throw new UnauthorizedAccessException();
    
    await next();
});

Audit Logging

// Update events
tree.RegisterNodeUpdatedEvent(async context =>
{
    await auditLogger.LogAsync(new AuditEntry
    {
        UserId = GetCurrentUser().Id,
        Action = "Update",
        NodeId = context.Node.Id,
        OldName = context.OldName,
        NewName = context.NewName,
        Timestamp = DateTime.UtcNow
    });
});

// Removal events - strongly typed contexts
tree.RegisterContainerRemovedEvent(async context =>
{
    await auditLogger.LogAsync(new AuditEntry
    {
        UserId = GetCurrentUser().Id,
        Action = "RemoveContainer",
        NodeId = context.Container.Id,
        NodeName = context.Container.Name,
        ParentId = context.Parent.Id,
        Timestamp = DateTime.UtcNow
    });
});

tree.RegisterItemRemovedEvent(async context =>
{
    await auditLogger.LogAsync(new AuditEntry
    {
        UserId = GetCurrentUser().Id,
        Action = "RemoveItem",
        NodeId = context.Item.Id,
        NodeName = context.Item.Name,
        ParentId = context.Parent.Id,
        Timestamp = DateTime.UtcNow
    });
});

Validation with Context

container.RegisterAddItemAction(async (context, next) =>
{
    var parent = context.Parent;
    var item = context.Item;
    
    // Business rule: max 10 items per container
    if (parent.Items.Count >= 10)
        throw new InvalidOperationException("Container full");
    
    // Check for duplicates
    if (parent.Items.Any(i => i.Name == item.Name))
        throw new DuplicateNameException(item.Name);
    
    await next();
});

Context Types

Strongly-Typed Removal Contexts

Removal operations use type-specific contexts for better type safety:

// Container removal - access .Container and .Parent (both strongly typed)
tree.RegisterRemoveContainerAction(async (context, next) =>
{
    Console.WriteLine($"Removing container: {context.Container.Name}");
    Console.WriteLine($"From parent: {context.Parent.Name}");

    // context.Container is IRealTreeContainer
    // and context.Parent is IRealTreeContainer

    await next();
});

// Item removal - access .Item and .Parent (both strongly typed)
tree.RegisterRemoveItemAction(async (context, next) =>
{
    Console.WriteLine($"Removing item: {context.Item.Name}");
    Console.WriteLine($"From parent: {context.Parent.Name}");

    // context.Item is IRealTreeItem
    // and context.Parent is IRealTreeContainer

    await next();
});

AddContainerContext Helpers

AddContainerContext.Parent is IRealTreeNode because both containers and items can hold containers. Use helper properties to avoid manual casting:

container.RegisterAddContainerAction(async (context, next) =>
{
    // Access parent as specific type
    if (context.ParentAsContainer != null)
    {
        Console.WriteLine($"Parent has {context.ParentAsContainer.Items.Count} items");
    }
    else if (context.ParentAsItem != null)
    {
        Console.WriteLine($"Parent is an item: {context.ParentAsItem.Name}");
    }

    await next();
});

Advanced Usage

Custom Node Types

public class FileContainer : RealTreeContainer
{
    public string ContentType { get; set; }
    public long MaxSize { get; set; }
    
    public FileContainer(string contentType, long maxSize) 
        : base(name: contentType)
    {
        ContentType = contentType;
        MaxSize = maxSize;
    }
}

Operation Context Access

tree.RegisterMoveAction(async (context, next) =>
{
    var node = context.Node;
    var oldPath = node.Path;
    
    await next(); // Perform the move
    
    var newPath = node.Path;
    await UpdateReferences(oldPath, newPath);
});

Delegate Management

// Store reference for later removal
AddContainerDelegate validator = async (context, next) => { /* validation */ };
container.RegisterAddContainerAction(validator);

// Remove when no longer needed
container.DeregisterAddContainerAction(validator);

Exception Handling

// Actions can throw exceptions to cancel operations
container.RegisterAddContainerAction(async (context, next) =>
{
    if (SomeValidationFails())
        throw new TreeValidationException("Operation not allowed");
    
    await next();
});

// Events log exceptions but don't propagate them
container.RegisterContainerAddedEvent(async context =>
{
    try
    {
        await SomeExternalService.NotifyAsync(context);
    }
    catch (Exception ex)
    {
        // Exception logged automatically, operation continues
    }
});

Thread Safety

  • Tree structure modifications are not thread-safe by design
  • Use external synchronization if needed
  • Events execute in parallel but individual event handlers should be thread-safe
  • ReaderWriterLockSlim is available on RealTreeRoot.Lock for custom locking

Dependency Injection

services.AddSingleton<IRealTreeFactory, RealTreeFactory>();
services.AddScoped<IRealTreeOperations, RealTreeOperations>();

// With logging
services.AddScoped<IRealTreeOperations>(provider =>
{
    var factory = provider.GetService<IRealTreeFactory>();
    var logger = provider.GetService<ILogger<RealTreeOperations>>();
    return new RealTreeOperations(factory, logger);
});

Performance Considerations

  • Use bulk operations for multiple related changes
  • Event handlers execute in parallel but should be lightweight
  • Consider unregistering delegates when no longer needed
  • Path lookups are O(depth), ID lookups are O(n)
  • Metadata dictionaries are not optimized for large datasets

Error Types

TreeValidationException      // Base validation error
├── DuplicateNameException   // Duplicate sibling names
├── CyclicReferenceException // Would create cycle
└── InvalidContainmentException // Invalid parent-child relationship
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.5 171 10/10/2025
0.0.4 163 10/7/2025
0.0.3 167 10/6/2025
0.0.2 167 10/6/2025
0.0.1 184 9/29/2025