Jim
Jim

Reputation: 2300

Why isn't Entity Framework updating the child object here?

I have a Customer model with a MailingAddress property - this is a navigation property to an Address class:

public class Customer
{
    public int Id { get; set; }

    public string Name { get; set; }

    public virtual Address MailingAddress { get; set; }
}

I have a form that posts a viewmodel back which contains a customer, along with the mailing address. Here is the save method in the controller:

public ActionResult Save(CustomerFormViewModel viewModel)
{
    if (!ModelState.IsValid)
    {
        return View("CustomerForm", viewModel);
    }

    if (viewModel.Customer.Id == 0)
    {
        _context.Customers.Add(viewModel.Customer);
    }
    else
    {
        var customerInDb = _context.Customers
            .Single(c => c.Id == viewModel.Customer.Id);

        _context.Entry(customerInDb).CurrentValues.SetValues(viewModel.Customer);
    }

    _context.SaveChanges();

    return RedirectToAction("Index", "Customers");
}

When posting a new customer, everything works fine (mostly, see note below) and the customer is created along with a corresponding address record. However, when I edit an existing entry, the customer is updated but the address is not. I verified the updated address is being passed in the customer object. If I add a line like this:

_context.Entry(customerInDb.MailingAddress).CurrentValues.SetValues(viewModel.Customer.MailingAddress);

Then it is updated.

Is the child here still considered a detached entity? I assumed since it is a property of the Customer I am fetching it would be automatically saved with the parent. Why does this work with a new record and not with an update?

One note about the new record creation - a Customer record is created and has a MailingAddress_Id pointing to the address. The Address record is also created, but its Customer_Id is null...an idea why EF is not adding the key on that side of the relationship? Address model and view model code in case it helps:

public class Address
{

    public int Id { get; set; }

    public string Street1 { get; set; }

    // Snip a bunch of address data properties     

    public virtual Customer Customer { get; set; }

}

public class CustomerFormViewModel
{
    // Snip irrelevant properties
    public Customer Customer { get; set; }

}

Upvotes: 1

Views: 270

Answers (1)

Adil Mammadov
Adil Mammadov

Reputation: 8676

First of all, if your Customer and Address are in one-to-one relationship, then no foreign key is needed. Actually, in one-to-one relatioonships primary key on dependant side of relationship is also foreign key to principal side. Secondly, when you create new Customer you use context.Customers.Add(viewModel.Customer); and it adds model with all of its child models, but when you try to update using _context.Entry(customerInDb).CurrentValues.SetValues(viewModel.Customer); it does not add all child navigation properties, to do so, you have to tell it to EntityFramework explicitly:

var customerInDb = _context.Customers
            .Single(c => c.Id == viewModel.Customer.Id);
_context.Entry(customerInDb)
    .CurrentValues
    .SetValues(viewModel.Customer);

var mailingAddressInDb = _context.Addresses
            .Single(m => m.Id = viewModel.Customer.MailingAddress.Id);
_context.Entry(mailingAddressInDb)
    .CurrentValues
    .SetValues(viewModel.Customer.MailingAddress);

It should work for you. But it is a bit awkward. When you have dozens of models, you would not even want to imagine it.

Good news

The good news is that, there is an API to solve this problem from its roots. Your problem will be solved in just a few steps. You install it from NuGet using Install-Package Ma.EntityFramework.GraphManager, configure your models to meet prerequisites (which are so easy) and handle whole graph using single line of code:

public ActionResult Save(CustomerFormViewModel viewModel)
{
    if (!ModelState.IsValid)
    {
        return View("CustomerForm", viewModel);
    }

    // Define state of whole graph with single line
    _context.AddOrUpdate(viewModel.Customer);
    _context.SaveChanges();

    return RedirectToAction("Index", "Customers");
}

Please, have a look at CodeProject article for a quick wallktrough. It has example code, so you can download and examine it. I am the owner of this API and I am ready to answer to your questions.

Upvotes: 3

Related Questions