Об'єкт тіла web-api POST завжди нульовий


77

Я все ще вивчаю веб-API, тож пробачте, якщо моє запитання звучить по-дурному.

У мене є це в моєму StudentController:

public HttpResponseMessage PostStudent([FromBody]Models.Student student)
{
    if (DBManager.createStudent(student) != null)
        return Request.CreateResponse(HttpStatusCode.Created, student);
    else
        return Request.CreateResponse(HttpStatusCode.BadRequest, student);
}

Для того, щоб перевірити, чи це працює, я використовую розширення Google Chrome «Листоноша» для побудови запиту HTTP POST для його тестування.

Це мій необроблений запит POST:

POST /api/Student HTTP/1.1
Host: localhost:1118
Content-Type: application/json
Cache-Control: no-cache

{"student": [{"name":"John Doe", "age":18, "country":"United States of America"}]}

studentповинен бути об'єктом, але коли я налагоджую програму, API отримує studentоб'єкт, але вміст завжди null.


3
Просто підказка для тих, хто знайде це пізніше (як я щойно зробив) під час розгляду подібної проблеми: Веб-API повинен повернути JSON, що містить виняток (як ніби ви виявили виняток у своєму коді), який можна використовувати для діагностики проблема. Здається очевидним, але я підозрюю, що я не єдина людина, яка не думала перевіряти відповідь і припускала, що це просто стандартний код відповіді HTTP!
Jon Story

Відповіді:


59

FromBody - це дивний атрибут, оскільки вхідні значення POST повинні мати певний формат, щоб параметр не був нульовим, якщо він не є примітивним типом. (студент тут)

  1. Спробуйте свій запит за {"name":"John Doe", "age":18, "country":"United States of America"}допомогою json.
  2. Видаліть [FromBody]атрибут і спробуйте рішення. Це має працювати для непримітивних типів. (студент)
  3. З [FromBody]атрибутом інший варіант - надсилати значення у =Valueформаті, а не у key=valueформаті. Це означало б, що вашим ключовим значенням studentмає бути порожній рядок ...

Існують також інші варіанти написання підшивки для користувацьких моделей для студентського класу та атрибуції параметра з вашим нестандартним підшивкою.


3
видалено [FromBody], все ще не працює. вміст об'єкта студента все ще нульовий .. (наприклад, ім'я = нуль, країна = нуль, вік = нуль)
InnovativeDan

ЦЕ СПРАЦЮВАЛО! не очікував, що проблема пов'язана з моїм форматом JSON. ДЯКУЮ ЛЮДИНО! позначено як відповідь.
Innovative,

1
№1 працював у мене. Дякую :) Мені просто потрібно було вилучити 'ключ' зі свого JSON і надіслати 'значення'
IsolatedStorage

12
для інших читачів .. початковою проблемою тут було те, що OP надсилав об’єкт JSON, який містив властивість студента, замість того, щоб надсилати лише студентський об’єкт, як очікував webapi.
Шон Вільсон

3
Це пізній коментар, але ваше рішення допомогло мені виправити проблему, з якою я боровся протягом останніх двох годин.
Вітаємо

59

Я вже кілька хвилин шукав рішення своєї проблеми, тому поділюсь своїм рішенням.

Коли у вашій моделі є власний конструктор, ваша модель також повинна мати порожній / конструктор за замовчуванням. Інакше модель очевидно неможливо створити. Будьте обережні під час рефакторингу.


1
Це важлива інформація, яку слід додати до прийнятої відповіді.
Youkko

1
це просто не точно. Класи (модель - це клас) мають конструктор за замовчуванням, навіть коли вони не вказані, і їх точно можна створити
Андрій Драготоню

Я згоден з @AndreiDragotoniu. Здається, це неправда. Я стикався з цією проблемою, і моя проблема полягала в тому, що я мав лише властивості для читання. Після додавання сетерів до кожного він працював нормально (навіть із зазначенням лише непустого конструктора).
Broots Waymb

