Click here to Skip to main content
Click here to Skip to main content

Representing relationships as interdependent collections

, 28 Feb 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
This article shows how to represent many-to-many relationships among objects as interdependent collections.

Introduction

Application objects frequently have a many-to-many relationship to one another. This occurs, for example, in representing orders for products. Each order requests, and thereby relates to, possibly several products. For inventory control purposes, it is frequently desirable to have products relate to the orders requesting them. Each product is requested by, and thereby relates to, possibly several orders.

Many-to-many relationships are frequently represented in databases as a linking table. In such a table, each table element references an instance of each of the objects in the relationship. The table representing the relationship between orders and products would contain an element for each order line item. The element would refer to the order and to the item ordered.

In .NET, each side of the relationship can be represented as a set of collections. Each order, for example, could have a collection of products ordered. Similarly, each product could have a collection of orders which request it. However, updating one of these collections—adding a product to an order, for example—then requires updating another of these collections—the collection of orders requesting the product. This requires each object participating in the relationship to be aware it participates in a relationship. Each object must maintain other objects’ views of the relationship as well as its own. This is cumbersome and error prone.

This article describes a generic class that avoids this complication. The companion ZIP file contains the source for the class, and the source for an NUnit test for the class. The class, Association <TLeft, TRight> maintains a list of object pairs. The class can provide a strongly typed ICollection<T> view for each object potentially in the relationship. Each ICollection<T> view dynamically changes the underlying relationship as the collection is manipulated. Each ICollection <T> view also dynamically changes content to reflect changes to the relationship made by other objects. For ease of data binding, each ICollection <T> view also implements the INotifyCollectionChanged interface. This permits event subscribers to receive an event when the content changes.

Using the code

Creating the relationship

A relationship is created with a declaration such as:

Association <TLeft, TRight> association = new Association <TLeft, TRight> ();

This creates an Association <TLeft, TRight> object which implements a relationship between objects of type TLeft and objects of type TRight.

Relationships between types are typically singletons. For example, there would be a single instance of a relationship between orders and products. Individual order objects and product objects would each have their own view of the relationship. The Association <TLeft, TRight> class does not enforce the singleton pattern. This allows types to have multiple relationships with one another. For example, orders and products might have an “order contains” relationship and an “items backordered” relationship.

Creating the views

Objects of type TLeft can obtain their view of the relationship with a declaration such as:

IAssociationCollection <TRight> _myTRights = association.LeftView 

(this);

This creates an object implementing the ICollection <TRight> interface and the INotifyCollectionChanged interface. (The object _TRights could also be declared with type ICollection <TRight>. This would, however, make it difficult to subscribe to the CollectionChanged event.) The ICollection<TRight> at any point contains references to each TRight in the relationship with the specific TLeft.

Similarly, an object of type TRight can obtain its view of the relationship with the declaration:

IAssociationCollection <TLeft> _myTLefts = association.RightView (this);

Using the views

The System.Collections.Generic.ICollection <T> interface defines the members:

void Add (T item);
void Clear ();
bool Contains (T item);
void CopyTo (T [] array, int startIndex);
int Count { get; }
bool IsReadOnly { get; }
bool Remove (T item);

The object returned by LeftView and RightView implement these by manipulating the underlying Association <TLeft, TRight> object.

The Add method adds a single item to the collection on which it is invoked. It ensures a (TLeft, TRight) pair corresponding to the item for which the IAssociationCollection <T> was created and to the item referenced by the Add method is in the underlying Association <TLeft, TRight> object. If the pair was already in the underlying Association <TLeft, TRight> object, the pair is not duplicated and no events are published. If the pair was not already in the underlying Association <TLeft, TRight> object, the pair is added and CollectionChanged events are published by all IAssociationCollection <T> objects associated with either of the items in the added pair.

