How to Implement Collection Property to Contain Unique Objects

by Zoran Horvat

Typical object diagram in modern applications contains many-to-one relationships between objects: companies have departments, departments have employees, machines have parts, user interfaces contain controls, to-do lists have items, books have pages... Possibilities are endless. In such a setting, one object contains a collection of child objects, while each child object is associated with a single parent object. So if we speak about a company that has one or more departments, then we also speak of a department that belongs to a single company. Figure below depicts a corresponding object-role model describing this case.

ORM diagram

In terms of program code, we rather want to have simple navigation properties: one that leads from parent object to all of its child objects, and another one on the child object that returns its corresponding parent object. In terms of a company-department example, that would mean that company object exposes a collection of departments and department object exposes a reference to its company.

Class diagram

But this design raises a couple of issues. Caller must not be able to destroy the collection, or to make abrupt changes to its content without notifying the parent and child objects involved. The two navigation properties must be synchronized in terms that if department appears in one company’s collection then its Company navigation property must reference that particular company rather than any other company in the system. Conversely, if one department references a parent company, then it must appear in that company’s collection of departments. Additionally, it is often required to disallow adding equal child objects to the same parent (e.g. company cannot have two departments with the same name).

In this article we are going to present a pattern which provides collection property functionality on parent objects and navigation to parent property on child objects. Solution will keep track of unique child objects in order to prevent any attempt to add child object which equals an existing child object within the same parent.

Solution Design

Solution that we are going to present will provide these features:

  • Parent class exposes ICollection<T> read-only property which returns a modifiable collection of child objects. Collection’s built-in methods can be used to manipulate the set of child objects that are contained in the parent (e.g. Add, Remove, Clear, etc.). Using these methods is quite safe as will be seen from the implementation.
  • Child class exposes a read-only property which returns reference to corresponding parent object.
  • When object is added to the collection, parent object automatically sets reference to itself in the child object. For this purpose a special internal method is used, so that outer types cannot modify reference to parent object. Operation fails if collection already contains object which is equal to the one that is being added. In addition, operation fails if reference to parent is already set in the child object. This measure protects the system from attempts to associate one object to more than one parent object.
  • When object is removed from the collection, parent object automatically sets parent reference in the child object to null.
  • Parent object ignores attempts to add null child object to its collection. Alternatively, implementation can be tweaked to throw exception on such attempt.
  • Child object may be changed after being added to its parent. Sensitive changes made to the object (i.e. changes to content that takes part in equality tests among child objects) must be propagated to the parent (if parent is set). The operation is performed by first removing the child object from its parent, then making the desired changes to its content, and then adding it back to the parent. Should the last operation fail - which indicates a collision with another child object in the parent's collection - child object's state must be restored and then the object added back to its parent in the state in which it was before the whole operation.

It will soon become clear that this pattern is not of the simplest kind. It might also be a signal that a more general solution should be devised which encapsulates this fairly complex set of features. But that would be beyond the scope of this article.

Implementation

