Using MVC2 to update an Entity Framework v4 object with foreign keys fails
With the following simple relational database structure: An Order has one or more OrderItems, and each OrderItem has one OrderItemStatus.
Order, OrderItem and OrderItemStatus tables linked with foreign key relationships http://www.mindthe.net/images/OrdersDB.jpg Entity Framework v4 is used to communicate with the database and entities have been generated from this schema. The Entities connection happens to be called EnumTestEntities in the example.
The trimmed down version of the Order Repository class looks like this:
public class OrderRepository
{
private EnumTestEntities entities = new EnumTestEntities();
// Query Methods
public Order Get(int id)
{
return entities.Orders.SingleOrDefault(d => d.OrderID == id);
}
// Persistence
public void Save()
{
entities.SaveChanges();
}
}
An MVC2 app uses Entity Framework models to drive the views. I'm using the EditorFor feature of MVC2 to drive the Edit view.
When it comes to POSTing back any changes to the model, the following code is called:
[HttpPost]
public ActionResult Edit(int id, FormCollection formValues)
{
// Get the current Order out of the database by ID
Order order = orderRepository.Get(id);
var orderItems = order.OrderItems;
try
{
// Update the Order from the values posted from the View
UpdateModel(order, "");
// Without the ValueProvider suffix it does not attempt to update the order items
UpdateModel(order.OrderItems, "OrderItems.OrderItems");
// All the Save() does is call SaveChanges() on the database context
orderRepository.Save();
return RedirectToAction("Details", new { id = order.OrderID });
}
catch (Exception e)
{
return View(order); // Inserted while debugging
}
}
The second call to UpdateModel has a ValueProvider suffix which matches the auto-generated HTML input name prefixes that MVC2 has generated for the foreign key collection of OrderItems within the View.
The call to SaveChanges() on the database context after updating the OrderItems collection of an Order using UpdateModel generates the following exception:
"The operation failed: The relationship could not be changed because one or more
of the foreign-key properties is non-nullable. When a change is made to a
relationship, the related foreign-key property is set to a null value. If the
foreign-key does not support null values, a new relationship must be defined,
the foreign-key property must be assigned another non-null value, or the
unrelated object must be deleted."
When debugging through this code, I can still see that the EntityKeys are not null and seem to be the same value as they should be. This still happens when you are not changing any of the extracted Order details from the database. Also the entity connection to the database doesn't change between the act of Getting and the SaveChanges so it doesn't appear to be a Context issue either.
Any ideas what might be causing this problem? I know EF4 has done work on foreign key properties but can anyone shed any light on h开发者_StackOverflowow to use EF4 and MVC2 to make things easy to update; rather than having to populate each property manually. I had hoped the simplicity of EditorFor and DisplayFor would also extend to Controllers updating data.
Thanks
I suspect it's tripping up on the OrderItemStatus. There is no way the formValues can contain the necessary OrderItem object, and thus the call to UpdateModel cannot possibly update it correctly. Perhaps it's setting it to null and orphaning an OrderItemStatus object in the process.
Before calling save changes, dump the contents of the ObjectStateEntries and take a look at what it's trying to save back to the database using code like:
var items = context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added | System.Data.EntityState.Modified);
foreach (var item in items)
{
...
You could also download the ASP.NET MVC2 code, link it in and step through UpdateModel to see what it's doing.
You shouldn't be updating your entity directly with the Form, I assume this also means you are sending the Order entity to your view in the GET?
Map the properties to a Dto/Resource/ViewModel/<Insert other overloaded, misused term here>.
That way the magic of UpdateModel cant set anything it shouldn't (including stopping hackers setting properties you dont want them to)
I suspect this will cure your issue as UpdateModel is probably cocking your entity up.
Do this first out of best-practice and see where you get ;)
I wrote a very simple automapper-esque tool that will copy an entity's properties to a model and vice-versa using reflection. It looks long, but mose of the lines are comments. If you use it, you can specify (in fact, you HAVE to specify) which properties are allowed to be copied from the model to the entity in order to prevent malformed entry attacks. You can see more of it here: http://kendoll.net/entity_framework_to_mvc2_model_conversion
/// <summary>
/// This is a class with a couple of static methods that make it easy to copy
/// an MVC2 model's property values to an Entity-Framework entity.
/// </summary>
/// <typeparam name="T">The type of the model declared in your MVC2 project.</typeparam>
/// <typeparam name="t">The type of the entity declared in your Entity Framework project.</typeparam>
public class EntityModeler<T, t>
where T : EntityModel<t>
where t : System.Data.Objects.DataClasses.EntityObject
{
/// <summary>
/// A new model of type T is created whose properties will have the same
/// value as the entity parameter respective of the property name.
/// </summary>
/// <param name="entity">
/// The entity whose property values should be copied to the model.
/// </param>
/// <returns>The new model object of type T.</returns>
public static T ModelEntity(t entity)
{
// create an object of type model and populate its properties
// with the value of the entity's properties. Then return the
// model.
var model = System.Activator.CreateInstance<T>();
model.UpdateModel(entity);
return model;
}
/// <summary>
/// A new IEnumerable set of models is created whose properties have the
/// same value as the entities' properties in the parameter.
/// </summary>
/// <param name="entities">
/// The entities whose properties should be copied to the models.
/// </param>
/// <returns>An IEnumerable set of models of type T.</returns>
public static IEnumerable<T> ModelEntities(IEnumerable<t> entities)
{
// Loop through all the entities in the entity list.
// Create a model type for each entity. This model will have its
// UpdateModel method called to copy the entity property values to
// the model. Then just add the model to a list and return the
// list.
var list = new List<T>();
foreach (var entity in entities)
list.Add(EntityModeler<T, t>.ModelEntity(entity));
return list;
}
}
/// <summary>
/// This is the base class for all MVC2 models that can be converted to an entity defined
/// by a class in an entity framework. Each child class should define its entity's type
/// using the "T" type parameter. This class provides methods to update its model properties
/// based on an entity and vice versa.
/// </summary>
/// <typeparam name="T">
/// The type of the entity that corresponds to the model that inherits this base class. This
/// object must descend from the base EntityObject type.
/// </typeparam>
public abstract class EntityModel<T>
where T : System.Data.Objects.DataClasses.EntityObject
{
/// <summary>
/// This method updates the inheriting model's properties to correspond with the
/// properties of an entity. The entity must be of the type defined by the inheriting
/// model. Reflection is used to get the public, readable properties from the entity
/// and the public, writable properties from the model. Then, we just write the entity's
/// value to the model for each property that shares the same name.
/// </summary>
/// <remarks>
/// Please note that this method will only copy an entity's public properties. Fields
/// are ignored (though they could easily be added). Also, this method will only perform
/// a shallow copy of the entity's properties.
/// </remarks>
/// <param name="entity">
/// The entity object whose properties should be copied to this model.
/// </param>
public void UpdateModel(T entity)
{
// get all the public properties of this model in an array
var myProperties = this.GetType().GetProperties(
BindingFlags.Instance |
BindingFlags.Public
);
// now get the type of the entity
var entityType = entity.GetType();
// loop through the properties
foreach (var myProp in myProperties)
{
// try to get the property with the same name from the entity.
// If we can read from the entity property and write to this
// object's property, then set this object's property according
// to the value of the entity property. But first, check if the
// value is null. If the value is null, then the corresponding
// property in this object must be of type Nullable.
var entityProp = entityType.GetProperty(myProp.Name);
if (entityProp != null && entityProp.CanRead && myProp.CanWrite)
{
var val = entityProp.GetValue(entity, null);
if (val == null)
if (myProp.PropertyType != typeof(Nullable))
continue;
myProp.SetValue(this, entityProp.GetValue(entity, null), null);
}
}
}
/// <summary>
/// This method updates the properties of an entity based on the values of
/// this model's properties. Reflection is used to get the properties of the
/// entity and the properties of this model, then the values of the model's
/// properties are copied to those of the entity that share the same name.
/// </summary>
/// <remarks>
/// Please note that this method only works on public properties. Fields are
/// ignored (though they could easily be added). Also, this method will only
/// perform a shallow copy of the model's properties.
/// </remarks>
/// <param name="entity">
/// This is the entity object whose properties should be updated. It must be
/// of the type defined by the inheriting class.
/// </param>
/// <param name="includeProperties">
/// This is an array of string values that represent the names of the properties
/// that the method should copy. If a property's name does not exist in this
/// array, then its value will not be copied.
/// </param>
public void UpdateEntity(ref T entity, params string[] includeProperties)
{
// get all the public properties of this model in an array
var propertyInfo = this.GetType().GetProperties(
BindingFlags.Instance |
BindingFlags.Public
);
// now get the type of the entity.
var entityType = entity.GetType();
// loop through each of the properties in the property array for the model type
foreach (var myProp in propertyInfo)
{
// check if this property is in the list of properties to include
if (includeProperties.Contains(myProp.Name))
{
// now try to get the property of the entity with the same name as the
// property of this type.
var entityProp = entityType.GetProperty(myProp.Name);
// check that the entity property exists and that we can write to it.
if (entityProp != null && entityProp.CanWrite && myProp.CanRead)
{
// get the current value of this property in the model object.
// If this property can't be read, the value will be set to null.
var val = myProp.GetValue(this, null);
// if the value is null, make sure the corresponding property of the
// entity is nullable. If it isn't, do not try to set the value. Just
// go to the next value in the array.
// TODO: We need a better method to determine if an entity property
// is nullable. This method won't allow NULL to be written to nullable
// columns.
if (val == null)
if (entityProp.PropertyType != typeof(Nullable))
continue;
// we're here, so the value should be ok to set. Set the value of the
// entity object to the value of the model object.
entityProp.SetValue(entity, val, null);
}
}
}
}
/// <summary>
/// This method works similarly to the one above, only it creates a new entity
/// instead of updating an existing one. The entity will be of the type T which
/// was declared by the inheriting class.
/// </summary>
/// <param name="includeProperties">
/// This is an array of string values that represent the names of the properties
/// that the method should copy. If a property's name does not exist in this
/// array, then its value will not be copied.
/// </param>
/// <returns>
/// The return value is the new entity with its properties set equal to
/// the respective properties of this model (well, at least those that were
/// specified in the parameter).
/// </returns>
public T CreateEntity(params string[] includeProperties)
{
var entity = System.Activator.CreateInstance<T>();
this.UpdateEntity(ref entity, includeProperties);
return entity as T;
}
}
And here's a short example on how it can be used:
// Create your model with any properties you choose. You won't
// have to worry about people hacking your forms, because you'll
// define the properties that are allowed to be inserted/updated
// as you convert the model to an entity.
public class PersonModel : EntityModel<PersonEntity>
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string SocialSecurity { get; set; }
}
// Create your MVC2 controller with an Index and Edit page.
public class PersonController : Controller
{
public ActionResult Index()
{
// Create a list of models based on every Person entity in your
// data set.
var models =
EntityModeler<PersonModel, PersonEntity>.ModelEntities(
YourEntities.PersonSet
);
return View("Index", models);
}
[HttpPost]
public ActionResult Edit(PersonModel model)
{
if (!ModelState.IsValid)
return View(model);
// Create an Person entity based on the Person model submitted
// by a user, allowing only the FirstName, LastName, and Address
// properties to be copied. This will protect the properties in
// your entity that shouldn't be editable by users (for example,
// a SocialSecurity property).
var entity = model.CreateEntity(
"FirstName",
"LastName",
"Address"
);
// The entity is ready to be used by your Entity Framework project,
// so you can add your UPDATE logic and error checking here.
// YourEntities.AddToPersonSet(entity);
// ...
// ...
return RedirectToAction("Details", new { id = entity.Id });
}
}
精彩评论