views:

3411

answers:

3

I need to set an EntityObject's EntityKey. I know its type and its id value. I don't want to query the database unnecessarily.

This works...

//
// POST: /Department/Edit/5

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Guid id, Department Model)
{
    Model.EntityKey = (from Department d in db.Department
                       where d.Id == id
                       select d).FirstOrDefault().EntityKey;
    db.ApplyPropertyChanges(Model.EntityKey.EntitySetName, Model);
    db.SaveChanges();
    return RedirectToAction("Index");
}

This fails...

//
// POST: /Department/Edit/5

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Guid id, Department Model)
{
    String EntitySetName = db.DefaultContainerName + "." + Model.GetType().Name;
    Model.EntityKey = new System.Data.EntityKey(EntitySetName, "Id", Model.Id);
    db.ApplyPropertyChanges(Model.EntityKey.EntitySetName, Model);
    db.SaveChanges();
    return RedirectToAction("Index");
}

The ApplyPropertyChanges() line fails with this exception:

The ObjectStateManager does not contain an ObjectStateEntry with a reference to an object of type 'Sample.Models.Department'.

The two EntityKeys are equal. Why does the second block of code fail? How can I fix it?

+4  A: 

The reason you second block of code fails is because EF can't find the object in the ObjectStateManager - i.e. when it pulls objects from the db it puts them in the state manager so it can track them - this is similar to the Identity Map pattern. Despite having an EntityKey, your object isn't in the state manager so EF is unable to persist the changes. You can get around this by putting the object into the state manager youself but you have be a bit sneaky about it.

This works:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Guid id, Department Model)
{
  var entitySetName = db.DefaultContainerName + "." + Model.GetType().Name;
  var entityKey = new System.Data.EntityKey(entitySetName, "Id", Model.Id);

  db.Attach(new Department{Id = id, EntityKey = entityKey});
  db.AcceptAllChanges();

  db.ApplyPropertyChanges(entitySetName, Model);
  db.SaveChanges();
}

... but it's not very clean. Basically this is attaching an 'empty' object with just an entity key, accepting all changes and then calling ApplyPropertyChanges with the actual real updated values.

Here's the same thing wrapped up in an extension method - this should work for anything that has uses a single db column for the primary key. The only interesting part of calling the method is that you need to tell it how to find the key property via a delegate as the second argument to the extension method:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Guid id, Department Model)
{
  db.ApplyDetachedPropertyChanges(Model, x => x.Id);
  db.SaveChanges();
}

and the extension method:

public static class EfExtensions
{
  public static void ApplyDetachedPropertyChanges<T>(this ObjectContext db, T entity, Func<T, int> getIdDelegate)
  where T : EntityObject
  {
    var entitySetName = db.DefaultContainerName + "." + entity.GetType().Name;
    var id = getIdDelegate(entity);
    var entityKey = new EntityKey(entitySetName, "Id", id);

    db.Attach(new Department {Id = id, EntityKey = entityKey});
    db.AcceptAllChanges();

    db.ApplyPropertyChanges(entitySetName, entity);
  }
}

As the extension method is calling AcceptAllChanges, you'd need to be careful about calling this if you are doing updates on multiple entities at once - you could easily 'lose' updates if you aren't careful. Hence this approach is only really suitable for simple update scenarios - e.g. a lot of MVC action methods :)

Steve Willcock
+1 for the extension method. That was pretty! =) How would the method have to change if the table uses a multiple column PK?
Tomas Lycken
You'd need to make an overload of the extension method that took an IEnumerable of KeyValuePair to specify the key, and then call the corresponding constructor overload on EntityKey. To maintain stype safety and avoid passing in strings in your KeyValuePair, you could pass an array of 'key-finding' delegates along with the values to put into the keys.
Steve Willcock
+3  A: 
public static class EfExtensions
{
    public static void ApplyDetachedPropertyChanges<T>(this ObjectContext db, T entity, Func<T, int> getIdDelegate)
    where T : EntityObject
    {
        var entitySetName = db.DefaultContainerName + "." + entity.GetType().Name;

        T newEntity = Activator.CreateInstance<T>();
        newEntity.EntityKey = db.CreateEntityKey(entitySetName, entity);

        Type t = typeof(T);
        foreach(EntityKeyMember keyMember in newEntity.EntityKey.EntityKeyValues) {
            PropertyInfo p = t.GetProperty(keyMember.Key);
            p.SetValue(newEntity, keyMember.Value, null);
        }

        db.Attach(newEntity);
        //db.AcceptAllChanges();

        db.ApplyPropertyChanges(entitySetName, entity);
    }
}
karsu
Thanks, I was looking for something like this, and you supplied the need! +1
camainc
I had to add a "ToPlural()" string extension method onto "entity.GetType().name"because my entitysets are pluralized by the code generator. Other than that, your code worked perfectly.
camainc
+2  A: 

Try to use below code and let me know if it is working for you.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Guid id, Department Model)
{
    using (var context =  new EntityContext())
    {
        try
        {
            Object entity = null;
            IEnumerable<KeyValuePair<string, object>> entityKeyValues =
                new KeyValuePair<string, object>[] {
                    new KeyValuePair<string, object>("DepartmentID", id) };

            // Create the  key for a specific SalesOrderHeader object. 
            EntityKey key = new EntityKey("EntityContext.Deparment",   
                                                                   entityKeyValues);

            // Get the object from the context or the persisted store by its key.
            if (context.TryGetObjectByKey(key, out entity))
            {
               context.ApplyPropertyChanges(key.EntitySetName, Model);
               context.SaveChanges();
            }
            else
            {
               // log message if we need
               //"An object with this key could not be found." 
            }                
        }
        catch (EntitySqlException ex)
        {
           // log message
        }
    }
 }       
Venkat