Creating Node Types

MeshWeaver's NodeType system lets you define richly typed data models — complete with custom UI, reference lookups, and CSV-backed data — entirely from source files that live alongside your data. This guide walks you through building a complete NodeType from scratch.


How a NodeType Is Structured

Every NodeType lives in its own namespace. Source code goes in the Source/ namespace and xUnit tests in Test/. The JSON definition file sits at the same level as the namespace it describes:

Source/ Namespace Content Record Reference Data Layout Areas CSV Data Loader Unit Tests NodeType JSON Project.json Runtime NodeType WithContentType<T>() AddData() + WithType<T>() AddLayout() + Views WithInitialData(loader) AddHubSource() (children) defines wires

Source files in Source/ are compiled at startup; the JSON definition wires them into the running NodeType configuration.

samples/Graph/Data/
  ACME/
    Project.json              # NodeType definition (nodeType: "NodeType")
    Project/
      Source/                 # C# code compiled at startup
        Project.cs            # Content record
        Status.cs             # Reference data type
        Category.cs           # Reference data type
        Priority.cs           # Reference data type
        ProjectLayoutAreas.cs # Custom layout areas
      Test/                   # xUnit tests
        ProjectTests.cs
      Todo.json               # Child NodeType definition
      Todo/
        Source/
          Todo.cs             # Child content record
          TodoLayoutAreas.cs  # Child layout areas

Key idea: the Source/ namespace is compiled at startup. Every .cs Code node you put there becomes live code — content types, dimension types, layout areas, and data loaders all coexist in this single namespace.


Step 1: Define the Content Type

The content type is a C# record that describes the fields of one node instance. Place it in Source/ with a <meshweaver> frontmatter comment so the compiler knows its identity:

// <meshweaver>
// Id: Project
// DisplayName: Project Data Model
// </meshweaver>

using MeshWeaver.Domain;

public record Project
{
    [Required]
    [MeshNodeProperty(nameof(MeshNode.Name))]
    public string Name { get; init; } = string.Empty;

    public string? Description { get; init; }

    public ProjectStatus Status { get; init; } = ProjectStatus.Active;

    [MeshNodeProperty(nameof(MeshNode.Icon))]
    public string Icon { get; init; } = "Folder";

    public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;

    public DateTimeOffset? TargetDate { get; init; }
}

public enum ProjectStatus
{
    Planning, Active, OnHold, Completed, Cancelled
}

Available Attributes

Attribute Purpose
[Key] Primary identifier field
[Required] Validation — field must be set
[MeshNodeProperty(nameof(MeshNode.Name))] Maps the field to the MeshNode's Name property
[MeshNodeProperty(nameof(MeshNode.Icon))] Maps the field to the MeshNode's Icon property
[MeshNode("nodeType:ACME/Category")] References another mesh node — renders a MeshNodePicker, stores the node's PATH. The query always uses the full path of the referenced NodeType (see Data Cubes)
[Dimension<Category>] References an in-hub lookup / dimension type seeded via WithType<T>(t => t.WithInitialData(...))
[Markdown(EditorHeight = "200px")] Renders a rich text editor for this field
[UiControl(Style = "width: 200px;")] Controls form layout width
[Browsable(false)] Hides the field from all UI
[DisplayName("Due Date")] Custom label in generated forms

Content Type with Dimensions

When a child type needs to reference lookup data from its parent, implement IContentInitializable to resolve dynamic defaults at creation time:

// <meshweaver>
// Id: Todo
// DisplayName: Todo Data Model
// </meshweaver>

using MeshWeaver.Domain;

public record Todo : IContentInitializable
{
    [Required]
    [UiControl(Style = "width: 100%;")]
    [MeshNodeProperty(nameof(MeshNode.Name))]
    public string Title { get; init; } = string.Empty;

    [Markdown(EditorHeight = "200px", ShowPreview = false)]
    public string? Description { get; init; }

    [Dimension<Category>]
    [UiControl(Style = "width: 200px;")]
    public string Category { get; init; } = "General";

    [Dimension<Priority>]
    [UiControl(Style = "width: 150px;")]
    public string Priority { get; init; } = "Medium";

