Modell immer null bei XML POST

Modell immer null bei XML POST

Zwei Dinge:

  1. Sie brauchen keine Anführungszeichen "" um den Inhaltstyp und akzeptieren Sie Header-Werte in Fiddler:

    User-Agent: Fiddler
    Content-Type: application/xml
    Accept: application/xml
    
  2. Die Web-API verwendet den DataContractSerializer standardmäßig für die XML-Serialisierung. Sie müssen also den Namensraum Ihres Typs in Ihre XML-Datei aufnehmen:

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

    Oder Sie können die Web-API so konfigurieren, dass sie XmlSerializer verwendet in Ihrem WebApiConfig.Register :

    config.Formatters.XmlFormatter.UseXmlSerializer = true;
    

    Dann brauchen Sie den Namensraum in Ihren XML-Daten nicht:

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

Während die Antwort bereits vergeben ist, fand ich ein paar andere Details, die eine Überlegung wert sind.

Das grundlegendste Beispiel für einen XML-Beitrag wird als Teil eines neuen WebAPI-Projekts automatisch von Visual Studio generiert, aber dieses Beispiel verwendet eine Zeichenfolge als Eingabeparameter.

Von Visual Studio generierter vereinfachter Beispiel-WebAPI-Controller

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

Dies ist nicht sehr hilfreich, da es die vorliegende Frage nicht beantwortet. Die meisten POST-Webdienste haben ziemlich komplexe Typen als Parameter und wahrscheinlich einen komplexen Typ als Antwort. Ich erweitere das obige Beispiel um eine komplexe Anfrage und eine komplexe Antwort...

Vereinfachtes Beispiel, aber mit hinzugefügten komplexen Typen

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

An dieser Stelle kann ich mit fiddler..

aufrufen

Fiddler-Anfragedetails

Anfrage-Header:

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

Anfragetext:

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

... und wenn ich einen Haltepunkt in meinem Controller setze, finde ich, dass das Anforderungsobjekt null ist. Das liegt an mehreren Faktoren...

  • WebAPI verwendet standardmäßig DataContractSerializer
  • Die Fiddler-Anfrage gibt weder den Inhaltstyp noch den Zeichensatz an
  • Der Anfragetext enthält keine XML-Deklaration
  • Der Anfragetext enthält keine Namespace-Definitionen.

Ohne Änderungen am Webdienst-Controller vorzunehmen, kann ich die Fiddler-Anforderung so ändern, dass sie funktioniert. Achten Sie genau auf die Namespace-Definitionen im XML-POST-Anforderungstext. Stellen Sie außerdem sicher, dass die XML-Deklaration mit korrekten UTF-Einstellungen enthalten ist, die mit dem Anforderungsheader übereinstimmen.

Fiddler-Anforderungstext wurde korrigiert, um mit komplexen Datentypen zu arbeiten

Anfrage-Header:

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

Anfragetext:

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

Beachten Sie, wie sich der Namespace in der Anfrage auf denselben Namespace in meiner C#-Controller-Klasse bezieht (sozusagen). Da wir dieses Projekt nicht geändert haben, um einen anderen Serialisierer als DataContractSerializer zu verwenden, und da wir unser Modell (Klasse MyRequest oder MyResponse) nicht mit bestimmten Namespaces versehen haben, nimmt es denselben Namespace wie der WebAPI-Controller selbst an. Das ist nicht ganz klar und sehr verwirrend. Ein besserer Ansatz wäre, einen bestimmten Namensraum zu definieren.

Um einen bestimmten Namespace zu definieren, modifizieren wir das Controller-Modell. Damit dies funktioniert, muss ein Verweis auf System.Runtime.Serialization hinzugefügt werden.

Namespaces zum Modell hinzufügen

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

Aktualisieren Sie jetzt die Fiddler-Anfrage, um diesen Namespace zu verwenden...

Fiddler-Anfrage mit benutzerdefiniertem Namespace

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

