2

In my ASP.net core application with Angular 2+ client, I work with a complicated object graph. In the object graph I have some objects with references to each other. I have a simplification included as an illustration below, where MainObject represents the graph:

public class MainObject
{
    public int Id { get; set; }   
    public ObjectProperty MyProperty { get; set; }
    public List<GreenObject> GreenObjects { get; set; }
    public List<YellowObject> YellowObjects { get; set; }
}

public class GreenObject
{
    public int Id { get; set; }
    public Guid Guid { get; set; }
    public GreenObjectProperties MyProperty { get; set; }
    public Guid? YellowObjectRef { get; set; }
}
public class YellowObject
{
    public int Id { get; set; }
    public Guid Guid { get; set; }
    public YellowProperties MyProperty { get; set; }
    public Guid? GreenObjectRef { get; set; }
}

Now I have the requirement to create clones of MainObject, such that any relationship between objects in the object graph is maintained, but also such that it can be persisted. This means I need new UUID/GUID for the clone (to be able to be persisted in my DB), and continue to respect the relationship to the cloned objects and thus the newly created UUID/GUID's.

I am already using Automapper in my ASP.net application, so I was considering to use this library to create the clone. But I am willing to consider other libraries, or even to perform the cloning on the client side. AutoMapper documentation state that AutoMapper should not be used for cloning objects.

Question: Is there a common pattern or library to easily clone the object graph, creating new UUID/GUID's while keeping the references to the newly created objects/GUID's?

In the simplification I could easily iterate over the list of GreenObject issue new GUID and then find the YellowObject where GreenObjectRef equals the original GUID and replace it with the new GUID (and vice versa for the list of YellowObject). However my actual implementation is very complicated, and writing such a custom cloning method that is specific to the relationship would also be troublesome to maintain.

2
  • Have you considered ways to avoid needing a clone in the first place? Presumably the reason for wanting a clone is that the objects are cached in memory somewhere and you have code which is modifying either the clone or the original. Could you do away with clones altogether and treat your objects as immutable instead? Feb 15, 2020 at 0:57
  • No I could not do without: the clone is triggered by end user when they need to have a duplicate of the object. Mar 13, 2020 at 4:21

3 Answers 3

3

The problem of copying the object graph and the problem of assigning new GUIDs to the object graph are very similar, but they can be handled separately and one after another, so let me start with copying first.

In case the object graph is just a tree, there may be a simpler solution, but in a comment you mentioned several MainObjects sharing references to Green and Yellow objects, so let me assume the graph is not a tree. The following solution could be simplified for the specific example in the question, but I guess your real graph is more complicated, so I will give you a general answer which works for arbitrary object graphs.

I would solve this in 3 stages:

  1. Determine all objects within the object graph. This can be accomplished by a standard graph traversal (like a simple depth-first search), with a Hashmap<object> where the already visited objects are held. The collection will serve for not visiting any object twice, and at the end it will held all the objects in the graph. All your objects which hold references to other objects will require something like a AddReferences(Hashmap<object> alreadyVisited) method for this.

  2. Make shallow copies of the found objects. For this, it is best to let each object implement the ICloneable interface, utilizing MemberwiseClone. Put the result into a Dictionary which maps each "old" object to its clone.

  3. Make a second graph traversal where the dictionary is passed to all "new" objects which can then replace all references to "old" objects they have to the new clones. Your objects will require a method like a ReplaceReferences(Dictionary<object,object> objectMap) for this.

Note I assumed the references to the objects can be used as keys here (in case you have overriden the Equals and GetHashCode methods, have a look into this SO post).

The GUID generation/replacement can be implemented in a similar fashion:

  1. Graph traversal for determination of all GUIDs

  2. Generation of new GUIDs and filling a dictionary which maps each "old" GUID to the related new one

  3. Graph traversal for replacing the GUIDs

And no, I don't know any library which can solve or support this. Note no library knows by itself which of the references inside a custom object graph belong themselves to the graph, and which ones are just references to objects "outside" the graph, that is implicit knowledge which is encoded somewhere, no lib can resolve this completely automatically.

7
  • "whereever a class X holds a reference to objects of class Y, utilize the copy constructor of Y inside the copy constructor of X" I am not sure I understand your approach here, but would this only make sense for instances of Y that are unique to an instance of X? I am not sure how this would work if multiple instances of X (or other classes) would hold a reference to the same instance of Y (and thus the same GUID)? Feb 14, 2020 at 8:15
  • "implement a method NewGuids in every class and use it for propagating the generation of new guids to referenced objects." I think this is effectively what I need to implement. But I am not sure what the pattern of the method would look like. Would you have an example or a reference to an example? Feb 14, 2020 at 8:17
  • 1
    @Superman.Lopez: I rewrote my answer completely. You example gives the impression you have only a tree to handle, but your comment says its more complicated, so I think I have to adress the problem more generally.
    – Doc Brown
    Feb 14, 2020 at 13:31
  • @Superman.Lopez it’s far better to edit revisions into the question then clarify in comments. Feb 14, 2020 at 19:54
  • 1
    @rayx: object graph traversal requires each object to deliver the referenced objects somehow, in a generic manner. In case one does not want to use reflection for this (see my comment below this answer), a virtual function in each class can be used for this.
    – Doc Brown
    Mar 21, 2021 at 15:40
