web-dev-qa-db-fra.com

Modèle toujours nul sur XML POST

Je travaille actuellement sur une intégration entre les systèmes et j'ai décidé d'utiliser WebApi pour cela, mais je rencontre un problème ...

Disons que j'ai un modèle:

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

et la méthode POST est:

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

Je crée une demande de Fiddler avec l'en-tête:

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

et le corps:

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

Le paramètre model dans la méthode Post est toujours null et je ne sais pas pourquoi. Quelqu'un a-t-il une idée?

42
Wowca

Deux choses:

  1. Vous n'avez pas besoin de guillemets "" autour du type de contenu et acceptez les valeurs d'en-tête dans Fiddler:

    User-Agent: Fiddler
    Content-Type: application/xml
    Accept: application/xml
    
  2. L'API Web utilise le DataContractSerializer par défaut pour la sérialisation xml. Vous devez donc inclure l'espace de noms de votre type dans votre xml:

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

    Ou vous pouvez configurer l'API Web pour utiliser XmlSerializer dans votre WebApiConfig.Register:

    config.Formatters.XmlFormatter.UseXmlSerializer = true;
    

    Ensuite, vous n'avez pas besoin de l'espace de noms dans vos données XML:

     <TestModel><Output>Sito</Output></TestModel>
    
68
nemesv

Bien que la réponse soit déjà attribuée, j'ai trouvé quelques autres détails qui méritent d'être examinés.

L'exemple le plus élémentaire d'une publication XML est généré dans le cadre d'un nouveau projet WebAPI automatiquement par Visual Studio, mais cet exemple utilise une chaîne comme paramètre d'entrée.

Exemple de contrôleur WebAPI simplifié généré par Visual Studio

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

Ce n'est pas très utile, car cela ne répond pas à la question qui se pose. La plupart des services Web POST ont des types plutôt complexes comme paramètres, et probablement un type complexe comme réponse. J'augmenterai l'exemple ci-dessus pour inclure une demande complexe et une réponse complexe ...

échantillon simplifié mais avec ajout de types complexes

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

À ce stade, je peux invoquer avec un violoneux ..

Détails de la demande du violoneux

En-têtes de demande:

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

Organe de demande:

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

... et lorsque je place un point d'arrêt dans mon contrôleur, je trouve que l'objet de demande est nul. C'est à cause de plusieurs facteurs ...

  • Par défaut, WebAPI utilise DataContractSerializer
  • La demande Fiddler ne spécifie pas le type de contenu ou le jeu de caractères
  • Le corps de la demande n'inclut pas de déclaration XML
  • Le corps de la demande n'inclut pas de définitions d'espace de noms.

Sans apporter de modifications au contrôleur de service Web, je peux modifier la demande du violon afin qu'elle fonctionne. Portez une attention particulière aux définitions d'espace de noms dans le corps de requête xml POST. Assurez-vous également que la déclaration XML est incluse avec les paramètres UTF corrects qui correspondent à l'en-tête de la requête.

Correction du corps de la requête Fiddler pour travailler avec les types de données complexes

En-têtes de demande:

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

Organe de demande:

<?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>

Remarquez comment l'espace de noms dans la demande fait référence au même espace de noms dans ma classe de contrôleur C # (sorte de). Parce que nous n'avons pas modifié ce projet pour utiliser un sérialiseur autre que DataContractSerializer et parce que nous n'avons pas décoré notre modèle (classe MyRequest ou MyResponse) avec des espaces de noms spécifiques, il suppose le même espace de noms que le contrôleur WebAPI lui-même. Ce n'est pas très clair et c'est très déroutant. Une meilleure approche serait de définir un espace de noms spécifique.

Pour définir un espace de noms spécifique, nous modifions le modèle de contrôleur. Vous devez ajouter une référence à System.Runtime.Serialization pour que cela fonctionne.

Ajouter des espaces de noms au modèle

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

Maintenant, mettez à jour la demande Fiddler pour utiliser cet espace de noms ...

demande Fiddler avec espace de noms personnalisé

<?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>

Nous pouvons pousser cette idée encore plus loin. Si une chaîne vide est spécifiée comme espace de noms sur le modèle, aucun espace de noms dans la demande du violon n'est requis.

Contrôleur avec espace de noms de chaîne vide

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

demande Fiddler sans espace de noms déclaré

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

Autres Gotchas

Attention, DataContractSerializer s'attend à ce que les éléments de la charge utile XML soient classés par ordre alphabétique par défaut. Si la charge utile XML est hors service, vous pouvez trouver que certains éléments sont nuls (ou si le type de données est un entier, il sera par défaut à zéro, ou s'il s'agit d'un booléen, il sera par défaut faux). Par exemple, si aucune commande n'est spécifiée et que le xml suivant est soumis ...

corps XML avec classement incorrect des éléments

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

... la valeur pour Age sera par défaut à zéro. Si du xml presque identique est envoyé ...

corps XML avec ordre correct des éléments

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

le contrôleur WebAPI sérialise et remplit correctement le paramètre Age. Si vous souhaitez modifier l'ordre par défaut afin que le XML puisse être envoyé dans un ordre spécifique, ajoutez alors l'élément 'Order' à l'attribut DataMember.

Exemple de spécification d'un ordre de propriété

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

Dans cet exemple, le corps xml doit spécifier l'élément Name avant l'élément Age pour se remplir correctement.

Conclusion

Ce que nous voyons, c'est qu'un corps de demande POST POST $ === malformé ou incomplet (du point de vue de DataContractSerializer) ne génère pas d'erreur, mais cause simplement un problème d'exécution. Si vous utilisez DataContractSerializer, nous devons satisfaire le sérialiseur (en particulier autour des espaces de noms). J'ai trouvé à l'aide d'un outil de test une bonne approche - où je passe une chaîne XML à une fonction qui utilise DataContractSerializer pour désérialiser le XML. Il génère des erreurs lorsque la désérialisation ne peut pas se produire. Voici le code pour les tests une chaîne XML à l'aide de DataContractSerializer (encore une fois, n'oubliez pas que si vous implémentez cela, vous devez ajouter une référence à System.Runtime.Serialization).

Exemple de code de test pour l'évaluation de la désérialisation de 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;
}

Options

Comme souligné par d'autres, DataContractSerializer est la valeur par défaut pour les projets WebAPI utilisant XML, mais il existe d'autres sérialiseurs XML. Vous pouvez supprimer le DataContractSerializer et utiliser à la place XmlSerializer. Le XmlSerializer est beaucoup plus indulgent sur les éléments d'espace de noms mal formés.

Une autre option consiste à limiter les demandes à l'utilisation de JSON au lieu de XML. Je n'ai effectué aucune analyse pour déterminer si DataContractSerializer est utilisé pendant la désérialisation JSON et si l'interaction JSON nécessite des attributs DataContract pour décorer les modèles.

55
barrypicker

J'essayais de résoudre cela pendant deux jours. Finalement, j'ai découvert que la balise externe doit être le nom du type, pas le nom de la variable. En effet, avec la méthode POST as

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

Je fournissais le corps

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

au lieu de

<TestModel><Output>Sito</Output></TestModel>
2
Dave the Sax

Une fois que vous vous êtes assuré d'avoir configuré le Content-Type en-tête à application/xml Et mettre config.Formatters.XmlFormatter.UseXmlSerializer = true; dans la méthode Register de WebApiConfig.cs, il est important de ne pas avoir besoin de version ou d'encodage en haut de votre document XML.

Cette dernière pièce me bloquait, j'espère que cela aide quelqu'un et vous fait gagner du temps.

2
zekedaddy17