    [Dimension<Status>]
    [UiControl(Style = "width: 150px;")]
    public string Status { get; init; } = "Pending";

    public string? Assignee { get; init; }

    public DateTime? DueDate { get; init; }

    [Browsable(false)]
    public int? DueDateOffsetDays { get; init; }

    public object Initialize()
    {
        if (DueDateOffsetDays.HasValue)
            return this with { DueDate = DateTime.UtcNow.Date.AddDays(DueDateOffsetDays.Value) };
        return this;
    }
}

Step 2: Define Reference Data Types

Reference data types supply the dropdown values for [Dimension<T>] fields. They follow the same <meshweaver> frontmatter convention and expose a static All array so the NodeType configuration can seed them at startup:

// <meshweaver>
// Id: Status
// DisplayName: Project Status Data Model
// </meshweaver>

public record Status
{
    [Key]
    public string Id { get; init; } = string.Empty;

    [Required]
    public string Name { get; init; } = string.Empty;

    public string Emoji { get; init; } = string.Empty;

    public int Order { get; init; }

    public bool IsExpandedByDefault { get; init; } = true;

    public static readonly Status Pending = new()
    {
        Id = "Pending", Name = "Pending", Emoji = "⏳", Order = 0
    };

    public static readonly Status InProgress = new()
    {
        Id = "InProgress", Name = "In Progress", Emoji = "🔄", Order = 1
    };

    public static readonly Status Completed = new()
    {
        Id = "Completed", Name = "Completed", Emoji = "✅", Order = 4,
        IsExpandedByDefault = false
    };

    public static readonly Status[] All = [Pending, InProgress, Completed];

    public static Status GetById(string? id) =>
        All.FirstOrDefault(s => s.Id == id) ?? Pending;
}

Step 3: Create the NodeType Definition (JSON)

The JSON file in the parent namespace wires everything together. The configuration field holds a C# lambda expression that is compiled and executed at startup:

