2

I am building a repository for a new application which uses Entity Framework 5 as the backend data source. I have the basic CRUD operation working for simple models but I am struggling to understand how to update related objects on an update to an exising object.

Model

enter image description here

Imports System Imports System.Collections.Generic

Partial Public Class tblUser
    Public Property idUser As Integer
    Public Property username As String
    Public Property pwd As String

    Public Overridable Property tblUsermmRoles As ICollection(Of tblUsermmRole) = New HashSet(Of tblUsermmRole)
End Class

Imports System
Imports System.Collections.Generic

Partial Public Class tblUsermmRole
    Public Property idUser As Integer
    Public Property idRole As Integer

    Public Overridable Property tblUser As tblUser
    Public Overridable Property tblUserRole As tblUserRole

End Class

I am trying to add records to the tblUsermmRole object against a related tblUser record but I can't get the update to work.

POCO

Public Class User

    Public Property ID As Int32
    Public Property Username As String
    Public Property Password As String
    Public Property AccessRoles As IEnumerable(Of Int32)

    Public Sub New()
    End Sub

    Public Sub New(id As Int32, userName As String, password As String, roles As List(Of Int32))
        Me.ID = id
        Me.Username = userName
        Me.Password = password
        Me.AccessRoles = roles
    End Sub

End Class

Public Class UserRoles

    Public Property RoleID As Int32

    Public Sub New(roleID As Int32)
        Me.RoleID = roleID
    End Sub

End Class

Repository

Imports System.Data.Entity

Namespace DataAccess.Repository

    Public MustInherit Class EntityFramworkContextBase
        Inherits DbContext
        Implements IUnitOfWork

        Public Sub New(entityConnectionStringOrName As String)
            MyBase.New(entityConnectionStringOrName)
        End Sub

        Public Sub Add(Of T As Class)(obj As T) Implements IUnitOfWork.Add
            [Set](Of T).Add(obj)
        End Sub

        Public Sub Attach(Of T As Class)(obj As T) Implements IUnitOfWork.Attach

            Dim entity As T

            If ExistsInContext(obj) Then
                entity = ObjectInContext(obj)
                Entry(entity).CurrentValues.SetValues(obj)

            Else
                entity = [Set](Of T).Attach(obj)
            End If

            Entry(entity).State = EntityState.Modified

        End Sub

        Public Sub Commit() Implements IUnitOfWork.Commit
            MyBase.SaveChanges()
        End Sub

        Public Function [Get](Of T As Class)() As IQueryable(Of T) Implements IUnitOfWork.Get
            Return [Set](Of T)()
        End Function

        Public Function Remove(Of T As Class)(obj As T) As Boolean Implements IUnitOfWork.Remove

            Dim entity As T

            If ExistsInContext(obj) Then
                entity = ObjectInContext(obj)
            Else
                entity = [Set](Of T).Attach(obj)
            End If

            [Set](Of T).Remove(entity)

            Return True

        End Function

        Private Function ExistsInContext(Of T As Class)(obj As T) As Boolean
            Return [Set](Of T).Local.Any(Function(o) o.Equals(obj))
        End Function

        Private Function ObjectInContext(Of T As Class)(obj As T) As T
            Return [Set](Of T).Local.FirstOrDefault(Function(o) o.Equals(obj))
        End Function

    End Class

End Namespace

Problem

When calling the Entry(entity).CurrentValues.SetValues(obj) line in the Attach method the basic properties are copied over but the new elements for the tblUsermmRole object are not.

New Object with updated

enter image description here

Entity after SetValues

enter image description here

From initial research it looks like the SetValues method does not copy over related navigation properties as discussed in this post

Question

Given the repository pattern ( plus the UnitOfWork pattern) that I am using how do you maintain the object graph relationships and update the database?

Additional note

This method works as expected for new instances of the tblUsers object with attached tblUsermmRoles. Records for both tables are added maintaining foreign keys.

Community
  • 1
  • 1
Phil Murray
  • 6,396
  • 9
  • 45
  • 95

2 Answers2

1

I find it quite surprising that Entity Framework does not already support this but as usual the community has come to the rescue with RefactorThis' GraphDiff which can be downloaded via their GitHub repository

Looks like the Entity Framework team will look at this functionality post EF6 as its currently number 2 on there issue list and Rowan Miller seems to think its a good idea

RoMiller wrote Feb 14 at 11:15 PM

EF Team Triage: We agree that this would be a good scenario to enable. Taking into account where we are in the EF6 release along with the size and the impact of this feature our team is not planning to implement it in EF6. Therefore, we are moving it to the Future release to reconsider in the next release.

In the mean time RefactorThis' GraphDiff solves most use cases. Thanks Brent

The updated method appears as below

Public Sub AttachObjectGraph(Of T As Class)(obj As T, mapping As Expression(Of Func(Of IUpdateConfiguration(Of T), Object))) Implements IUnitOfWork.AttachObjectGraph
        Me.UpdateGraph(obj, mapping)
End Sub

With the calling method appearing like this, specifying the parent --> child entity mapping

_usersRepository.AttachObjectGraph(dbUser, Function(map) map.OwnedCollection(Function(u) u.tblUsermmRoles))
_unitOfWork.Commit()
Phil Murray
  • 6,396
  • 9
  • 45
  • 95
0

Basically, Entity Framework lacks ability to do so. Or to say it better, it lacks proper optimized way to do so.

Say you have a User that has 4 roles, and you want to remove 1, and add 2 new.

You need to load adequate navigational property when selecting the user so that your collection of roles is populated. Then you remove roles with classic Remove from collection, and add new roles with Add.

Once you hit SaveChanges, roles that have been removed, will be removed from the database (if 1 to many) or at least many 2 many relationships will be removed. New roles will be saved - m2m relationships will be updated.

This is usually quite ok for a small set of entities.

However assume that user has 100.000 friends and you want to remove one from collection. Clearly loading 100.000 just to remove 1 is completely weird and this is where Entity Framework is not good solution (or at least I'm not familiar with it).

In scenarios such as above, I'm rather targeting direct relationship by querying it with both user id and friend id, instead of pulling list of all friends and operating on that collection.

There is also a faster way by doing ESQL command with DbContext.Database.ExecuteSqlCommand, for sending batch Update and Delete commands. Keep in mind though that Entity Framework is not tracking these changes so your DbContext will not be aware that you've updated enything.

Hope this clarification helps you at least a bit.

Admir Tuzović
  • 10,997
  • 7
  • 35
  • 71