How to insert a model with a Foreign Key in Entity Framework in MVC 3?

asp.net-mvc-3 entity-framework foreign-key-relationship insert

Question

I have a Product Model, which contains a foreign key to a ProductCategory record, and looks like this.

public class ProductModel : IProductModel
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public string ProductDescription { get; set; }
    public string ProductImagePath { get; set; }
    public decimal? PricePerMonth { get; set; }
    public bool ProductActive { get; set; }
    public ProductCategory ProductCategory { get; set; }
}

For some reason, every time a new Product is created, a new ProductCategory is also created with all of it's values in the database being set to NULL, except for the ProductCategoryID field, which is auto generated.

In my Create view, I have a form, which contains a drop list populated with ProductCategories. Once the form is submitted, Create(Product product) Action is being called, and the new Product is being created. At this point, the new ProductCategory is also created, which is behavior that I don't want.

My Create(Product product) Action looks like this:

    //
    // POST: /Product/Create
    [HttpPost]
    public ActionResult Create(Product product)
    {
        int productCategoryID = product.ProductCategory.ProductCategoryID;
        product.ProductCategoryID = productCategoryID;
        _productService.InsertProduct(product);
    }

... and my InsertProduct methods looks like this ...

    public void InsertProduct(Product product)
    {
        Db.Products.AddObject(product);
        Save();
    }

Why is this code causing a new ProductCategory to be created every time?

EDIT: Adding table definition, in response to an answer given, whcih asked for more information:

TABLE Product (
ProductID INT PRIMARY KEY IDENTITY(1,1),
ProductCategoryID INT, -- FK to ProductCategory 
ProductName NVARCHAR(255),
ProductDescription NVARCHAR(MAX),
ProductImagePath NVARCHAR(1024),
PricePerMonth DECIMAL(7,2), -- ex 11111.11
ProductActive BIT NOT NULL DEFAULT(1)
)

TABLE ProductCategory (
ProductCategoryID INT PRIMARY KEY IDENTITY(1,1),
ProductCategoryName NVARCHAR(255),
ProductCategoryDescription NVARCHAR(MAX),
ProductCategoryActive BIT NOT NULL DEFAULT(1)
)

EDIT 2:

I still don't understand why, exactly, but I was able to work around this problem with the following changes made to my Create(Product product) Action:

var productCategoryID = product.ProductCategory.ProductCategoryID;
product.ProductCategoryID = productCategoryID;
var newProduct = new Product
        {
            ProductName = product.ProductName,
            ProductDescription = product.ProductDescription,
            PricePerMonth = product.PricePerMonth,
            ProductImagePath = product.ProductImagePath,
            ProductCategoryID = productCategoryID
        };

_productService.InsertProduct(newProduct);

EDIT 3:

I'm still trying to wrap my head around all of this, but ... I have discovered that this problem has something to do with the ProductCategory member of the Product class. From what I can tell, ModelBinding is automatically populating that property with a new ProductCategory every time the form is submitted. I can consistently avoid the problem I've been having here by explicitly setting Product.ProductCategory = null;

1
0
8/14/2011 7:22:19 AM

Accepted Answer

I was curious about your problem, so I got some pointers from a well-known author and trainer on MVC, and figured out the following. Key concepts are available in an MSDN blog post.

The MVC model binding process binds the form data from the view to the model that you pass to the Create action. It finds the appropriate values in the form data to bind to the Product and ProductCategory objects, and carries out the model binding.

The problem in your case is that the Entity Framework is not aware of the objects, because you created Product and ProductCategory objects on their own, outside of the EF context, rather than entities from the EF context. In your case, you're using EF 4.1 with the DbContext, but this would also apply with different methods to EF 4.0 with the ObjectContext.

Your new Product object doesn't exist within your EF context or database --- you haven't added it yet --- but your ProductCategory does. You need to map the ProductCategory object hanging off your new Product object to an existing ProductCategory from your EF entities. To do that, you can either find the existing ProductCategory by looking it up by primary key (ProductCategoryId) then assigning it to your product.ProductCategory, or you can "attach" your free-floating ProductCategory object to your entities. You pass it your ProductCategory object, and it essentially does the same thing as finding an existing entity --- it looks up the entity by primary key. Both methods essentially take an existing ProductCategory object, tells the EF that your object is an entity, and sets the entity's "state" to "Unchanged", so when you call context.SaveChanges() it ignores the ProductCategory entity and therefore finds no changes to send back to the underlying database.

So, before you add your Product to the EF context, you first need to A) find an existing ProductCategory entity that is already attached to EF, B) attach the detached ProductCategory object, or C) set the product.ProductCategoryId and null out the product.ProductCategory reference so the EF will find the ProductCategory for you. You chose Option C.

Option A (Find an existing, attached entity):

public ActionResult Create(Product product)
{
    // find an attached entity based on primary key
    var productCat = 
        Db.ProductCategories.Find(product.ProductCategory.ProductCategoryId); 
    product.ProductCategoryId = productCat.ProductCategoryId;
    product.ProductCategory = productCat;
    _productService.InsertProduct(product);
}

Option B (Attach a detached entity):

public ActionResult Create(Product product)
{
    // attach an entity - EF will find the entity to attach using the entity's primary key
    Db.ProductCategories.Attach(product.ProductCategory); 
    product.ProductCategoryId = product.ProductCategory.ProductCategoryId;
    _productService.InsertProduct(product);
}

Option C (Null out the entity reference):

public ActionResult Create(Product product)
{
    product.ProductCategoryId = product.ProductCategory.ProductCategoryId;
    product.ProductCategory = null;
    _productService.InsertProduct(product);
}
4
8/17/2011 4:21:52 AM

Popular Answer

@campbelt: Not sure why it is happening. This is how I did it. I didn't pass my domain object to the view, but passed through a view model called EditGrantApplicationViewModel. In my scenario I have a grant application form. On this form the user can select a bank from a dropdown list. Below is my partial view model containing the details for the bank dropdown.

public class EditGrantApplicationViewModel
{
   // Other properties

   public int BankId { get; set; }
   public List<Bank> Banks { get; set; }
}

My view for this dropdown looks like this:

<td valign="top"><label>Bank:</label> <span class="red">*</span></td>
<td>
   @Html.DropDownListFor(x => x.BankId, new SelectList(Model.Banks, "Id", "Name", Model.BankId), "-- Select --")<br>
   @Html.ValidationMessageFor(x => x.BankId)
</td>

My Create action method:

public ActionResult Create(EditGrantApplicationViewModel viewModel)
{
   // Check input parameter

   if (!ModelState.IsValid)
   {
      viewModel.Banks = bankService.GetAll();

      return View("Create", viewModel);
   }

   // Map from view model to domain object
   GrantApplication grantApplication = (GrantApplication)grantApplicationMapper.Map(viewModel, typeof(EditGrantApplicationViewModel), typeof(GrantApplication));

   // Insert to database
   grantApplicationService.Insert(grantApplication);

   return RedirectToRoute(Url.GrantApplicationIndex());
}

Partial GrantApplication object:

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

   public int BankId { get; set; }
}

The mapping will populate the BankId property in my GrantApplication object (which was selected) from the dropdown and this is what will be added to the column in my GrantApplication table.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow