91

I've been looking for a proper way to mark a property to NOT be changed when updating a model in MVC.

For example, let's take this small model:

class Model
{
    [Key]
    public Guid Id {get; set;}
    public Guid Token {get; set;}

    //... lots of properties here ...
}

then the edit method MVC creates looks like this:

[HttpPost]
public ActionResult Edit(Model model)
{
    if (ModelState.IsValid)
    {
        db.Entry(model).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(model);
}

now if my View does not contain the Token, it will be nullified through that edit.

I'm looking for something like this:

db.Entry(model).State = EntityState.Modified;
db.Entry(model).Property(x => x.Token).State = PropertyState.Unmodified;
db.SaveChanges();

The best way so far I found is to be inclusive and set all properties I want included by hand, but I really only want to say which ones to be excluded.

casperOne
  • 72,334
  • 18
  • 180
  • 242
Manuel Schweigert
  • 4,584
  • 3
  • 18
  • 32
  • Possible duplicate: http://stackoverflow.com/questions/3809583/prevent-updating-unchanged-value-in-asp-net-mvc-and-entity-framework – Nate-Wilkins Sep 30 '12 at 14:17
  • I don't think it's duplicate: I want to *always* exclude a certain property from being updated at all. The user should have no ability to change it. – Manuel Schweigert Sep 30 '12 at 14:20
  • 3
    you could use viewmodels and just map what you want to update. – frennky Sep 30 '12 at 14:34
  • I could. There are a few ways *around* this issue. But I want to know if there is nice way of doing this, and if there is one, how it works. btw, smallest "solution" I have to this atm is to open another transaction: `using (var db2 = new DataContext()) model.Token = db2.Models.Find(model.Id).Token;` But I am not happy with this one either. – Manuel Schweigert Sep 30 '12 at 14:42
  • Using a viewModel would not be a way 'around the issue'. It would be the proper way to pass just the data you need to the view, and decouple your EF objects from the UI. This is how asp.mvc is meant to be used. – Forty-Two Oct 01 '12 at 12:53
  • 4
    I acknowledge that this is the "proper" way to do it, but there's reasons for not doing it that way in this case: a) overhead, b) not agile, c) unmaintainable/error prone. So yeah, I refuse to create two identical classes except for one property. – Manuel Schweigert Oct 03 '12 at 16:09

6 Answers6

182

we can use like this

 db.Entry(model).State = EntityState.Modified;
 db.Entry(model).Property(x => x.Token).IsModified = false;
 db.SaveChanges();

it will update but without Token property

Andrei
  • 40,274
  • 34
  • 151
  • 206
Nitin Dominic
  • 2,489
  • 1
  • 19
  • 22
  • 3
    What if you are using `AddOrUpdate`? - How do you know to use `EntityState.Modified` or `EntityState.Added`? – Jess Oct 26 '15 at 18:50
  • 3
    UPDATE: To make it work in EF 6.. you need to db.Model.Attach(model); – Maxi Sep 26 '17 at 13:49
  • 9
    Just a note to others that the order here is important: if you set `db.Entry(model).State = EntityState.Modified;` after setting `db.Entry(model).Property(x => x.Token).IsModified = false; `, the property will be updated on save. – akerra May 09 '19 at 13:59
  • 3
    And also this should be after updating model values db.Entry (model).CurrentValues.SetValues(sourceModel); If not, then the property is also updated on save. – DotNet Fan Sep 17 '19 at 10:37
16

Anyone looking for how to achieve this on EF Core. It's basically the same but your IsModified needs to be after you add the model to be updated.

db.Update(model);
db.Entry(model).Property(x => x.Token).IsModified = false;
db.SaveChanges();

      

@svendk updated:

And if you (as me) are wondering why model don't have the token either before or after db.SaveChanges(), it's because with Update, the entity is actually not retrieved - only an SQL Update clause is sent - so the context don't know of your model's preexisting data, only the information you gave it in db.Update(mode). Even if you Find(model.id) you are not getting your context updated, as there is already loaded a model in the context, it is still not retrieved from database.

If you (as me) wanted to return the finished model as it looks like in the database, you can do something like this:

db.Update(model);
db.Entry(model).Property(x => x.Token).IsModified = false;
db.SaveChanges();

// New: Reload AFTER savechanges, otherwise you'll forgot the updated values
db.Entry(model).Reload();

Now model is loaded from database with all the values, the updated and the (other) preexisting ones.

SvendK
  • 433
  • 3
  • 15
Jesse
  • 1,678
  • 2
  • 18
  • 32
  • Don't know why, but my EF Core only had string version and I couldn't use lambda. I used nameof instead but this is the way to go. Thanks – Cubelaster Jan 18 '20 at 18:57
  • This answer is so "Microsofty" I knew it would work before testing it. Contrary to the above comment, both the string and lambda version yielded the same results. perhaps we're using different versions. – T3.0 Jul 01 '20 at 17:49
  • I added section about missing values from the model in the database, I hope it is okay. It took me quite a while to figure this out, and as helpful as this answer has been, it only lead me some part of the way. I hope others giving Google the same search terms as me now finds my addition and gets going even faster. – SvendK Apr 01 '22 at 20:39
11

Create new model that will have limited set of properties that you want to update.

I.e. if your entity model is:

public class User
{
    public int Id {get;set;}
    public string Name {get;set;}
    public bool Enabled {get;set;}
}

You can create custom view model that will allow user to change Name, but not Enabled flag:

public class UserProfileModel
{
   public int Id {get;set;}
   public string Name {get;set;}
}

When you want to do database update, you do the following:

YourUpdateMethod(UserProfileModel model)
{
    using(YourContext ctx = new YourContext())
    { 
        User user = new User { Id = model.Id } ;   /// stub model, only has Id
        ctx.Users.Attach(user); /// track your stub model
        ctx.Entry(user).CurrentValues.SetValues(model); /// reflection
        ctx.SaveChanges();
    }
}

When you call this method, you will update the Name, but Enabled property will remain unchanged. I used simple models, but I think you'll get the picture how to use it.

Admir Tuzović
  • 10,787
  • 7
  • 33
  • 70
  • thanks, this looks good, but this still is a whitelisting, not a blacklisting of properties. – Manuel Schweigert Sep 30 '12 at 17:01
  • You're "blacklisting" whatever is not in your view model and this requires no additional coding, you're using only EF features. Furthermore, when stub entity is attached with Attach, all property values are set to null / default. When you use SetValues(model), if your view model property is null, since it was already attached as null, change tracker will not mark it as modified and thus that property will be skipped from saving. Try it. – Admir Tuzović Sep 30 '12 at 18:04
  • 4
    I do not want to argue with you. Blacklisting and whitelisting are different approaches with the same result, your approach is whitelisting. As I said earlier, there are many ways, but I was asking about one in particular. Furthermore, your solution only works with nullable types. – Manuel Schweigert Sep 30 '12 at 18:17
  • 1. Attach your model with Attach 2. loop through properties with db.Entry(model).Property("Propertyname").State = PropertyState.Modified; 3. Do SaveChanges. – Admir Tuzović Sep 30 '12 at 18:22
  • What you do is set entire model to be modified, then set some properties to Unmodified. What I wrote to you is attach model first (nothing is set as modified), and then mark properties you want to update as Modified, which is exactly what you wanted => whitelisting. – Admir Tuzović Oct 01 '12 at 18:06
  • no, you misread my initial post :) I do whitelist now because I haven't found a solution to blacklist properties. The example code I posted in the end is how I would like it to work, but unfortunately haven't found a way to do it like that. – Manuel Schweigert Oct 03 '12 at 16:01
