Skip to content

ContainerList

samatstarion edited this page Oct 4, 2026 · 2 revisions

Introduction

ContainerList<T> (interface IContainerList<T>, namespace uml4net) is the collection type that uml4net uses for every composite (containment) property of the UML metamodel: Package::packagedElement, Class::ownedAttribute, Element::ownedComment, Region::subvertex, and so on. It is a List<T> where T : class, IElement, with one extra responsibility: it keeps the ownership of the contained elements in sync with the list.

In UML, composite aggregation means that the owner is responsible for the existence of the owned element, and that an element has at most one owner (Element::owner [0..1]). An XMI document encodes this by nesting: the XML element of the owned element is written inside the XML element of its owner. ContainerList<T> is what turns that nesting into object references in both directions.

The generated classes never expose a plain List<T> for a composite property. Property.WriteForClass in uml4net.HandleBars generates, for every composite property, a lazily created backing field:

public IContainerList<IGeneralization> Generalization
{
    get => this.generalization ??= new ContainerList<IGeneralization>(this,
        containedElement => { containedElement.Specific = this; },
        containedElement => { if (ReferenceEquals(containedElement.Specific, this)) { containedElement.Specific = null; } });
    set => this.generalization = value;
}

See uml4net project for how composite properties are represented in the generated interfaces and classes.

What the list maintains

The Possessor

IElement.Possessor is a hand-coded property on the partial IElement interface. It is the settable backing of the derived, read-only union Element::owner: element.Owner returns element.Possessor.

Every mutator of ContainerList<T> maintains it:

Operation Effect on the elements
Add, AddRange, Insert Possessor of the added element is set to the container
this[index] = value Possessor of the new element is set; the replaced element is released (Possessor = null)
Remove, RemoveAt Possessor of the removed element is set to null
Clear Possessor of every element is set to null

Because Possessor is maintained here, the derived unions Element::owner and Element::ownedElement and every query that walks up the ownership chain (NamedElement::namespace, qualifiedName, Action::context, Vertex::containingStateMachine, …) work on a model read from XMI without the document ever serializing an owner attribute.

The owner end of the composite association

Most composite properties are one end of a binary association whose other end is a single-valued, settable property owned by the contained type: Classifier::generalization ↔ Generalization::specific, Namespace::packageImport ↔ PackageImport::importingNamespace, Region::subvertex ↔ Vertex::container, Activity::node ↔ ActivityNode::activity. XMI does not serialize that owner end either, it is implied by the nesting, so the reader cannot set it from the document.

The constructor ContainerList(IElement container, Action<T> attachOwnerEnd, Action<T> detachOwnerEnd) takes two actions that are invoked wherever the Possessor is maintained. The generator supplies them for every composite property that has such an owner end (54 distinct ends across the metamodel), so that after classA.Generalization.Add(g) the value of g.Specific is classA, and after classA.Generalization.Remove(g) it is null again.

Two details worth knowing:

  • Detaching is guarded with ReferenceEquals(end, container). An element that has been added to another container in the meantime already points at its new owner, and removing it from the old list does not clear that.
  • For a composite property that is subsetted by a derived composite property with a narrower type, the actions select by element type. Package::packagedElement sets Type::package (the owner end of the derived Package::ownedType) for the elements that are IType, and Package::nestingPackage (of the derived Package::nestedPackage) for those that are IPackage.

A ContainerList<T> created with the constructor ContainerList(IElement container) maintains the Possessor only. This is the constructor to use for a composite property whose opposite is owned by the association (such as Element::ownedComment, whose opposite owningElement is not a property of Comment).

The upper bound of a single-valued composite property

A composite property with upper bound 1, such as Constraint::specification [1..1] or Property::defaultValue [0..1], is an IContainerList<T> as well, so that containment is handled by one mechanism (see design-decisions.md). The generator creates its list with the constructor ContainerList(IElement container, string propertyName, int upperBound, Action<T> attachOwnerEnd = null, Action<T> detachOwnerEnd = null), which enforces the bound; this is the case for all 86 single-valued composite properties of the metamodel:

public IContainerList<IValueSpecification> Specification
{
    get => this.specification ??= new ContainerList<IValueSpecification>(this, "Constraint::specification", 1);
    set => this.specification = value;
}

UpperBound returns the bound, int.MaxValue for an unbounded list. Multi-valued composite properties are not bounded.

Rules enforced

ContainerList<T> throws rather than silently building an invalid model:

  • Add(null), AddRange(null), Remove(null) and this[index] = null throw ArgumentNullException.
  • Adding the container to its own list throws InvalidOperationException ("The container shall not be added as contained item to itself").
  • Adding an element that is already in the list throws InvalidOperationException (composite properties are sets; the same element cannot be contained twice).
  • Adding or inserting an element beyond the upper bound throws InvalidOperationException that names the property, for example Constraint::specification holds at most 1 value(s); [second] cannot be added. Replacing a value through the indexer is allowed.
  • A rejected element is left untouched: its Possessor is only set once all the checks pass.
  • Out-of-range indices throw ArgumentOutOfRangeException.

The readers report a single-valued composite property that a document gives more than once like any other invalid value: XmiReadException in strict mode, otherwise an error log, and the first value is kept.

Note that adding an element to a second container does not throw; the element simply moves: its Possessor and owner end now point at the new container. It is the caller's responsibility to remove it from the old list if that is what is intended, the derived Element::ownedElement of the old container still enumerates the old list.

Copying

ContainerList(IContainerList<T> containerList, IElement container, bool updateContaineeContainer = false) creates a list with the same elements for another container. By default the copied elements keep their original Possessor; with updateContaineeContainer: true they are re-parented to the new container. The copy constructor does not carry owner-end actions.

Where it is used

  • Every composite property of the generated classes in uml4net/AutoGenClasses.
  • The Assembler inserts the definitions that composite proxies (xmi:idref/href inside a composite property, XMI 2.5.1 clause 7.10.1) refer to into these lists at the position of the proxy, which sets the Possessor and the owner end through the same mechanism.
  • The XMI writers iterate these lists to write the owned elements nested inside their owner.

See also

  • XmiElement — the base class of everything that is put in a ContainerList<T>
  • XmiElementCache — where the elements are registered by identifier
  • Assembler — turns identifiers into object references after reading

Clone this wiki locally