[Journal - Flexibly Navigated Object Graphs]

Flexibly Navigated Object Graphs

Sunday, September 25, 2005

Remember that I introduced a general API for navigating among objects using reflection this spring? This is implemented at two levels:

  1. The GetChildObjects() methods in the Gregor.Core.Reflect module provide a simple, procedural query approach for immediate child objects.
  2. The CObjectGraph class and related types (in Gregor.Core.Collections) wrap a graph of objects into a lazily-built tree structure. The nodes in the tree (CGraphNode) allow adding arbitrary properties. Navigation is based on the general Reflect.GetChildObjects() API.

Building on the second layer, I've found another way to use the .NET Console code interpreter: object relationships need not be deduced blindly from references and collections stored in fields or returned from properties, but can be obtained more flexibly from expressions that a code interpreter (implementing Gregor.Core.ICodeInterpreter2) is capable of evaluating.

Using the code interpreter opens up a world of opportunities, such as filtering, sorting, or grouping objects using standard APIs, formatting strings, and so on, but the most useful task is even establishing a relationship between otherwise unrelated (that is, by object references) objects:

class Customer {
    // no link to orders available!
}

static class OrderUtil {
    public static List<Order> GetOrders(Customer cust);
}

The code interpreter can simply call the latter function, and pass the current customer object. And thereby establish an association.

Code Time

So let's look at an example. Our root object is the following:

object root = new object();

Our task shall be to create a tree that contains the object's type, its members, and a few gimmicks later on. How will it work?

A CObjectGraph can be built with path information contained in an XML document, known as a path information document. A path info document describes a multifold path through a data structure. The XML document may (but doesn't have to) contain expressions for the code interpreter. Here's the template:

<Root>
    <Type select="Root.GetType()">
        <FullName />
        <Members select="Type.GetMembers()">
            <Member select="Members[*]" />
        </Members>
    </Type>
</Root>

A few observations:

Let's run this example:

private static void TestPathInfoGraph(){

    ICodeInterpreter2 preter = new Gregor.NetConsole.Engine.CCurlyInterpreter();
    XmlDocument xd = Dev.ReadXmlFile("ObjectPathSimple.xml");
    
    CObjectGraph graph = CObjectGraph.CreateWithPathInfo(new object(), xd, preter);
    TraceGraph(graph);
}

private static void TraceGraph(CObjectGraph graph){
    ITreeWalkHelper helper = new CDefaultTreeWalkHelper(graph);
    CTreeWalker walker = new CTreeWalker(helper);
    NStringIndents indents = new NStringIndents("  ");
    while(walker.MoveNext()){
        CGraphNode graphNode = (CGraphNode) walker.Current;
        Dev.Trace(indents[walker.Level] + graphNode.Value);
    }
}

The following output will be produced:

System.Object
  System.Object
    System.Object
    System.Reflection.MemberInfo[]
      Int32 GetHashCode()
      Boolean Equals(System.Object)
      System.String ToString()
      Boolean Equals(System.Object, System.Object)
      Boolean ReferenceEquals(System.Object, System.Object)
      System.Type GetType()
      Void .ctor()

Working with the Graph

The graph's nodes (CGraphNode) store their corresponding XML elements, which may be used by an application for assistance in further processing. Here's how you can obtain an element, and read its attributes:

foreach(CGraphNode graphNode in Walk.Tree(graph)){
    XmlElement xe = (XmlElement) graphNode.Properties[CGraphNode.PROPERTY_PATHINFOELEMENT];
    XmlAttribute xa = xe.Attributes["MyAttribute"];
    // ... use xa
}

That said, there are a number of attribute names reserved for use by the graphing implementation:

Here are some examples for using the filtering attributes:

<!-- select member type only for constructors -->
<Member select="Members[*]">
    <MemberType filter="System.Reflection.MemberTypes.Constructor" />
</Member>

<!-- select members containing "q" -->
<Member select="Members[*]" match="*q*" />

<!-- select members whose name starts with "Get" -->
<Member select="Members[*]" condition="Member.Name.StartsWith(&#x22;Get&#x22;)" />

<!-- select declared members only
     ("Type" is the element name of an ancestor node, see above) -->
<Member select="Members[*]" condition="Member.DeclaringType.Equals(Type)" />

See the TestCore3 project in the Gregor.Core source download for a comprehensive expample using these attributes.

Outlook

Object graphs built with path information can be used for a variety of purposes, such as:

The idea is that a data model is defined once, and ideally only once, and everything else - GUI, persistance, querying - builds on that model. Things should work automatically, or at least with little effort, like templated code generation. The data model, while it should be maintained as class definitions ("only once"; aspect orientation) can be modified somewhat if object graphs can be created more flexibly. This is about "smart data structures" versus "smart tools"; I'm leaning toward the latter, lately.