Question

I'm sure I am misunderstanding something fundamental about how EF5 works.

In a [previous question] I asked about how to pass values between actions in an ASP.NET MVC application and it was suggested I could use TempData as a mechanism to pass around data (in my case I've gone for the POCOs that represent my data model in EF).

My controllers in MVC are not aware of any persistence mechanism within EF. They make use of a service layer which I've called "Managers" to perform common tasks on my POCOs and read/persist them to the underlying datastore.

I'm writing a workflow to allow an "employee" of my site to cancel a "LeaveRequest". In terms of controllers and actions, there's an HttpGet action "CancelLeaveRequest" which takes the ID of the LeaveRequest in question, retrieves the LeaveRequest through the service layer, and displays some details, a warning and a confirm button. Before the controller returns the relevant View, it commits the LeaveRequest entity into TempData ready to be picked up in the next step...

The confirm button causes an HttpPost to "LeaveRequest" which then uses the LeaveRequest from TempData and a call down to the service layer to make changes to the LeaveRequest and save them back to the database with EF.

Each instance of a manager class in my code has it's own EF DBContext. The controllers in MVC instantiate a manager and dispose of it within the page lifecycle. Thus, the LeaveRequest is retrieved using one instance of a DBContext, and changes are made and submitted via another instance.

My understanding is that the entity becomes "detached" when the first DBContext falls out of scope. So, when I try to commit changes against the second DBContext, I have to attach the entity to the context using DBContext.LeaveRequests.Attach()? There is an added complication that I need to use an "Employee" entity to note which employee cancelled the leave request.

My code in the service layer for cancelling the leave request reads as follows.

public void CancelLeaveRequest(int employeeId, LeaveRequest request)
{
  _DBContext.LeaveRequests.Attach(request);
  request.State = LeaveRequestApprovalState.Cancelled;
  request.ResponseDate = DateTime.Now;

  using (var em = new EmployeesManager())
  {
    var employee = em.GetEmployeeById(employeeId);
    request.Responder = employee;
    _DBContext.Entry(request.Responder).State = System.Data.EntityState.Unchanged;
  }

  _CommitDatabaseChanges();
}

You can see that I retrieve an Employee entity from the EmployeesManager and assign this employee as the responder to the leave request.

In my test case, the "responder" to the Leave Request is the same employee as the "requestor", another property on Leave Request. The relationships are many-to-one between leave requests and a requesting employee, and many-to-one between leave requests and a responding employee.

When my code runs in it's present state, I get the following error:

AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges.

I suspect this is because EF thinks it knows about the employee in question already. The line that fails is:

_DBContext.Entry(request.Responder).State = System.Data.EntityState.Unchanged;

However, if I remove this line and don't try to be clever by telling EF not to change my employee object, the leave request gets cancelled as expected but some very strange things happen to my Employees.

Firstly, the employee who made/responded to the request is duplicated. Then, any navigation properties (like "Manager", a many-to-one relationship between an Employee and other Employees) seem to get duplicated too. I can understand that the duplication of the Manager property on Employee is because I am loading the Manager object graph in as part of GetEmployeeById and I think I understand that the original Employee is being duplicated because, as far as the LeaveRequest DBContext is concerned, it has just appeared out of nowhere (I retrieved the Employee through a different DBContext). However, assuming those two points are correct, I'm at a loss as to how I can a) prevent the Employee and it's associated object graph being duplicate in the database and b) how I can ensure the modified LeaveRequest is persisted correctly (which it seems to stop doing with various combinations of attaching, changing state to modified etc... on the employee and leave request).

Please can someone highlight the error of my ways?


My LeaveRequest entity:

public class LeaveRequest
{
    public LeaveRequest()
    {
        HalfDays = new List<LeaveRequestHalfDay>();
    }

    public int CalculatedHalfDaysConsumed { get; set; }

    public Employee Employee { get; set; }

    public virtual ICollection<LeaveRequestHalfDay> HalfDays { get; set; }

    public int LeaveRequestId { get; set; }

    public DateTime RequestDate { get; set; }

    public int ResponderId { get; set; }
    public virtual Employee Responder { get; set; }

    public DateTime? ResponseDate { get; set; }

    public LeaveRequestApprovalState State { get; set; }

    public LeaveRequestType Type { get; set; }

    public ICollection<LeaveRequest> ChildRequests { get; set; }
    public LeaveRequest ParentRequest { get; set; }
}

The "Employee" field (of type Employee...) is the person who submitted the request. The "Responder" is potentially a different, but could be the same, employee.

Was it helpful?

Solution

You should change your navigation properties to this:

public int ResponderId {get;set;}
public virtual Employee Responder { get; set; }

This scalar property will be auto-mapped to the navigation property by EF. Next you can simply do the following (and you don't need the Unchanged state):

var employee = em.GetEmployeeById(employeeId);
request.ResponderId = employee.Id;

See also this article about relationships in EF.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top