# Revitlookup Descriptor Model

> Teach RevitLookup to inspect a new Revit type by adding a LookupEngine descriptor and registering it in the type map, giving the object resolved members, synthetic extensions, and context-menu actions. USE FOR: creating a descriptor class, resolving members that need arguments or the active view, disabling or marking members unsupported, registering synthetic extension members, redirecting one type to another, adding context-menu commands that touch Revit through an external event, and wiring the descriptor into DescriptorsMap. DO NOT USE FOR: building a UI page or view model (use revitlookup-ui-development), or changing the LookupEngine engine itself in the submodule.

- Skill: `lookup-foundation/revitlookup-descriptor-model` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lookup-foundation/revitlookup-descriptor-model`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lookup-foundation/revitlookup-descriptor-model/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: lookup-foundation (https://skillmd.com/u/lookup-foundation)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lookup-foundation/revitlookup-descriptor-model

---


# RevitLookup Descriptor Model

RevitLookup decomposes any object through LookupEngine, and a descriptor is how it teaches the engine about one Revit type.
Add support for a type by writing a descriptor and adding one `case` to the type map — never by special-casing the type anywhere else.
Descriptors live in `source/RevitLookup/Decomposition/Descriptors/`; the worked reference is `ElementDescriptor.cs`.

## When to use

- Adding or changing how a Revit type decomposes: resolving members, exposing synthetic members, redirecting, or adding context-menu actions.

## When not to use

- Changing the decompose pipeline, the never-throw contract, or the `Variants` API, which live in the `source/LookupEngine` submodule.

## Workflow

### Step 1: Create the descriptor and set its name

Add `XxxDescriptor.cs` deriving from `Descriptor` (`LookupEngine.Abstractions.Decomposition`), or from RevitLookup's `ResolvingDescriptor` when you need its protected `Resolve...` helpers.
Capture the object in the constructor and set `Name` to a human label.

```csharp
public sealed class WallDescriptor : Descriptor
{
    private readonly Wall _wall;

    public WallDescriptor(Wall wall)
    {
        _wall = wall;
        Name = $"{wall.Name}, ID{wall.Id}";
    }
}
```

If the type derived from another type and you want to reuse its constructor, inherit from it:

```csharp
public sealed class FamilyDescriptor(Family family) : ElementDescriptor(family)
{
    //No constructor, the Name policy derived from ElementDescriptor.
}
```

### Step 2: Opt into behavior through the configuration interfaces

Implement only the interfaces the type needs:

- `IDescriptorConfigurator` and `IDescriptorConfigurator<Document>` — `Configure(IMemberConfigurator)` to shape members.
- `IDescriptorRedirector<Document>` — `TryRedirect(string target, Document context, out object result)` hands off to a richer type, as `ElementIdDescriptor` resolves an `ElementId` to its `Element`.
- `IContextMenuConnector` — `RegisterMenu(...)` to add actions in the UI.
- `IDescriptorCollector` — a marker for UI that the object exposes members worth drilling into.

### Step 3: Resolve, disable, and register members in Configure

Use `configuration.Member(nameof(...))` to `Resolve(...)` a member the engine cannot invoke blindly (it needs arguments or the active view), and `configuration.Extension(name)` to `Register(...)` a synthetic member, `.Map(...)` a display name, or mark it `.NotSupported()`.
Build multi-value results with `Variants.Value(...)`, `Variants.Values<T>(capacity).Add(...).Consume()`, or `Variants.Empty<T>()`.
Gate a version-specific member for the Revit version it targets.

```csharp
public void Configure(IMemberConfigurator configuration)
{
    configuration.Member(nameof(Wall.Dispose)).Disable();
    configuration.Member("BoundingBox").Resolve(() => Variants.Values<BoundingBoxXYZ>(2)
        .Add(_wall.get_BoundingBox(null), "Model")
        .Add(_wall.get_BoundingBox(RevitContext.ActiveView), "Active view")
        .Consume());

    configuration.Extension(nameof(JoinGeometryUtils.GetJoinedElements)).Register(() => JoinGeometryUtils.GetJoinedElements(_wall.Document, _wall));
}
```

### Step 4: Call Revit only on the Revit thread

A context-menu command that reads or mutates the model raises an `[ExternalEvent(AllowDirectInvocation = true)]`; never call the Revit API from the UI thread or through `Dispatcher.Invoke`.
Log on a failure path through a source-generated `[LoggerMessage]`.

```csharp
public void RegisterMenu(ContextMenu contextMenu, IServiceProvider serviceProvider)
{
    contextMenu.AddMenuItem("SelectMenuItem")
        .SetCommand(_wall, wall => SelectElementEvent.Raise(wall))
        .SetShortcut(Key.F6);
}

[ExternalEvent(AllowDirectInvocation = true)]
private static void SelectElement(UIApplication application, Element element)
{
    if (application.ActiveUIDocument is null) return;
    application.ActiveUIDocument.Selection.SetElementIds([element.Id]);
}
```

### Step 5: Register the descriptor in DescriptorsMap

Add a `case` to the `FindDescriptor` switch in `source/RevitLookup/Decomposition/DescriptorsMap.cs`, guarding on the exact type; it matches both the approximate (display) and exact (member-resolution) lookups.

```csharp
Wall value when type is null || type == typeof(Wall) => new WallDescriptor(value),
```

Place the case in the matching category block; the default arm stays `_ => new ObjectDescriptor(obj)`.

### Step 6: Verify

Add a test in `tests/RevitLookup.Tests` covering the new resolution, then run the add-in in a `Debug.R##` configuration and decompose an instance of the type inside Revit.

```shell
dotnet test -c Debug.R##
```

## Validation

- [ ] The descriptor lives in `Decomposition/Descriptors/`, captures its object, and sets `Name`.
- [ ] It implements only the configuration interfaces it needs; the resolution context is `Document`.
- [ ] `Configure` resolves members that need arguments or the active view, disables noise, and registers synthetic extensions; version-specific members are `#if`-gated.
- [ ] A menu command that touches Revit raises an `[ExternalEvent]` method, not a direct API call on the UI thread; failures log through a source-generated `[LoggerMessage]`.
- [ ] The type is registered with a single `case` in `DescriptorsMap.FindDescriptor`, guarded for the exact type.
- [ ] A test covers the change and it decomposes correctly in a `Debug.RNN` run.

## Common Pitfalls

| Pitfall                                                           | Correct approach                                                                          |
|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| Special-casing a type outside a descriptor                        | Add a descriptor and one `DescriptorsMap` case; keep type knowledge in the descriptor.    |
| Letting the engine invoke a member that needs arguments or a view | `Resolve` it explicitly with `Variants`, or `Disable` it.                                 |
| Calling the Revit API from a menu handler on the UI thread        | Raise a static `[ExternalEvent]` method; never `Dispatcher.Invoke` for Revit work.        |
| Registering the descriptor case without the exact-type guard      | Guard with `type is null \|\| type == typeof(T)`; member resolution needs the exact type. |
| Silent exception handling                                         | Declare a source-generated `[LoggerMessage]` method.                                      |

