I created a C# class based on the test file using paste special -> XML to class. That worked fine, structurally all seems good. However, I am running into an issue where if I paste this into Swagger in the test section, it'd error with 400 saying the xml field is required. After a lot of testing, I found that if I stripped out the <! DOCTYPE line, it worked fine.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.014/cXML.dtd">
<cXML xml:lang="en-US" payloadID="1452186890.009162@ip-10-7-14-126" timestamp="2008-01-07T09:14:50-08:00">
...
</cXML>
Source for the test file from Coupa
What do I need to do to allow it to go through as is? This XML is being posted in the body of the request so I don't think I can pre-scrap the <!DOCTYPE line (and frankly, may not be best to as want to dump the entire string into a local file anyway). the cXML is the parent element so I don't think it is as simple as adding a docType object to it and calling it a day as they're siblings and not nested in some manner.
[HttpPost(Name = "PostPurchaseOrder")]
public ContentResult Post([FromBody]XElement xml)
{
cXML cxml = new cXML();
var serializer = new XmlSerializer(typeof(cXML));
using (var reader = new StringReader(xml.ToString()))
{
cxml = (cXML)serializer.Deserialize(reader);
}
...
}
Here is the request per Swagger UI:
curl -X 'POST' \
'https://localhost:7200/PurchaseOrder' \
-H 'accept: */*' \
-H 'Content-Type: application/xml' \
-d '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM
"http://xml.cxml.org/schemas/cXML/1.2.014/cXML.dtd">
<cXML xml:lang="en-US" payloadID="1452186890.009162@ip-10-7-14-126"
timestamp="2008-01-07T09:14:50-08:00">
<Header>
<From>
<Credential domain="duns">
<!-- Customer domain typically provided by Supplier, typically DUNS -->
<Identity>dell</Identity>
<!-- Customer id typically provided by Supplier, string -->
</Credential>
</From>
<To>
<Credential domain="Duns">
<!-- Supplier domain typically provided by Supplier, typically DUNS -->
<Identity>128293714</Identity>
<!-- Supplier id typically provided by Supplier, string -->
</Credential>
</To>
<Sender>
<Credential domain="duns">
<Identity>dell</Identity>
<!-- same as From -->
<Identity>dell</Identity>
<!-- same as From -->
</Credential>
<UserAgent>Coupa Procurement 1.0</UserAgent>
<!-- does not change -->
</Sender>
</Header>
<Request deploymentMode="production">
<OrderRequest>
<OrderRequestHeader orderID="6112" orderDate="2008-01-07T09:14:50-08:00" type="new">
<!-- Coupa supports "new" and "update" -->
<Total>
<Money currency="USD">1505.0</Money>
<!-- Currency code configured in Coupa -->
</Total>
<ShipTo>
<Address isoCountryCode="US" addressID="3119">
<Name xml:lang="en">jmadden</Name>
<PostalAddress name="default">
<DeliverTo>j maddedn</DeliverTo>
<Street>333 East Hill Dr</Street>
<City>san leandro</City>
<State>ca</State>
<PostalCode>22222</PostalCode>
<Country isoCountryCode="US">United States</Country>
</PostalAddress>
<Email name="default">jmadden@coupa1.com</Email>
</Address>
</ShipTo>
<BillTo>
<Address isoCountryCode="US" addressID="142">
<Name xml:lang="en">SOB1</Name>
<!-- Company Name under Company Information in Coupa -->
<PostalAddress name="default">
<DeliverTo>Noah Sanity Attn: Noah Noah</DeliverTo>
<Street>3420 Flatiron Way</Street>
<City>West Index</City>
<State>NJ</State>
<PostalCode>43023</PostalCode>
<Country isoCountryCode="US">United States</Country>
</PostalAddress>
</Address>
</BillTo>
<Contact role="endUser">
<Name xml:lang="en">j maddedn</Name>
<Email name="default">jmadden@coupa1.com</Email>
</Contact>
<Comments xml:lang="en">header comment goes here if entered by user</Comments>
</OrderRequestHeader>
<ItemOut quantity="1" lineNumber="1">
<ItemID>
<SupplierPartID>223-4511</SupplierPartID>
<!-- Coupa Item Part Number -->
<SupplierPartAuxiliaryID>1005379527029\1</SupplierPartAuxiliaryID>
<!-- Auxiliary Part Number is optional, typically used by punchout suppliers -->
</ItemID>
<ItemDetail>
<UnitPrice>
<Money currency="USD">1505.0</Money>
<!-- Currency code configured in Coupa -->
</UnitPrice>
<Description xml:lang="en">OptiPlex 755 Energy Smart Minitower;IntelREG CoreTM 2 Quad Processor Q6600 (2.40GHz, 2X4M, 1066MHz FSB)</Description>
<UnitOfMeasure>EA</UnitOfMeasure>
<Classification domain="UNSPSC">44000000</Classification>
<!-- Future expansion -->
</ItemDetail>
<Distribution>
<Accounting name="bbbb">
<!-- Coupa Account name -->
<Segment id="bbb" description="ORG" type="Organization" />
<Segment id="b" description="DEPT" type="Department" />
<Segment id="bb" description="PROJ" type="Project" />
</Accounting>
<Charge>
<Money currency="USD">1505.0</Money>
</Charge>
</Distribution>
<Comments xml:lang="en">line item comment goes here if entered by user</Comments>
</ItemOut>
</OrderRequest>
</Request>
</cXML>'
And here is the response per Swagger UI:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-long-string-00",
"errors": {
"": [
"An error occurred while deserializing input data."
],
"xml": [
"The xml field is required."
]
}
}
You could add a custom input formatter:
public class XElementInputFormatter : XmlSerializerInputFormatter
{
public XElementInputFormatter(MvcOptions options) : base(options)
{
SupportedMediaTypes.Add("application/xml");
}
protected override bool CanReadType(Type type)
{
if (type.IsAssignableFrom(typeof(XElement)))
{
return true;
}
return base.CanReadType(type);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var xmlDoc = await XDocument.LoadAsync(context.HttpContext.Request.Body, LoadOptions.None, CancellationToken.None);
return InputFormatterResult.Success(xmlDoc.Root);
}
}
Then register it in your services collection:
builder.Services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new XElementInputFormatter(options));
});
You could probably write a cXMLInputFormatter
and skip the the extra step of using XDocument/XElement and just get the cXML directly from the body in the action.
public ContentResult Post([FromBody] cXML xml) {}