WebAPI создает экземпляры объектов, даже если они нулевые в JSON

При публикации моделей JSON в методах контроллера WebAPI я заметил, что если в модели JSON есть нулевые объекты, связыватель модели будет создавать экземпляры этих элементов вместо того, чтобы сохранять их нулевыми в объекте на стороне сервера.

Это отличается от того, как обычный контроллер MVC связывает данные... он не создает экземпляр объекта, если он имеет значение null в JSON.

Контроллер MVC

public class HomeController : Controller
{
    [HttpPost]
    public ActionResult Test(Model model)
    {
        return Json(model);
    }
}

Контроллер веб-API

public class APIController : ApiController
{
    [HttpPost]
    public Model Test(Model model)
    {
        return model;
    }
}

Класс модели, который будет отправлен в POST

public class Model
{
    public int ID { get; set; }
    public Widget MyWidget { get; set; }
}

Класс, используемый в классе Model

public class Widget
{
    public int ID { get; set; }
    public string Name { get; set; }
}

Вот мои результаты, когда я публикую модель JSON для каждого контроллера:

$.post('/Home/Test', { ID: 29, MyWidget: null })
//Results in: {"ID":29,"MyWidget":null}

$.post('/api/api/Test', { ID: 29, MyWidget: null })
//Results in: {"ID":29,"MyWidget":{"ID":0,"Name":null}}

Как видите, метод WebAPI создал экземпляр свойства MyWidget с объектом, тогда как действие MVC оставило его нулевым.

Мне не кажется интуитивно понятным, что WebAPI будет работать таким образом. Зачем это делать? Могу ли я заставить его вести себя как действие MVC в этом отношении?


person Casey Williams    schedule 05.01.2016    source источник
comment
Нет никаких шансов, что ваш Model создаст Widget в своем конструкторе по умолчанию и присвоит его MyWidget, не так ли?   -  person dbc    schedule 06.01.2016
comment
Какую версию веб-API вы используете?   -  person Nikolai Samteladze    schedule 06.01.2016
comment
@dbc, нет, класс именно такой, каким ты его видишь. Конструктор не указан. Кроме того, если бы это была проблема, основанная на конструкторе, это произошло бы для обоих контроллеров.   -  person Casey Williams    schedule 06.01.2016
comment
@Николай, WebAPI версии 2   -  person Casey Williams    schedule 06.01.2016


Ответы (2)


Я думаю, что это похоже на проблемы, с которыми мы сталкивались ранее в наших проектах.

Вы должны изменить почтовый индекс для jQuery на следующий:

$.ajax({
            type: 'POST',
            url: '/api/api/Test',
            data: JSON.stringify({ ID: 29, MyWidget: null }),
            contentType: "application/json",
            dataType: 'json',
            timeout: 30000
        })
        .done(function (data) {
        })
        .fail(function() {
        });

По умолчанию jQuery 'posts' отправляет параметры в виде данных, закодированных в виде URL-адреса формы.

application/x-www-form-urlencoded
ID 29
MyWidget

ID=29&Мой виджет=

Так что он был десериализован абсолютно правильно. MyWidget — это пустая строка, поэтому она будет иметь пустое значение класса Widget.

Кроме того, я рекомендую вам добавить конфигурацию Formatters для контроллеров WebApi:

public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        // Formatters
        JsonMediaTypeFormatter json = config.Formatters.JsonFormatter;

        config.Formatters.Clear();
        config.Formatters.Add(json);
    }

Таким образом, вы будете использовать только форматировщик JSON для вызовов API.

ОБНОВЛЕНИЕ

Основное отличие заключается в том, что данные, закодированные в форме URL-адреса, передаваемые контроллеру MVC, будут обрабатываться во время выполнения и, наконец, обрабатываться DefaultModelBinder (если пользовательский связыватель недоступен). Таким образом, данные, закодированные как URL-адрес формы, являются обычными для MVC, потому что обычно данные генерируются публикацией HTML-формы. Но веб-API не полагается на какую-либо конкретную кодировку. Поэтому он использует специальный механизм (форматеры) для анализа данных... например, json, как показано выше. Таким образом, FormUrlEncodedMediaTypeFormatter из System.Net.Http.Formatting и DefaultModelBinder из System.Web.Mvc по-разному обрабатывают пустую строку.

Для DefaultModelBinder пустая строка будет преобразована в null. Анализируя код, я могу решить, что метод BindModel сначала создает пустую модель:

 if (model == null)
 {
     model = CreateModel(controllerContext, bindingContext, modelType);
 }

После того, как он заполнит свойства:

// call into the property's model binder
        IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
        object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
        ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
        propertyMetadata.Model = originalPropertyValue;
        ModelBindingContext innerBindingContext = new ModelBindingContext()
        {
            ModelMetadata = propertyMetadata,
            ModelName = fullPropertyKey,
            ModelState = bindingContext.ModelState,
            ValueProvider = bindingContext.ValueProvider
        };
        object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);

И, наконец, GetBinder вернет fallbackBinder для типа виджета (типа свойства). И сам fallbackBinder вызовет ConvertSimpleType, где строка обрабатывается следующим образом:

        string valueAsString = value as string;
        if (valueAsString != null && String.IsNullOrWhiteSpace(valueAsString))
        {
            return null;
        }

Я предполагаю, что нет никаких стандартов, описывающих преобразование из строк в кодировке URL в объекты C#. Так что я не знаю, какой из них правильный. В любом случае я уверен, что вам нужно передавать json через вызовы AJAX, а не данные в формате URL-адреса.

person Maxim    schedule 06.01.2016
comment
Это сработало (в частности, с использованием $.ajax), спасибо за понимание! Мне интересно, что MVC Action и метод WebAPI ведут себя в этом отношении по-разному. - person Casey Williams; 06.01.2016
comment
Я добавлю объяснение относительно разницы в исходном ответе. - person Maxim; 06.01.2016
comment
Отличное объяснение, спасибо! Хотел бы я снова проголосовать за ваш ответ. - person Casey Williams; 06.01.2016

Используйте сериализатор newtonsoft. Newtonsoft.Json сохраняет их нулевыми

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;


public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
              var jsonformatter = new JsonMediaTypeFormatter();
              config.Formatters.Clear();
              config.Formatters.Add(jsonformatter);
         }
    }
person halit    schedule 05.01.2016
comment
Разве веб-API не использует сериализатор Newtonsoft.Json по умолчанию? - person Nikolai Samteladze; 06.01.2016
comment
@NikolaiSamteladze ты прав. Я проверил сейчас. Но Json.Net не инициализирует нулевые объекты. - person halit; 06.01.2016
comment
Объект создается во время привязки. Если установить точку останова в действии контроллера, я увижу уже созданный новый объект (или null в случае контроллера MVC). - person Casey Williams; 06.01.2016