2
\$\begingroup\$

I have an object graph which contains two types of objects that I care about:

Link objects that contain an ID and a reference to the object with that ID.

ILinkable objects that can be pointed to by Link objects.

These objects can exist at an arbitrary depth within my object graph and can also be contained within collections within that graph.

To solve this, I've written a method which recursively enumerates every object within the object graph that is in the relevant namespace.


    private static bool IsInModelNamespace(Type t) => t.Namespace!.StartsWith("UniversalDiveDataFormat.Models");
    private static bool IsList(Type type) => type.IsGenericType && type.GetGenericTypeDefinition().IsAssignableTo(typeof(IList));
    private static bool IsListOfModelObjects(Type type) => IsList(type) && IsInModelNamespace(type.GetGenericArguments().Single());

private IEnumerable<object> AllModelObjectsInGraph(object root)
    {
        PropertyInfo[] properties = root.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty);

        foreach (PropertyInfo property in properties)
        {
            object? propertyValue = property.GetValue(root);
            if (propertyValue is null) continue;
            if (IsListOfModelObjects(property.PropertyType))
            {
                IList list = (IList)propertyValue;
                foreach (object? item in list)
                {
                    if (item is null) continue;
                    yield return item;
                    foreach (var innerObject in AllModelObjectsInGraph(item))
                    {
                        yield return innerObject;
                    }
                }
                continue;
            }
            
            if (!IsInModelNamespace(property.PropertyType)) continue;
            
            yield return propertyValue;
            foreach (object innerObject in AllModelObjectsInGraph(propertyValue))
            {
                yield return innerObject;
            }
        }
    }

All of the ILinkable objects are indexed by ID. Then all of the Link objects have their references set.

    private void BuildLinks()
    {
        foreach (Link link in _links)
        {
            if (!_linkableLookup.TryGetValue(link.Ref, out ILinkable? linkable) && _failOnMissingLink)
            {
                throw new ArgumentException($"The linkable with id {link.Ref} could not be found");
            }

            link.SetLinkedObject(linkable);
        }
    }

    private void FindAllLinkablesAndLinksInSubgraph(object root)
    {
        foreach (object model in AllModelObjectsInGraph(root))
        {
            if (model.GetType().IsAssignableTo(typeof(ILinkable)))
            {
                ILinkable linkable = (ILinkable)model;
                if (linkable.Id is not null)
                {
                    _linkableLookup[linkable.Id] = linkable;
                }
            }
            
            if (model is Link link)
            {
                _links.Add(link);
            }
        }
    }

And this is all called from this method:

    public void ResolveAllLinksInObjectGraph(object root)
    {
        // Maybe make everything inherit from an empty base and use that rather than object
        if (!IsInModelNamespace(root.GetType()))
        {
            throw new ArgumentException("The root object must belong to the UDDF Models namespace");
        }

        FindAllLinkablesAndLinksInSubgraph(root);
        BuildLinks();
    }

Now, based on some limited testing this works.

However, I absolutely hate the AllModelObjectsInGraph() method. For one, this snippet of code basically appears twice where an item is returned and then recursed on:

yield return item;
foreach (var innerObject in AllModelObjectsInGraph(item))
{
    yield return innerObject;
}

But I can't think of how to pull that out because I don't think you can yield return from a deeper method.

I then have the same issue with everything inside if (IsListOfModelObjects(property.PropertyType)) I want to pull that out into a separate HandleCollectionEnumeration() method or something.

Really, I think what I'm looking for is some kind of multi yield keyword that would let me have other methods that return Enumerables and call those yielding on each entry until they're done and I move on. I don't think that mechanism exists though.

Any ideas on how I can make this nicer or is it not actually as bad as I think it is?

\$\endgroup\$
4
  • \$\begingroup\$ Do Link and ILinkable objects have a shared interface or shared abstraction ? \$\endgroup\$
    – iSR5
    Commented May 10 at 20:01
  • \$\begingroup\$ @iSR5 - No, a Link contains a Linkable \$\endgroup\$ Commented May 10 at 20:14
  • \$\begingroup\$ Is it possible to filter out the properties to only target Link and IList objects only to return IEnumerable<Link> ? \$\endgroup\$
    – iSR5
    Commented May 10 at 20:58
  • \$\begingroup\$ I want both. The purpose of this whole thing is that I need to find all the ILinkable objects, read their Id, and then set them as the reference in the Link where ref == id. \$\endgroup\$ Commented May 11 at 21:24

