Использование MVC2 для обновления объекта Entity Framework v4 с помощью внешних ключей завершается неудачно - PullRequest
3 голосов
/ 20 мая 2010

Со следующей простой структурой реляционной базы данных: у Order есть один или несколько OrderItems, и у каждого OrderItem есть один OrderItemStatus.

Таблицы Order, OrderItem и OrderItemStatus, связанные с отношениями внешнего ключа http://www.mindthe.net/images/OrdersDB.jpg Entity Framework v4 используется для связи с базой данных, и сущности были сгенерированы из этой схемы. В этом примере соединение сущностей называется EnumTestEntities.

Урезанная версия класса Order Repository выглядит следующим образом:

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();
  }
}

Приложение MVC2 использует модели Entity Framework для управления представлениями. Я использую функцию EditorFor MVC2 для управления представлением редактирования.

Когда дело доходит до отправки любых изменений в модель, вызывается следующий код:

[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
  }
}

Второй вызов UpdateModel имеет суффикс ValueProvider, который соответствует автоматически сгенерированным префиксам входных имен HTML, которые MVC2 сгенерировал для коллекции внешнего ключа OrderItems в представлении.

При вызове SaveChanges () в контексте базы данных после обновления коллекции OrderItems Order с помощью UpdateModel генерируется следующее исключение:

"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."

При отладке с помощью этого кода я все еще вижу, что EntityKeys не равны NULL и, по-видимому, имеют то же значение, что и должно быть. Это все еще происходит, когда вы не меняете какие-либо извлеченные детали заказа из базы данных. Кроме того, соединение сущности с базой данных не изменяется между действием Get и SaveChanges, так что это также не является проблемой контекста.

Есть идеи, что может быть причиной этой проблемы? Я знаю, что EF4 проделал работу со свойствами внешнего ключа, но кто-нибудь может пролить свет на то, как использовать EF4 и MVC2 для упрощения обновления; вместо того, чтобы заполнять каждое свойство вручную. Я надеялся, что простота EditorFor и DisplayFor также распространится на контроллеры, обновляющие данные.

Спасибо

Ответы [ 3 ]

2 голосов
/ 20 мая 2010

Я подозреваю, что он срабатывает на OrderItemStatus. Нет никакого способа, которым formValues ​​может содержать необходимый объект OrderItem, и, таким образом, вызов UpdateModel не может корректно обновить его. Возможно, он устанавливает его в null и теряет объект OrderItemStatus в процессе.

Перед вызовом сохранения изменений, сбросьте содержимое ObjectStateEntries и посмотрите, что он пытается сохранить обратно в базу данных, используя такой код:

var items = context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added | System.Data.EntityState.Modified);
foreach (var item in items)
{
  ...

Вы также можете загрузить код ASP.NET MVC2, связать его и пройти через UpdateModel, чтобы посмотреть, что он делает.

1 голос
/ 09 марта 2011

Я написал очень простой инструмент-автомат, который будет копировать свойства объекта в модель и наоборот, используя отражение. Выглядит долго, но моис из строк - это комментарии. Если вы используете его, вы можете указать (на самом деле, вы должны указать), какие свойства разрешено копировать из модели в сущность, чтобы предотвратить неправильные атаки входа. Вы можете увидеть больше этого здесь: 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;
    }
}

И вот небольшой пример того, как его можно использовать:

    // 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 });
        }
    }
1 голос
/ 20 мая 2010

Вы не должны обновлять свою сущность напрямую с помощью Формы, я полагаю, это также означает, что вы отправляете сущность Заказа на ваш взгляд в GET?

Сопоставить свойства с Dto / Resource / ViewModel / <Вставить сюда другой перегруженный, неправильно используемый термин>.

Таким образом, магия UpdateModel не может устанавливать все, что не должно (в том числе останавливать хакеров, устанавливая свойства, которые вам не нужны)

Я подозреваю, что это излечит вашу проблему, поскольку UpdateModel, вероятно, поднимает вашу сущность.

Сделайте это сначала из передового опыта и посмотрите, где вы получите;)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...