{
  "id": "Project",
  "namespace": "ACME",
  "name": "Project",
  "nodeType": "NodeType",
  "category": "Types",
  "description": "A project containing tasks and deliverables",
  "icon": "/api/content/ACME/Project/icon.svg",
  "isPersistent": true,
  "content": {
    "$type": "NodeTypeDefinition",
    "namespace": "ACME",
    "displayName": "Project",
    "description": "A project containing tasks and deliverables",
    "configuration": "config => config
      .WithContentType<Project>()
      .AddData(data => data
        .AddSource(source => source
          .WithType<Status>(t => t.WithInitialData(Status.All))
          .WithType<Category>(t => t.WithInitialData(Category.All))
          .WithType<Priority>(t => t.WithInitialData(Priority.All))))
      .AddLayout(layout => layout
        .AddLayoutAreaCatalog()
        .AddProjectLayoutAreas()
        .WithDefaultArea(\"LayoutAreas\"))"
  }
}

What each builder call does

Builder call Purpose
WithContentType<T>() Registers the content record for the editor form
AddData(...) Configures the MeshDataSource with reference data and virtual types
AddSource(source => source.WithType<T>(...)) Registers types in the data source
WithInitialData(IEnumerable<T> items) Seeds reference data from a static collection
WithInitialData(Func<IObservable<IEnumerable<T>>>) Seeds data from a reactive loader (e.g., CSV — see Step 4). There is no Task-returning overload
AddLayout(...) Configures views and layout areas

Step 4: Loading Data from CSV Files

When data comes from CSV files rather than static arrays — as in the Northwind sample — define a loader in Source/ and wire it up with WithInitialData.

Define the Type

// <meshweaver>
// Id: Product
// DisplayName: Product
// </meshweaver>

using MeshWeaver.Domain;

public record Product : INamed
{
    [Key]
    public int ProductId { get; init; }

    public string ProductName { get; init; } = string.Empty;

    [Dimension(typeof(Supplier))]
    public int SupplierId { get; init; }

    [Dimension(typeof(Category))]
    public int CategoryId { get; init; }

    public double UnitPrice { get; init; }

    public short UnitsInStock { get; init; }

    string INamed.DisplayName => ProductName;
}

Create the CSV Loader

Loaders are reactive — never async/await/Task<T>/Task.FromResult. Return IObservable<IEnumerable<T>> (the shape WithInitialData takes) and run the blocking CSV read + parse on the bounded FileSystem I/O pool via InvokeBlocking, so the file read never executes on the configuring hub's thread:

// <meshweaver>
// Id: DataLoader
// DisplayName: Data Loader
// </meshweaver>

using System.Globalization;
using MeshWeaver.Messaging;
using MeshWeaver.Mesh.Threading;
using Microsoft.Extensions.DependencyInjection;

public static class DataLoader
{
    private static readonly string BasePath =
        Path.Combine("../../samples/Graph/attachments/MyData");

    private static IIoPool FileSystemPool(IMessageHub hub) =>
        hub.ServiceProvider.GetService<IoPoolRegistry>()?.Get(IoPoolNames.FileSystem)
        ?? IoPool.Unbounded;

    public static IObservable<IEnumerable<Product>> LoadProducts(IMessageHub hub)
        // The .ToList() INSIDE the pool slot matters: ParseCsv is lazy, and
        // without it the parse would run later on whatever thread enumerates.
        => FileSystemPool(hub).InvokeBlocking(_ =>
            (IEnumerable<Product>)ParseCsv(
                File.ReadAllLines(Path.Combine(BasePath, "products.csv")),
                parts => new Product
                {
                    ProductId = int.Parse(parts[0]),
                    ProductName = parts[1],
                    SupplierId = int.Parse(parts[2]),
                    CategoryId = int.Parse(parts[3]),
                    UnitPrice = double.Parse(parts[4], CultureInfo.InvariantCulture),
                    UnitsInStock = short.Parse(parts[5]),
                }).ToList());

    private static IEnumerable<T> ParseCsv<T>(
        string[] lines, Func<string[], T> factory)
    {
        foreach (var line in lines.Skip(1))
        {
            if (string.IsNullOrWhiteSpace(line)) continue;
            yield return factory(line.Split(','));
        }
    }
}

Wire Up in the NodeType Configuration

{
  "content": {
    "$type": "NodeTypeDefinition",
    "configuration": "config => config
      .WithContentType<CatalogContent>()
      .AddContentCollection(sp => new ContentCollectionConfig {
        SourceType = FileSystemStreamProvider.SourceType,
        Name = \"Data\",
        BasePath = \"../../samples/Graph/attachments/MyData\",
        DisplayName = \"Data Files\"
      })
      .AddData(data => data
        .AddSource(source => {
          var hub = source.Workspace.Hub;
          return source
            .WithType<Category>(t => t.WithInitialData(() => DataLoader.LoadCategories(hub)))
            .WithType<Product>(t => t.WithInitialData(() => DataLoader.LoadProducts(hub)))
            .WithType<Order>(t => t.WithInitialData(() => DataLoader.LoadOrders(hub)));
        }))
      .AddDefaultLayoutAreas()
      .AddLayout(layout => layout.WithDefaultArea(\"LayoutAreas\"))"
  }
}

Key points for CSV data:


Step 5: Create Layout Areas

Layout areas define what users see when they open a node. Register them as extension methods on MessageHubConfiguration so the NodeType configuration lambda can call AddProjectLayoutAreas():

// <meshweaver>
// Id: ProjectLayoutAreas
// DisplayName: Project Layout Areas
// </meshweaver>

using MeshWeaver.Layout;
using MeshWeaver.Layout.Composition;
using MeshWeaver.Messaging;

public static class ProjectLayoutAreas
{
    public static MessageHubConfiguration AddProjectLayoutAreas(
        this MessageHubConfiguration config)
        => config.AddLayout(layout => layout
            .WithView("Dashboard", Dashboard));

    public static IObservable<UiControl?> Dashboard(
        LayoutAreaHost host, RenderingContext _)
    {
        return host.Workspace.GetObservable<Status>()
            .CombineLatest(
                host.Workspace.GetObservable<Todo>(),
                (statuses, todos) =>
                {
                    // Build view from live data
                    return Controls.Stack
                        .WithView(Controls.Html("<h2>Dashboard</h2>"));
                });
    }
}

Child NodeType Definitions

Child types are defined in subfolders and follow the same pattern. The key difference is AddHubSource, which imports reference types from the parent node's data source — so child instances automatically share the parent's lookup data without re-declaring it:

{
  "id": "Todo",
  "namespace": "ACME/Project",
  "name": "Task",
  "nodeType": "NodeType",
  "content": {
    "$type": "NodeTypeDefinition",
    "namespace": "ACME/Project",
    "displayName": "Task",
    "configuration": "config => config
      .WithContentType<Todo>()
      .AddData(data => data
        .AddHubSource(
          new Address(config.Address.Segments.Take(
            config.Address.Segments.Length - 2).ToArray()),
          source => source
            .WithType<Status>()
            .WithType<Category>()
            .WithType<Priority>()))
      .AddDefaultLayoutAreas()"
  }
}

AddHubSource(parentAddress, ...) opens a live subscription to the parent node's data source, so any updates to the parent's reference data are immediately visible to child instances.


Live Example: Attribute Reference

The table below summarises which attribute to reach for at each stage of model definition. It is rendered live from a small in-kernel data table:

var rows = new[]
{
    ("[Key]",                                       "Record identity", "Primary key field for dimension types"),
    ("[Required]",                                  "Validation",      "Field must be non-null / non-empty"),
    ("[MeshNodeProperty(nameof(MeshNode.Name))]",   "Node mapping",    "Binds the field to the MeshNode Name shown in navigation"),
    ("[MeshNodeProperty(nameof(MeshNode.Icon))]",   "Node mapping",    "Binds the field to the MeshNode Icon"),
    ("[Dimension&lt;T&gt;]",                        "Relationships",   "Declares a lookup relationship to dimension type T"),
    ("[Dimension(typeof(T))]",                      "Relationships",   "Alternative syntax for non-generic dimension reference"),
    ("[Markdown(...)]",                             "Editor control",  "Renders a rich Markdown editor for the field"),
    ("[UiControl(Style = &quot;...&quot;)]",        "Layout",          "Applies inline CSS to the form control"),
    ("[Browsable(false)]",                          "Visibility",      "Excludes the field from all generated UI"),
    ("[DisplayName(&quot;...&quot;)]",              "Labels",          "Custom label in generated forms"),
};

var bodyRows = string.Join("", rows.Select((r, i) =>
{
    var bg = i % 2 == 0 ? "#f9f9f9" : "white";
    return $"<tr style='background:{bg}'>" +
           $"<td style='padding:5px 10px;font-family:monospace;font-size:0.85em'>{r.Item1}</td>" +
           $"<td style='padding:5px 10px'>{r.Item2}</td>" +
           $"<td style='padding:5px 10px'>{r.Item3}</td>" +
           "</tr>";
}));

MeshWeaver.Layout.Controls.Stack
    .WithView(MeshWeaver.Layout.Controls.Markdown("### Content Type Attribute Reference"))
    .WithView(MeshWeaver.Layout.Controls.Html(
        "<table style='width:100%;border-collapse:collapse'>" +
        "<thead><tr>" +
        "<th style='text-align:left;padding:6px 10px;border-bottom:2px solid #ccc'>Attribute</th>" +
        "<th style='text-align:left;padding:6px 10px;border-bottom:2px solid #ccc'>Category</th>" +
        "<th style='text-align:left;padding:6px 10px;border-bottom:2px solid #ccc'>Effect</th>" +
        "</tr></thead><tbody>" +
        bodyRows +
        "</tbody></table>"
    ))

Summary

Here is the complete checklist for a new NodeType:

Step What to create Where
1 Content record (Project.cs) Source/
2 Reference data types (Status.cs, Category.cs, …) Source/
3 CSV data loaders (optional) Source/DataLoader.cs
4 Layout areas Source/ProjectLayoutAreas.cs
5 NodeType JSON definition Project.json in the parent folder
6 Unit tests Test/ProjectTests.cs
7 CSV data files (optional) attachments/ folder
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.