Частичные обновления объектов в WebAPI PUT/POST

Скажем, у вас есть метод репозитория для обновления документа:

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

В этом случае сущность имеет два свойства. При обновлении документа оба эти свойства требуются в запросе JSON, поэтому запрос к PUT /api/folder с телом

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

вернет ошибку, потому что «контент» не был предоставлен. Причина, по которой я это делаю, заключается в том, что даже для свойств, допускающих значение NULL, и свойств, которые пользователь не обновляет, кажется более безопасным заставить клиента указать эти поля в запросе, чтобы избежать перезаписи неуказанных полей нулевыми значениями на стороне сервера.

Это привело меня к тому, что я всегда требовал каждое обновляемое свойство в запросах PUT и POST, даже если это означает, что для этих свойств указано значение null.

Это круто, или есть шаблон/практика, о которой я еще не узнал, которая может облегчить частичные обновления, отправляя по сети только то, что необходимо?


person SB2055    schedule 22.04.2013    source источник


Ответы (2)


Лучшей практикой в ​​разработке API является использование HTTP PATCH для частичных обновлений. На самом деле именно такие варианты использования, как ваш, и есть та самая причина, по которой IETF ввел его в первую очередь.

RFC 5789 определяет его очень точно:

PATCH используется для применения частичных изменений к ресурсу.

Новый метод необходим для улучшения совместимости и предотвращения
ошибок. Метод PUT уже определен для перезаписи ресурса
полностью новым телом, и его нельзя повторно использовать для внесения частичных изменений. В противном случае прокси-серверы и кэши, и даже клиенты и серверы могут
запутаться в результате операции. POST уже используется, но без широкого взаимодействия (во-первых, нет стандартного способа
обнаружить поддержку форматов исправлений).

Марк Ноттингем написал отличную статью об использовании PATCH в разработке API — http://www.mnot.net/blog/2012/09/05/patch

В вашем случае это будет:

  [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;
  }
person Filip W    schedule 22.04.2013
comment
Пример не будет работать с Content-Type: application/json-patch {replace: /count, value: 5}, если не будет фильтра действий или средства форматирования типа мультимедиа для преобразования схемы исправления в объект. - person Clive; 12.03.2014
comment
Этот ответ совершенно непрактичен. Что делать, если вы хотите обновить несколько других полей в Document? Как узнать, какие из этих полей нужно обновить? Нулевой заголовок означает удаление или не обновлять? Без правильного формата патча в таких вещах разобраться невозможно. Насколько мне известно, поддержка JSON-Patch ASP.Net не поддерживается. Хотя я бы хотел оказаться неправым. О, и я большой поклонник ваших статей! :) - person Mrchief; 19.08.2015
comment
Забыл упомянуть, что Delta OData может помочь, хотя я не уверен, что вы можете использовать его, не вступая в брак с OData в целом. - person Mrchief; 19.08.2015

Это круто, или есть шаблон/практика, о которой я еще не узнал, которая может облегчить частичные обновления, отправляя по сети только то, что необходимо?

Хорошей практикой выполнения POST или PUT является включение только тех значений, которые вам нужны для этого конкретного запроса. Выполняя UpdateDocument, вы должны спросить себя, что «на самом деле следует здесь сделать»? Если у вас есть сотни полей в этом объекте, вам нужно обновить их все или только часть из них. Какое «действие» вы действительно пытаетесь сделать?

Давайте проиллюстрируем эти вопросы, скажем, у нас есть объект User со следующими полями:

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

Затем у вас есть два варианта использования:

  1. Обновить профиль пользователя
  2. Обновите пароль пользователя

Когда вы делаете каждый из них, вы не будете иметь один метод обновления, который будет делать оба. Вместо общего метода UpdateUser у вас должны быть следующие методы:

  1. UpdateProfile
  2. UpdatePassword

Методы, которые принимают поля, которые им просто нужны, ни больше, ни меньше.

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

Теперь возникает вопрос:

У меня есть вариант использования, когда «действие пользователя» позволяет обновлять несколько полей, где некоторые поля могут «не вводиться» пользователем, но я не хочу обновлять это поле в моей модели.

Предположим, пользователь обновил свой профиль и указал значения для Username, RealName, но не для Bio. Но вы не хотите устанавливать Bio как нулевое или пустое, если оно уже имеет значение. Затем это становится частью бизнес-логики вашего приложения и обрабатывается явно.

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;
    }
}
person von v.    schedule 22.04.2013
comment
эти сигнатуры методов не будут работать с веб-API ASP.NET, поскольку: 1) он не будет искать в теле примитивные параметры, если только явно не указано с помощью атрибута [FromBody], и 2) он может привязать только один объект из тела в любом случае - person Filip W; 22.04.2013
comment
Согласно спецификации HTTP, вы не должны использовать PUT для выполнения частичных обновлений. Однако вы можете делать с POST все, что хотите. - person Darrel Miller; 22.04.2013