3

I made an easy way to edit properties of entities I will share with you. this code will edit Name and Family properties of entity:

    public void EditProfileInfo(ProfileInfo profileInfo)
    {
        using (var context = new TestContext())
        {
            context.EditEntity(profileInfo, TypeOfEditEntityProperty.Take, nameof(profileInfo.Name), nameof(profileInfo.Family));
        }
    }

And this code will ignore to edit Name and Family properties of entity and it will edit another properties:

    public void EditProfileInfo(ProfileInfo profileInfo)
    {
        using (var context = new TestContext())
        {
            context.EditEntity(profileInfo, TypeOfEditEntityProperty.Ignore, nameof(profileInfo.Name), nameof(profileInfo.Family));
        }
    }

Use this extension:

public static void EditEntity<TEntity>(this DbContext context, TEntity entity, TypeOfEditEntityProperty typeOfEditEntityProperty, params string[] properties)
   where TEntity : class
{
    var find = context.Set<TEntity>().Find(entity.GetType().GetProperty("Id").GetValue(entity, null));
    if (find == null)
        throw new Exception("id not found in database");
    if (typeOfEditEntityProperty == TypeOfEditEntityProperty.Ignore)
    {
        foreach (var item in entity.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetProperty))
        {
            if (!item.CanRead || !item.CanWrite)
                continue;
            if (properties.Contains(item.Name))
                continue;
            item.SetValue(find, item.GetValue(entity, null), null);
        }
    }
    else if (typeOfEditEntityProperty == TypeOfEditEntityProperty.Take)
    {
        foreach (var item in entity.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetProperty))
        {
            if (!item.CanRead || !item.CanWrite)
                continue;
            if (!properties.Contains(item.Name))
                continue;
            item.SetValue(find, item.GetValue(entity, null), null);
        }
    }
    else
    {
        foreach (var item in entity.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetProperty))
        {
            if (!item.CanRead || !item.CanWrite)
                continue;
            item.SetValue(find, item.GetValue(entity, null), null);
        }
    }
    context.SaveChanges();
}

