Merging XML

Note: this entry has moved.

Sometimes, it's useful to be able to merge two XML nodes/documents. One concrete case is where you have a configuration file where some global element is configured and later it can be specialized by concrete usages. There are other uses too, such as consolidating information from various sources, etc. I "found" a way to do this, "stealing" Oliver Becker's XSLT stylesheet and writing the following .NET class that takes advantage of it:

/// <summary>
/// Merges two <see cref="XmlNode"/> objects.
/// </summary>
public class XmlMerge
{
  const string First = "first";
  const string Second = "second";
  static XslTransform Transformation;
  static IXPathNavigable Input;

  static XmlMerge()
  {
    Transformation = new XslTransform();
    Transformation.Load( new XmlTextReader( 
      Assembly.GetExecutingAssembly().GetManifestResourceStream(
      "XmlMerger.merge.xslt") ), 
      null, null);

    // Dummy input document for the merge stylesheet.
    Input = new XPathDocument( new StringReader ( String.Format( 
      @"<?xml version='1.0'?>
      <merge xmlns='http://informatik.hu-berlin.de/merge'>
        <file1>mem://{0}</file1>
        <file2>mem://{1}</file2>
      </merge>", First, Second ) ) );
  }

  private XmlMerge() {}

  /// <summary>
  /// Merges the first XML with the second.
  /// </summary>
  /// <param name="first">First XML.</param>
  /// <param name="second">Second XML.</param>
  /// <param name="replace">If set to <see langword="true"/> replaces 
  /// text values from <paramref name="first"/> with the ones in 
  /// <paramref name="second"/> if nodes are equal.</param>
  /// <returns>The merged XML.</returns>
  public static XmlDocument Merge( IXPathNavigable first, IXPathNavigable second, bool replace )
  {
    // Holds the merged results.
    StringBuilder sb = new StringBuilder();
    XmlTextWriter tw = new XmlTextWriter(new StringWriter(sb));
    tw.Formatting = Formatting.None;

    // Specify whether second node replaces text from first one.
    XsltArgumentList args = new XsltArgumentList();
    args.AddParam("replace", String.Empty, replace);

    Transformation.Transform(Input, args, tw, new XmlNodeResolver(first, second));
    tw.Flush();

    XmlDocument doc = new XmlDocument();
    doc.LoadXml(sb.ToString());

    return doc;
  }
}

Note that I embedded the stylesheet in the assembly, and loaded everything only once in the static ctor. Oliver' stylesheet receives the names of the two files to process and loads them with the XSLT document function. I have "fooled" the XslTransform by providing dummy urls for both files (mem://first and mem://second and through a custom XmlResolver answer the stylesheet request for those files. That's what the XmlNodeResolver I created is about (not rocket science, I must admit :o)):

  /// <summary>
  /// Resolves the dummy URL locations to the parameters received.
  /// </summary>
  private class XmlNodeResolver : XmlResolver
  {
    IXPathNavigable _first;
    IXPathNavigable _second;

    public XmlNodeResolver(IXPathNavigable first, IXPathNavigable second)
    {
      _first = first;
      _second = second;
    }

    public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
    {
      if ( absoluteUri.Authority == First )
        return _first.CreateNavigator();
      else if ( absoluteUri.Authority == Second )
        return _second.CreateNavigator();
      return null;
    }

    public override System.Net.ICredentials Credentials 
    { 
      set {} 
    }
  }

This is a private class nested in the XmlMerger class.
Given the following NUnit test for it:

    [Test]
    public void MergeNodes()
    {  
      XmlDocument first = new XmlDocument();
      XmlDocument second = new XmlDocument();

      first.LoadXml(@"
   <themes>
      <theme id='appl'>
         <title xml:lang='nl'>Toepassingen</title>
      </theme>
   </themes>");

      second.LoadXml(@"
   <themes>
      <theme id='doc' />
      <theme id='appl'>
         <title xml:lang='en'>Applications</title>
      </theme>
   </themes>");
      
      XmlNode result = XmlMerge.Merge(first, second, true);
      System.Diagnostics.Debug.Write(result.OuterXml);
    }

The merged results are:

<themes>
  <theme id="doc" />
  <theme id="appl">
    <title xml:lang="nl">Toepassingen</title>
    <title xml:lang="en">Applications</title>
  </theme>
</themes>
+ The 'real' NUnit test without pretty indenting and a proper assert for passing:

3 Comments

  • i tried your approach.

    when i'm calling Merge i get the error:

    'No input file specified (parameter 'with')' return from XSLT. am i missing something?



    cheers

  • Nope, I didn't. Note that the stylesheet first tries to match &quot;m:merge&quot; which is one of the usage options. Just in case you can try deleting the template that matches *, but it's working for me.

  • I apply this to a Web.Config ....

    this works perfectly with .NET 2.0 but with the Framework 1.1, I have some strange behaviors like some nodes are missing or duplicated.



    Any thoughts?

Comments have been disabled for this content.