ChatGPT解决这个技术问题 Extra ChatGPT

Entity Framework 5 Updating a Record

I have been exploring different methods of editing/updating a record within Entity Framework 5 in an ASP.NET MVC3 environment, but so far none of them tick all of the boxes I need. I'll explain why.

I have found three methods to which I'll mention the pros and cons:

Method 1 - Load original record, update each property

var original = db.Users.Find(updatedUser.UserId);

if (original != null)
{
    original.BusinessEntityId = updatedUser.BusinessEntityId;
    original.Email = updatedUser.Email;
    original.EmployeeId = updatedUser.EmployeeId;
    original.Forename = updatedUser.Forename;
    original.Surname = updatedUser.Surname;
    original.Telephone = updatedUser.Telephone;
    original.Title = updatedUser.Title;
    original.Fax = updatedUser.Fax;
    original.ASPNetUserId = updatedUser.ASPNetUserId;
    db.SaveChanges();
}    

Pros

Can specify which properties change

Views don't need to contain every property

Cons

2 x queries on database to load original then update it

Method 2 - Load original record, set changed values

var original = db.Users.Find(updatedUser.UserId);

if (original != null)
{
    db.Entry(original).CurrentValues.SetValues(updatedUser);
    db.SaveChanges();
}

Pros

Only modified properties are sent to database

Cons

Views need to contain every property

2 x queries on database to load original then update it

Method 3 - Attach updated record and set state to EntityState.Modified

db.Users.Attach(updatedUser);
db.Entry(updatedUser).State = EntityState.Modified;
db.SaveChanges();

Pros

1 x query on database to update

Cons

Can't specify which properties change

Views must contain every property

Question

My question to you guys; is there a clean way that I can achieve this set of goals?

Can specify which properties change

Views don't need to contain every property (such as password!)

1 x query on database to update

I understand this is quite a minor thing to point out but I may be missing a simple solution to this. If not method one will prevail ;-)

Use ViewModels and a good mapping engine ? You get only "properties to update" to populate your view (and then to update). There's still will be the 2 queries for updating (get original + update it), but I wouldn't call this a "Con". If that's your only performance problem, you're an happy man ;)
Thanks @RaphaëlAlthaus, very valid point. I could do this, but I have to create CRUD operation for a number of tables so I'm looking for a method that can work with the model directly to save me creating n-1 ViewModel for each Model.
Well, in my current project (many entities too) we started with working on Models, thinking we would lose time working with ViewModels. We're now going to ViewModels, and with (not negligible) infrastructure work at start, it's far, far, far clearer and easier to maintain now. And more secure (no need to fear about malicious "hidden fields" or things like that)
And no more (awful) ViewBags to populate your DropDownLists (we have at least one DropDownList on almost all our CRU(D) views...)
I think you are right, my bad for trying to overlook ViewModels. Yes, ViewBag just seems a bit dirty at times. I usually go one step further as per Dino Esposito's blog and create InputModels too, a tad belt and braces but it works quite well. Just means 2 extra models per models - doh ;-)

L
Ladislav Mrnka

You are looking for:

db.Users.Attach(updatedUser);
var entry = db.Entry(updatedUser);
entry.Property(e => e.Email).IsModified = true;
// other changed properties
db.SaveChanges();

hi @Ladislav Mrnka,if I want to update all properties at once, can I use the below code? db.Departments.Attach(department); db.Entry(department).State = EntityState.Modified; db.SaveChanges();
One of the problems with this approach is that you can't mock db.Entry(), which is a serious PITA. EF has a reasonably good mocking story elsewhere - it's pretty annoying that (so far as I can tell) they don't have one here.
@Foysal Doing context.Entry(entity).State = EntityState.Modified alone is enough no need to do the attach. It will be automatically attached as its modified...
@Sandman4, that means every other property needs to be there and be set to the current value. In some application designs, this isn't feasible.
"EF has a reasonably good mocking story" - Why does everything nowadays have to be a story?
s
smd

I really like the accepted answer. I believe there is yet another way to approach this as well. Let's say you have a very short list of properties that you wouldn't want to ever include in a View, so when updating the entity, those would be omitted. Let's say that those two fields are Password and SSN.

db.Users.Attach(updatedUser);

var entry = db.Entry(updatedUser);
entry.State = EntityState.Modified;

entry.Property(e => e.Password).IsModified = false;
entry.Property(e => e.SSN).IsModified = false;   

db.SaveChanges();   

This example allows you to essentially leave your business logic alone after adding a new field to your Users table and to your View.


Still I will receive an error if I don't specify a value for SSN property, even though I set IsModified to false it still validate the property against the model rules. So if the property is marked as NOT NULL it will fail if I dont set any value different than null.
You won't receive an error because those fields won't be in your form. You leave out the fields you will definitely not be updating, grab the entry from the database using the form passed back by attaching it, and tell the entry that those fields aren't being modified. Model validation is controlled in the ModelState, not in the context. This example is referencing an existing user, hence "updatedUser". If your SSN is a required field, it would have been there when it was first created.
If I understand correctly, "updatedUser" is an instance of an object already populated with a FirstOrDefault() or similar, so I am updating only the properties I changed and setting others to ISModified=false. This works fine. But, what I am trying to do is to update an object without populating it first, without making any FirstOrDefault() bofore the update. This is when I receive an error if I don't specify a value for all the requiered fields, even thoug I set ISModified = false on those properties. entry.Property(e => e.columnA).IsModified = false; Without this line ColumnA will fail.
What you are describing is creating a new entity. This applies to updating only.
RolandoCC, put db.Configuration.ValidateOnSaveEnabled = false; before the db.SaveChanges();
A
Anik Islam Abhi
foreach(PropertyInfo propertyInfo in original.GetType().GetProperties()) {
    if (propertyInfo.GetValue(updatedUser, null) == null)
        propertyInfo.SetValue(updatedUser, propertyInfo.GetValue(original, null), null);
}
db.Entry(original).CurrentValues.SetValues(updatedUser);
db.SaveChanges();

