MeshWeaver enforces strict data access patterns to ensure security (RLS validation), consistency, and traceability. Application code must never use IMeshStorage or IMeshCatalog directly β€” these are internal infrastructure interfaces.

There are three access patterns, each covering a distinct class of operation:

Operation Pattern Interface / Type
Read nodes Query IMeshQuery
Read/write one node by path Stream hub.GetMeshNodeStream(path) / workspace.GetMeshNodeStream(path)
Create node Service meshService.CreateNode(node).Subscribe(...)
Update node Service meshService.UpdateNode(node).Subscribe(...) (routes through GetMeshNodeStream(path).Update)
Create-or-update (upsert) Request hub.Observe<CreateOrUpdateNodeResponse>(new CreateOrUpdateNodeRequest(node)).Subscribe(...)
Delete node Service meshService.DeleteNode(path).Subscribe(...)
Write as system / hub AccessService using (accessService.ImpersonateAsSystem()) { … } / ImpersonateAsHub(hub)
Move node Message hub.Observe(new MoveNodeRequest(src, dst)).Subscribe(...)
Update typed-entity data Message DataChangeRequest β†’ hub (EntityStore collections β€” see CRUD)

🚨 One mesh node by path β†’ GetMeshNodeStream, never GetRemoteStream<MeshNode>

The single canonical API for reading or writing one mesh node by path is hub.GetMeshNodeStream(path) / workspace.GetMeshNodeStream(path) (extension methods in MeshWeaver.Mesh.Contract). It routes every reader and writer through the shared IMeshNodeStreamCache β€” one process-wide upstream per path, so writes are visible to all readers. Read by subscribing to the handle (IObservable<MeshNode>, Content already typed); write via .Update(current => current with { … }).Subscribe(...) (cold observable β€” the write only runs on Subscribe).

workspace.GetRemoteStream<MeshNode, …> / GetRemoteStream<MeshNode>(addr) throws InvalidOperationException β€” the single-node remote reduce does not converge (divergent mirror streams, writes invisible to readers), so Workspace.ThrowIfMeshNode refuses it at the call site rather than letting a latent bug ship. The only sanctioned callers are the cache's own upstream and the MeshNode reduce-callback plumbing, which use the internal GetRemoteStreamUnchecked overload.


IMeshQuery Reads / Queries IMeshService Create Β· Update Β· Delete Message Bus Move Β· DataChange Security & Validation Layer INodeValidator Β· RLS policies Β· AccessContext Β· audit trail Storage Backend IMeshStorage Β· IMeshCatalog (internal β€” not accessible from app code)

Three access patterns funnel through the security layer before reaching storage β€” direct storage access bypasses RLS and auditing.

1. Reads β€” IMeshQuery

All read operations go through the query surface β€” IMeshQuery.Query<T>(MeshQueryRequest) (or the same-shaped IMeshService.Query<T>(...) extensions) β€” which uses a GitHub-style query syntax to filter, search, and enumerate nodes. It is reactive: the first emission carries the full initial result set (ChangeType == Initial), later emissions carry Added / Updated / Removed deltas.

🚨 There is no QueryAsync on the interface. QueryAsync survives only as a test-only bridge (MeshWeaver.Fixture/IMeshQueryTestExtensions) that materialises the Initial emission as IAsyncEnumerable. Production code composes IObservable<T> end-to-end β€” see Asynchronous Calls. And for a live collection, don't call the query surface directly at all: use workspace.GetQuery(id, queries…) (Synced Mesh Node Queries).

// Resolve from the hub's service provider
var query = hub.ServiceProvider.GetRequiredService<IMeshQuery>();

// List direct children of a path β€” project to the fields you consume.
query.Query<MeshNode>(MeshQueryRequest.FromQuery(
        "path:org/Acme scope:children select:path,name,nodeType"))
    .Take(1)
    .Subscribe(change => { /* change.Items */ },
               ex => logger.LogWarning(ex, "query failed"));

// Search by text within a namespace, capped.
query.Query<MeshNode>(MeshQueryRequest.FromQuery(
        "namespace:org nodeType:Team Report limit:10"))
    .Subscribe(change => { /* change.Items */ },
               ex => logger.LogWarning(ex, "query failed"));

Reserved qualifiers (QueryParser.ReservedQualifiers) β€” everything else in the string is either a property filter (field:value, with >/</>=/<=, * wildcards, A|B|C alternation and - negation) or free text (routed to vector search on Postgres when an embedding provider is registered):