Wir können diese Idee noch weiter führen. Wenn eine leere Zeichenfolge als Namespace im Modell angegeben ist, ist kein Namespace in der Fiddler-Anfrage erforderlich.

Controller mit leerem String-Namespace

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-Anfrage ohne deklarierten Namespace

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

Andere Fallstricke

Achtung, DataContractSerializer erwartet, dass die Elemente in der XML-Nutzlast standardmäßig alphabetisch geordnet werden. Wenn die XML-Nutzdaten nicht in der richtigen Reihenfolge sind, stellen Sie möglicherweise fest, dass einige Elemente null sind (oder wenn der Datentyp eine Ganzzahl ist, wird er standardmäßig auf Null gesetzt, oder wenn es sich um einen booleschen Wert handelt, wird er standardmäßig auf falsch gesetzt). Wenn beispielsweise keine Bestellung angegeben ist und die folgende XML-Datei übermittelt wird...

XML-Text mit falscher Reihenfolge der Elemente

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

... wird der Wert für Alter standardmäßig auf Null gesetzt. Wenn nahezu identisches xml gesendet wird ...

XML-Text mit korrekter Reihenfolge der Elemente

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

dann wird der WebAPI-Controller den Age-Parameter korrekt serialisieren und füllen. Wenn Sie die Standardreihenfolge ändern möchten, damit das XML in einer bestimmten Reihenfolge gesendet werden kann, fügen Sie dem DataMember-Attribut das Element „Order“ hinzu.

Beispiel für die Angabe einer Eigenschaftsreihenfolge

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

In diesem Beispiel muss der XML-Hauptteil das Name-Element vor dem Age-Element angeben, um korrekt ausgefüllt zu werden.

Fazit

Wir sehen, dass ein fehlerhafter oder unvollständiger POST-Request-Body (aus Sicht von DataContractSerializer) keinen Fehler auslöst, sondern nur ein Laufzeitproblem verursacht. Wenn Sie den DataContractSerializer verwenden, müssen wir den Serializer erfüllen (insbesondere um Namespaces herum). Ich habe festgestellt, dass die Verwendung eines Testwerkzeugs ein guter Ansatz ist, bei dem ich eine XML-Zeichenfolge an eine Funktion übergebe, die DataContractSerializer verwendet, um das XML zu deserialisieren. Es gibt Fehler aus, wenn keine Deserialisierung stattfinden kann. Hier ist der Code zum Testen einer XML-Zeichenfolge mit DataContractSerializer (denken Sie auch hier daran, dass Sie bei der Implementierung einen Verweis auf System.Runtime.Serialization hinzufügen müssen).

Beispiel-Testcode zur Evaluierung der DataContractSerializer-Deserialisierung

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

Optionen

Wie von anderen erwähnt, ist der DataContractSerializer die Standardeinstellung für WebAPI-Projekte, die XML verwenden, aber es gibt andere XML-Serialisierer. Sie könnten DataContractSerializer entfernen und stattdessen XmlSerializer verwenden. Der XmlSerializer ist viel nachsichtiger bei falsch geformten Namespace-Sachen.

Eine weitere Option besteht darin, Anforderungen auf die Verwendung von JSON anstelle von XML zu beschränken. Ich habe keine Analyse durchgeführt, um festzustellen, ob DataContractSerializer während der JSON-Deserialisierung verwendet wird und ob die JSON-Interaktion DataContract-Attribute zum Dekorieren der Modelle erfordert.


Sobald Sie sicherstellen, dass Sie Content-Type eingerichtet haben Header zu application/xml und setzen Sie config.Formatters.XmlFormatter.UseXmlSerializer = true; im Register Methode des WebApiConfig.cs , ist es wichtig, dass Sie am Anfang Ihres XML-Dokuments keine Versionierung oder Kodierung benötigen.

Dieses letzte Stück hat mich stecken lassen, ich hoffe, das hilft jemandem da draußen und spart Ihre Zeit.