Подтвердить что ты не робот

Модель всегда не указана в XML POST

В настоящее время я работаю над интеграцией между системами, и я решил использовать для этого WebApi, но у меня проблема...

Скажем, у меня есть модель:

public class TestModel
{
    public string Output { get; set; }
}

и метод POST:

public string Post(TestModel model)
{
    return model.Output;
}

Я создаю запрос от Fiddler с заголовком:

User-Agent: Fiddler
Content-Type: "application/xml"
Accept: "application/xml"
Host: localhost:8616
Content-Length: 57

и тело:

<TestModel><Output>Sito</Output></TestModel>

Параметр model в методе Post всегда null, и я понятия не имею, почему. Кто-нибудь знает?

4b9b3361

Ответ 1

Две вещи:

  • Вам не нужны кавычки "" вокруг типа содержимого и принимать значения заголовка в Fiddler:

    User-Agent: Fiddler
    Content-Type: application/xml
    Accept: application/xml
    
  • Web API использует DataContractSerializer по умолчанию для сериализации XML. Поэтому вам нужно включить пространство имен типов в свой XML-адрес:

    <TestModel 
    xmlns="http://schemas.datacontract.org/2004/07/YourMvcApp.YourNameSpace"> 
        <Output>Sito</Output>
    </TestModel> 
    

    Или вы можете настроить веб-API для использования XmlSerializer в WebApiConfig.Register:

    config.Formatters.XmlFormatter.UseXmlSerializer = true;
    

    Тогда вам не нужно пространство имен в ваших XML-данных:

     <TestModel><Output>Sito</Output></TestModel>
    

Ответ 2

Пока ответ уже получен, я нашел пару других деталей, которые стоит рассмотреть.

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

Упрощенный образец WebAPI-контроллера, созданный Visual Studio

using System.Web.Http;
namespace webAPI_Test.Controllers
{
    public class ValuesController : ApiController
    {
        // POST api/values
        public void Post([FromBody]string value)
        {
        }
    }
}

Это не очень полезно, потому что оно не затрагивает вопрос. Большинство веб-служб POST имеют довольно сложные типы в качестве параметров и, вероятно, сложный тип в качестве ответа. Я добавлю пример выше, чтобы включить сложный запрос и сложный ответ...

Упрощенный образец, но с добавлением сложных типов

using System.Web.Http;
namespace webAPI_Test.Controllers
{
    public class ValuesController : ApiController
    {
        // POST api/values
        public MyResponse Post([FromBody] MyRequest value)
        {
            var response = new MyResponse();
            response.Name = value.Name;
            response.Age = value.Age;
            return response;
        }
    }

    public class MyRequest
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    public class MyResponse
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

В этот момент я могу вызвать скрипт.

Детали запроса Fiddler

Заголовок запроса:

User-Agent: Fiddler
Host: localhost:54842
Content-Length: 63

Тело запроса:

<MyRequest>
   <Age>99</Age>
   <Name>MyName</Name>
</MyRequest>

... и при размещении точки останова в моем контроллере я обнаружил, что объект запроса имеет значение null. Это связано с несколькими факторами...

  • WebAPI по умолчанию использует DataContractSerializer
  • В запросе Fiddler не указан тип содержимого или кодировка
  • Тело запроса не включает объявление XML
  • Тело запроса не включает определения пространства имен.

Не внося никаких изменений в контроллер веб-службы, я могу изменить запрос на скрипт, чтобы он работал. Обратите внимание на определения пространства имен в блоке запроса xml POST. Кроме того, убедитесь, что объявление XML включено с правильными настройками UTF, которые соответствуют заголовку запроса.

Исправлено тело запроса Fiddler для работы со сложными типами данных

Заголовок запроса:

User-Agent: Fiddler
Host: localhost:54842
Content-Length: 276
Content-Type: application/xml; charset=utf-16

Тело запроса:

<?xml version="1.0" encoding="utf-16"?>
   <MyRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/webAPI_Test.Controllers">
      <Age>99</Age>
      <Name>MyName</Name>
   </MyRequest>

Обратите внимание, как пространство имен в запросе относится к тому же пространству имен в моем классе контроллера С# (вид). Поскольку мы не изменили этот проект на использование сериализатора, отличного от DataContractSerializer, и потому, что мы не украсили нашу модель (класс MyRequest или MyResponse) конкретными пространствами имен, она предполагает такое же пространство имен, что и сам WebAPI-контроллер. Это не очень понятно и очень запутанно. Лучшим подходом было бы определение определенного пространства имен.

Чтобы определить конкретное пространство имен, мы модифицируем модель контроллера. Необходимо добавить ссылку на System.Runtime.Serialization, чтобы сделать эту работу.

Добавить пространства имен в модель

using System.Runtime.Serialization;
using System.Web.Http;
namespace webAPI_Test.Controllers
{
    public class ValuesController : ApiController
    {
        // POST api/values
        public MyResponse Post([FromBody] MyRequest value)
        {
            var response = new MyResponse();
            response.Name = value.Name;
            response.Age = value.Age;
            return response;
        }
    }

    [DataContract(Namespace = "MyCustomNamespace")]
    public class MyRequest
    {
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Age { get; set; }
    }