2
@AndreiDragotoniu Я думаю, що проблема виникає, коли ви створили конструктор, який вимагає аргументів. Якщо ви зробили це, це замінює конструктор за замовчуванням. У цьому випадку вам також потрібно створити додатковий порожній конструктор.
LairdPleng

1
@LairdPleng Дякую за роз'яснення. Відповідь я відредагував.
chrjs

38

Я витрачаю кілька годин на цю проблему ... :( Геттери та сетери потрібні в оголошенні об’єктів параметрів POST. Я не рекомендую використовувати прості об’єкти даних (рядок, int, ...), оскільки вони вимагають спеціального формату запиту.

[HttpPost]
public HttpResponseMessage PostProcedure(EdiconLogFilter filter){
...
}

Не працює, коли:

public class EdiconLogFilter
{
    public string fClientName;
    public string fUserName;
    public string fMinutes;
    public string fLogDate;
}

Прекрасно працює, коли:

public class EdiconLogFilter
{
    public string fClientName { get; set; }
    public string fUserName { get; set; }
    public string fMinutes { get; set; }
    public string fLogDate { get; set; }
}

1
Ох, вау, я не можу повірити, що це насправді було моєю проблемою ... Дякую купу.
Кріс

Потрібні властивості з getter та setter виправили мою проблему для мого запиту GET, яка неприємна проблема
березня

Дякую! Якби я не прочитав вашу відповідь, я б не турбувався подвійною перевіркою і помітив, що модифікатори доступу до встановлених властивостей мого DTO були internalі це спричинило проблему.
Саймон Морган

Ого, це теж було моє рішення. Люди, остерігайтеся приватних сетерів! Заміна "приватного набору;" встановлювати;" виправлено мою проблему.
FBaez51,

19

Якщо будь-яке зі значень об'єкта JSON запиту не є тим типом, як очікується службою, [FromBody]аргументом буде null.

Наприклад, якщо властивість age у json мала floatзначення:

"вік": 18,0

але служба API очікує, що це буде int

"вік": 18

тоді studentбуде null. (У відповіді повідомлення про помилки не надсилатимуться, якщо не буде перевірено нульове посилання).


Дуже дякую! Ми намагалися прив’язати складний об’єкт до рядка (Бог знає чому). Після виправлення це спрацювало. Ця проблема зводила мене з розуму. Яке дурне рішення невдало провалитися і повернутися null ...
Лукас Браганса

13

Це трохи старий варіант, і моя відповідь піде до останнього місця, але навіть тому я хотів би поділитися своїм досвідом.

Випробував кожну пропозицію, але все одно мав те саме "нульове" значення в PUT [FromBody].

Нарешті знайшов, що все стосується формату Date, тоді як JSON серіалізує властивість EndDate мого Angular Object.

Не було виявлено помилки, просто отримано порожній об'єкт FromBody ....


3
Я передавав йому всілякі дивні дані, очікуючи, що він буде розумним у своєму картографуванні, але ні ... Він гине і не дає вам нічого, крім нуля. Я спробував ще раз із добре вихованими даними та привіт, попередньо, відсортовано. Твоя відповідь спонукала мене до цього!
MGDavies

2
Ця відповідь була дуже корисною, оскільки форматування дати стало причиною помилки, з якою я також стикався.
Елайджа Лофгрен

Уникайте використання [FromBody] перед параметрами, коли вони передаються як JSON у тілі запиту POST. Також переконайтесь, що ім’я верхнього рівня у вашому JSON відповідає імені змінної вашого параметра.
justdan23

Дякую, Ти справжній бог.
Хірофумі Окіно,

10

Якщо ви використовуєте Листоноша, переконайтесь, що:

  • Ви встановили для заголовка "Content-Type" значення "application / json"
  • Ви відправляєте тіло як "сире"
  • Вам не потрібно ніде вказувати назву параметра, якщо ви використовуєте [FromBody]

Я тупо намагався надіслати свій JSON як дані форми, так ...


Те саме для мене. Дякуємо за надання детальної відповіді.
goku_da_master

Як тоді форматувати своє тіло? Я маю на увазі, якщо я просто вклав "= Це моє значення" в тілі (оголошеному як JSON), це просто скаже мені, що JSON є помилковим. Хочете навести невеликий приклад? РЕДАГУВАТИ: Неважливо, просто знайшов рішення. У моєму випадку "Content-Type" мав бути "application / x-www-form-urlencoded", а моє тіло "raw" типу Text.
Джекс

Якщо ви НЕ використовуєте [FromBody], тоді він повинен проаналізувати ваш JSON, доки ім'я верхнього рівня у вашому JSON відповідає імені змінної вашого параметра.
justdan23

Саме в цьому була проблема для мене! Дуже дякую.
сойка-небезпека

8

TL; DR: Не використовуйте [FromBody], а прокатуйте свою власну версію з кращою обробкою помилок. Причини, наведені нижче.

Інші відповіді описують багато можливих причин цієї проблеми. Однак першопричиною є те, що [FromBody]просто існує жахлива обробка помилок, що робить її майже марною у виробничому коді.

Наприклад, однією з найбільш типових причин для параметра nullє те, що тіло запиту має неприпустимий синтаксис (наприклад, недійсний JSON). У цьому випадку повернеться розумний API 400 BAD REQUEST, і розумна веб-структура зробить це автоматично. Однак веб-API ASP.NET не є обґрунтованим у цьому відношенні. Він просто встановлює для параметра параметр null, а обробнику запитів потрібен код "вручну", щоб перевірити, чи є цей параметр null.

Багато з наведених тут відповідей є неповними щодо обробки помилок, і помилка або зловмисний клієнт може спричинити несподівану поведінку на стороні сервера, надіславши невірний запит, який (у найкращому випадку) кине NullReferenceExceptionкудись і поверне неправильний статус 500 INTERNAL SERVER ERRORабо, що ще гірше, зробити щось несподіване або збити або виявити вразливість системи безпеки.

Правильним рішенням буде написання власного [FromBody]атрибута, який " " обробляє помилки і повертає належні коди стану, в ідеалі з деякою діагностичною інформацією для допомоги розробникам клієнтів.

Рішення, яке може допомогти (ще не перевірено), полягає у введенні необхідних параметрів, як показано нижче: https://stackoverflow.com/a/19322688/2279059

Наступне незграбне рішення також працює:

// BAD EXAMPLE, but may work reasonably well for "internal" APIs...

public HttpResponseMessage MyAction([FromBody] JObject json)
{
  // Check if JSON from request body has been parsed correctly
  if (json == null) {
    var response = new HttpResponseMessage(HttpStatusCode.BadRequest) {
      ReasonPhrase = "Invalid JSON"
    };
    throw new HttpResponseException(response);
  }

  MyParameterModel param;
  try {
    param = json.ToObject<MyParameterModel>();
  }
  catch (JsonException e) {
    var response = new HttpResponseMessage(HttpStatusCode.BadRequest) {
      ReasonPhrase = String.Format("Invalid parameter: {0}", e.Message)
    };
    throw new HttpResponseException(response);
  }

  // ... Request handling goes here ...

}

Це робить (сподіваємось) належну обробку помилок, але є менш декларативним. Наприклад, якщо ви використовуєте Swagger для документування вашого API, він не знатиме тип параметра, а це означає, що вам потрібно знайти певний обхідний спосіб для документування ваших параметрів. Це лише для ілюстрації того, що [FromBody]слід робити.

EDIT: Менш незграбним рішенням є перевірка ModelState: https://stackoverflow.com/a/38515689/2279059

РЕДАГУВАТИ: Здається, що ModelState.IsValidне, як можна було б очікувати, встановлено, falseякщо використовується JsonPropertyз, Required = Required.Alwaysа параметр відсутній. Тож це теж марно.

Однак, на мій погляд, будь-яке рішення, яке вимагає введення додаткового коду в кожен обробник запитів, є неприйнятним. У такій мові, як .NET, з потужними можливостями серіалізації та в такій структурі, як ASP.NET Web API, перевірка запитів повинна бути автоматичною та вбудованою, і це цілком здійсненно, навіть якщо Microsoft не забезпечує необхідну вбудовану програму інструменти.


Так, додавши згаданий вище код, що саме я шукав! Додайте код для перевірки ModelState: stackoverflow.com/a/38515689/2279059
Роб Бремхолл

@RobBramhall Якщо ви використовуєте DataContract, а не атрибути Newtonsoft.Json, тоді використання ModelState, мабуть, добре.
Флоріан Зима

4

Я також намагався використовувати [FromBody], однак, я намагався заповнити змінну рядка, оскільки вхідні дані будуть змінюватися, і мені просто потрібно передати його в серверну службу, але це завжди було нулем

Post([FromBody]string Input]) 