Token Meaning
path:<path> Anchor path (combine with scope: to walk)
namespace:<path> Anchor namespace; implies scope:children unless scope: is given
scope:<exact\|children\|descendants\|subtree\|ancestors\|selfAndAncestors\|hierarchy\|nextLevel> How to walk from the anchor
select:<fields> Column projection β€” content is loaded only if named
sort:<field>[-desc] Β· limit:<n> Ordering and cap
source:<activity\|accessed> Β· context:<ctx> Β· is:main Result source / visibility filters

Filter by node type with the ordinary property filter nodeType:Team β€” there is no type: or parent: qualifier. Tokens compose freely: "path:org scope:descendants nodeType:Team Alpha" matches Team nodes under org scored against the text "Alpha".


2. Creates, Updates, and Deletes β€” IMeshService (reactive)

Node lifecycle operations route through IMeshService and return IObservable<MeshNode> β€” cold observables that run on Subscribe. All operations travel through the message bus (CreateNodeRequest etc.) so that security validators (INodeValidator, RLS) are enforced for every write. The caller''s identity is captured automatically from AccessService.Context at call time.

var meshService = hub.ServiceProvider.GetRequiredService<IMeshService>();

// Create β€” identity auto-captured from the current user
var node = MeshNode.FromPath("org/Acme/NewTeam") with
{
    Name = "New Team",
    NodeType = "Team"
};
meshService.CreateNode(node)
    .Subscribe(
        created => logger.LogInformation("Created {Path}", created.Path),
        ex => logger.LogWarning(ex, "Create failed"));

// Chained create β†’ update: compose with SelectMany β€” never nest Subscribes
meshService.CreateNode(node)
    .SelectMany(created => meshService.UpdateNode(created with { Name = "Renamed Team" }))
    .Subscribe(_ => { }, ex => logger.LogWarning(ex, "create+update failed"));

// Delete β€” removes the node and all its descendants, bottom to top
meshService.DeleteNode("org/Acme/OldTeam").Subscribe(...);

// Create-or-update (upsert) β€” single verb when the caller has the full target shape
hub.Observe<CreateOrUpdateNodeResponse>(new CreateOrUpdateNodeRequest(node))
    .Subscribe(resp => { /* resp.Message.WasCreated */ }, ex => ...);

Behaviour summary:

Writing as system / hub β€” explicit impersonation

By default every write runs under the calling user''s identity. Infrastructure code with no human in the loop (cache hydration, seeds, sync heartbeats) opts in explicitly:

// System identity β€” Permission.All, well-known "system-security" principal
using (accessService.ImpersonateAsSystem())
{
    meshService.CreateNode(systemNode).Subscribe(...);
}

// Hub identity β€” stamps the hub''s address as principal
using (accessService.ImpersonateAsHub(hub))
{
    meshService.CreateNode(hubOwnedNode).Subscribe(...);
}

PostPipeline fails closed when no context is set β€” a write with neither a user nor an explicit impersonation is rejected, never silently stamped. Full reference: AccessContextPropagation.

Reading as hub / system β€” the same explicit impersonation

Reads scope through the same AccessService impersonation: wrap the subscription, and every query / stream opened inside the scope runs under that identity for RLS filtering β€” useful when infrastructure code reads before a user context is established.

// Query with hub identity β€” RLS filters against the hub's own permissions
using (accessService.ImpersonateAsHub(hub))
{
    meshService.Query<MeshNode>(MeshQueryRequest.FromQuery($"path:{nodePath}"))
        .Subscribe(result => { /* … */ }, ex => logger.LogWarning(ex, "query failed"));
}

Example: aggregating across business-unit sub-hubs (FutuRe)

The FutuRe/Analysis group hub needs to read data from two business-unit sub-hubs: FutuRe/AsiaRe/Analysis and FutuRe/EuropeRe/Analysis. Each sub-hub has its own RLS scope, so the parent hub must be granted explicit read access via AccessAssignment nodes, then query using its own identity.

Step 1 β€” grant read access in each sub-hub (see samples/Graph/Data/FutuRe/AsiaRe/Analysis/_Access/FutuRe_Analysis_Access.json):

{
  "id": "FutuRe_Analysis_Access",
  "namespace": "FutuRe/AsiaRe/Analysis/_Access",
  "name": "FutuRe/Analysis Node Access",
  "nodeType": "AccessAssignment",
  "content": {
    "$type": "AccessAssignment",
    "accessObject": "FutuRe/Analysis",
    "displayName": "Group Analysis Hub",
    "roles": [
      { "role": "Viewer" }
    ]
  }
}

