Skip to main content

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​

MethodPurpose
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 / xmlGetValueNodeInt64Integer
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​

  • XmlDataDocument vs XmlDocument. Read with XmlDataDocument (it loads from a stream and behaves well with the framework's payloads); write with XmlDocument.
  • 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 with doc.createXmlDeclaration('1.0', 'UTF-8', null) and prepend it — some partners insist on it.

Tutorial​