    [DataContract(Namespace = "MyCustomNamespace")]
    public class MyResponse
    {
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Age { get; set; }
    }
}

Теперь обновите запрос Fiddler, чтобы использовать это пространство имен...

Запрос Fiddler с пользовательским пространством имен

<?xml version="1.0" encoding="utf-16"?>
   <MyRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="MyCustomNamespace">
      <Age>99</Age>
      <Name>MyName</Name>
   </MyRequest>

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

Контроллер с пустым пространством имен строк

using System.Runtime.Serialization;
using System.Web.Http;

namespace webAPI_Test.Controllers
{
    public class ValuesController : ApiController
    {
        // POST api/values
        public MyResponse Post([FromBody] MyRequest value)
        {
            var response = new MyResponse();
            response.Name = value.Name;
            response.Age = value.Age;
            return response;
        }
    }

    [DataContract(Namespace = "")]
    public class MyRequest
    {
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Age { get; set; }
    }

    [DataContract(Namespace = "")]
    public class MyResponse
    {
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Age { get; set; }
    }
}

Запрос Fiddler без объявленного пространства имен

<?xml version="1.0" encoding="utf-16"?>
   <MyRequest>
      <Age>99</Age>
      <Name>MyName</Name>
   </MyRequest>

Другие Gotchas

Остерегайтесь, DataContractSerializer ожидает, что элементы в полезной нагрузке XML будут упорядочены по алфавиту по умолчанию. Если полезная нагрузка XML вышла из строя, вы можете обнаружить, что некоторые элементы являются нулевыми (или если тип данных является целым числом, по умолчанию он равен нулю, или если это значение bool по умолчанию равно false). Например, если не указан порядок, и отправляется следующий xml...

Тело XML с неправильным упорядочением элементов

<?xml version="1.0" encoding="utf-16"?>
<MyRequest>
   <Name>MyName</Name>
   <Age>99</Age>
</MyRequest>  

... значение Age по умолчанию будет равно нулю. Если отправлен почти идентичный xml...

Тело XML с правильным упорядочением элементов

<?xml version="1.0" encoding="utf-16"?>
<MyRequest>
   <Age>99</Age>
   <Name>MyName</Name>
</MyRequest>  

то контроллер WebAPI будет правильно сериализовать и заполнить параметр Age. Если вы хотите изменить порядок по умолчанию, чтобы XML можно было отправить в определенном порядке, добавьте элемент "Заказ" в атрибут DataMember.

Пример указания порядка свойств

using System.Runtime.Serialization;
using System.Web.Http;

namespace webAPI_Test.Controllers
{
    public class ValuesController : ApiController
    {
        // POST api/values
        public MyResponse Post([FromBody] MyRequest value)
        {
            var response = new MyResponse();
            response.Name = value.Name;
            response.Age = value.Age;
            return response;
        }
    }

    [DataContract(Namespace = "")]
    public class MyRequest
    {
        [DataMember(Order = 1)]
        public string Name { get; set; }

        [DataMember(Order = 2)]
        public int Age { get; set; }
    }

    [DataContract(Namespace = "")]
    public class MyResponse
    {
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Age { get; set; }
    }
}

В этом примере тело xml должно указать элемент Name перед тем, как элемент Age будет правильно заполнен.

Заключение

Мы видим, что неправильный или неполный орган запроса POST (с точки зрения DataContractSerializer) не выдает ошибку, а просто вызывает проблему времени выполнения. Если вы используете DataContractSerializer, нам нужно выполнить сериализатор (особенно вокруг пространств имен). Я нашел с помощью инструмента тестирования хороший подход - где я передаю XML-строку функции, которая использует DataContractSerializer для десериализации XML. Он выдает ошибки, когда десериализация не может произойти. Вот код для тестирования XML-строки с использованием DataContractSerializer (опять же, помните, если вы реализуете это, вам нужно добавить ссылку на System.Runtime.Serialization).

Пример кода тестирования для оценки десериализации DataContractSerializer

public MyRequest Deserialize(string inboundXML)
{
    var ms = new MemoryStream(Encoding.Unicode.GetBytes(inboundXML));
    var serializer = new DataContractSerializer(typeof(MyRequest));
    var request = new MyRequest();
    request = (MyRequest)serializer.ReadObject(ms);

    return request;
}

Опции

Как указывалось другими, DataContractSerializer является стандартом для проектов WebAPI с использованием XML, но есть и другие сериализаторы XML. Вы можете удалить DataContractSerializer и вместо этого использовать XmlSerializer. XmlSerializer гораздо более прощающий в искаженном пространстве имен.

Другой вариант - ограничить запросы на использование JSON вместо XML. Я не проводил анализ, чтобы определить, используется ли DataContractSerializer во время десериализации JSON, и если для взаимодействия JSON требуются атрибуты DataContract для украшения моделей.

Ответ 3

Я пытался решить это в течение двух дней. В конце концов я обнаружил, что внешний тег должен быть именем типа, а не именем переменной. Эффективно, используя метод POST как

public string Post([FromBody]TestModel model)
{
    return model.Output;
}

Я предоставлял тело

<model><Output>Sito</Output></model>

вместо

<TestModel><Output>Sito</Output></TestModel>

Ответ 4

Как только вы убедитесь, что вы установили заголовок Content-Type на application/xml и задали config.Formatters.XmlFormatter.UseXmlSerializer = true; в методе Register WebApiConfig.cs важно, чтобы вам не понадобилось управление версиями или кодировкой в верхней части XML-документа.

Эта последняя часть заставила меня застрять, надеюсь, это поможет кому-то там и сэкономит ваше время.