Apply the same pattern to FutuRe/EuropeRe/Analysis/_Access/FutuRe_Analysis_Access.json.

🚨 Viewer, and only a role the mesh actually defines. This snippet used to read "Reader" β€” a role that exists nowhere (Role.cs defines Admin, Editor, Viewer, Commenter, PlatformAdmin), so it resolved to no permissions at all while AccessAssignmentGuard's fail-closed allowlist counted it as WRITE and refused the grant on every GitSynced partition. Read-only means Viewer or Commenter; anything else is an ownership claim.

Step 2 β€” query with the hub's identity:

// The parent hub can now read descendants of each sub-hub
using (accessService.ImpersonateAsHub(hub))
{
    meshService.Query<MeshNode>(
            MeshQueryRequest.FromQuery("path:FutuRe/AsiaRe/Analysis scope:descendants"))
        .Subscribe(change => { /* aggregate sub-hub data */ },
                   ex => logger.LogWarning(ex, "aggregation failed"));
}

Without the impersonation scope, the query runs under the end user's identity β€” which may lack access to all sub-hubs. With it, the parent hub reads using its own permissions and can always aggregate across business units.


3. Moves and Data Changes β€” Message-Based

A small number of operations are driven by request messages posted directly to the hub rather than through IMeshService.

Moving a node

hub.Observe(new MoveNodeRequest("org/Acme/OldPath", "org/Acme/NewPath"))
    .Subscribe(
        response =>
        {
            if (response.Message is MoveNodeResponse { Node: not null } moveResult)
            {
                // Move succeeded
            }
        },
        ex => logger.LogWarning(ex, "Move failed"));

Updating data collections (typed entities)

// Replace a set of entities in an EntityStore collection β€” see /Doc/DataMesh/CRUD
hub.Post(new DataChangeRequest
{
    Updates = [updatedEntity]
});

Updating a MeshNode's content

// The ONE mutation API β€” cold observable, the trailing Subscribe runs the write.
workspace.GetMeshNodeStream(path).Update(node => node with { Content = updated })
    .Subscribe(_ => { }, ex => logger.LogWarning(ex, "update failed"));

(PatchDataChangeRequest is the internal stream-protocol message the framework ships for you β€” never post it from application code.)


Defining Message Types

Request/response messages must implement IRequest<TResponse> so the messaging framework can route responses correctly and hub.Observe can infer the response type.

// Request β€” implements IRequest<TResponse> for type-safe hub.Observe
public record MoveNodeRequest(string SourcePath, string TargetPath)
    : IRequest<MoveNodeResponse>;

// Response
public record MoveNodeResponse
{
    public bool Success { get; init; }
    public MeshNode? Node { get; init; }
    public string? Error { get; init; }
}

Sending and observing:

// TResponse is inferred from IRequest<MoveNodeResponse>
hub.Observe(new MoveNodeRequest("old/path", "new/path"),
        o => o.WithTarget(targetAddress))
    .Subscribe(
        response => { if (response.Message.Success) { ... } },
        ex => logger.LogWarning(ex, "Move failed"));

// Tests bridge to Task via MonolithMeshTestBase.AwaitResponseAsync(request, ...).

Registering the handler:

// 🚨 Synchronous handler β€” never `async`, never `await` (it deadlocks the hub's
// single-threaded action block). Long work composes reactively and posts the
// response from inside the Subscribe callback; the handler returns immediately.
config.WithHandler<MoveNodeRequest>((hub, delivery) =>
{
    // process the request...
    hub.Post(new MoveNodeResponse { Success = true, Node = moved },
        o => o.ResponseFor(delivery));
    return delivery.Processed();
});

Registering types for serialization:

config.TypeRegistry.WithType(typeof(MoveNodeRequest), nameof(MoveNodeRequest));
config.TypeRegistry.WithType(typeof(MoveNodeResponse), nameof(MoveNodeResponse));

Why Are the Internal Interfaces Off-Limits?

IMeshStorage and IMeshCatalog are internal to infrastructure assemblies. Using them directly from application code:

  1. Bypasses RLS validation β€” security policies are enforced at the message handler layer, not in storage.
  2. Breaks traceability β€” message-based operations are logged and auditable; direct storage calls are not.
  3. Couples to storage details β€” backends (Cosmos, PostgreSQL, file system) are an implementation detail and may change.
  4. Skips business rules β€” INodeValidator runs at the handler level on every create, update, move, and delete.

Only infrastructure assemblies (MeshWeaver.Hosting, MeshWeaver.Hosting.Orleans, etc.) have InternalsVisibleTo access to these interfaces.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.