How do I reuse BizTalk 2004 maps outside of BizTalk?

Recently, I shed some light on how Maps are compiled to .NET assemblies . Perhaps one of the most asked question on microsoft.public.biztalk.* is "Calling a map from C# or VB.NET? ". This post attempts to answer that question and clarifies a few things.

It is possible to run a map produced by the BizTalk 2004 mapper outside of BizTalk and it is even possible; under certain conditions; to run the map on a machine that does not have BizTalk installed. The steps required for using a map outside of BizTalk are outlined below:

  1. Extract the XSL document produced by the mapper,
  2. If the map uses functoids (out of the box or custom functoids), you will need to extract the Xsl Transformation arguments,
  3. Create a .NET System.Xml.Xsl.XslTransfom() object,
  4. Create a .NET System.Xml.Xsl.XsltArgumentList() if there were any functoids in the map and instantiate appropriate objects,
  5. Call Transform() on the XSL and optionally, the XsltArgumentList.

In the list of steps above 3, parts of 4 and 5 have nothing to do with BizTalk: these are just plain .NET programming. Creating the XsltArgumentList requires us to understand how the mapper saves functoids.

Extracting the XSL and the extension objects (if any) can be achieved by at least three different methods:

  1. If you have the map file (.btm) and can open it in Visual Studio 2003, you can right click on the map file in the solution explorer and select "Validate Map". The output window will give you the path(s) to the XSL and the extension Object XML. The links can be shift-clicked to retrieve the files,
  2. If you only have the compiled assembly, you can use the excellent Lutz Roeder's .NET Reflector to extract the required information as strings ,
  3. If you only have the compiled assembly, you can write some code that loads the assembly, creates an instance of the map object and calls the appropriate members. See the format of maps assemblies .

The only speed bump is the format of the Extension Object XML document. I have extracted the extension associated with the map Scriptor_CallExternalAssembly from the " ExtendingMapper " SDK sample and formatted it:

<ExtensionObjects>
<ExtensionObject Namespace="http://schemas.microsoft.com/BizTalk/2003/ScriptNS0"
         AssemblyName="Microsoft.Samples.BizTalk.ExtendingMapper.MapperClassLibrary,
         Version=1.0.0.0, Culture=neutral, PublicKeyToken=f2aaad746c3d94f5"
         ClassName="Microsoft.Samples.BizTalk.ExtendingMapper.MapperHelper" /> 
</ExtensionObjects>

Creating the extension objects is now very simple. For each "ExtensionObject" node, we need to load the assembly, create an instance of the given class and add the object along with its namespace to the XsltArgumentList. Of course, the map will only run if all needed assemblies are available. This is true for custom assemblies as well as out of the box functoids. The code below does exactly this and the full solution can be downloaded here :

 using System;
using System.Reflection;
using System.Xml;
using System.Xml.Xsl;
using System.IO;
using System.Text;


namespace MapReuser
{
 /// <summary>
   /// Transforms XML instances using a BizTalk map.
 /// </summary>
  public class BizTalkMap
    {
       /// <summary>
       /// Caches the XSLT stream.
       /// </summary>
      private Stream xsltStream;

     /// <summary>
       /// Caches the XSLT Arguments stream.
     /// </summary>
      private Stream xsltArguments;
      

        /// <summary>
       /// Cache the XslTransform.
       /// </summary>
      private XslTransform     xslTransform;

     /// <summary>
       /// Caches the XSltArgumentList.
      /// </summary>
      private XsltArgumentList xslArgumentList;




      /// <summary>
       /// Constructor.
      /// </summary>
      /// <param name="XsltStream">Stream of XSLT as XML.</param>
       /// <param name="XsltArguments">Stream of Extension Objects as XML.</param>
       public BizTalkMap(Stream XsltStream, Stream XsltArguments)
     {
           xsltStream    = XsltStream;
         xsltArguments = XsltArguments;
      }