public enum TypeOfEditEntityProperty
{
    Ignore,
    Take
}
Ali Yousefi
  • 2,225
  • 2
  • 30
  • 45
1

I guess you don't want the property to be changed just in some cases, because if you are not going to use it never in your application, just remove it from your model.

In case you want to use it just in some scenarios and avoid its "nullification" in the case above, you can try to:

  • Hide the parameter in the view with HiddenFor:

    @Html.HiddenFor(m => m.Token)

This will make your original value to be kept unmodified and passed back to the controller.

Load again your object in the controller from your DBSet and run this method. You can specify both a white list and a blacklist of parameters that shall or shall not be update.

T.S.
  • 15,962
  • 10
  • 52
  • 71
Jaime
  • 1,100
  • 1
  • 8
  • 14
  • You can find a good discussion about TryUpdateModel here: [link](http://stackoverflow.com/questions/7735635/real-example-of-tryupdatemodel-asp-net-mvc-3). As it is said in the validated answer, it is better to create viewmodels to exactly match the properties needed in each view. – Jaime Oct 02 '12 at 06:44
  • 2
    Using `@Html.HiddenFor` will write the value into the view's HTML and allow the user to use the inspect element within their browser and modify it. After they do that, it's still passed to the controller but with a different value and will be updated. I just tested it. – duck Jan 25 '19 at 18:01
0

I use dapper but my solution will work for EF too. If you are potentially going to change your ORM in the future my solution might be better for you.

class Model
{
    public Foo { get; set; }
    public Boo { get; set; }
    public Bar { get; set; }
    // More properties...

    public void SafeUpdate(Model updateModel, bool updateBoo = false)
    {
        // Notice Foo is excluded

        // An optional update
        if (updateBoo)
            Boo = updateModel.Boo;

        // A property that is always allowed to be updated
        Bar = updateModel.Bar;
        
        // More property mappings...
    }
}

As you can observe I allow updates for only the properties that I wish.

A downside of my approach is that you'll need to manually update this method if you introduce new properties (that are allowed to be updated) to your model. But I believe this in not always a downside but sometimes an advantage, in the sense that you'll need to be aware of what is being updated, this might be beneficial in terms of security.

Let us see a demonstration of this approach.

// Some code, DI etc...

public IActionResult Put([FromBody] Model updateModel)
{
   var safeModel = new Model();
   safeModel.Update(updateModel);

   // Add validation logic for safeModel here...

   _modelRepository.Update(safeModel);
}
Efe Zaladin
  • 159
  • 2
  • 12