Partial Entity Updates in WebAPI PUT/POST

2019-02-01 11:42发布

问题:

Say you have a repository method to update a Document:

public Document UpdateDocument(Document document)
  {
  Document serverDocument = _db.Documents.Find(document.Id);
  serverDocument.Title = document.Title;
  serverDocument.Content = document.Content;
  _db.SaveChanges();
  return serverDocument;
  }

In this case, the entity has two properties. When updating a Document, both of these properties are required in the JSON request, so a request to PUT /api/folder with a body of

{
  "documentId" = "1",
  "title" = "Updated Title"
}

would return an error because "content" was not provided. The reason I'm doing this is because, even for nullable properties and properties that the user doesn't update, it seems safer to force the client to specify these fields in the request to avoid overwriting unspecified fields with nulls serverside.

This has led me to the practice of always requiring every updatable property in PUT and POST requests, even if it means specifying null for those properties.

Is this cool, or is there a pattern/practice that I haven't learned about yet that might facilitate partial updates by sending only what is needed over the wire?

回答1:

The best practice in API design is to use HTTP PATCH for partial updates. In fact, use cases like yours are the very reason why IETF introduced it in the first place.

RFC 5789 defines it very precisely:

PATCH is used to apply partial modifications to a resource.

A new method is necessary to improve interoperability and prevent
errors. The PUT method is already defined to overwrite a resource
with a complete new body, and cannot be reused to do partial changes. Otherwise, proxies and caches, and even clients and servers, may get
confused as to the result of the operation. POST is already used but without broad interoperability (for one, there is no standard way to
discover patch format support).

Mark Nottingham has written a great article about the use of PATCH in API design - http://www.mnot.net/blog/2012/09/05/patch

In your case, that would be:

  [AcceptVerbs("PATCH")]
  public Document PatchDocument(Document document)
  {
      Document serverDocument = _db.Documents.Find(document.Id);
      serverDocument.Title = document.Title;
      serverDocument.Content = document.Content;
      _db.SaveChanges();
      return serverDocument;
  }


回答2:

Is this cool, or is there a pattern/practice that I haven't learned about yet that might facilitate partial updates by sending only what is needed over the wire?

A good practice of doing a POST or PUT is to only include values that you need for that specific request. In doing the UpdateDocument you should ask yourself what "really should be done here"? If you have a hundred fields on that object do you need to update all of them or only part of them. What "action" are you really trying to do?

Let's have an illustration for those questions, say we have a User object that has the following fields:

public class User {
    public int Id {get;set;}
    public string Username {get;set;}
    public string RealName {get;set;}
    public string Password {get;set;}
    public string Bio {get;set;}
}

You then have two use cases:

  1. Update the profile of a User
  2. Update the password of a User

When you do each of those you will not, or it's a good idea to, have one update method that will do both. Instead of having a generic UpdateUser method you should have the following methods:

  1. UpdateProfile
  2. UpdatePassword

Methods that accepts fields that they just need, nothing more, nothing less.

public User UpdateProfile(int id, string username, string realname, string bio) {
}
public User UpdatePassword(int id, string password) {
}

Now comes the question:

I have a use case that a "user action" allows for an update on multiple fields where some of the fields can have "no input" from the user but I don't want to update that field in my model.

Suppose a user updates his/her profile and provided values for Username, RealName but not for Bio. But you do not want to set Bio as null or empty if it has a value already. Then that becomes a part of your application's business logic and that should be handled explicitly.

public User UpdateProfile(int id, string username, string realname, string bio) {
    var user = db.Users.Find(id);
    // perhaps a validation here (e.g. if user is not null)
    user.Username = username;
    user.RealName = realname;
    if (!string.IsNullOrEmptyWHiteSpace(bio)) {
        user.Bio = bio;
    }
}