Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exclude property from updating when SaveChanges() is called

There appears to be two ways to update a disconnected Entity Framework entity using the "attach" method.

Method One is to simply set the disconnected entity's state as modified:

myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).State = EntityState.Modified;
myDbContext.SaveChanges();

This will save all fields on the "dog" object. But say you are doing this from an mvc web page where you only allow editing of Dog.Name, and the only Dog property contained on the page is Name. Then one could do Method Two:

myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).Property(o => o.Name).CurrentValue = dog.Name;
myDbContext.Entry(dog).Property(o => o.Name).IsModified = true;
myDbContext.SaveChanges();

Method Two could get quite verbose when there are a lot of properties to update. This prompted me to attempt Method Three, setting IsModified = false on the properties I don't want to change. This does not work, throwing the runtime error "Setting IsModified to false for a modified property is not supported":

myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).State = EntityState.Modified;
myDbContext.Entry(dog).Property(o => o.Owner).IsModified = false;
myDbContext.SaveChanges();

I'd much prefer to use Method One everywhere, but there are many instances where my asp.net mvc view does not contain every scalar property of the Dog class.

My questions are:

  1. Are there any attributes I could use on the POCO class that would tell Entity Framework that I never want the property to up updated? Eg, [NeverUpdate]. I am aware of the [NotMapped] attribute, but that is not what I need.
  2. Failing that, is there any way I can use Method One above (myDbContext.Entry(dog).State = EntityState.Modified; ) and exclude fields that I don't want updated?

P.S. I am aware of another way, to not use "attach" and simply fetch a fresh object from the database, update the desired properties, and save. That is what I am doing, but I'm curious if there is a way to use "attach," thus avoiding that extra trip to the database, but do it in a way that is not so verbose as Method Two above. By "fetch a fresh object" I mean:

Dog dbDog = myDbContext.Dogs.FirstOrDefault(d => d.ID = dog.ID);
dbDog.Name = dog.Name;
myDbContext.SaveChanges();
like image 723
Tom Regan Avatar asked Jul 24 '13 16:07

Tom Regan


1 Answers

The following may work works.

myDbContext.Dogs.Attach(dog);
myDbContext.Entry(dog).State = EntityState.Modified;

var objectContext = ((IObjectContextAdapter) myDbContext).ObjectContext;
foreach (var entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified).Where(entity => entity.Entity.GetType() == typeof(Dogs)))
{
    // You need to give Foreign Key Property name
    // instead of Navigation Property name
    entry.RejectPropertyChanges("OwnerID"); 

}

myDbContext.SaveChanges();

If you want to do it in a single line, use the following extension method:

public static void DontUpdateProperty<TEntity>(this DbContext context, string propertyName)
{
    var objectContext = ((IObjectContextAdapter) context).ObjectContext;
    foreach (var entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified).Where(entity => entity.Entity.GetType() == typeof(TEntity)))
    {
        entry.RejectPropertyChanges(propertyName); 
    }
}

And use it like this

// After you modify some POCOs
myDbContext.DontUpdateProperty<Dogs>("OwnerID");
myDbContext.SaveChanges();

As you can see, you can modify this solution to fit your needs, e.g. use string[] properties instead of string propertyName as the argument.


Suggested Approach

A better solution would be to use an Attribute as you suggested ([NeverUpdate]). To make it work, you need to use SavingChanges event (check my blog):

void ObjectContext_SavingChanges(object sender, System.Data.Objects.SavingChangesEventArgs e)
{
    ObjectContext context = sender as ObjectContext;
    if(context != null)
    {
        foreach(ObjectStateEntry entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
        {
            var type = typeof(entry.Entity);
            var properties = type.GetProperties();
            foreach( var property in properties )
            {
                var attributes = property.GetCustomAttributes(typeof(NeverUpdateAttribute), false);
                if(attributes.Length > 0)
                    entry.RejectPropertyChanges(property.Name);
            }
        }
    }
}
// Check Microsoft documentation on how to create custom attributes:
// http://msdn.microsoft.com/en-us/library/sw480ze8(v=vs.80).aspx
public class NeverUpdateAttribute: SystemAttribute
{

}

//In your POCO
public class Dogs
{
    [NeverUpdate]
    public int OwnerID { get; set; }
}

Warning: I did not compile this code. I'm not at home :/


Warning 2: I have just read the MSDN documentation and it says:

ObjectStateEntry.RejectPropertyChanges Method

Rejects any changes made to the property with the given name since the property was last loaded, attached, saved, or changes were accepted. The orginal value of the property is stored and the property will no longer be marked as modified.

I am not sure what its behavior would be in the case of attaching a modified entity. I will try this tomorrow.


Warning 3: I have tried it now. This solution works. Property that is rejected with RejectPropertyChanges() method are not updated in the persistence unit (database).

HOWEVER, if the entity that is updated is attached by calling Attach(), the current context remains dirty after SaveChanges(). Assume that the following row exists in the database:

Dogs
ID: 1
Name: Max
OwnerID: 1

Consider the following code:

var myDog = new Dogs();
myDog.ID = 1;
myDog.Name = Achilles;
myDog.OwnerID = 2;

myDbContext.Dogs.Attach(myDog);
myDbContext.Entry(myDog).State = EntityState.Modified;
myDbContext.SaveChanges();

The current state of database after SaveChanges():

Dogs:
ID: 1
Name: Achilles
OwnerID: 1

The current state of myDbContext after SaveChanges():

var ownerId = myDog.OwnerID;  // it is 2
var status = myDbContext.Entry(myDog).State; // it is Unchanged

So what you should do? Detach it after SaveChanges():

Dogs myDog = new Dogs();
//Set properties
...
myDbContext.Dogs.Attach(myDog);
myDbContext.Entry(myDog).State = EntityState.Modified;
myDbContext.SaveChanges();
myDbContext.Entry(myDog).State = EntityState.Detached;
like image 90
mostruash Avatar answered Oct 12 '22 22:10

mostruash