Similarly, the Remove method removes a single item from the collection on which it is invoked. It ensures a (TLeft, TRight) pair corresponding to the item for which the IAssociationCollection <T> object was created and to the item referenced by the Add method is not in the underlying Association <TLeft, TRight> object. If the pair was already in the underlying Association <TLeft, TRight> object, the pair is removed and CollectionChanged events are published by all IAssociationCollection <T> objects associated with either of the items in the removed pair. If the pair was not already in the underlying Association <TLeft, TRight> object, nothing is removed and no events are published.

Finally the Clear method removes all items from the collection on which it is invoked. It ensures no (TLeft, TRight) pair corresponding to the item for which the IAssociationCollection <T> object was created is in the underlying Association <TLeft, TRight> object. If any pairs were removed from the Association <Left Item, TRight> object, a single CollectionChanged event is published (listing all formerly associated objects) for the object for which the IAssociationCollection <T> was created, and a separate CollectionChanged event is published (listing the single object for which the IAssociationCollection <T> was created) for each object formerly associated with the object for which the IAssociationCollection <T> was created.

In summary, adding an item to an IAssociationCollection <T> object causes it to appear in other IAssociationCollection <T> objects as required. Removing an item from an IAssociationCollection <T> object causes it to disappear from other IAssociationCollection <T> objects as required. Finally, IAssociationCollection <T> publish CollectionChanged events as required to describe the changes they undergo.

The CollectionChanged event handler has the signature:

void handler (object publisher,
    System.Collections.Specialized.NotifyCollectionChangedEventArgs eventArgs);

The NotifyCollectionChangedEventArgs type defines the members:

NotifyCollectionChangedAction Action { get; }
IList NewItems { get; }
int NewStartingIndex { get; }
IList OldItems { get; }
int OldStartingIndex { get; }

The CollectionChanged events published by the IAssociationCollection <T> objects do not use the NewStartingIndex and OldStartingIndex members, as collections are unordered. The only NotifyCollectionChangedAction values used for the Action member are Add and Remove. The NewItems list is non-null only if the Action value is Add. The OldItems list is non-null only if the Action value is Remove.

Threading concerns

The IAssociationCollection <T> objects themselves are thread-safe. However, the enumerators obtained through the GetEnumerator() method are not thread-safe. All events are published on the thread invoking the Add, Clear, or Remove method causing the event to be published.

Serialization concerns

The Association <TLeft, TRight> type is serializable. If the types TLeft and TRight are serializable, then an Association <Left Item, TRight> object is serializable. The IAssociationCollection <T> objects are not serializable. If you intend your object using IAssociationCollection <T> fields to be serializable, it must use the [NonSerialized] attribute on those fields, or implement custom serialization through the System.Runtime.Serialization.ISerializable interface.

The collections implemented by IAssociationCollection <T> are not independent entities. They are instead a view of data held elsewhere. Each IAssociationCollection <T> object references its parent Association <TLeft, TRight> object, which in turn references all TLeft and TRight objects currently participating in the relationship. Were IAssociationCollection <T> objects serializable, attempts to serialize a single TLeft or TRight object would instead serialize possibly all TLeft and TRight objects in the program.

Typically, a Association <TLeft, TRight> object would be serialized only along with all TLeft and TRight objects in the program.

Points of interest

Why a central repository?

As mentioned in the Introduction, relationships are typically modeled in .NET with collections. These collections are required to update one another as each changes. This is certainly feasible, and relatively easily implemented. Why, then, have a central repository of relationship information at all?

There are two factors that argue for a central repository of relationship information. The first is consistency: a single definitive source cannot get out of synchronization with itself. If relationship information is spread out through a series of collections owned by individual objects, the information can be lost (through, e.g., garbage collection of individual objects which are collectively referenced only by one another) or become contradictory (through, e.g., A claiming it is related to B but B claiming it is not related to A).

