XML
XML is the format of B2B document standards — OAGIS, cXML, carrier manifests, bank statements. You rarely get to design the schema: a partner hands you an XSD and the code has to match it. That makes two things important — reading nested paths and attributes without pages of boilerplate, and failing with a message that names the element that was wrong.
Class: DEVIntegXMLReadHelper — a thin wrapper over System.Xml that adds typed getters, path navigation, and clear errors. Writing uses System.Xml.XmlDocument directly.
Reading a document
Load the payload into an XmlDataDocument, then pull values out by path:
DEVIntegXMLReadHelper readHelper = new DEVIntegXMLReadHelper();
System.Xml.XmlDataDocument xmldoc = new System.Xml.XmlDataDocument();
xmldoc.Load(messageTable.getFileStream());
System.Xml.XmlNodeList nodeList = xmldoc.GetElementsByTagName('PurchaseOrderHeader');
if (! nodeList || nodeList.get_Count() == 0)
{
throw error(strFmt("Element %1 is not found", 'PurchaseOrderHeader'));
}
System.Xml.XmlNode nodeHeader = nodeList.get_ItemOf(0);
staging.PurchId = readHelper.xmlGetValueNodeStr(nodeHeader, 'DocumentID');
staging.DeliveryDate = readHelper.xmlGetValueNodeDate(nodeHeader, 'DocumentDateTime');
staging.TotalAmount = readHelper.xmlGetValueNodeReal(nodeHeader, 'TotalAmount/Amount');
Reader methods
| Method | Purpose |
|---|---|
xmlGetNodeByPath(node, 'A/B/C', checkExists) | Walks a child path; throws naming the missing element, or returns null when checkExists is false |
xmlGetValueNodeStr(node, path) | Inner text |
xmlGetValueNodeReal(node, path, checkExists) | Number |
xmlGetValueNodeInt / xmlGetValueNodeInt64 | Integer |
xmlGetValueNodeDate(node, path, format) | Date, parsed with an exact format (default yyyy-MM-dd, or set parmDateFormat) |
xmlGetValueNodeDateTime(node, path) | Date and time, invariant culture |
xmlSelectSingleNode(node, xpath) | Full XPath, including predicates; throws when the path matches nothing |
getNodeListByTagName(xmlString, tag) | All elements with a tag name, from a string; throws when there are none |
initNamespace(doc, alias, uri) | Registers a namespace prefix for XPath |
xmlStr2Real(text) | Converts a value you read yourself |
The ...ByPath methods take a simple Parent/Child/Grandchild path — enough for most documents. Reach for xmlSelectSingleNode when you need a real XPath predicate.
Optional elements
Pass false as the last argument when an element may legitimately be absent:
System.Xml.XmlNode nodeDiscount = readHelper.xmlGetNodeByPath(nodeLine, 'Discount/Amount', false);
if (nodeDiscount)
{
staging.Discount = readHelper.xmlStr2Real(nodeDiscount.get_InnerText());
}
Leave the default true for everything the schema says is mandatory — a missing element then produces "Can't find Amount element for path Discount/Amount" in the message log instead of a silent zero.
Attributes
Values often live in attributes rather than element text. Get the node, then the attribute:
System.Xml.XmlNode node = readHelper.xmlGetNodeByPath(nodeHeader, 'SupplierParty/PartyIDs/ID');
staging.CompanyId = node.get_Attributes().get_ItemOf('accountingEntity').get_InnerText();
staging.OrderAccount = node.get_InnerText();
That is the typical B2B shape — the element carries the value and the attribute carries its qualifier (company, currency, unit of measure).
Namespaces
A document with xmlns="..." needs the namespace registered before any XPath will match. Register an alias once, then prefix every step of the path with it:
readHelper.initNamespace(xmldoc, 'infor', @'http://schema.infor.com/InforOAGIS/2');
System.Xml.XmlNode node = readHelper.xmlSelectSingleNode(nodeHeader,
'infor:ShipToParty//infor:Location[@type=\'Warehouse\']');
staging.InventLocationId = node.get_InnerText();
The alias is yours to choose — it does not have to match the prefix in the file, only the URI does. GetElementsByTagName ignores namespaces altogether, which is why it is convenient for finding repeating blocks.
Repeating elements
Lines are a node list you index:
System.Xml.XmlNodeList nodeList = xmldoc.GetElementsByTagName('PurchaseOrderLine');
int nodeCount = nodeList.get_Count();
for (int i = 0; i < nodeCount; i++)
{
System.Xml.XmlNode nodePOLine = nodeList.get_ItemOf(i);
stagingLine.clear();
stagingLine.MessageRefRecId = staging.MessageRefRecId;
stagingLine.POLineNumber = str2int(readHelper.xmlGetValueNodeStr(nodePOLine, 'LineNumber'));
System.Xml.XmlNode nodeQty = readHelper.xmlGetNodeByPath(nodePOLine, 'Quantity');
stagingLine.PurchQty = readHelper.xmlStr2Real(nodeQty.get_InnerText());
stagingLine.PurchUnit = nodeQty.get_Attributes().get_ItemOf('unitCode').get_InnerText();
stagingLine.PurchPrice = readHelper.xmlGetValueNodeReal(nodePOLine, 'UnitPrice/Amount');
DEV::validateWriteRecordCheck(stagingLine);
stagingLine.insert();
}
Node lists are 0-based, unlike everything else in X++.
Example: a purchase order confirmation
Putting it together — this is the shape of the public sample DEVIntegTutorialProcessPurchConfirmXML, which imports confirmations dropped on an Azure File Share. The document combines all four patterns above — a namespace, nested paths, attributes carrying the company and unit, and custom fields hidden in a UserArea:
<?xml version="1.0" encoding="UTF-8"?>
<SyncPurchaseOrder xmlns="http://schema.infor.com/InforOAGIS/2" languageCode="en-US">
<DataArea>
<PurchaseOrder>
<PurchaseOrderHeader>
<DocumentID>PO00001</DocumentID>
<DocumentDateTime>2022-04-16</DocumentDateTime>
<SupplierParty>
<PartyIDs><ID accountingEntity="USMF">1001</ID></PartyIDs>
</SupplierParty>
<ShipToParty>
<Location type="Warehouse"><ID accountingEntity="USMF">11</ID></Location>
</ShipToParty>
</PurchaseOrderHeader>
<PurchaseOrderLine>
<LineNumber>10</LineNumber>
<Quantity unitCode="EA">15</Quantity>
<UnitPrice><Amount currencyID="AUD">10</Amount></UnitPrice>
<UserArea>
<Property>
<NameValue name="eam.UDFCHAR01" type="StringType">M0001</NameValue>
</Property>
</UserArea>
</PurchaseOrderLine>
</PurchaseOrder>
</DataArea>
</SyncPurchaseOrder>
The item number is not a field of its own — it is a named property in UserArea, which is exactly what XPath predicates are for:
System.Xml.XmlNode node = readHelper.xmlSelectSingleNode(nodePOLine,
'infor:UserArea//infor:Property//infor:NameValue[@name=\'eam.UDFCHAR01\']');
stagingLine.ItemId = strLRTrim(node.get_InnerText());
Writing a document
There is no XML writer helper — System.Xml.XmlDocument is used directly. The pattern is create element, add text, append to parent, which becomes repetitive fast, so define a small local function first:
private System.Xml.XmlDocument buildManifest(WHSShipmentTable _shipmentTable)
{
System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
System.Xml.XmlElement addElement(System.Xml.XmlElement _parent, str _name, str _value)
{
System.Xml.XmlElement element = doc.createElement(_name);
if (_value)
{
element.appendChild(doc.createTextNode(_value));
}
_parent.appendChild(element);
return element;
}
System.Xml.XmlElement root = doc.createElement('connotes');
doc.appendChild(root);
System.Xml.XmlElement connote = addElement(root, 'connote', ''); // container element
addElement(connote, 'carriername', carrierName);
addElement(connote, 'service', isExpress ? 'EXP SIGN' : 'PAR SIGN');
addElement(connote, 'accno', accountAssignment.AccountNum);
// Receiver — a nested block
CustTable custTable = /* ... */;
LogisticsPostalAddress deliveryAddress = /* ... */;
addElement(connote, 'recaccno', custTable.AccountNum);
addElement(connote, 'recname', custTable.name());
System.Xml.XmlElement recAddr = addElement(connote, 'recaddr', '');
addElement(recAddr, 'add1', strLine(deliveryAddress.Street, 0));
if (strLine(deliveryAddress.Street, 1))
{
addElement(recAddr, 'add2', strLine(deliveryAddress.Street, 1)); // optional
}
addElement(recAddr, 'add3', deliveryAddress.City);
addElement(recAddr, 'add4', deliveryAddress.State);
addElement(recAddr, 'add5', deliveryAddress.ZipCode);
// Freight totals
System.Xml.XmlElement freight = addElement(connote, 'freightlinedetails', '');
addElement(freight, 'ref', _shipmentTable.ShipmentId);
addElement(freight, 'amt', num2str(containerCount, 1, 0, 0, 0));
addElement(freight, 'wgt', num2str(totalWeight, 1, 2, 1, 0)); // dot decimal, 2 places
return doc;
}
Then save it to a stream and send it through the connector. In an event-based export that means one file per document:
public void exportFileMessage(DEVIntegExportDocumentLog _exportDocumentLog,
DEVIntegMessagesLoadBaseType _loadFileStorageCache)
{
WHSShipmentTable shipmentTable;
select shipmentTable
where shipmentTable.RecId == _exportDocumentLog.RefRecId;
System.Xml.XmlDocument doc = this.buildManifest(shipmentTable);
System.IO.MemoryStream stream = new System.IO.MemoryStream();
doc.Save(stream);
stream.Position = 0;
// File name from the message type, e.g. 'Manifest_%1_%d.xml'
Filename fileName = _exportDocumentLog.generateFileName([shipmentTable.ShipmentId], 'Manifest.xml');
this.sendMessageFileStorageCache(_exportDocumentLog, _loadFileStorageCache, fileName, stream);
}
For a periodic export the same document goes out through writeFileAzureStorage(fileName, stream) or writeFileSFTP(fileName, stream).
Attributes and namespaces when writing
System.Xml.XmlElement amount = addElement(price, 'Amount', num2str(purchLine.PurchPrice, 1, 2, 1, 0));
amount.SetAttribute('currencyID', purchTable.CurrencyCode);
// declare a default namespace on the root
System.Xml.XmlElement root = doc.createElement('SyncPurchaseOrder');
root.SetAttribute('xmlns', @'http://schema.infor.com/InforOAGIS/2');
Practical notes
XmlDataDocumentvsXmlDocument. Read withXmlDataDocument(it loads from a stream and behaves well with the framework's payloads); write withXmlDocument.- Numbers and dates. Always format explicitly —
num2str(value, 1, 2, 1, 0)for a two-decimal dot-separated amount, and an explicit date pattern. A file that changes shape with the user's regional settings will fail at the partner, not at you. - Empty optional elements. Decide with the partner whether an unknown value means omit the element or send it empty; the two are different in most schemas. The helper above omits when the value is blank.
- Validation. The framework does not validate against an XSD. If the partner supplies one, the cheapest safety net is a summary/total element you can compare against the lines you parsed.
- Encoding.
doc.Save(stream)writes UTF-8 without a declaration unless you add one withdoc.createXmlDeclaration('1.0', 'UTF-8', null)and prepend it — some partners insist on it.
Tutorial
- XppInteg – Import purchase orders from XML files into D365FO — the full import from an Azure File Share, including staging, namespaces, and the error handling users see.