I came across an issue where I had a requirement to load a full object tree with all its relationships. This article will show you how to traverse the full tree of the object through their relationship till you reach the leaf.
Introduction
Years ago, I read about the Golden rule of Entity Framework from a blog post of Julia Lerman. As the Golden Rule says – EF will not do anything that the developer has not explicitly told you to do.
Some days ago, I came across an issue where I needed to load a full object tree with all its relationship. It means I had to load the object with all children and also with grand children of each child which means I had to traverse the full tree of the object through their relationship/navigation until I reached the leaf.
Background
For example, the tree can be represented as:
<Customer>
...
<Orders>
<Order>
...
<Order_Details>
</Order_Detail>
................So on...(Continue..)
</Order_Detail>
...
</Order_Details>
</Order>
...
</Orders>
...
</Customer>
Or can be represented as:
Customer
|
|
-----Orders
|
|
------OrderDetails
|
|………So on…….......(Continue...)
So I don’t know how much depth the object-tree will have and that’s why I am going to avoid the LINQ way. You can also do this by disabling the Lazy-loading, but POCO entities do not have the same relationship requirements as objects that inherit from EntityObject, a slightly different process is required to load related objects. OK, it is a very simple matter of Recursive solution by loading entity for each navigation object. To load the navigation property, I got a simple and useful method there:
ObjectContext.LoadProperty Method (Object, String)
public void LoadProperty(
Object entity,
string navigationProperty)
Using the Code
Before going into my recursive method, I would like to talk about my Entity. All of my entities are children of the BaseDataContract
class. While traversing the object, you can set a list of data type also to ignore while traversing them. Here, I check the navigation property whether that is a one or many relationship with the current entity. If it is one – then I simply call the same method that it is currently in Recursive way; otherwise, I call another Generic method which calls the Traverse
method for each entity in a collection for many relationship ends.
private ObjectContext _ctx;
private bool IsInheritFromType(Type childType, Type parentType)
{
var type = childType;
while (type.BaseType != null)
{
if (type.BaseType.Equals(parentType))
return true;
type = type.BaseType;
}
return false;
}
public void TraverseToTheEnd<T>(T entity, Type parentType =null,
Type[] ignoreType =null ) where T : BaseDataContract
{
if (null != entity)
{
var navigationProperties = _ctx.GetNavigationProperty<T>();
if (null != navigationProperties)
{
foreach (var navigationProperty in navigationProperties)
{
var info = entity.GetType().GetProperty(navigationProperty.Name);
if (null != info)
{
Type t = info.PropertyType;
if (null == ignoreType || !Array.Exists(ignoreType, i => i.Equals(t)))
{
if (((AssociationType)
navigationProperty.RelationshipType).IsForeignKey)
{
_ctx.LoadProperty(entity, navigationProperty.Name);
var value = info.GetValue(entity, null);
if (value != null)
{
Type baseType;
if (!t.IsGenericType)
{
#region One to One or Zero
baseType = t.BaseType;
if (null != baseType && (parentType == null ||
!t.Equals(parentType))
&& baseType.BaseType != null
&& IsInheritFromType
(baseType, typeof (BaseDataContract)))
{
MethodInfo method =
this.GetType().GetMethod("TraverseToTheEnd");
MethodInfo generic = method.MakeGenericMethod(t);
generic.Invoke(this, new object[]
{value, typeof (T), ignoreType});
}
#endregion
}
else
{
#region One to Many
Type[] genericBases = t.GetGenericArguments();
baseType = genericBases[0];
if (null != baseType && (parentType == null ||
!baseType.Equals(parentType))
&& baseType.BaseType != null &&
IsInheritFromType(baseType, typeof (BaseDataContract)))
{
MethodInfo method =
this.GetType().GetMethod
("TraverseToTheEndForCollection");
MethodInfo generic =
method.MakeGenericMethod(baseType);
generic.Invoke(this, new object[]
{value, typeof (T), ignoreType});
}
#endregion
}
}
}
}
}
}
}
}
}
public void TraverseToTheEndForCollection<T>(ICollection<T> entities,
Type parentType =null ,Type[] ignoreType =null )
where T : BaseDataContract
{
if (null != entities)
{
for (int i = 0; i < entities.Count; i++)
{
TraverseToTheEnd(entities[i], parentType, ignoreType);
}
}
}
To load the navigation property value, I just call the above method –loadProperty
which is provided by the ObjectContext
:
_ctx.LoadProperty(entity, navigationProperty.Name);
var value = info.GetValue(entity, null);
After that, check the navigation property type to see whether that is generic type or not. In EF, Generic navigation type is used for Collection of entity in case of Many End and on the other hand, normal property type for One or Zero End. Here, the method – IsInheritFromType
is nothing but check for BaseType
to some desired parent Type. This has been checked to determine whether it is database object or not. To call a Generic method:
MethodInfo method = this.GetType().GetMethod("TraverseToTheEnd");
MethodInfo generic = method.MakeGenericMethod(t);
generic.Invoke(this, new object[] {value, typeof (T), ignoreType});
And TraverseToTheEndForCollection
method has been called in the same way with the parameter of child/navigation property value parent type and other parameters. Here, I need to create some extension of ObjectContext
to get all navigation properties of an entity by manipulating the metadata
information. In GetNavigationProperty
method, we need to retrieve the entity set name (that’s why we need the GetEntitySet
method) and from its type members, it checks for navigationProperty
as BuiltInTypeKind
.
public static EntitySetBase GetEntitySet(this ObjectContext context, Type entityType)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (entityType == null)
{
throw new ArgumentNullException("entityType");
}
EntityContainer container = context.MetadataWorkspace.GetEntityContainer
(context.DefaultContainerName, DataSpace.CSpace);
EntitySetBase entitySet = container.BaseEntitySets.Where
(item => item.ElementType.Name.Equals(entityType.Name))
.FirstOrDefault();
return entitySet;
}
public static List<NavigationProperty> GetNavigationProperty<T>(
this ObjectContext context)
{
var containerName = context.DefaultContainerName;
var model = DataSpace.CSpace;
var workspace = context.MetadataWorkspace;
var container = workspace.GetEntityContainer(containerName, model);
EntitySetBase entitySet = context.GetEntitySet(typeof(T));
if (entitySet == null)
return null;
var navigationProps = entitySet.ElementType.Members
.Where(m => m.BuiltInTypeKind == BuiltInTypeKind.NavigationProperty
)
.Cast<NavigationProperty>()
.ToList();
return navigationProps;
}
OK, that is it!
Conclusion
Yes, you can ask me why I am going to such great lengths. The answer is, because I want to backup the object tree into a backup file and later on, it will be attached to a database that might be the same database or another one with same schema. It will not override any entity if any entity of this object-tree has already been existing. If not, then it will insert the object with related object as it is currently on its own. But that’s a different story. This method gives an object tree with all its related objects to the leaf and I can do whatever I want. For now, I would like to serialize this object in a file and in the next post, I will try to re-attach the object to database. Thanks for reading and hope you like it.
History
- 19th June, 2012: Initial version