WebApi Укажите, как указать не указанные свойства из заданных свойств равными нулю?

Вот сценарий. Существует сетевой запрос api для изменения объекта в базе данных sql-сервера. Мы хотим только изменить поля в объекте базы данных, если они были явно указаны в webapi call json. Например:

{ "Name":"newName", "Colour":null }

Это должно изменить поле "Имя" в поле "newName" и "Color" равным null. В отличие от этого json:

{ "Name":"newName" }

который должен изменить только поле Name, оставив прежнее значение цвета.

Что такое хороший способ с WebApi определить, было ли передано свойство или нет?

Если я определяю свой метод следующим образом:

[HttpPut]
[Route("/item/{id}")]
public void ChangeItem(int id, Item item)
{
    ...
}

item.Colour в любом случае будет нулевым. Обратите внимание, что я работаю с различными типами данных здесь, а свойство Colour в этом примере может быть int, string, DateTime, Guid и т.д.

Я понимаю, что я могу получить сырой json с атрибутом [FromBody], а затем проанализировать его сам, но похоже, что связующее по умолчанию уже выполняет большую часть работы (включая проверку), поэтому мне было бы интересно, как я могу повторно использовать его, но и добиться того, чего я хочу. Что является самым простым способом?

Обновление

Я хотел бы уточнить, что шахта - это "случайно связанный" сценарий. То есть устройства, использующие API, большую часть времени не имеют сетевого покрытия, и они время от времени синхронизируются с использованием API.

Практически это означает, что большинство данных, которые необходимы для синхронизации, агрегируются в нуль или один вызов "push update to server", за которым следует вызов "получить последнее состояние от сервера". С Sql Server и EF в back-end, что приводит к нескольким различным (а иногда и не связанным) объектам, содержится в одном json. Например:

class TaskData
{ 
    public IList<User> AssignedUsers {get; set;} 
    public IList<Product> Products {get; set;} 
    public Task Task {get; set}
}

Кроме того, классы моделей, которые используются для генерации вызовов json для GET, отделены от EF Entites, поскольку схема базы данных точно не соответствует объектной модели API.

Ответы

Ответ 1

В итоге я использовал динамический прокси для свойств, чтобы я мог пометить свойства, написанные JsonMediaTypeFormatter как "грязные". Я использовал слегка измененный yappi (на самом деле не нужно было его изменять, просто хотел - упомянуть об этом, если приведенный ниже код точно не соответствует yappi образцы /API ). Я предполагаю, что вы можете использовать свою любимую динамическую прокси-библиотеку. Просто для удовольствия я попытался перенести его на NProxy.Core, но это не сработало, потому что по какой-то причине json.net отказался писать в прокси, что NProxy.Core.

Итак, это работает так. У нас есть базовый класс по этим строкам:

public class DirtyPropertiesBase
{
    ...

    // most of these come from Yappi
    public static class Create<TConcept> where TConcept : DirtyPropertiesBase
    {
        public static readonly Type Type =PropertyProxy.ConstructType<TConcept, PropertyMap<TConcept>>(new Type[0], true);
        public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
    }

    private readonly List<string> _dirtyList = new List<string>();

    protected void OnPropertyChanged(string name)
    {
        if (!_dirtyList.Contains(name))
        {
            _dirtyList.Add(name);
        }
    }
    public bool IsPropertyDirty(string name)
    {
        return _dirtyList.Contains(name);
    }

    ...
    // some more Yappi specific code that calls OnPropertyChanged
    // when a property setter is called
}

Где-то в реализации прокси вызываем OnPropertyChanged, чтобы мы помнили, какие свойства были записаны.

Тогда у нас есть пользовательский JsonCreationConverter:

class MyJsonCreationConverter : JsonConverter
{
    private static readonly ConcurrentDictionary<Type, Func<DirtyPropertiesBase>> ContructorCache = new ConcurrentDictionary<Type, Func<DirtyPropertiesBase>>();
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException("MyJsonCreationConverter should only be used while deserializing.");
    }
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        Func<DirtyPropertiesBase> constructor = ContructorCache.GetOrAdd(objectType, x =>
            (Func<DirtyPropertiesBase>)typeof(DirtyPropertiesBase.Create<>).MakeGenericType(objectType).GetField("New").GetValue(null));

        DirtyPropertiesBase value = constructor();
        serializer.Populate(reader, value);
        return value;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof (DirtyPropertiesBase).IsAssignableFrom(objectType);
    }
}

Идея здесь, так как JsonMediaTypeFormatter преобразует входящий json, мы заменяем начальный пустой объект как прокси, который мы определили ранее.

Мы регистрируем этот конвертер в WebApiConfig.cs, как этот

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new MyJsonCreationConverter());

Теперь, когда наша модель заполняется из json вместо каждого объекта, полученного из DirtyPropertiesBase, будет прокси с правильно заполненной коллекцией _dirtyList. Теперь нам нужно только привязать каждую из этих моделей к объекту EF. Это достаточно просто с AutoMapper. Мы регистрируем каждую модель следующим образом:

Mapper.CreateMap<Model, Entity>().ForAllMembers(x => x.Condition(z => ((Model)z.Parent.SourceValue).IsPropertyDirty(z.MemberName)));

И тогда у вас есть свой обычный код отображения:

Entity current = _db.Entity.Single(x => x.Id == Id);
Mapper.Map(update, current);
_db.SaveChanges();

Это обеспечит обновление только Dirty-свойств.

Ответ 2

В то время как для служб OData вы можете попробовать использовать System.Web.Http.OData.Delta<T>. Это позволяет частично обновлять объекты.

Взгляните на этот пост в блоге за хорошее обсуждение использования Delta<T>. По существу это сводится к определению методов Put и Patch, таких как:

public class MyController : ApiController
{
    // Other actions omitted…

    [AcceptVerbs("Patch")]
    public async Task<IHttpActionResult> Patch(int key, Delta<Item> model)
    {
        var entity = _items.FindAsync(o => o.Id == key);

        if (entity == null) {
            return NotFound();
        }

        model.Patch(entity);

        return StatusCode(HttpStatusCode.NoContent);
    }

    public async Task<IHttpActionResult> Put(int key, Delta<Item> model)
    {
        var entity = _items.FindAsync(o => o.Id == key);

        if (entity == null) {
            return NotFound();
        }

        model.Put(entity);

        return StatusCode(HttpStatusCode.NoContent);
    }
}

Здесь запрос Put будет обновлять всю модель, тогда как запрос на Patch будет только частично обновлять модель (используя только свойства, переданные клиентом).

Ответ 3

Несомненно, это проблема персистентности, а не проблема с привязкой к модели.

Вашему API предоставляется нулевое значение для данного свойства, поэтому связующее выполняет его.

Возможно, в упорстве вы можете посоветовать какую-либо структуру, которую вы используете, чтобы игнорировать пустые записи (я предполагаю, что вы передаете значение nullable int вместо ss)

Ответ 4

Я решил проблему, используя этот шаблон.

public class ValuesController : ApiController
{
    public void Put(int id, [FromBody]Item value)
    {
        if (value.NameSpecified)
        {

        }
        else
        {

        }
    }
}

public class Item
{
    internal bool NameSpecified = false;
    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            NameSpecified = true;
        }
    }
}