Method to load an entire object graph using ADO.Net Entity Framework

When using the ADO.Net Entity Framework, you will likely run into a situation where you are going to want to use the "Include" method to shape your results.  For example, if you are pulling a list of Order objects you may also want to pull back the Customer object linked to that order and possibly the OrderItems.  Your query may look like this:

Dim ord() As MyModel.Order = (from o As MyModel.Order.Include("Customer").Include("Items.OrderedBy")).ToArray

After running numerous tests, I found (at least for my model) that the Include method is rarely performant.  Instead, I've found the Load method to be much quicker even though you may potentially cause alot of server round-trips to occur.  I don't really want to get into too much detail about when to use either method, instead I think you should test both methods out in your own environment and decide the method that works best for you.  There is a pretty good article on MSDN that explains the difference between Load and Include. It is available at: http://msdn.microsoft.com/en-us/library/bb896272.aspx

Seeing as I prefer Load over Include, I still find it an inconvenient method to use.  I really like being able to specify an object graph such as when using the Include method.  I am not sure if there's a way to provide the object graph with Load but I can safely say, I found no such way and I am unsure as to why MS wouldn't provide one.

Below, I have posted a method that I have written to load an object graph of an entity or collection of entities.  This eliminates the need to traverse entity sets in my program.  The result is the same as Include except it uses the Load method to populate the paths.

Before I reveal the code, a few comments/warnings:

  • You must enable MultipleActiveResultSets (MARS) on your connection.  If you don't, the IncludeByRoundTrip method will not work on enumerables.
  • The method will not force a reload of a property that is already loaded.  This helps speed it up.
  • Make sure you manually open the connection on your model before calling IncludeByRoundTrip.  If you don't, the EF will automatically open and close the connection for each call to Load resulting in poor performance.
  • You can call IncludeByRoundTrip at any time, not just in queries.  It will populate the properties you specify.
  • I haven't had a chance to optimize the code.  I am sure a few things can be cleaned up a bit.  Feel free to make some mods and post.
  • The code is in VB.Net.  If anyone has time to convert to C#, send me the code and I can post it.

When using the IncludeByRoundTrip method, the above statement could be rewritten as:

Dim ord() As MyModel.Order = (from o As MyModel.Order.IncludeByRoundTrip("Customer").IncludeByRoundTrip("Items.OrderedBy")).ToArray

If you are filtering the results, you will want to filter them before calling the IncludeByRoundTrip method.  If you don't, you will waste resources pulling back entities you don't want or need.

Dim ord() As MyModel.Order = (from o As MyModel.Order Where o.ID=123).IncludeByRoundTrip("Customer").IncludeByRoundTrip("Items.OrderedBy").ToArray

Lemme know if you have any questions.  Thanks to JK for your work on this... Here is the code: 