1 Answer 1

2
\$\begingroup\$

I think the answer here is to just not use a yielding iterator because I don't really need to:

public void ResolveAllLinksInObjectGraph(object root)
    {
        // Maybe make everything inherit from an empty base and use that rather than object
        if (!IsInModelNamespace(root.GetType()))
        {
            throw new ArgumentException("The root object must belong to the UDDF Models namespace");
        }

        FindAllLinkablesAndLinksInSubgraph(root);
        BuildLinks();
    }

    private void BuildLinks()
    {
        foreach (Link link in _links)
        {
            if (!_linkableLookup.TryGetValue(link.Ref, out ILinkable? linkable) && _failOnMissingLink)
            {
                throw new ArgumentException($"The linkable with id {link.Ref} could not be found");
            }

            link.SetLinkedObject(linkable);
        }
    }

    private static bool IsInModelNamespace(Type t) => t.Namespace!.StartsWith("UniversalDiveDataFormat.Models");
    private static bool IsList(Type type) => type.IsGenericType && type.GetGenericTypeDefinition().IsAssignableTo(typeof(IList));
    private static bool IsListOfModelObjects(Type type) => IsList(type) && IsInModelNamespace(type.GetGenericArguments().Single());
    
    private void FindAllLinkablesAndLinksInSubgraph(object root)
    {
        PropertyInfo[] properties = root.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty);

        foreach (PropertyInfo property in properties)
        {
            object? propertyValue = property.GetValue(root);
            if (propertyValue is null) continue;
            if (IsListOfModelObjects(property.PropertyType))
            {
                ProcessListOfModelObjects((IList)propertyValue);
                continue;
            }
            
            if (!IsInModelNamespace(property.PropertyType)) continue;
            
            ProcessModelObject(propertyValue);
            FindAllLinkablesAndLinksInSubgraph(propertyValue);
        }
    }

    private void ProcessListOfModelObjects(IList list)
    {
        foreach (object? item in list)
        {
            if (item is null) continue;
            ProcessModelObject(item);
            FindAllLinkablesAndLinksInSubgraph(item);
        }
    }
    
    private void ProcessModelObject(object model)
    {
        if (model.GetType().IsAssignableTo(typeof(ILinkable)))
        {
            ILinkable linkable = (ILinkable)model;
            if (linkable.Id is not null)
            {
                _linkableLookup[linkable.Id] = linkable;
            }
        }
        
        if (model is Link link)
        {
            _links.Add(link);
        }
    }

This solves the problem in my specific case, but if I was exposing this method externally and wanted it to be available as an enumerator, this solution wouldn't be viable.


Or potentially (I'm not sure) even nicer:

    private void FindAllLinkablesAndLinksInSubgraph(object? root)
    {
        if (root is null) return;
        PropertyInfo[] properties = root.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty);
        ProcessModelObject(root);
        foreach (PropertyInfo property in properties)
        {
            object? propertyValue = property.GetValue(root);
            if (propertyValue is null) continue;
            if (IsListOfModelObjects(property.PropertyType))
            {
                ((IEnumerable)propertyValue).Cast<object>().ToList().ForEach(FindAllLinkablesAndLinksInSubgraph);
                continue;
            }
            
            if (!IsInModelNamespace(property.PropertyType)) continue;
            FindAllLinkablesAndLinksInSubgraph(propertyValue);
        }
    }
    
    private void ProcessModelObject(object model)
    {
        if (model.GetType().IsAssignableTo(typeof(ILinkable)))
        {
            ILinkable linkable = (ILinkable)model;
            if (linkable.Id is not null)
            {
                _linkableLookup[linkable.Id] = linkable;
            }
        }
        
        if (model is Link link)
        {
            _links.Add(link);
        }
    }
```
\$\endgroup\$

Your Answer

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

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