Skip to main content

Goal

Find walls, doors, rooms, and other elements in a received model graph — and build indexes for QA checks, schedules, and quantity reports.

What you will build

Queries that count elements by category, check required properties, find duplicate applicationId values, and group quantities by level.

When to use this

After Receive2 on connector-published data — before CSV export, version comparison, or sending analysis results back. Read BIM data patterns first if property names are unfamiliar. The recommended approach for most filtering tasks is Flatten() on the root object, then filter on DataObject.properties. Use GraphTraversal with DefaultTraversal only when you need parent context or connector-style walk rules.

Prerequisites

On DataObject instances, semantic fields live in properties: dataObject.properties.GetValueOrDefault("category"). Connector-produced BIM data often uses proxy collections — see Working with Proxies.

Complete example

https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Complete example
using Speckle.Objects.Data;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Extensions;

static string? Category(Base obj) =>
    obj is DataObject d
        ? d.properties.GetValueOrDefault("category")?.ToString()
        : obj["category"] as string;

// Count walls, doors, rooms
var walls = root.Flatten().Where(o => Category(o) == "Walls").ToList();
var doors = root.Flatten().Where(o => Category(o)?.Contains("Door") == true).ToList();
var rooms = root.Flatten().Where(o => Category(o)?.Contains("Room") == true).ToList();

Console.WriteLine($"Walls: {walls.Count}, Doors: {doors.Count}, Rooms: {rooms.Count}");

// Quantities by category
var byCategory = root.Flatten()
    .GroupBy(o => Category(o) ?? "Unknown")
    .ToDictionary(g => g.Key, g => g.Count());

// Quantities by level
var byLevel = root.Flatten()
    .OfType<DataObject>()
    .GroupBy(d => d.properties.GetValueOrDefault("level")?.ToString() ?? "Unknown")
    .ToDictionary(g => g.Key, g => g.Count());

// Required property check
var required = new[] { "category", "level" };
var missing = root.Flatten()
    .OfType<DataObject>()
    .Where(d => required.Any(k => !d.properties.ContainsKey(k)))
    .ToList();

// Duplicate applicationId check
var duplicateIds = root.Flatten()
    .Where(o => o.applicationId is not null)
    .GroupBy(o => o.applicationId!)
    .Where(g => g.Count() > 1)
    .Select(g => g.Key)
    .ToList();

How it works

Flatten vs GraphTraversal

Flatten returns every Base in the graph — the right default for filtering and indexing:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
foreach (var obj in root.Flatten())
    Console.WriteLine(obj.TryGetName() ?? "unnamed");
GraphTraversal adds context (parent, property name) and custom walk rules:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
using Speckle.Sdk.Models.GraphTraversal;

var traversal = DefaultTraversal.CreateTraversalFunc();
foreach (TraversalContext ctx in traversal.Traverse(root))
{
    var obj = ctx.Current;
    var parentName = ctx.Parent?.Current.TryGetName() ?? "root";
    Console.WriteLine($"{obj.TryGetName()} (from {parentName}.{ctx.PropName})");
}
DefaultTraversal.CreateTraversalFunc() is what Rhino, Grasshopper, Revit, AutoCAD, Tekla, and Navisworks use for receive. Prefer it unless you need to walk different properties.

Building indexes for repeated lookups

Each Flatten() is O(n). Build a dictionary once for multiple lookups:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
var byCategory = root.Flatten()
    .GroupBy(Category)
    .ToDictionary(g => g.Key ?? "Unknown", g => g.ToList());

var walls = byCategory.GetValueOrDefault("Walls", new());
An applicationId index resolves proxies:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
var byAppId = root.Flatten()
    .Where(o => o.applicationId is not null)
    .ToDictionary(o => o.applicationId!);

Common mistakes

MistakeFix
Hand-written recursion on large graphsUse Flatten or GraphTraversal — they guard against cycles
Rebuilding an index inside a loopBuild the dictionary once outside your query loop
Reading obj["category"] on all connector dataUse DataObject.properties
new GraphTraversal() with no rulesAlways pass at least one rule, or use DefaultTraversal
Duplicate results from displayValueExclude displayValue in custom traversal rules when extracting BIM properties separately

Next steps

Export model data to CSV

Door and room schedules

Working with Proxies

Resolve levels and groups

Compare two model versions

Diff between publishes
Last modified on July 10, 2026