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