stride3d/stride · error · InvalidOperationException

An item has been added to a collection that does not have a…

Error message

An item has been added to a collection that does not have a predictable Add. Consider using NonIdentifiableCollectionItemsAttribute on this collection.

What it means

When an item is added to an identifiable collection, AssetObjectNodeExtended records the new item's id at the position reported by the change event. For ordered lists (non-set categories) an add must carry a concrete NodeIndex (predictable Add/Insert); when the index is Empty the library cannot place the id and throws, suggesting NonIdentifiableCollectionItemsAttribute.

Solutions

  1. Apply [NonIdentifiableCollectionItemsAttribute] to the offending collection property so ids are tracked by index instead of insertion position.
  2. Replace the collection with a standard List<T> (predictable Add) or a true Set type so the descriptor category matches behavior.
  3. Implement Add on the custom collection so it appends at a deterministic end index and reports NodeIndex accordingly.
  4. If the index can be computed, fix the event source to supply a valid e.Index before raising the change.

Example fix

// before
public HashSet<MyItem> Items { get; set; } = new(); // set-like, but descriptor is List category
// after
[NonIdentifiableCollectionItems]
public HashSet<MyItem> Items { get; set; } = new();
Defensive patterns

Strategy: validation

Validate before calling

var cat = collectionDescriptor?.Category;
bool needsIndex = cat != DescriptorCategory.Set;
bool indexKnown = e.Index != NodeIndex.Empty;
bool safe = !needsIndex || indexKnown; // otherwise mark collection non-identifiable

Type guard

static bool SupportsIdentifiableAdd(ICollectionDescriptor d) =>
    d == null || d.Category == DescriptorCategory.Set || d is { /* predictable Add */ };

Try / catch

try { nodeExtended.TrackItemChange(e); }
catch (InvalidOperationException) { /* treat collection as non-identifiable */ }

Prevention

When it happens

Trigger: An add notification (OnItemChanged with ItemChangeType.Add) arrives with e.Index == NodeIndex.Empty on a collection whose descriptor Category is not Set — i.e. a list-like collection whose Add does not report where the item lands (e.g. HashSet-like or custom collection without predictable Add).

Common situations: Using a collection type whose Add doesn't append at a known index (dict-like or set-like behavior without Set descriptor); a custom collection added to an asset without the NonIdentifiableCollectionItemsAttribute; descriptor category changed between versions.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/bc63d4349b954e1c. Report an issue: GitHub.

Appendix: source

Thrown at sources/assets/Stride.Core.Assets.Quantum/Internal/AssetObjectNodeExtended.cs:344

                        newValue = setDescriptor.ElementType.Default();
                    }
                    if (!itemIds.ContainsKey(newValue))
                    {
                        var itemId = restoringId != ItemId.Empty ? restoringId : ItemId.New();
                        itemIds[newValue] = itemId;
                    }
                }
                break;
            }
            case ContentChangeType.CollectionAdd:
            {
                // Compute the id we will add for this item
                var itemId = restoringId != ItemId.Empty ? restoringId : ItemId.New();
                // Add the id to the proper location (insert or add)
                if (collectionDescriptor is not null && collectionDescriptor.Category != DescriptorCategory.Set)
                {
                    if (e.Index == NodeIndex.Empty)
                        throw new InvalidOperationException("An item has been added to a collection that does not have a predictable Add. Consider using NonIdentifiableCollectionItemsAttribute on this collection.");

                    itemIds.Insert(e.Index.Int, itemId);
                }
                else
                {
                    itemIds[e.Index.Value] = itemId;
                }
                break;
            }
            case ContentChangeType.CollectionRemove:
            {
                var itemId = itemIds[e.Index.Value];
                // update isOverriding, it should be true only if the item being removed exist in the base.
                isOverriding = isOverriding && (baseNode?.HasId(itemId) ?? false);
                if (collectionDescriptor is not null && collectionDescriptor.Category != DescriptorCategory.Set)
                {
                    removedId = itemIds.DeleteAndShift(e.Index.Int, isOverriding);
                }

View on GitHub (pinned to 96fad776d2)