Below is the complete source code of a console application which demonstrates use of navigation properties. All consistency checks are applied both to client and the server. Following the Company and Department classes, there is a rather detailed Main function which demonstrates behavior of these classes in several characteristic situations.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace CollectionProp
{
    /// <summary>
    /// Wraps information about one company, including a collection of its departments.
    /// </summary>
    public class Company
    {

        /// <summary>
        /// Default cnostructor.
        /// </summary>
        public Company()
        {

            _uniqueDepartments = new HashSet<Department>();
            _departments = new ObservableCollection<Department>();

            // Subscribe to NotifyCollectionChanged event so that Company object knows when other entities
            // are modifying its collection of departments
            _departments.CollectionChanged += new NotifyCollectionChangedEventHandler(DepartmentsChanged);

        }

        /// <summary>
        /// Handles NotifyCollectionChanged event raised by the observable collection which contains
        /// company's departments.
        /// </summary>
        /// <param name="sender">Object which has raised the event.</param>
        /// <param name="e">Event arguments.</param>
        void DepartmentsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {

            if (e.Action == NotifyCollectionChangedAction.Reset)
            {   // Dramatic change has been made to the collection of departments.
                // Parent object will clear the collection of contained objects and then
                // add those that may have survived in the observable collection.

                foreach (Department d in _uniqueDepartments)
                    d.SetCompany(null);
                _uniqueDepartments.Clear();

                // Now add the survivors, although they should not exist
                // since Reset action on observable collection occurs as a result
                // of calling its Clear method.
                foreach (Department d in _departments)
                    OnDepartmentAdded(d);

            }
            else
            {   // Otherwise traverse through collections of old and new items and
                // refresh collections accordingly

                if (e.OldItems != null)
                    foreach (Department d in e.OldItems)
                        OnDepartmentRemoved(d);

                if (e.NewItems != null)
                    foreach (Department d in e.NewItems)
                        OnDepartmentAdded(d);

            }

        }

        /// <summary>
        /// Handles case when specified department has been added to the observable collection.
        /// </summary>
        /// <param name="d">Department which was added to the observable collection.</param>
        /// <exception cref="System.ArgumentException">Specified department already exists
        /// in the collection of departments in this company object.</exception>
        private void OnDepartmentAdded(Department d)
        {

            if (d == null)
            {   // Attempted to add null department; simply remove it from observable collection
                SilentlyRemoveDepartment(d);
            }
            else if (object.ReferenceEquals(d.Company, this) || _uniqueDepartments.Contains(d))
            {   // Attempted to add duplicate department - remove it and throw exception
                SilentlyRemoveDepartment(d);
                throw new System.ArgumentException("Duplicate department.");
            }
            else
            {   // Otherwise try to add department to collection of unique departments
                // and set its parent reference to this object

                try
                {
                    d.SetCompany(this); // Throws ArgumentException if d already has a parent
                    _uniqueDepartments.Add(d);
                }
                catch
                {
                    SilentlyRemoveDepartment(d);
                    throw;
                }

            }

        }

        /// <summary>
        /// Handles case when specified department has been removed from the observable collection.
        /// </summary>
        /// <param name="d">Department which was removed from the observable collection.</param>
        private void OnDepartmentRemoved(Department d)
        {
            _uniqueDepartments.Remove(d);
            d.SetCompany(null);
        }

        /// <summary>
        /// Removes specified department from observable collection, preivously removing
        /// NotifyCollectionChanged event handler. This prevents event handler from
        /// executing. Method is intended to be called from inside the event handler,
        /// so to avoid recursive events.
        /// </summary>
        /// <param name="d">Department which should be removed from the observable collection.</param>
        private void SilentlyRemoveDepartment(Department d)
        {

            _departments.CollectionChanged -= new NotifyCollectionChangedEventHandler(DepartmentsChanged);

            // Remove item by reference equality, not by semantic equality!
            int i = 0;
            foreach (Department dept in _departments)
                if (object.ReferenceEquals(dept, d))
                {
                    _departments.RemoveAt(i);
                    break;
                }
                else
                {
                    i++;
                }

            _departments.CollectionChanged += new NotifyCollectionChangedEventHandler(DepartmentsChanged);

        }

        /// <summary>
        /// Gets or sets company name.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Gets collection of unique departments of this company.
        /// </summary>
        public ICollection<Department> Departments
        {
             get
            {
                return _departments;
            }
        }

        /// <summary>
        /// Collection of departments added to this company object.
        /// </summary>
        private ObservableCollection<Department> _departments;

        /// <summary>
        /// Collection which ensures uniqueness of departments
        /// added to the company object.
        /// </summary>
        private HashSet<Department> _uniqueDepartments;

    }

    /// <summary>
    /// Wraps information about one department of a company.
    /// Department is uniquely identified by its name and parent company.
    /// </summary>
    public class Department
    {

        /// <summary>
        /// Default constructor.
        /// </summary>
        public Department() { }

        /// <summary>
        /// Constructor which initializes department name.
        /// </summary>
        /// <param name="name">Name of the company department.</param>
        public Department(string name) { Name = name; }

        /// <summary>
        /// Gets or sets the department name. Attempting to change name of the department
        /// which is already part of a company may cause name collision within the company,
        /// in which case this operation fails and name remains the same.
        /// </summary>
        /// <exception cref="System.ArgumentException">Changing the name to desired value
        /// would cause name collision within the company.</exception>
        public string Name
        {
            get
            {
                return _name;
            }
            set
            {

                bool failed = false;

                if (_company != null && value != _name)
                {   // Must remove this department from the company and then
                    // add it again after setting the new name.
                    // That will reorganize company object's internal structures.

                    string prevName = _name;
                    Company prevCompany = _company;

                    _company.Departments.Remove(this);

                    try
                    {
                        _name = value;
                        prevCompany.Departments.Add(this); // Will throw exception if same name is in use
                    }
                    catch
                    {   // Name collision
                        failed = true;
                        _name = prevName;
                        prevCompany.Departments.Add(this); // Return into the company with previous name
                    }
                }
                else
                {   // Department is not part of the company - there is no fear of name collision
                    _name = value;
                }

                if (failed) // Throw exception only after all content has been returned to original state
                    throw new System.ArgumentException("Error changing department name.");

            }
        }

        /// <summary>
        /// Gets hash code which is determined by content of this object.
        /// </summary>
        /// <returns>Value determined by the name of the department.</returns>
        public override int GetHashCode()
        {
            return (Name ?? string.Empty).GetHashCode();
        }

        /// <summary>
        /// Tests whether this instance is equal to other object.
        /// </summary>
        /// <param name="obj">Object against which this instance is compared for equality.</param>
        /// <returns>true if <paramref name="obj"/> is a department with same name as this
        /// department.</returns>
        public override bool Equals(object obj)
        {
            return obj != null && obj is Department && (obj as Department).Name == this.Name;
        }

        /// <summary>
        /// Gets company to which this department belongs.
        /// </summary>
        public Company Company
        {
            get
            {
                return _company;
            }
        }

        /// <summary>
        /// Non-public method which allows setting the company to which this department belongs.
        /// </summary>
        /// <param name="comp">Company to which this department has been added.</param>
        /// <exception cref="System.ArgumentException">This department is already part
        /// of another company. One department object cannot appear in more than one company.</exception>
        internal void SetCompany(Company comp)
        {
            if (_company != null && comp != null && _company != comp)
                throw new System.ArgumentException("Department already belongs to another company.");
            _company = comp;
        }

        /// <summary>
        /// Company to which this department object has been added.
        /// </summary>
        private Company _company;

        /// <summary>
        /// Name of the company department.
        /// </summary>
        private string _name;

    }

    public class Program
    {
        static void PrintDepartments(Company comp)
        {
            Console.Write("Departments of {0}:", comp.Name);
            foreach (Department d in comp.Departments)
                Console.Write(" {0}", d.Name);
            Console.WriteLine();
        }

        static void Main(string[] args)
        {

            Console.WriteLine("Departments.Add test:");
            Company comp1 = new Company();
            comp1.Name = "First Corp.";

            comp1.Departments.Add(new Department("Retail"));
            comp1.Departments.Add(new Department("Planning"));

            Department dept = new Department("Cleaning");
            comp1.Departments.Add(dept);

            comp1.Departments.Add(null);

            PrintDepartments(comp1);

            Console.WriteLine();
            Console.WriteLine("Adding another company's department - failure test:");

            Company comp2 = new Company();
            comp2.Name = "Second Corp.";

            comp2.Departments.Add(new Department("Retail"));    // Not the same Retail as in First Corp.
            comp2.Departments.Add(new Department("Planning"));

            PrintDepartments(comp2);

            try
            {
                comp2.Departments.Add(dept);    // Same department as in First Corp.
            }
            catch (System.ArgumentException ex)
            {
                // Expected exception: added another company's department
                Console.WriteLine("ERROR: {0}", ex.Message);
            }

            PrintDepartments(comp2);

            Console.WriteLine();
            Console.WriteLine("Renaming test - name collision failure:");

            dept = new Department("Cleaning");
            comp2.Departments.Add(dept);
            PrintDepartments(comp2);

            try
            {
                dept.Name = "Planning"; // Name collision - Planning already exists in comp2
            }
            catch (System.ArgumentException ex)
            {
                Console.WriteLine("ERROR: {0}", ex.Message);
            }

            PrintDepartments(comp2);

            Console.WriteLine();
            Console.WriteLine("Renaming test - no collision:");
            PrintDepartments(comp2);
            dept.Name = "Catering"; // Successful name change
            PrintDepartments(comp2);

            Console.WriteLine();
            Console.WriteLine("Adding equal department test - failure:");
            comp1.Departments.Remove(new Department("Retail")); // Finds department that is equal to supplied object
            comp1.Departments.Remove(dept);

            PrintDepartments(comp1);

            try
            {
                comp1.Departments.Add(new Department("Planning"));  // Duplicate department
            }
            catch (System.ArgumentException ex)
            {
                // Expected exception: attempted to add another department with same name
                Console.WriteLine("ERROR: {0}", ex.Message);
            }

            PrintDepartments(comp1);

            Console.WriteLine();
            Console.WriteLine("Clear test:");

            PrintDepartments(comp1);
            comp1.Departments.Clear();
            PrintDepartments(comp1);
            comp1.Departments.Add(new Department("Planning"));  // Doesn't fail after emptying the collection
            PrintDepartments(comp1);

            Console.WriteLine();
            Console.WriteLine("Department.Company navigation test:");

            dept = new Department("Retail");
            comp1.Departments.Add(dept);
            PrintDepartments(dept.Company);     // Print company to which this department belongs

            Console.WriteLine();
            Console.Write("Press ENTER to continue... ");
            Console.ReadLine();

        }
    }
}