       /// <summary>
       /// Transforms the given instance and returns the result as a stream.
     /// </summary>
      /// <param name="inXml">Stream of the instance to transform (XML)</param>
     /// <returns>Stream of the transformed XML.</returns>
     public Stream TransformInstance(Stream inXml)
      {
           XslTransform transform   = Transform;
           XmlDocument  xmlInputDoc = new XmlDocument();

           // Make sure we do not destroy the formatting
          xmlInputDoc.Load(inXml);

            // Output stream
           MemoryStream  outStream = new MemoryStream();
           XmlTextWriter xmlWriter = new XmlTextWriter(outStream, System.Text.Encoding.UTF8);

          // Formatting options
          xmlWriter.Formatting  = Formatting.Indented;
            xmlWriter.Indentation = 2;
          
            // Perform transformation - We do not specify a resolver
           transform.Transform(xmlInputDoc, TransformArgs, xmlWriter, null);

           // Prepare the output stream
           outStream.Seek(0, SeekOrigin.Begin);

            return outStream;
       }

       /// <summary>
       /// Gets an instance of XslTransform for the given XSL/Extension objects.
     /// </summary>
      private XslTransform Transform
     {
           get
         {
               if (xslTransform == null)
               {
                   // Create a new transform
                  XmlTextReader xsltReader = new XmlTextReader(xsltStream);
                   XslTransform transformTemp = new XslTransform();
                    transformTemp.Load(xsltReader, (XmlResolver) null, GetType().Assembly.Evidence);
                    
                    // Cache the transform
                 xslTransform = transformTemp;
               }
               return xslTransform;
            }
       }

       /// <summary>
       /// Gets a XsltArgumentList from a BizTalk Extension Object XML.
      /// </summary>
      private XsltArgumentList TransformArgs
     {
           get
         {
               if (xslArgumentList == null)
                {
                   XmlDocument      xmlExtension = new XmlDocument();
                  XsltArgumentList xslArgList   = new XsltArgumentList();

                 if (xsltArguments != null)
                  {
                       // Load the argument list and create all the needed instances
                      xmlExtension.Load(xsltArguments);
                       XmlNodeList xmlExtensionNodes = xmlExtension.SelectNodes("//ExtensionObjects/ExtensionObject");
                      foreach (XmlNode extObjNode in xmlExtensionNodes)
                       {
                           XmlAttributeCollection extAttributes = extObjNode.Attributes;

                           XmlNode  namespaceNode = extAttributes.GetNamedItem("Namespace");
                            XmlNode  assemblyNode  = extAttributes.GetNamedItem("AssemblyName");
                         XmlNode  classNode     = extAttributes.GetNamedItem("ClassName");
                            Assembly extAssembly   = Assembly.Load(assemblyNode.Value);
                         object   extObj        = extAssembly.CreateInstance(classNode.Value);
                           xslArgList.AddExtensionObject(namespaceNode.Value, extObj);
                     }
                   }
                   // Cache the list
                  xslArgumentList = xslArgList;
               }
               return xslArgumentList;
         }
       }
   }
}

This class can be used as follows:

 FileStream fsXslt       = null;
FileStream fsInput      = null;
FileStream fsExtensions = null;
FileStream outStream    = null;

try
{
    // Transform
   fsXslt         = new FileStream(xsltPath, FileMode.Open, FileAccess.Read);
  fsInput        = new FileStream(instancePath, FileMode.Open, FileAccess.Read);
  fsExtensions   = (extensionPath != null) && (extensionPath.Length > 0) ? new FileStream(extensionPath, FileMode.Open, FileAccess.Read) : null;
   BizTalkMap map = new BizTalkMap(fsXslt, fsExtensions);

  Stream sOut = map.TransformInstance(fsInput);

   // Save stream to a file
   string destPath = Path.Combine(Path.GetDirectoryName(instancePath), Path.GetFileName(instancePath) + ".trans.xml");
  outStream = new FileStream(destPath, FileMode.Create, FileAccess.Write);
    outStream.Write(((MemoryStream) sOut).ToArray(), 0, (int) sOut.Length);
}
finally
{
  if (fsXslt       != null) fsXslt.Close();
   if (fsInput      != null) fsInput.Close(); 
 if (fsExtensions != null) fsExtensions.Close();
 if (outStream    != null) outStream.Close(); 
}