4

I have a problem in my UserRepository in which I want to update a user. I dont want certain fields updated, such as password, unless specified. For example, When I pass the User from the view, to the service to the repository, it sends up the user with a null or empty password string. This null gets written to the database (which I dont want).

How do I handle a situation like this?

Domain

public class User
{
    public int UserId { get; set; }

    public string Email { get; set; }
    public string Password { get; set; }
}

Repository

    public User Save(User user)
    {
        if (user.UserId > 0)
        {
            User dbUser = context.Users.FirstOrDefault(u => u.UserId == user.UserId);
            //What do I do here?
        }
        context.Users.AddObject(user);
        context.SaveChanges();
        return user;
    }

Lets say in this case, my view allows me to change only Email, so the only thing that gets sent back to the Save() method are: user.UserId and user.Email while user.Password is null. In my case, the database throws error because Password should be nullable.

Shawn Mclean
  • 56,733
  • 95
  • 279
  • 406

3 Answers3

10

Detached POCO scenario (you will not load user from DB before update):

You can selectively say which properties must be updated:

public User Save(User user)     
{         
    if (user.UserId == 0)         
    {             
        context.Users.AddObject(user);         
    }
    else
    {
        context.Users.Attach(user);
        ObjectStateEntry entry = context.ObjectStateManager.GetObjectStateEntry(user);
        entry.SetModifiedProperty("Email");
    }

    context.SaveChanges();         
    return user;     
}

You can also create two overloads of you Save method. First will update whole object, second will update only explicitly selected properties:

public User Save(User user)     
{         
    if (user.UserId == 0)         
    {             
        context.Users.AddObject(user);         
    }
    else
    {
        context.Users.Attach(user);
        context.ObjectStateManager.ChangeObjectState(user, EntityState.Modified);        
    }

    context.SaveChanges();         
    return user;     
}

public User Save(User user, IEnumerable<Expression<Func<User, object>>> properties)     
{         
    if (user.UserId == 0)         
    {             
        context.Users.AddObject(user);         
    }
    else
    {
        context.Users.Attach(user);
        ObjectStateEntry entry = context.ObjectStateManager.GetObjectStateEntry(user);
        foreach(var selector in properties)
        {
            string propertyName = PropertyToString(selector.Body);
            entry.SetModifiedProperty(propertyName);
        }
    }

    context.SaveChanges();         
    return user;     
}

// Doesn't work for navigation properties!
private static string PropertyToString(Expression selector)
{
    if (selector.NodeType == ExpressionType.MemberAccess)
    {
        return ((selector as MemberExpression).Member as PropertyInfo).Name;
    }

    throw new InvalidOperationException();
}

You will call the second overload this way:

userRepository.Save(user, new List<Expression<Func<User, object>>> 
    { 
        u => u.Email 
    });

Attached scenario (you will load user from DB before update):

You can modify your Save method to accept delegate so that you can control how update will be performed:

public User Save(User user, Action<User, User> updateStrategy)                                
{                                  
    if (user.UserId > 0)                                  
    {
        User dbUser = context.Users.FirstOrDefault(u => u.UserId == user.UserId);
        updateStrategy(dbUser, user);                                                                        
    }        
    else
    {                          
        // New object - all properties should be saved
        context.Users.AddObject(user);
    }

    context.SaveChanges();                                  
    return user;                              
}  

You will call the method this way:

var user = GetUpdatedUserFromSomewhere();
repository.Save(user, (dbUser, mergedUser) => 
    {
        dbUser.Email = mergedUser.Email;
    });

Anyway, despite of my examples you should definitely think about Darin's post and special ModelViews for updating.

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • For the one that explicitly updates the objects, why doesn't the PropertyToString work for bool? – Shawn Mclean Jan 18 '11 at 19:53
  • That updateStrategy is a thing of beauty. Love it! I'm using that for my repository's update methods now; the calling code gets to say what to update, and how to update it. – Jez Oct 25 '12 at 13:01
1

You should use view models. View models are classes which are specifically tailored to the needs of a view and contain only the properties needed by this given view. So your controller action should look like this:

[HttpPost]
public ActionResult Update(UserViewModel model) { ... }

instead of:

[HttpPost]
public ActionResult Update(User model) { ... }

Inside the controller action you could map between the view model and the model. AutoMapper is a great tool that could simplify this task.

You should really be very careful and never expose your models like this. Always use view models to and from a view. Just imagine if there was an IsAdministrator boolean property on your model.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    This is what I do. This doesn't solve the problem of the repository trying to update the Password field to null. – Shawn Mclean Jan 15 '11 at 00:12
  • This is great advice but it doesn't show how to handle updates of specific fields in the repository. – Shawn Mclean Jan 15 '11 at 01:01
  • I up voted this answer (at this time bringing it back to 0) because I do think it's the right one. In your action, you can pull the user out of your repository, apply only the changes needed, then persist it again. Since you are in an action that is only meant to update an email address, then you know that is all that needs to be updated. – Brian Ball Jan 15 '11 at 03:05
  • @Brian Ball, but this still has nothing to do with how the repository saves the data because mapping the UserViewModel to User still lets User.Password be null. – Shawn Mclean Jan 15 '11 at 03:23
  • You can use the view model to get the fields you need to edit. Then retrieve the object from the database and update only the fields exposed in the view model. Since you retrieved the object from the database the password field will be populated and save correctly when you save the object via your repository. – Brownman98 Jan 15 '11 at 03:59
0

Can you do this?

public User Save(User user)
    {
        if (user.UserId > 0)
        {
            User dbUser = context.Users.FirstOrDefault(u => u.UserId == user.UserId);
            //What do I do here?
            dbUser.Email = user.Email
            user = dbUser;
        }
        else
        {
            context.Users.AddObject(user);
        }
        context.SaveChanges();
        return user;
    }
Simon Hazelton
  • 1,245
  • 10
  • 13
  • I guess you could also check the fields you want to update with if(!string.IsNullOrEmpty(user.Email)), if(!string.IsNullOrEmpty(user.Password)), etc – Simon Hazelton Jan 15 '11 at 03:04