Asp.net-mvc – Adding a child object to parent without explicit foreign key id fields with EF Code-First in ASP.Net MVC

asp.net-mvccode-firstef-code-firstentity-framework

I have two model classes – Parent and Child which are only linked via typed navigational properties.

public class Parent {
    [Key]
    [Required]
    public int Id { get; set; }

    [Required]
    public string ParentName { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child {
    [Key]
    [Required]
    public int Id { get; set; }

    [Required]
    public string ChildName { get; set; }

    [Required]
    public virtual Parent Parent { get; set; }
}

Now I want to create a new Child for a parent using ASP.Net MVC. First, I need to show a view to the user. I need to somehow pass the parent object key to the view. I also want to show the ParentName. I just fetch the Parent object from the database, create a new Child object, set its Parent property to the fetched parent object.

    public ActionResult Create(int parentId) {
        var parent = db.Parents.Find(parentId);
        if (parent == null) {
            return HttpNotFound();
        }
        var child = new Child() { Parent = parent};
        return View(child);
    }

After the user fills the form, the data is sent to the Create action using HTTP POST.

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Child child)
    {
        if (ModelState.IsValid)
        {
            //db.Parents.Attach(child.Parent); //Added later
            db.Children.Add(child);
            db.SaveChanges();
            return RedirectToAction("Index", new { parentId = child.Parent.Id });
        }
    }

Here I've hit my first problem. The child.Parent was not null and child.Parent.Id was correct, but EF trashed it and created a new empty parent (with a different key) in the database and linked the child to it. I've fixed this problem by attaching the child.Parent to the data context before adding the child (db.Parents.Attach(child.Parent)).

But then I was hit with another problem. At first, my model classes were wrong and didn't have the [Required] attributes thus creating nullable database table columns. I've added the attribute and the code stopped working. The code doesn't work because ModelState.IsValid is false which happens because child.Parent.Name of the passed child is null.

How can the problem of adding the child to the parent be solved? I'm interested in solution which:

  • Uses EF Code-First and ASP.Net MVC
  • Doesn't involve fetching the child.Parent from the database just to make the model validator happy.
  • Doesn't involve adding explicit foreign key (ParentId) to the model.

Is this possible?

Best Answer

I think attempting to attach a parent to the child is a little backwards. Typically you would attach a child to a parent. A new parent is being created most likely because you are not including an input element with the parent id in your child model. So when Child child is ModelBound coming into the POST, parent id is probably null. EF sees this and thinks you want to create a new parent too.

Also, since your parentId is part of your route, you don't need to specify it in your view model unless you are doing special things to your Html.BeginForm() in your view. Meaning, if you just use Html.BeginForm, it will post with the same URL values that you sent to the GET request.

Create Method

public ActionResult Create(int parentId) {
        var parent = db.Parents.Find(parentId);
        if (parent == null) {
            return HttpNotFound();
        }
        return View(new Child());
    }

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(int parentId, Child child)
{
    if (ModelState.IsValid)
    {
        //Probably not a bad idea to check again...just to be sure.
        //Especially since we are attaching a child to the parent object anyways.
        var parent = db.Parents.Find(parentId);
        if (parent == null) {
            return HttpNotFound();
        }
        parent.Childern.Add(child);
        db.SaveChanges();
        return RedirectToAction("Index", new { parentId = parentid });
    }
}