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 connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.