This guide walks through how to define data models in MeshWeaver: from basic C# record definitions and attribute annotations to rich reference data, content initialization, and hub-level configuration.
Overview
MeshWeaver data models are plain C# records that layer three concerns:
| Layer | Mechanism | Purpose |
|---|---|---|
| Shape | Record properties + CLR types | Store and transport data |
| Behavior | Attributes ([Key], [Required], [Markdown], …) |
Drive validation, UI, and persistence |
| Lookup values | Static reference-data records | Provide dropdowns, ordering, and rich metadata |
The diagram below shows how a Todo entity references Category and Priority dimension data, both hosted in the same or a parent hub.
The three modeling layers: record properties carry shape, attributes drive framework behavior, and static reference-data records supply lookup values via [Dimension<T>].
C# Records
Records are the idiomatic carrier for MeshWeaver data models. Compared to classes, they offer:
- Immutability by default —
init-only properties prevent accidental mutation. - Value-based equality — two records with identical property values are equal.
withexpressions — create modified copies without touching the original.
// Define a record
public record Person { public string Name { get; init; } }
// Create an instance
var alice = new Person { Name = "Alice" };
// Create a modified copy — original is unchanged
var bob = alice with { Name = "Bob" };
Controls.Markdown($"`alice` is still **{alice.Name}**; the copy `bob` is **{bob.Name}**.")
These guarantees simplify change tracking and make records a natural fit for MeshWeaver's reactive, stream-based data layer.
Attributes
Attributes are metadata annotations ([...]) that MeshWeaver reads at runtime to drive validation, UI rendering, and persistence decisions. They do not change what the C# code computes — they describe how the framework should treat a property.
[Key] // Marks as primary key
[Required] // Validates non-empty value before save
[DisplayName("Due Date")] // Sets UI label
public DateTime? DueDate { get; init; }
Standard CLR Types
| Type | Description | Example values |
|---|---|---|
string |
Text | "Hello", "ACME" |
int |
Whole numbers | 1, 42, -5 |
decimal |
Precise decimals | 19.99m, 0.001m |
bool |
True/false | true, false |
DateTime |
Date and time | DateTime.UtcNow |
Guid |
Unique identifiers | Guid.NewGuid() |
Append ? to make any type nullable (e.g., int?, DateTime?) when the value may be absent.
Reference Data Pattern
Reference data — statuses, categories, priorities — benefits from carrying rich metadata that a plain enum cannot express: display names, icons, sort order, and per-value UI hints. MeshWeaver models these as record types with static instances.
Enum (limited):
public enum Priority { Low, Medium, High }
Reference data record (rich):
public record Priority
{
public string Id { get; init; }
public string Name { get; init; }
public string Emoji { get; init; } // Display icon
public int Order { get; init; } // Sort position
public static readonly Priority High = new() { Id = "High", Name = "High Priority", Emoji = "🔥", Order = 0 };
public static readonly Priority[] All = [High, Medium, Low];
}
This pattern lets every consumer — dropdowns, groupings, data grids — share the same display contract without bespoke mapping code.
Basic Record Structure
Example: Simple Entity
public record Todo
{
[Key]
[Browsable(false)]
public string Id { get; init; } = string.Empty;
[Required]
public string Title { get; init; } = string.Empty;
[Markdown(EditorHeight = "200px", ShowPreview = false)]
public string? Description { get; init; }
[DisplayName("Due Date")]
public DateTime? DueDate { get; init; }
public TodoStatus Status { get; init; } = TodoStatus.Pending;
}
How the editor renders each field:
| Field | Rendered as |
|---|---|
Id |
Hidden (Browsable=false) |
Title |
Required text input with validation message |
Description |
Markdown editor, 200 px height, no preview pane |
DueDate |
Date picker labelled "Due Date" |
Status |
Dropdown with enum values |
Key Attribute Reference
[Key]
Marks the primary key. Every data model requires exactly one.
[Key]
public string Id { get; init; } = string.Empty;
[Required]
Prevents saving until the field contains a non-empty value.
[Required]
public string Title { get; init; } = string.Empty;
Attempting to save without a Title shows: "Title is required".
[Browsable(false)]
Hides the field from editor forms and data grids. Use for:
- Internal identifiers users should not edit
- Computed properties
- System-managed timestamps
[Browsable(false)]
public string Id { get; init; } = string.Empty;
[DisplayName]
Overrides the property name in all UI surfaces.
[DisplayName("Due Date")]
public DateTime? DueDate { get; init; }
[DisplayName("Created At")]
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
[Markdown]
Renders a markdown editor for rich text fields.
[Markdown(EditorHeight = "200px", ShowPreview = false)]
public string? Description { get; init; }
| Parameter | Default | Purpose |
|---|---|---|
EditorHeight |
300px |
Height of the edit pane |
ShowPreview |
true |
Show live preview alongside the editor |
[Dimension<T>]
Links a string field to a reference-data type, rendering a dropdown populated from T.All.
[Dimension<Category>]
public string Category { get; init; } = "General";
[Dimension<Priority>]
public string Priority { get; init; } = "Medium";
Reference Data in Depth
Full Priority Example
public record Priority
{
[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 Priority Critical = new()
{
Id = "Critical", Name = "Critical Priority",
Emoji = "🚨", Order = 0, IsExpandedByDefault = true
};
public static readonly Priority High = new()
{
Id = "High", Name = "High Priority",
Emoji = "🔥", Order = 1, IsExpandedByDefault = true
};
public static readonly Priority Medium = new()
{
Id = "Medium", Name = "Medium Priority",
Emoji = "🟡", Order = 2, IsExpandedByDefault = false
};
public static readonly Priority Low = new()
{
Id = "Low", Name = "Low Priority",
Emoji = "🟢", Order = 3, IsExpandedByDefault = false
};
public static readonly Priority Unset = new()
{
Id = "Unset", Name = "Unset Priority",
Emoji = "❓", Order = 4, IsExpandedByDefault = false
};
// Canonical collection used for data initialization
public static readonly Priority[] All = [Critical, High, Medium, Low, Unset];
// Safe lookup with fallback
public static Priority GetById(string? id) =>
All.FirstOrDefault(p => p.Id == id) ?? Unset;
}
Key Components
| Component | Purpose |
|---|---|
| Static instances | Named constants for type-safe references in code |
All array |
Collection passed to WithInitialData(...) on hub startup |
Order |
Controls dropdown sort order and group-by rendering |
GetById |
Safe lookup returning Unset for unknown keys |
Wiring Reference Data into a Hub
Reference data is seeded when configuring the NodeType:
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))))
When the hub initialises, it populates its data store with every predefined instance from each All array.
Enums vs Reference Data
Choose the right tool for the complexity of your domain.
When to Use Enums
public enum TodoStatus
{
Pending,
InProgress,
InReview,
Completed,
Blocked
}
Good fit when:
- No display metadata needed beyond the member name
- Values are stable and change only with a recompile
- Simple type safety is the goal
Limitations: no icons, no ordering hints, no descriptions.
When to Use Reference Data
public record Status
{
[Key]
public string Id { get; init; } = string.Empty;
[Required]
public string Name { get; init; } = string.Empty;
public string? Description { get; init; }
public int Order { get; init; }
public static readonly Status Planning = new()
{
Id = "Planning", Name = "Planning",
Description = "Project is in planning phase", Order = 1
};
// ... additional instances
public static IEnumerable<Status> All => new[] { Planning, Active, OnHold, Completed };
}
Good fit when:
- Values carry descriptions, icons, or colors
- Ordering varies by context
- Values are shared across hub boundaries
- Per-value UI customisation (e.g.,
IsExpandedByDefault) is needed
Content Initialization
Implement IContentInitializable to transform a record when it is loaded from storage. The common use case is converting stored relative offsets into absolute dates for demo or template data.
public record Todo : IContentInitializable
{
[Key]
[Browsable(false)]
public string Id { get; init; } = string.Empty;
[Required]
public string Title { get; init; } = string.Empty;
[DisplayName("Due Date")]
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;
}
}
JSON stored in the mesh:
{
"id": "ReviewDocs",
"title": "Review documentation",
"dueDateOffsetDays": 3
}
Record after Initialize() runs (today = 2026-01-29):
{
"id": "ReviewDocs",
"title": "Review documentation",
"dueDate": "2026-02-01T00:00:00Z",
"dueDateOffsetDays": 3
}
Data Relationships
One-to-Many via [Dimension<T>]
[Dimension<T>] creates a foreign-key-style reference from a string field to a reference-data type.
public record Todo
{
[Dimension<Category>]
public string Category { get; init; } = "General";
}
Data flow between hubs:
Parent-Child via AddHubSource
A child hub can pull reference data from a parent hub using AddHubSource with a computed parent address:
// Todo NodeType 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>()))
Address arithmetic:
| Path | Value |
|---|---|
| Todo hub | ACME/ProductLaunch/Todo/AnalystBriefings |
| Parent hub | ACME/ProductLaunch |
| Formula | Remove last 2 segments |
Complete Example
Project Data Model
public record Project
{
[Key]
[Browsable(false)]
public string Id { get; init; } = string.Empty;
[Required]
public string Name { get; init; } = string.Empty;
[Markdown(EditorHeight = "150px")]
public string? Description { get; init; }
[Dimension<Status>]
public string Status { get; init; } = "Planning";
[DisplayName("Target Date")]
public DateTime? TargetDate { get; init; }
}
Project NodeType Configuration
{
"id": "Project",
"namespace": "ACME",
"nodeType": "NodeType",
"content": {
"$type": "NodeTypeDefinition",
"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)))).AddDefaultLayoutAreas()"
}
}
Todo Data Model (Full)
public record Todo : IContentInitializable
{
[Key]
[Browsable(false)]
public string Id { get; init; } = string.Empty;
[Required]
public string Title { get; init; } = string.Empty;
[Markdown(EditorHeight = "200px", ShowPreview = false)]
public string? Description { get; init; }
[Dimension<Category>]
public string Category { get; init; } = "General";
[Dimension<Priority>]
public string Priority { get; init; } = "Medium";
public string? Assignee { get; init; }
[DisplayName("Due Date")]
public DateTime? DueDate { get; init; }
[Browsable(false)]
public int? DueDateOffsetDays { get; init; }
public TodoStatus Status { get; init; } = TodoStatus.Pending;
public object Initialize()
{
if (DueDateOffsetDays.HasValue)
return this with { DueDate = DateTime.UtcNow.Date.AddDays(DueDateOffsetDays.Value) };
return this;
}
}
Todo NodeType Configuration
{
"id": "Todo",
"namespace": "ACME/Project",
"nodeType": "NodeType",
"content": {
"$type": "NodeTypeDefinition",
"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()"
}
}
Attribute Quick Reference
The cell below renders a live summary of the attributes covered in this guide.
MeshWeaver.Layout.Controls.Stack
.WithView(MeshWeaver.Layout.Controls.Markdown("""
## Common Data Modeling Attributes
| Attribute | Applied to | Effect |
|---|---|---|
| `[Key]` | One property per record | Marks the primary key |
| `[Required]` | Any property | Validates non-empty before save |
| `[Browsable(false)]` | Any property | Hides from forms and grids |
| `[DisplayName("…")]` | Any property | Overrides the UI label |
| `[Markdown(…)]` | `string?` properties | Renders a markdown editor |
| `[Dimension<T>]` | `string` FK properties | Dropdown from `T.All` |
> **Tip:** Combine `[Key]` with `[Browsable(false)]` on your `Id` field so the primary key is managed by the framework but never shown to end users.
"""))
Best Practices
Use records, not classes. Records give you immutability and value semantics for free.
Always provide sensible defaults to avoid null surprises at runtime:
public string Status { get; init; } = "Pending";Hide internal fields with
[Browsable(false)]— IDs, system timestamps, and computed properties should not appear in editor forms.Prefer reference data over enums whenever values need metadata, ordering, or UI customisation.
Define
Orderexplicitly in reference data so dropdown and grouping order is deterministic and independent of declaration order.Always include a
GetByIdfallback to handle unknown or legacy values gracefully:public static Priority GetById(string? id) => All.FirstOrDefault(p => p.Id == id) ?? Unset;Document complex fields with XML comments:
/// <summary> /// Offset in days from today used to compute DueDate at load time. /// </summary> public int? DueDateOffsetDays { get; init; }Prefer
CategoryoverCategoryIdwhen naming[Dimension<T>]properties — the string stores the key, so theIdsuffix is redundant and clutters the UI label.