1
  1. Memberwise clone all the green and yellow objects
  2. Walk the list of green and yellow objects and update their Guids
  3. Add the old and new guid pairs to a Dictionary
  4. Walk the list of green and yellow objects again and swap the Refs using the Dictionary created in the previous step

You can also serialize and then deserialize to a format that supports object references, like protobuf or maybe avro. You'd have to have actual object references however (not GUIDs).

1

Following Doc Brown's recommended steps of GUID generation/replacement, and this SO answer to recursively go through all properties of the object graph (https://stackoverflow.com/a/20554262/10340388) I came with the following solution. The function is recursive for collections of objects as well as classes that could hold GUID's. It can probably be cleaned up and made DRY-er, but it works for me so far.

Creation of the dictionary:

private Dictionary<Guid, Guid> CreateDictionaryOfGuids(object obj, Dictionary<Guid, Guid> guidDictionary)
{
    Type objType = obj.GetType();  // Type of object
    PropertyInfo[] typeProperties = objType.GetProperties();  // Properties of type
    foreach (PropertyInfo property in typeProperties) // Go through all properties
    {
        object propValue = property.GetValue(obj, null); // Get the value of that property in Obj
        Type propType = property.PropertyType;
        var elems = propValue as IList;  // cast the value of the property as IList 
        if (elems != null) // and see if it's valid if != null
        {
            foreach (var item in elems)  // If IList become recursive into the collection
            {
                Type itemType = item.GetType();
                if (PropertyTypeRequiresRecursive(itemType))
                    guidDictionary = CreateDictionaryOfGuids(item, guidDictionary); 
            }
        } 
        else if (propType == typeof(Guid)) // Check if property is a Guid
        {
            Guid existingGuid = (Guid)property.GetValue(obj, null);
            if (!guidDictionary.ContainsKey(existingGuid)) // If Guid doesn't exist in dictionary
            {
                Guid newGuid = Guid.NewGuid();
                guidDictionary.Add(existingGuid, newGuid);  // Add the old and the new Guid to the dictionary
            }
        } 
        else if (PropertyTypeRequiresRecursive(propType) && propValue != null)  // Make method recursive into properties of that class
        {
            guidDictionary = CreateDictionaryOfGuids(propValue, guidDictionary); 
        }
    }
    return guidDictionary;
}

Replacement of the GUID values:

private object ReplaceGuids(object obj, Dictionary<Guid, Guid> guidDictionary)
{
    Type objType = obj.GetType();  // Type of object
    PropertyInfo[] typeProperties = objType.GetProperties();  // Properties of type
    foreach (PropertyInfo property in typeProperties) // Go through all properties
    {
        object propValue = property.GetValue(obj, null); // Get the value of that property in Obj
        Type propType = property.PropertyType;
        var elems = propValue as IList;  // cast the value of the property as IList 
        if (elems != null) // and see if it's valid if != null
        {
            for (int i = 0; i < elems.Count; i++) // If IList become recursive into the collection
            {
                Type itemType = elems[i].GetType();
                if (PropertyTypeRequiresRecursive(itemType))
                    elems[i] = ReplaceGuids(elems[i], guidDictionary); 
            }
        } else if (propType == typeof(Guid)) // Check if property is a Guid
        {
            Guid existingGuid = (Guid)property.GetValue(obj, null);
            if (guidDictionary.ContainsKey(existingGuid))
            {
                Guid newGuid = guidDictionary[existingGuid];
                property.SetValue(obj, newGuid);  // set to the new GUID from dictionary
            }
        } else if (PropertyTypeRequiresRecursive(propType) && propValue != null)  // Make method recursive into properties of that class
        {
            object newPropValue = ReplaceGuids(propValue, guidDictionary);
            property.SetValue(obj, newPropValue);
        }
    }
    return obj;
}
private bool PropertyTypeRequiresRecursive(Type type)
{
    bool propIsPrimitive = type.IsPrimitive;
    if (propIsPrimitive == true || type == typeof(string) || type == typeof(DateTime)) return false;
    return true;
}

and then I call this in the following fashion, where MainObject mainObject is assumed to already be a deep copy/clone:

Dictionary<Guid, Guid> guidDictionary = new Dictionary<Guid, Guid>();
guidDictionary = CreateDictionaryOfGuids(mainObject, guidDictionary);
mainObject = (MainObject)ReplaceGuids(mainObject, guidDictionary);
3
  • My actual graph doesn't have Guid's that are typed as nullable, but I noticed my example did. For those it might be necessary to test for Nullable<Guid>. Feb 17, 2020 at 7:56
  • To be honest, I am not really a fan of such a reflection based solution. It might work for this specific case, but as soon as there are objects in the graph which hold reference to objects which don't belong to the graph, PropertyTypeRequiresRecursive cannot distinguish them any more and the whole things breaks. I upvoted it, nevertheless, there are surely cases where this is useful.
    – Doc Brown
    Mar 21, 2021 at 15:32
  • @DocBrown Thanks, I will review your suggestion on Graph Traversal once more when I have some time. I was somewhat familiar with reflections, which is why I implemented it this way as it was working for me. Mar 23, 2021 at 1:20

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.