Reputation: 4844
lengthy software architecture question ahead
CLARITY EDIT: I am trying to convert an object graph that consists of types like
NodeA
, NodeB
, ... to an object graph that consists of types like *My*NodeA
, *My*NodeB
..., and vice versa. The properties in the NodeX
types correspond to similar properties in the MyNodeX
types, but in many cases it is not just a trivial assignment.
If I have two similar class structures like this:
// pure model, i.e. minimal information that is convenient for storage
abstract class Node
{
public int BaseProperty { get; set; }
public NodeCollection Children { get; private set; } // : Collection<Node>
}
class NodeA /* NodeB, NodeC ... */ : Node
{
public int DerivedAProperty { get; set; }
}
and
// objects that are convenient for being used by the application
abstract class MyNode
{
public int MyBaseProperty { get; set; }
public MyNodeCollection Children { get; private set; } // : Collection<MyNode>
}
class MyNodeA /* MyNodeB, MyNodeC ... */ : MyNode
{
public int MyDerivedAProperty { get; set; }
}
, and I need to convert an object graph of the NodeX
type to one of the MyNodeX
type, or vice versa, without changing any of the NodeX
classes at all, I've found myself using this pattern regularly:
NodeX -> MyNodeX
// USAGE / external code
Node node = ...
MyNode myNode = MyNode.Load(node, ARGS); // static factory
abstract class MyNode
{
...
// factory
public static MyNode Load(Node node, ARGS)
{
var type = node.GetType();
MyNode myNode;
// no 'is' usage because NodeB could be derived from NodeC etc.
if (type == typeof(NodeA))
myNode = new MyNodeA(ARGS); // arbitrary ctor
else if (...)
...
myNode.Load(Node);
return myNode
}
public virtual void Load(Node node)
{
this.MyBaseProperty = node.BaseProperty;
foreach (var child in node.Children)
this.Children.Add(MyNode.Load(child, this.ARGS));
}
}
class MyNodeA : MyNode
{
...
public override void Load(Node node)
{
var m = (NodeA)node; // provoke InvalidCastException if coding error
base.Load(node);
this.MyDerivedAProperty = m.DerivedAProperty;
}
}
MyNodeX -> NodeX
// USAGE / external code
MyNode myNode = ...
Node node = myNode.Commit();
abstract class MyNode
{
...
// 'kind of' factory
public abstract Node Commit();
public virtual Commit(Node node)
{
node.BaseProperty = this.MyBaseProperty;
foreach (var child in this.Children)
node.Children.Add(child.Commit());
}
}
class MyNodeA : MyNode
{
...
public override Node Commit()
{
var m = new NodeA(); // "factory" method for each type
this.Commit(m);
return m;
}
public override void Commit(Node node)
{
var m = (NodeA)node; // provoke InvalidCastException if coding error
base.Commit(node);
m.DerivedAProperty = this.MyDerivedAProperty;
}
}
I have used this approach multiple times successfully and I generally like it, because the methods that have to be added to the class are straight forward, and so is the external code. Also, it avoids code duplication by calling base.Load(node)
/ base.Commit(node)
. However, I really don't like that if/else ladder in the static Load
factory method.
I would prefer to have a factory method in each type for the Node -> MyNode (Load
) case, similar to how it is in the MyNode -> Node (Commit
) case. But static
and virtual
is obviously a bit problematic. I would also prefer to not do the two casts I have to do now.
Is achieving such a thing somehow possible?
Upvotes: 0
Views: 261
Reputation: 9474
My recommendation would be to solve the problem incrementally. First you'll need something to traverse the tree and convert each node along the way:
public static class NodeExtensions
{
public static MyNode ToMy( this Node node )
{
var result = node.Transform();
result.Children = node.Children.Select( ToMy ).ToList();
}
public static Node FromMy( this MyNode node )
{
var result = node.Transform();
result.Children = node.Children.Select( ToMy ).ToList();
}
public static MyNode Transform( this Node node )
{
// TODO code to transform any single node here
}
public static Node Transform( this MyNode node )
{
// TODO code to transform any single node here
}
}
Since you mention that the transformation from Node to MyNode is not a simple matter of copying properties, yet also indicate that there will be a lot of that going on, my initial thought is that this is a task for AutoMapper.
AutoMapper lets you create a "conversion profile" that describes which properties to map and any special rules you want to apply to any given mapping. Also, it provides both generic and non-generic methods, so you can use it even if you do not know the types at compile-time. It is commonly used to convert between entities and view models, so you'll find plenty of questions and answers related to its usage elsewhere here.
Defining type maps basically consists of a number of calls like this:
Mapper.CreateMap<Node,MyNode>(); // no special rules for this map
You'll have to consule the AutoMapper documentation for the specifics about how to create special mappings, like splitting properties or performing type conversions. You'll also need to create maps going both ways in order to be able to map in either direction.
Once you have defined all of your mappings, the Transform extension methods can be as simple as:
public static MyNode Transform( this Node node )
{
return Mapper.Map( node.GetType(), node.GetMatchingMyType(), node );
}
public static Type GetMatchingType( this Node node )
{
// you can use a dictionary lookup or some other logic if this doesn't work
var typeName = "My" + node.GetType().Name;
return typeof(MyNode).Assembly.GetTypes().Single( t => t.Name == typeName );
}
When everything is in place, you can convert the entire tree by writing:
var myTree = node.ToMy();
// and back
node = myTree.FromMy();
Upvotes: 1
Reputation: 2256
Is everything as consistently named as you present above?
If so, you could build a set of generic convert-to convert-from functions that use reflection.
Here is what I'm thinking (this is stream of conciousness, not verified compiled code):
<T> ConvertTo<TMy, T>(TMy object)
{
// create an object of type T
T newObj = new T();
// iterate the members of T using reflection
foreach(member in T)
{
// find the equavalent My members in TMy
// transfer the data
}
return newObj;
}
I will look into this a bit more and possibly generate working code sometime this weekend.
Upvotes: 0