在实体框架中更新时排除属性

我一直在寻找一个适当的方式来标记一个属性不要改变时,更新模型在 MVC。

例如,让我们以这个小模型为例:

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


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

然后 MVC 创建的编辑方法看起来像这样:

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

现在,如果我的视图不包含令牌,它将通过该编辑失效。

我在找这样的东西:

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

到目前为止,我发现最好的方法是包含并设置所有我想手工包含的属性,但我真的只想说哪些属性被排除在外。

90174 次浏览

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.

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.

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

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
}

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.

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