Тому я змінив підпис методу на використання динамічного класу, а потім перетворив його на рядок

Post(dynamic DynamicClass)
{
   string Input = JsonConvert.SerializeObject(DynamicClass);

Це добре працює.


4

Може бути корисно додати TRACING до серіалізатора json, щоб ви могли бачити, що відбувається, коли щось піде не так.

Визначте реалізацію ITraceWriter для відображення результатів налагодження, наприклад:

class TraceWriter : Newtonsoft.Json.Serialization.ITraceWriter
{
    public TraceLevel LevelFilter {
        get {
            return TraceLevel.Error;
        }
    }

    public void Trace(TraceLevel level, string message, Exception ex)
    {
        Console.WriteLine("JSON {0} {1}: {2}", level, message, ex);
    }
}

Тоді у вашому WebApiConfig виконайте:

    config.Formatters.JsonFormatter.SerializerSettings.TraceWriter = new TraceWriter();

(можливо, оберніть це #if DEBUG)


3

Після трьох днів пошуку, і жодне з наведених вище рішень не працювало для мене, я знайшов інший підхід до цієї проблеми за цим посиланням: HttpRequestMessage

Я використав одне з рішень на цьому сайті

[HttpPost]
public async System.Threading.Tasks.Task<string> Post(HttpRequestMessage request)
{
    string body = await request.Content.ReadAsStringAsync();
    return body;
}

Добрі лорди. Це було 2 дні headbenging тут. Дуже тобі дякую!
Йордан

Це не має сенсу, чому працює лише параметр HttpRequestMessage, мій контролер має інший метод із використанням (параметр [[FromBody] ICollection <type>]), і він працює ідеально, як і всі інші мої контролери. І метод дії, який почав давати мені цю проблему, працював нормально, коли я вперше впровадив його кілька днів тому, і різниця полягає в тому, що стандарт .NET, який посилається на фреймворковий проект .net, викликає цей метод веб-API, тому я цікаво, чи це "невідома територія", на яку ви потрапили, якщо .net стандартні посилання .net framework? Я просто не хочу повторно робити проект.
Нінос

2

У моєму випадку проблемою був DateTimeоб’єкт, який я відправляв. Я створив DateTimeз "yyyy-MM-dd", і те, DateTimeщо було потрібно об'єкту, який я зіставляв, також потребувало "HH-mm-ss". Тож додавання "00-00" вирішило проблему (через це повний елемент був нульовим).


2

Це ще одна проблема, пов’язана з недійсними значеннями властивостей у запиті Angular Typescript.

Це було пов’язано з перетворенням між номером Typescript на int (Int32) у C #. Я використовував Ticks (UTC мілісекунди), який перевищує підписаний діапазон Int32 (int у C #). Змінено модель C # з int на long і все працювало нормально.


1

У мене була та сама проблема.

У моєму випадку проблема полягала в public int? CreditLimitBasedOn { get; set; }майні, яке я мав.

мій JSON мав значення, "CreditLimitBasedOn":trueколи він повинен містити ціле число. Ця властивість запобігла десеріалізації всього об’єкта в моєму методі api.


1

Можливо, комусь це буде корисно: перевірте модифікатори доступу для властивостей класу DTO / Model, вони повинні бути загальнодоступними . У моєму випадку під час рефакторингу внутрішні об'єкти домену були переміщені до DTO наступним чином:

// Domain object
public class MyDomainObject {
    public string Name { get; internal set; }
    public string Info { get; internal set; }
}
// DTO
public class MyDomainObjectDto {
    public Name { get; internal set; } // <-- The problem is in setter access modifier (and carelessly performed refactoring).
    public string Info { get; internal set; }
}

DTO точно передається клієнту, але коли настає час передавати об'єкт назад серверу, він мав лише порожні поля (значення null / за замовчуванням). Видалення "внутрішнього" наводить порядок, дозволяючи механізму десериалізації писати властивості об'єкта.

public class MyDomainObjectDto {
    public Name { get; set; }
    public string Info { get; set; }
}

це була моя проблема. Я оголосив учасників internalмоїми поганими і вирішив це через два дні ..
Ірфан,

1

Перевірте, чи встановлено JsonPropertyатрибут у полях, які мають нульовий значення - можливо, вони зіставляються з різними іменами властивостей json.


Це була проблема і для мене. Я також помітив, що я можу прикрасити свої властивості за допомогою [JsonProperty ("myProperty")] або я можу використовувати [DataMember]атрибут для отримання значень властивостей для розповсюдження.
Ден Больо,

1

Я стільки разів стикався з цією проблемою, але насправді виявити причину досить просто.

Ось сьогоднішній приклад. Я викликав свою службу POST з AccountRequestоб'єктом, але коли я ставив точку зупинки на початку цієї функції, значення параметра завжди було null. Але чому ?!

[ProducesResponseType(typeof(DocumentInfo[]), 201)]
[HttpPost]
public async Task<IActionResult> Post([FromBody] AccountRequest accountRequest)
{
    //  At this point...  accountRequest is null... but why ?!

    //  ... other code ... 
}

Щоб визначити проблему, змініть тип параметра на string, додайте рядок, щоб JSON.Netдісериалізувати об’єкт на тип, який ви очікували, і поставте точку зупинки на цей рядок:

[ProducesResponseType(typeof(DocumentInfo[]), 201)]
[HttpPost]
public async Task<IActionResult> Post([FromBody] string ar)
{
    //  Put a breakpoint on the following line... what is the value of "ar" ?
    AccountRequest accountRequest = JsonConvert.DeserializeObject<AccountRequest>(ar);

    //  ... other code ...
}

Тепер, коли ви спробуєте це, якщо параметр все ще пустий або null, ви просто не викликаєте службу належним чином.

Однак, якщо рядок дійсно містить значення, то DeserializeObjectслід вказати вам на причину проблеми, а також повинні не перетворити рядок в потрібний формат. Але з необробленими ( string) даними, які він намагається десериалізувати, ви тепер зможете побачити, що не так із вашим значенням параметра.

(У моєму випадку ми викликали службу з AccountRequestоб’єктом, який випадково двічі був серіалізований !)


1

Я використовував HttpRequestMessage, і моя проблема вирішилася після великих досліджень

[HttpPost]
public HttpResponseMessage PostProcedure(HttpRequestMessage req){
...
}

1

Просто щоб додати свою історію до цієї теми. Моя модель:

public class UserProfileResource
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Phone { get; set; }

    public UserProfileResource()
    {
    }
}

Вищезазначений об'єкт не міг бути серіалізований у моєму контролері API, і він завжди повертав би null. Проблема була в ідентифікаторі типу Guid : кожного разу, коли я передавав порожній рядок як ідентифікатор (будучи наївним, що він буде автоматично перетворений Guid.Empty) з мого інтерфейсу, я отримував нульовий об'єкт як [FromBody]параметр.

Рішення було або

  • пропуск дійсний Guid значення
  • або змінити GuidнаString

1

У моєму випадку, використовуючи листоношу, я надсилав DateTime з недійсними роздільниками (%), тому синтаксичний розбір не вдався. Переконайтеся, що ви передаєте дійсні параметри конструктору класів.


1

Жодне з наведеного не було моїм рішенням: у моєму випадку проблема полягає в тому, що [ApiController] не був доданий до контролера, тому він надає значення Null

[Produces("application/json")]
[Route("api/[controller]")]
[ApiController] // This was my problem, make sure that it is there!
public class OrderController : Controller

...


А як щодо успадкування від ApiControllerсебе?
Zimano

Перевірте свою структуру json, можливо, невірний json, приклад минув введені дані через jsonformatter.curiousconcept.com
Адель

Якщо ви відповідали мені: я мав на увазі, що ваша відповідь пропонує додати ApiControllerатрибут as як клас, поки клас успадковується від Controller; Я вважаю, що ви можете досягти того ж результату, успадкувавши ApiControllerбезпосередньо, що виконується за замовчуванням у контролерах, створених проектом WebAPI у VS.
Zimano

1

Я просто наткнувся на це і розчарував. Моє налаштування: Заголовок був встановлений на Content-Type: application / JSON і передавав інформацію з тіла у форматі JSON і читав [FromBody] на контролері.

Все було налаштовано нормально, і я сподіваюся, що це спрацює, але проблема полягала в надісланому JSON. Оскільки це була складна структура, один із моїх класів, який був визначений як "Анотація", не отримував ініціалізації, а отже, значення не були належним чином присвоєні моделі. Я видалив абстрактне ключове слово, і воно просто спрацювало .. !!!

Однією порадою, як я міг це зрозуміти, було відправлення даних частинами на мій контролер і перевірка, коли він стає нульовим ... оскільки це була складна модель, я додавав по одній моделі до параметрів мого запиту. Сподіваюся, це допоможе тому, хто стикається з цим дурним питанням.


0

Здається, причин цієї проблеми може бути багато різних ...

Я виявив, що додавання OnDeserializedзворотного виклику до класу моделі призвело до того, що параметр завжди був null. Точна причина невідома.

using System.Runtime.Serialization;

// Validate request
[OnDeserialized]  // TODO: Causes parameter to be null
public void DoAdditionalValidatation() {...}

0

У мене була ця проблема у моєму веб-API .NET Framework, оскільки моя модель існувала у проекті .NET Standard, який посилався на іншу версію анотацій даних.

Додавши рядок ReadAsAsync нижче, я висвітлив причину для мене:

public async Task<HttpResponseMessage> Register(RegistrationDetails registrationDetails)
{
    var regDetails = await Request.Content.ReadAsAsync<RegistrationDetails>();

0

Якщо це пов’язано з тим, що веб-API 2 зіткнувся з проблемою десериалізації через невідповідні типи типів даних, можна з’ясувати, де це не вдалося, перевіривши потік вмісту. Він буде читатись доти, доки не з’явиться помилка, тому, якщо ви читаєте вміст як рядок, у вас повинна бути задня половина даних, які ви опублікували:

string json = await Request.Content.ReadAsStringAsync();

Виправте цей параметр, і наступного разу він повинен продовжити (або досягти успіху, якщо пощастить!) ...


Request.Content.ReadAsStringAsync зазнає впливу, якщо ваша служба отримує 1000+ запитів в секунду, тоді рядок отримує значення null. нам потрібно з цим впоратися.
Амол Шиледар

0

У моєму випадку ( .NET Core 3.0 ) мені довелося налаштувати серіалізацію JSON для вирішення властивостей camelCase за допомогою AddNewtonsoftJson () :

services.AddMvc(options =>
{
    // (Irrelevant for the answer)
})
.AddNewtonsoftJson(options =>
{
    options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});

Зробіть це у налаштуваннях запуску / введення залежностей.


-1

Ще на одне, на що слід поглянути ... моя модель була позначена як [Серіалізована], і це спричинило несправність.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.