Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF 4: How to properly update object in DbContext using MVC with repository pattern

I'm trying to implement an AuditLog using the DBContext's ChangeTracker object, I ran into an issue where the DbEntityEntry.OriginalValues were getting wiped out and replaced with the DbEntityEntry.CurrentValues. It was brought to my attention that the problem was how I was updating the object that was being tracked in the DbContext (original post: Entity Framework DbContext SaveChanges() OriginalValue Incorrect).

So now I need some help on the proper way to update a persisted object using the repository pattern in MVC 3 with Entity Framework 4. This example code is adapted from the SportsStore application in the Pro Asp.NET MVC 3 Framework book put out by Apress.

Here is my 'Edit' post action in the AdminController:

[HttpPost]
public ActionResult Edit(Product product)
{
    if (ModelState.IsValid)
    {
        // Here is the line of code of interest...
        repository.SaveProduct(product, User.Identity.Name);

        TempData["message"] = string.Format("{0} has been saved", product.Name);
        return RedirectToAction("Index");
    }
    else
    {
        // there is something wrong with the data values
        return View(product);
    }
}

This calls into concrete class EFProductRepository (which is implementing the IProductRepository interface and injected via Ninject). Here is the SaveProduct method in the concrete repository class:

public void SaveProduct(Product product, string userID)
{
    if (product.ProductID == 0)
    {
        context.Products.Add(product);
    }
    else
    {
        context.Entry(product).State = EntityState.Modified;
    }
    context.SaveChanges(userID);
}

The problem (as was brought to my attention in my previous SO post), is that when context.Entry(product).State = EntityState.Modified; is called it somehow messes up the ChangeTracker's ability to report on the changes. So in my overloaded DBContext.SaveChanges(string userID) method, I am not seeing accurate values in the ChangeTracker.Entries().Where(p => p.State == System.Data.EntityState.Modified).OriginalValues object.

If I update my EFProductRepository.SaveProduct method to this it works:

public void SaveProduct(Product product, string userID)
{
    if (product.ProductID == 0)
    {
        context.Products.Add(product);
    }
    else
    {
        Product prodToUpdate = context.Products
          .Where(p => p.ProductID == product.ProductID).FirstOrDefault();

        if (prodToUpdate != null)
        {
            // Just updating one property to demonstrate....
            prodToUpdate.Name = product.Name;
        }
    }
    context.SaveChanges(userID);
}

I would like to know the proper way to update the Product object and persist it in this scenario in such a way that the ChangeTracker accurately tracks my changes to the POCO class in the repository. Am I supposed to do the latter example (except of course copying over all fields that may have been updated), or should I be taking a different approach?

In this example the "Product" class is very simple and only has string properties and decimal properties. In our real application we will have "complex" types and the POCO classes will reference other objects (i.e. Person that has a list of addresses). I know I may also need to do something special to track the changes in this case. Perhaps knowledge of this will change some advice that I receive here.

like image 937
Joe DePung Avatar asked Mar 06 '12 20:03

Joe DePung


1 Answers

it somehow messes up the ChangeTracker's ability to report on the changes

No it doesn't messes anything. Change tracker ability is based on the fact that change tracker knows the entity prior to making changes. But in your case the change tracker is informed about the entity with changes already applied and POCO entity doesn't keep any information about its original values. POCO entity has only single set of values which is interpreted as both current and original. If you want anything else you must code it yourselves.

Am I supposed to do the latter example

In your simple case yes and you can simply use:

public void SaveProduct(Product product, string userID)
{
    if (product.ProductID == 0)
    {
        context.Products.Add(product);
    }
    else
    {
        Product prodToUpdate = context.Products
          .Where(p => p.ProductID == product.ProductID).FirstOrDefault();

        if (prodToUpdate != null)
        {
            context.Entry(prodToUpdate).CurrentValues.SetValues(product);
        }
    }

    context.SaveChanges(userID);
}

The problem is that this works only for simple and complex properties. Another problem is that this overwrites all properties so if for example your entity has some field you don't want to show in UI (or don't want to let user to edit the field) you must still set correct current value to your product instance otherwise that value will be overwritten when applying current values.

The whole situation becomes significantly more complex once you try to apply this to the real scenario. You will fail and you will fail many times before you write a lot of code to support exactly your cases because there is probably no generic solution EF has no supporting methods for this scenarios. The reason is that EF has internal state machine for every entity and some associations and you must configure the state for every single entity or association you want to update, insert or delete and you must do it in compliance with EF internal rules. Setting state of the entity will change the state of that single entity but not its relations.

I do it simply by loading current entity with all relations from database and manually (in code) merging whole entity graph (simply you have detached and attached entity graph and you must transfer all changes from detached to attached one).

like image 146
Ladislav Mrnka Avatar answered Nov 13 '22 11:11

Ladislav Mrnka