A second factor lies in recording the relationship on external storage. If the set of objects to which a given object is related is a view of some other object, then relationship information need not be recorded along with the given object. Instead, all relationship information can be recorded at once, and refer to the individual objects which are already recorded. It is much simpler to store rows in two database tables, and then create the linking table, if the relationship is centralized.

Why not IList views?

Collections are unordered, and lists are ordered. Typically an application presents a set of items associated with a given item as a list, in some particular order. Providing an IList <T> view, rather than an ICollection <T> view, would make this common task easier.

The two-way nature of the views provided by the Association <TLeft, TRight> class make this problematic. Not only would each TLeft instance impose an order on the TRight instances in its IList, each TRight instance would also impose an order on the TLeft instances in its IList. Each sorting request would need to apply a permutation to the base list of items which simultaneously sorted the view being sorted, and avoided changing both the order of other views and the validity of any enumerators they had created. While this is quite likely doable, the complexity of the solution would outweigh the complexity of creating and sorting IList objects from ICollection objects when sorting was required.

Enumerating virtual collections

One issue of interest to arise in the construction of the Association <TLeft, TRight> class had to do with enumerators. Each IAssociationCollection <T> object providing a view of the collection associated with an item implements the ICollection <T> interface. This interface in turn inherits from the IEnumerable <T> interface. Therefore, the IAssociationCollection <T> object must be able to provide an IEnumerator <T> object. This object would need to enumerate the objects appearing in (TLeft, TRight) pairs with the object for which the IAssociationCollection <T> object was prepared.

An IEnumerator for the underlying storage object cannot be used for the IEnumerator for a view. This is because the underlying storage object can be manipulated by ICollection objects entirely unrelated to the view. IEnumerator objects become invalid when the collection to which they refer is modified. Attempting to use an IEnumerator object for the underlying storage object as the base for the IEnumerator object for a view would result in the IEnumerator object for a view becoming invalid when some other ICollection was modified. This is clearly undesirable (and breaks the IEnumerator contract).

The approach taken in the Association <TLeft, TRight> class is to use a List object as the underlying storage object, and have views’ IEnumerator objects maintain their current position in that List with an index, rather than an IEnumerator object. Also, (TLeft, TRight) pairs are never removed from the List: if items are to be removed from one another’s ICollection views, instead the (TLeft, TRight) pair is marked “deleted.” The IEnumerator objects skip all (TLeft, TRight) pairs marked “deleted.” Pairs marked “deleted” are reused if the items are added to one another’s ICollection views.

“Deleted” pairs are not serialized, so serializing and deserializing the Association <TLeft, TRight> object effectively removes the “deleted” pairs from the Association <TLeft, TRight> object.

Publishing CollectionChanged events 

The purpose of the Association <TLeft, TRight> object is to provide a set of collections that reflect one another’s changes. So, for example, using Add on one collection is intended to cause CollectionChanged events to be published by both the collection on which Add was executed, and the collection associated with the item added. However, the collection on which Add is executed has no knowledge of other collections. This makes modifying these other collections, and publishing events on them, somewhat difficult.

The solution used in the Association <TLeft, TRight> class is to have all IAssociationCollection<T> objects subscribe to a CollectionChanged event published by the Association <TLeft, TRight> itself. As the Association <TLeft, TRight> object does not itself implement an ICollection interface, this event is not public. It is available only to the IAssociationCollection <T> objects. When the Association <TLeft, TRight> object publishes a CollectionChanged event, each IAssociationCollection <T> object examines the set of (TLeft, TRight) pairs changed, and determines if the object with which it is associated is in any of these pairs. If so, it extracts the relevant object references (that is, the other element of the pairs of which it is an element) and publishes its own CollectionChanged event listing those object references.

History

  • 30 August 2012: Original version of the article.
  • 31 August 2012: Reorganized article and included design discussions.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Brian Hetrick

United States United States
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.150128.1 | Last Updated 28 Feb 2013
Article Copyright 2012 by Brian Hetrick
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid