Web Service Results – XPath is your friend!

Yesterday Eric Shupps posted about retrieving error messages from SharePoint web services. I noticed he uses XPath to retrieve the error codes, but then falls back on object manipulation to filter for real errors and to retrieve the error message:

XmlNode ndRoot = xmlResult.SelectSingleNode("sp:Results", nsMgr);
// Find the ErrorCode node, which exists for all operations regardless of status.
XmlNodeList nlResults = ndRoot.SelectNodes("//sp:Result/sp:ErrorCode", nsMgr);
// Loop through the node collection and find each ErrorCode entry
foreach (XmlNode ndResult in nlResults) {
  // Check the value of the node to determine its status
  if (ndResult.InnerText != "0x00000000") {
    XmlNode ndError = ndResult.NextSibling;
    string sError = ndError.InnerText;
    …

Just like we wouldn’t omit a SQL WHERE clause in favor of filtering a DataSet with string comparison, we should probably let XPath do as much for us as possible. In this case a few tweaks reduce the logic to a single selection, also offering a quick exit if there are no errors found:

// Find the ErrorText for Result nodes with non-zero ErrorCode.
string xpath = "/sp:Results/sp:Result[sp:ErrorCode != '0x00000000']/sp:ErrorText";
XmlNodeList nlErrors = xmlResult.SelectNodes(xpath, nsMgr);
if (nlErrors.Count == 0) {
  // No errors - return "Success", false, null, whatever
}
foreach (XmlNode ndError in nlErrors) {
  string sErrorText = ndError.InnerText;
  …

As Eric mentioned, there are a few ways to handle the error(s) returned. I thought it might be useful to pair the ErrorText with the ID of the method that generated it:

// Key = Method ID; Value = Error Text
static Dictionary<string, string> GetErrors(XmlDocument xmlResult) {
  XmlNamespaceManager nsMgr = new XmlNamespaceManager(xmlResult.NameTable);
  XmlElement ndRoot = xmlResult.DocumentElement;
  nsMgr.AddNamespace("sp", ndRoot.NamespaceURI);

  // Find the ErrorText for Result nodes with non-zero ErrorCode.
  string xpath = "/sp:Results/sp:Result[sp:ErrorCode != '0x00000000']/sp:ErrorText";
  XmlNodeList nlErrors = ndRoot.SelectNodes(xpath, nsMgr);

  if (nlErrors.Count == 0) {
    return null;
  }

  Dictionary<string, string> errorDict = new Dictionary<string, string>(nlErrors.Count);
  foreach (XmlNode ndError in nlErrors) {
    XmlNode ndResultId = ndError.ParentNode.Attributes["ID"];
    if (ndResultId != null) {
      errorDict.Add(ndResultId.Value, ndError.InnerText);
    }
  }
  return errorDict;
}

I figure a single XPath selection plus a reference to ParentNode would be more efficient than selecting the Result node for the ID and then finding its ErrorText child from there. And of course if there are better ways to do this, please share!