3
\$\begingroup\$

This code allows me to compare two complex objects, which are stored in a relational database, to see what has been removed from the objects. These objects have a unique integer primary key. They are also composed of other objects hierarchically.

The Difference method is used within a database application (code not shown here). When an object is retrieved from the database, it is cloned. At the point of saving the "new" object this is compared with the "old" clone, and Difference returns a summary of the types and IDs of those objects which have been removed by the user. Thus the database application can:

  1. Insert new database entities and update existing ones.
  2. Delete removed records returned by Difference to prevent orphaned records in the database.
/// <summary>
/// Data which has a unique integer identifier and is composed of other similar objects
/// </summary>
interface IHierarchicalDatabaseEntity
{
    /// <summary>
    /// The unique identifier
    /// </summary>
    int ID { get; }

    /// <summary>
    /// The other objects that this is composed of
    /// </summary>
    IEnumerable<IHierarchicalDatabaseEntity> Children { get; }
}

/// <summary>
/// Helper methods for IHierarchicalDatabaseEntity
/// </summary>
public static class HierarchicalDatabaseEntityExtensions
{
    /// <summary>
    /// Summarises the differences between the old and new database entities. 
    /// Each entity which is present in the old data but not in the new will result in an element being returned.
    /// </summary>
    /// <param name="oldData">the old database entity</param>
    /// <param name="newData">the new database entity</param>
    /// <returns>an enumeration over the types and IDs of data which is present in the old data but not in the new</returns>
    public static IEnumerable<KeyValuePair<Type, List<int>>> Difference(IHierarchicalDatabaseEntity oldData, IHierarchicalDatabaseEntity newData)
    {
        ListLookup<Type, int> result = oldData.CollateIDs();
        result.RemoveAll(newData.CollateIDs());
        return result;
    }

    /// <summary>
    /// Gets a summary of the data within some database entities
    /// </summary>
    /// <param name="databaseEntities">the objects to be summarised</param>
    /// <returns>a ListLookup where the keys are the types of the objects and the values are the unique IDs</returns>
    public static ListLookup<Type, int> CollateIDs(IEnumerable<IHierarchicalDatabaseEntity> databaseEntities)
    {
        ListLookup<Type, int> listLookup = new ListLookup<Type, int>();
        foreach (IHierarchicalDatabaseEntity entity in databaseEntities)
        {
            listLookup.Add(entity.GetType(), entity.ID);
        }
        return listLookup;
    }

    /// <summary>
    /// Gets a summary of a database entity and all its children
    /// </summary>
    /// <param name="parent">the parent object</param>
    /// <returns>a ListLookup where the keys are the types of the objects and the values are the unique IDs</returns>
    public static ListLookup<Type, int> CollateIDs(this IHierarchicalDatabaseEntity parent)
    {
        return CollateIDs(parent.GetAllChildren());
    }

    /// <summary>
    /// Gets all the children of a IHierarchicalDatabaseEntity
    /// </summary>
    /// <param name="parent">the parent object</param>
    /// <returns>the parent and all its children</returns>
    public static IEnumerable<IHierarchicalDatabaseEntity> GetAllChildren(this IHierarchicalDatabaseEntity parent)
    {
        Queue<IHierarchicalDatabaseEntity> children = new Queue<IHierarchicalDatabaseEntity>();
        children.Enqueue(parent);
        while (children.Any())
        {
            IHierarchicalDatabaseEntity first = children.Dequeue();
            yield return first;
            foreach (IHierarchicalDatabaseEntity child in first.Children)
            {
                children.Enqueue(child);
            }
        }
    }
}

/// <summary>
/// An association between key objects and multiple value objects
/// </summary>
/// <typeparam name="TK">the keys</typeparam>
/// <typeparam name="TV">the values</typeparam>
public class ListLookup<TK, TV>: IEnumerable<KeyValuePair<TK, List<TV>>>
{
    private readonly Dictionary<TK, List<TV>> dictionary = new Dictionary<TK, List<TV>>();

    /// <summary>
    /// Add an association
    /// </summary>
    /// <param name="key">the key</param>
    /// <param name="value">the value</param>
    public void Add(TK key, TV value)
    {
        List<TV> values;
        if (!dictionary.TryGetValue(key, out values))
        {
            values = new List<TV>();
            dictionary[key] = values;
        }
    }

    /// <summary>
    /// Removes all the values from this object which are present in the other object
    /// </summary>
    /// <param name="other"></param>
    public void RemoveAll(ListLookup<TK, TV> other)
    {
        foreach (KeyValuePair<TK, List<TV>> kvp in dictionary)
        {
            List<TV> otherList;
            if(other.dictionary.TryGetValue(kvp.Key, out otherList)) 
            {
                kvp.Value.RemoveAll(v => otherList.Contains(v));
            }
        }
    }

    public IEnumerator<KeyValuePair<TK, List<TV>>> GetEnumerator()
    {
        return dictionary.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ GetAllChildren should be called GetDescendants or GetSuccessors \$\endgroup\$
    – dfhwze
    Jun 5, 2019 at 5:32

1 Answer 1

2
\$\begingroup\$

Couple of quick comments, since you don't really specify what you were looking for.

Seems like everything other than Difference should be private; I probably wouldn't make them extension methods either. I think the whole thing would be better suited to a class rather than 3 or 4 coupled methods.

Your recursive GetAllChildren is an odd implementation that I haven't seen before. It looks like it'd work OK, but doesn't seem standard.

Your ListLookup.Add method seems broken; it never adds the value; I'd probably just replace that class with a Dictionary<Type, List<int>> or something since it doesn't seem to add much value but could be a breeding ground for bugs.

No reason to throw on GetEnumerator; just return the generic IEnumerator.

What happens if oldData or newData is null? Looks like a NRE, when I'd expect a list of all the added or deleted Ids instead.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.