Module
ExtensionMethods

    <System.Runtime.CompilerServices.Extension(), DebuggerNonUserCode()> _

    Public Function IncludeByRoundTrip(Of T As System.Data.Objects.DataClasses.EntityObject)(ByVal entity As T, ByVal includes As String) As T

        Return IncludeByRoundTrip(entity, New String() {includes})

    End Function

 

    <System.Runtime.CompilerServices.Extension(), DebuggerNonUserCode()> _

    Public Function IncludeByRoundTrip(Of T As System.Data.Objects.DataClasses.EntityObject)(ByVal entity As T, ByVal includes() As String) As T

        If includes IsNot Nothing Then

            For Each include As String In includes

                Dim includeSplit() As String

                includeSplit = include.Split("."c)

 

                For i As Integer = 1 To includeSplit.Length

                    LoadInclude(entity, String.Join("."c, includeSplit.Take(i).ToArray))

                Next

            Next

        End If

 

        Return entity

    End Function

 

    <System.Runtime.CompilerServices.Extension(), DebuggerNonUserCode()> _

    Public Function IncludeByRoundTrip(Of T As System.Data.Objects.DataClasses.EntityObject)(ByVal col As System.Data.Objects.DataClasses.EntityCollection(Of T), ByVal includes As String) As System.Data.Objects.DataClasses.EntityCollection(Of T)

        Return IncludeByRoundTrip(col, New String() {includes})

    End Function

 

    <System.Runtime.CompilerServices.Extension(), DebuggerNonUserCode()> _

    Public Function IncludeByRoundTrip(Of T As System.Data.Objects.DataClasses.EntityObject)(ByVal col As System.Data.Objects.DataClasses.EntityCollection(Of T), ByVal includes() As String) As System.Data.Objects.DataClasses.EntityCollection(Of T)

        If includes IsNot Nothing Then

            For Each entity In col

                For Each include As String In includes

                    Dim includeSplit() As String

                    includeSplit = include.Split("."c)

 

                    For i As Integer = 1 To includeSplit.Length

                        LoadInclude(entity, String.Join("."c, includeSplit.Take(i).ToArray))

                    Next

                Next

            Next

        End If

 

        Return col

    End Function

 

    <System.Runtime.CompilerServices.Extension(), DebuggerNonUserCode()> _

    Public Function IncludeByRoundTrip(Of T As System.Data.Objects.DataClasses.EntityObject)(ByVal col As System.Collections.Generic.IEnumerable(Of T), ByVal includes As String) As System.Collections.Generic.IEnumerable(Of T)

        Return IncludeByRoundTrip(col, New String() {includes})

    End Function

 

    <System.Runtime.CompilerServices.Extension(), DebuggerNonUserCode()> _

    Public Function IncludeByRoundTrip(Of T As System.Data.Objects.DataClasses.EntityObject)(ByVal col As System.Collections.Generic.IEnumerable(Of T), ByVal includes() As String) As System.Collections.Generic.IEnumerable(Of T)

        If includes IsNot Nothing Then

            For Each entity In col

                For Each include As String In includes

                    Dim includeSplit() As String

                    includeSplit = include.Split("."c)

 

                    For i As Integer = 1 To includeSplit.Length

                        LoadInclude(entity, String.Join("."c, includeSplit.Take(i).ToArray))

                    Next

                Next

            Next

        End If

 

        Return col

    End Function

 

    ''' <summary>

    ''' Loads the specified object graph on the supplied entity.  The last node of the object graph must be the only unloaded property.

    ''' </summary>

    ''' <param name="entity">The entity to load the path of.</param>

    ''' <param name="include">The path to load.  The last node of the object graph must be the only unloaded property.</param>

    ''' <remarks></remarks>

    <DebuggerNonUserCode()> _

    Private Sub LoadInclude(ByVal entity As Object, ByVal include As String)

        Dim refProperty As System.Reflection.PropertyInfo = Nothing

        Dim refPropertyValue As Object

        Dim isLoadedProperty As System.Reflection.PropertyInfo

        Dim loadMethod As System.Reflection.MethodInfo

        Dim objTraverse As Object

 

        If entity Is Nothing Then Exit Sub

        If include Is Nothing OrElse include.Length = 0 Then Throw New Exception("Include must refer to a property.")

 

        ' Parse the object graph.

        Dim includes() As String = include.Split("."c)

        Dim includesLast As Integer = includes.Length - 1

 

        ' Find the Reference property for the object graph provided.  For example, if the

        ' entity has a reference called Customer, this would find the property

        ' CustomerReference.

        objTraverse = entity

 

        For i As Integer = 0 To includesLast

            Dim inc As String = includes(i)

 

            If objTraverse Is Nothing Then Exit Sub

 

            If i = includesLast Then

                ' This is the last node in the object graph. Search for the Reference property.

                refProperty = objTraverse.GetType.GetProperty(inc & "Reference")

 

                ' Reference property was not found, check to see if the property itself exists.

                If refProperty Is Nothing Then refProperty = objTraverse.GetType.GetProperty(inc)

            Else

                ' Search for the property in the object graph.

                refProperty = objTraverse.GetType.GetProperty(inc)

            End If

 

            ' Property was not found, throw an exception.

            If refProperty Is Nothing Then

                Throw New Exception("Invalid property name in includes.")

            End If

 

            If refProperty.PropertyType.Name = "EntityCollection`1" AndAlso i < includesLast Then

                ' We stumbled upon an EntityCollection property.  Furthermore, we are not at

                ' the end of the object graph we are traversing, so we need to continue

                ' traversing the object path against each entity within the collection.

 

                objTraverse = refProperty.GetValue(objTraverse, Nothing)

 

                ' Get the elements out of the collection.

                ' I didn't want to call ToArray but it seems when working with the enumerable

                ' the code would fail.  Not sure why.

                Dim enumType As System.Type = GetType(System.Linq.Enumerable)

                Dim collectionType As System.Type = objTraverse.GetType.GetGenericArguments(0)

 

                Dim arrSubEntities As System.Array = DirectCast(enumType.GetMethod("ToArray").MakeGenericMethod(collectionType).Invoke(Nothing, New Object() {objTraverse}), System.Array)

 

                For Each subEntity As Object In arrSubEntities

                    LoadInclude(subEntity, String.Join(".", includes.Skip(i + 1).ToArray))

                Next

 

                Exit Sub

            ElseIf i < includesLast Then

                ' We are not at the last property in our graph yet.

                ' Reiterate off of the property we just landed on.

                objTraverse = refProperty.GetValue(objTraverse, Nothing)

            End If

        Next

 

        ' Get the value of the reference property.  This should be an

        ' EntityReference(Of T) object.

        refPropertyValue = refProperty.GetValue(objTraverse, Nothing)

 

        ' Get the IsLoaded property from the EntityReference.

        isLoadedProperty = refPropertyValue.GetType.GetProperty("IsLoaded", GetType(Boolean))

 

        ' Get the value of the IsLoaded Property.  If true, there is nothing to load.

        If DirectCast(isLoadedProperty.GetValue(refPropertyValue, Nothing), Boolean) Then Exit Sub

 

        ' The included reference is not loaded, go ahead and load it.

        ' Get the Load method from the EntityReference property.

        loadMethod = refPropertyValue.GetType.GetMethod("Load", New System.Type() {})

 

        ' Invokde the Load method.

        loadMethod.Invoke(refPropertyValue, Nothing)

    End Sub

 

End Module

 

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments

  • July 30, 2009 6:36 PM Colby wrote:
    I like the idea...
    I've not yet began opts on my project, but since I have solely used ".Include()"s this could prove to be quite worthwhile.
    Reply to this
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.