Is there a better way to add a child record using

2019-05-08 10:10发布

问题:

I am just learning MVC and dealing with its stateless nature in combination with Entity Framework. My question is, is there a more elegant way of handling the scenario below?

I have two POCO entities:

public class Contest
{
    public long ID { get; set; }
    ....   .....
    public ICollection<ContestPrize> Prizes { get; set; }
}

public class ContestPrize
{
    public long ID { get; set; }

    public Contest Contest { get; set; }

}

I then have a MVC View that displays the Contest and all the Prizes associated with them. On that view I have a "Create Prize" link to create a new prize which passes the ID of the contest to the ContestPrize Controller like so:

@Html.ActionLink("Create Prize", "Create", "ContestPrizes", new { ContestId = Model.ID }, null)

The ContestId is persisted as a hidden field in the form that submits a new prize so that I can retrieve it on create. The create method on the prize controller is below:

    [HttpPost]
    public ActionResult Create(int ParentID, ContestPrize ContestPrize)
    {


        if (ModelState.IsValid)
        {

            db.ContestPrizes.Add(ContestPrize);
            db.SaveChanges();
            return RedirectToAction("Index", "Contests", ParentID);
        }

        return View(ContestPrize);
    }

This is where I run into some ugliness that I don't like. Here are the problems:

  1. The ModelState.IsValid test fails. This is because the navigation property for the parent is null on the model. It hasn't been populated. I can skip the validation on the model and go right to the database but is there a way to populate this without doing #2 below?
  2. The parent property isn't populated at this point, but I do have the ParentID that comes in from the hidden form field. Since everything is stateless, the db object doesn't have the parent object (which is fine). So, I could perform another database read to get the parent object using the parentID and then do Parent.Prizes.Add(ContestPrize) or ContestPrize.Contest = Parent. But I really don't like this because I am making an extra database hit to read the parent, when I have all the information I need to create the child record.

So, is there a way with Entity Framework to just set the ID of the parent and save a child without having to retrieve the parent object? Or...ideally is there a way using MVC to pre-populate the new model on create with a parent object since I do have the parent before heading into the create views?? How bad would it be to pass around the parent object in ViewData maybe so that it was available?

I can make this work by reloading the parent. But I want to avoid it if possible.

Thoughts?

UPDATE: Adding the ContestID to the ContestPrize POCO and then decorating the navigation property with [ForeignKey("ContestID")] as @NSGaga suggested works perfectly and eliminates some of the messy ParentID passing around problems. The new POCOs looks like this:

public class Contest
{
    public long ID { get; set; }
    ....   .....
    public ICollection<ContestPrize> Prizes { get; set; }
}

public class ContestPrize
{
    public long ID { get; set; }

    public long ContestID { get; set; }

    [ForeignKey("ContestID")]
    public Contest Contest { get; set; }

}

I can eliminate the ParentID being passed and set in the views as a hidden field because data binding in MVC takes care of the ContestID on the new entity (I still have to pass the id into the initial view, but that is all. It is just bound from there on out). The new Create action on the Controller looks like this:

[HttpPost]
public ActionResult Create(ContestPrize ContestPrize)
{
    if (ModelState.IsValid)
    {
        db.ContestPrizes.Add(ContestPrize);
        db.SaveChanges();
        return RedirectToAction("Index", "Contests", ContestPrize.ContestID);
    }

    return View(ContestPrize);
}

Clean and simple with no mess. I like it.

回答1:

If you're using 'code first' (all things suggest), then you can do something like...

public class ContestPrize
{
    public long ID { get; set; }

    public long ContestID { get; set; }
    public Contest Contest { get; set; }
}

That should create (by convention) and map the ContestID to the FK. In some complicated cases you may need to specify that relationship in the fluent code, but this should suffice normally.

Then you can just reference the parent by ContestID - fill that in - and do Add.

That's in rough strokes, let me know if you need more info.



回答2:

You probably shouldn't be using entities as view models directly. I suggest using something like this:

// Entitiy Type
public class ContestPrize { ... }

// ViewModel Type
public class ContestPrizeViewModel { ... }

// In your controller
[HttpPost]
public ActionResult Create(int ParentID, ContestPrizeViewModel ContestPrize)
{
    if (ModelState.IsValid)
    {
        var entity = db.Create<ContestPrize>();
        entity.ContestID = ParentID;
        entity.Blah = ContestPrize.Blah
        db.ContestPrizes.Add(entity);
        db.SaveChanges();
        return RedirectToAction("Index", "Contests", ParentID);
    }

    return View(ContestPrize);
}

This allows you to easily separate your domain logic from your view logic. It may require extra code, but it's a much more robust way of doing things.