This seems like a really nice solution - no muss or fuss; you don't have to manually specify properties and it takes into account all of the OPs bullets - is there any reason this does not have more votes?
It doesn't though. It has one of the biggest "cons", more than one hit to the database. You'd still have to load the original with this answer.
@smd why do you say it hits the database more than once? I don't see that happening unless using SetValues() has that effect but that doesn't seem like it would be true.
@parliament I think I must have been asleep when I wrote that. Apologies. The actual problem is overriding an intended null value. If the updated user no longer has reference to something, it would not be right to replace it with the original value if you meant to clear it.
I
Ian Warburton

I have added an extra update method onto my repository base class that's similar to the update method generated by Scaffolding. Instead of setting the entire object to "modified", it sets a set of individual properties. (T is a class generic parameter.)

public void Update(T obj, params Expression<Func<T, object>>[] propertiesToUpdate)
{
    Context.Set<T>().Attach(obj);

    foreach (var p in propertiesToUpdate)
    {
        Context.Entry(obj).Property(p).IsModified = true;
    }
}

And then to call, for example:

public void UpdatePasswordAndEmail(long userId, string password, string email)
{
    var user = new User {UserId = userId, Password = password, Email = email};

    Update(user, u => u.Password, u => u.Email);

    Save();
}

I like one trip to the database. Its probably better to do this with view models, though, in order to avoid repeating sets of properties. I haven't done that yet because I don't know how to avoid bringing the validation messages on my view model validators into my domain project.


Aha... separate project for view models and separate project for repositories that work with view models.
Really like this approach. Still explicit, but easier.
M
Matthew Steven Monkan
public interface IRepository
{
    void Update<T>(T obj, params Expression<Func<T, object>>[] propertiesToUpdate) where T : class;
}

public class Repository : DbContext, IRepository
{
    public void Update<T>(T obj, params Expression<Func<T, object>>[] propertiesToUpdate) where T : class
    {
        Set<T>().Attach(obj);
        propertiesToUpdate.ToList().ForEach(p => Entry(obj).Property(p).IsModified = true);
        SaveChanges();
    }
}

Why not just DbContext.Attach(obj); DbContext.Entry(obj).State = EntityState.Modified;
This controls the set part of the update statement.
B
Bostwick

Just to add to the list of options. You can also grab the object from the database, and use an auto mapping tool like Auto Mapper to update the parts of the record you want to change..


C
Chriss

Depending on your use case, all the above solutions apply. This is how i usually do it however :

For server side code (e.g. a batch process) I usually load the entities and work with dynamic proxies. Usually in batch processes you need to load the data anyways at the time the service runs. I try to batch load the data instead of using the find method to save some time. Depending on the process I use optimistic or pessimistic concurrency control (I always use optimistic except for parallel execution scenarios where I need to lock some records with plain sql statements, this is rare though). Depending on the code and scenario the impact can be reduced to almost zero.

For client side scenarios, you have a few options

Use view models. The models should have a property UpdateStatus(unmodified-inserted-updated-deleted). It is the responsibility of the client to set the correct value to this column depending on the user actions (insert-update-delete). The server can either query the db for the original values or the client should send the original values to the server along with the changed rows. The server should attach the original values and use the UpdateStatus column for each row to decide how to handle the new values. In this scenario I always use optimistic concurrency. This will only do the insert - update - delete statements and not any selects, but it might need some clever code to walk the graph and update the entities (depends on your scenario - application). A mapper can help but does not handle the CRUD logic Use a library like breeze.js that hides most of this complexity (as described in 1) and try to fit it to your use case.

Hope it helps


G
George Fabish

There are some really good answers given already, but I wanted to throw in my two cents. Here is a very simple way to convert a view object into a entity. The simple idea is that only the properties that exist in the view model get written to the entity. This is similar to @Anik Islam Abhi's answer, but has null propagation.

public static T MapVMUpdate<T>(object updatedVM, T original)
{
    PropertyInfo[] originalProps = original.GetType().GetProperties();
    PropertyInfo[] vmProps = updatedVM.GetType().GetProperties();
    foreach (PropertyInfo prop in vmProps)
    {
        PropertyInfo projectProp = originalProps.FirstOrDefault(x => x.Name == prop.Name);
        if (projectProp != null)
        {
            projectProp.SetValue(original, prop.GetValue(updatedVM));
        }
    }
    return original;
}

Pros

Views don't need to have all the properties of the entity.

You never have to update code when you add remove a property to a view.

Completely generic

Cons

2 hits on the database, one to load the original entity, and one to save it.

To me the simplicity and low maintenance requirements of this approach outweigh the added database call.