When this code is run, it produces the following output:

Departments.Add test:
Departments of First Corp.: Retail Planning Cleaning

Adding another company's department - failure test:
Departments of Second Corp.: Retail Planning
ERROR: Department already belongs to another company.
Departments of Second Corp.: Retail Planning

Renaming test - name collision failure:
Departments of Second Corp.: Retail Planning Cleaning
ERROR: Error changing department name.
Departments of Second Corp.: Retail Planning Cleaning

Renaming test - no collision:
Departments of Second Corp.: Retail Planning Cleaning
Departments of Second Corp.: Retail Planning Catering

Adding equal department test - failure:
Departments of First Corp.: Planning Cleaning
ERROR: Duplicate department.
Departments of First Corp.: Planning Cleaning

Clear test:
Departments of First Corp.: Planning Cleaning
Departments of First Corp.:
Departments of First Corp.: Planning

Department.Company navigation test:
Departments of First Corp.: Planning Retail

Press ENTER to continue...

Conclusion

In this article we have demonstrated means by which two classes can be tied in a many-to-one relationship. Number of special cases have been covered in order to support all conceivable scenarios that might occur when using these classes. Goal of the exercise was to reach a design in which caller is free to add, change and remove child objects at will, at the same time resting assured that all moving parts are automatically kept in their proper places.

Design demonstrated above can be a sound base on which a fully general solution could be built.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub