Okay, I admit it, I have never spent the time to learn the intricacies of Enterprise Single Sign On. I guess I had a hard time seeing the value when the only description I heard for quite a long time is that it is where BizTalk stores credentials.

I finally sat down and created a sample application that I could see would be useful.

I want to create a generic pipeline that interrogates the target namespace of the xml document in the decode stage, and applies a map. I actually have this requirement where I need to change the document into a different xml document that I can actually apply an envelope schema to.

<Rant>

I have always seen tutorials about how easy it is to create an envelope schema, reference a document schema and viola, BizTalk is splitting documents like crazy! I watched the podcasts, seen the demos eleventeen times and so I was tasked with doing it. I thought, hey, this is going to be easy!

NOT SO

This is a representation of what the order came in as

<orders xmlns="http://WebSiteVendor">
  <order>
    <Header>
      Stuff
    </Header>
    <Detail>
      Crap
    </Detail>
    <Details>
      Even More Crap
    </Details>
  </order>
  <order>
    <Header>
      More Stuff
    </Header>
    <Detail>
      Yes, Even More Crap
    </Detail>
    <Details>
      Hard to believe, but yes, More Crap
    </Details>
  </order>
</orders>

How am I supposed to create an envelope schema with a document node out of this type of document? I can’t go back to the Website Vendor and have them change it.

However, for the DASM, I require an xml document that looks like this:

<orders xmlns="http://WebSiteVendor">
  <order xmlsn="http://WebSiteVendorOrder">
    <Header>
      Stuff
    </Header>
    <Detail>
      Crap
    </Detail>
    <Details>
      Even More Crap
    </Details>
  </order>
  <order xmlsn="http://WebSiteVendorOrder">
    <Header>
      More Stuff
    </Header>
    <Detail>
      Yes, Even More Crap
    </Detail>
    <Details>
      Hard to believe, but yes, More Crap
    </Details>
  </order>
</orders>

Probably the worst thing is that a resolution (how to split this type of document up) has not been explained very well, if at all anywhere on the web.

</Rant>

After exploring all sorts of ways I can prep the data by removing name spaces, adding them back in, etc…, I started feeling like Thomas Edison. When Thomas Edison was interviewed by a young reporter who boldly asked Mr. Edison if he felt like a failure and if he thought he should just give up by now. Perplexed, Edison replied, “Young man, why would I feel like a failure? And why would I ever give up? I now know definitively over 9,000 ways that an electric light bulb will not work. Success is almost in my grasp.” And shortly after that, and over 10,000 attempts, Edison invented the light bulb.

Finally it dawned on me, that if I simply had a map that would execute in the Decode stage, I could then use the XML Disassembler to break up the ’modified’ message into individual orders. I had created the envelope schema and imported the ’document’ schema and made it a child.

Oddly enough, what is not terribly documented well is that the Body Xpath attribute needs to be pointing to the ’parent’ of the document that you want to split up. (What is that about?)

So I looked around and voil%u00e0: there is a pipeline component in the SDK that does EXACTLY what I need. (SDK\Samples\Pipelines\XslTransformComponent)

So here is what I wanted to do:

  1. I wanted it to be on the receive side, in the decode stage.
  2. I wanted it to be dynamic so that if multiple messages came in, it would be able to figure out what the correct map was and invoke it.

So I decided that the SSO would be the perfect place to store the following key values:

Key Value
http://targetnamespace.com#node c:\{path}\map.xsl

Obvious, that this table would increase as more and more documents started to be processed by this splitting component.

The first thing I did was download the Enterprise Single Sign On application to allow you to put key value pairs in the SSO Database. You can download the SSO Configuration Application MMC Snap-In. When you install it, it creates a new menu in your Start Menu. You have to register it, but then you can start placing key value pairs.

I created an application called DecodeTransformations. and here is what my dictionary looks like:

So now I have to take the existing code and have it use ESSO to resolve the xslt instead of hard coding it in the properties of the pipeline.

As part of the download, Microsoft in their kindness, included sample code on how to retrieve values. You have to supply two values:

  1. Application Name (DecodeTransformations)
  2. Key ({targetnamespace})

I included the class and referenced it in the primary class. Instead of giving you bits and pieces, I am going to tell you what lines I modified, and then show the resulting code (so you can use it yourself).

  1. I changed the property bag to get the SSOApplication (line 151)
  2. I commented out the code to ensure that there was value during build (line 248)
  3. Instead of loading the xsl first, I wanted to load the xml document up so I can determine what is its namespace. (line 91)
  4. I then determined what the document schema is it, by calling the ReturnTargetNamespace method. (line 94 -> 257)
  5. Now that I had the namespace, I then passed the Application in from the pipeline properties along with the targetnamespace to return the xsl path and returned the xsl path. (line 95)
  1: using System;
  2: using System.IO;
  3: using System.Xml;
  4: using System.Xml.Xsl;
  5: using System.Xml.Linq;
  6: using System.ComponentModel;
  7: using System.Collections;
  8: using Microsoft.BizTalk.Message.Interop;
  9: using Microsoft.BizTalk.Component.Interop;
 10: using Microsoft.Win32;
 11: using SSO.Utility;
 12: 
 13: namespace Microsoft.BizTalk.SDKSamples.Pipelines.XslTransformComponent
 14: {
 15: 	/// <summary>
 16: 	/// Implements a pipeline component that applies Xsl Transformations to XML messages
 17: 	/// </summary>
 18: 	/// <remarks>
 19: 	/// XslTransformer class implements pipeline components that can be used in send pipelines
 20: 	/// to convert XML messages to HTML format for sending using SMTP transport. Component can
 21: 	/// be placed only in Encoding stage of send pipeline
 22: 	/// </remarks>
 23: 	[ComponentCategory(CategoryTypes.CATID_PipelineComponent)]
 24: 	[ComponentCategory(CategoryTypes.CATID_Decoder)]
 25: 	[System.Runtime.InteropServices.Guid("FA7F9C55-6E8E-4855-8DAC-FA1BC8A499E2")]
 26: 	public class XslTransformer		: Microsoft.BizTalk.Component.Interop.IBaseComponent, 
 27: 									  Microsoft.BizTalk.Component.Interop.IComponent, 
 28: 									  Microsoft.BizTalk.Component.Interop.IPersistPropertyBag,
 29: 									  Microsoft.BizTalk.Component.Interop.IComponentUI
 30: 	{		
 31: 		#region IBaseComponent
 32: 		/// <summary>
 33: 		/// Name of the component.
 34: 		/// </summary>
 35: 		[Browsable(false)]
 36: 		public string Name
 37: 		{
 38: 			get {	return "XSL Transform Component";	}
 39: 		}
 40: 		/// <summary>
 41: 		/// Version of the component.
 42: 		/// </summary>
 43: 		[Browsable(false)]
 44: 		public string Version
 45: 		{
 46: 			get	{	return "1.0";	}
 47: 		}
 48: 		/// <summary>
 49: 		/// Description of the component.
 50: 		/// </summary>
 51:         [Browsable(false)]
 52:         public string Description
 53:         {
 54:             get { return "XSL Transform Pipeline Component"; }
 55:         }
 56: 		#endregion
 57: 		#region IComponent
 58: 		/// <summary>
 59: 		/// Implements IComponent.Execute method.
 60: 		/// </summary>
 61: 		/// <param name="pc">Pipeline context</param>
 62: 		/// <param name="inmsg">Input message.</param>
 63: 		/// <returns>Converted to HTML input message.</returns>
 64: 		/// <remarks>
 65: 		/// IComponent.Execute method is used to convert XML messages
 66: 		/// to HTML messages using provided Xslt file.
 67: 		/// It also sets the content type of the message part to be "text/html"
 68: 		/// which is necessary for client mail applications to correctly render
 69: 		/// the message
 70: 		/// </remarks>
 71: 		public IBaseMessage Execute(IPipelineContext pc, IBaseMessage inmsg)
 72: 		{
 73: 			inmsg.BodyPart.Data = TransformMessage(inmsg.BodyPart.Data);
 74: 			inmsg.BodyPart.ContentType = "text/html";
 75: 			return inmsg;
 76: 		}
 77: 		#endregion
 78: 		#region Helper function
 79: 		/// <summary>
 80: 		/// Transforms XML message in input stream to xml message
 81: 		/// </summary>
 82: 		/// <param name="stm">Stream with input XML message</param>
 83: 		/// <returns>Stream with output xml message</returns>
 84: 		private Stream TransformMessage(Stream stm)
 85: 		{
 86: 			MemoryStream ms = null;
 87: 			string validXsltPath = null;
 88: 			try 
 89: 			{
 90: 				//Load Xml stream in XmlDocument.
 91: 				XmlDocument doc = new XmlDocument();
 92: 				doc.Load(stm);
 93:                 // Get the target name space
 94:                 string tns = ReturnTargetNamespace(doc);
 95:                 string xsltPath = SSOClientHelper.Read(this.SSOApplication, tns);
 96: 				// Get the full path to the Xslt file
 97: 				validXsltPath = GetValidXsltPath(xsltPath);
 98:                 //Create memory stream to hold transformed data.
 99:                 ms = new MemoryStream();
100: 				// Load transform
101: 				XslTransform transform = new XslTransform();
102: 				transform.Load(validXsltPath);
103: 				//Preform transform
104: 				transform.Transform(doc, null, ms, null);
105: 				ms.Seek(0, SeekOrigin.Begin);
106: 			}
107: 			catch(Exception e) 
108: 			{
109: 				System.Diagnostics.Trace.WriteLine(e.Message);
110: 				System.Diagnostics.Trace.WriteLine(e.StackTrace);
111: 				throw e;
112: 			}
113: 			return ms;
114: 		}
115: 		/// <summary>
116: 		/// Get a valid full path to a Xslt file
117: 		/// </summary>
118: 		/// <param name="path">Path provided by user in Pipeline Designer</param>
119: 		/// <returns>The full path</returns>
120: 		/// <remarks>
121: 		/// If user provides absolute path then it is used as long as the file can be opened there
122: 		/// If user provides just a name of file or relative path then we try to open a file in 
123: 		/// [Install foder]\Pipeline Components
124: 		/// </remarks>
125: 		private string GetValidXsltPath(string path)
126: 		{
127: 			string validPath = path;
128: 			if (!System.IO.File.Exists(path))
129: 			{
130: 						RegistryKey rk = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\BizTalk Server\3.0");
131: 						string InstallPath = string.Empty;
132: 				
133: 						if (null != rk)
134: 					InstallPath = (String)rk.GetValue("InstallPath");
135: 						
136: 						validPath = InstallPath + @"Pipeline Components\" + path;
137: 				if (!System.IO.File.Exists(validPath))
138: 				{
139: 					throw new ArgumentException("The XSL transformation file " + path + " can not be found");
140: 				}
141: 			}	
142: 			return validPath;
143: 		}
144: 		private string GetXSLPathFromSSO(string Application,string MessageType)
145: 		{
146: 			string MapName = SSOClientHelper.Read(Application, MessageType);
147: 			return MapName;
148: 		}
149: 		#endregion	
150: 		#region IPersistPropertyBag
151: 		private string _SSOApplication;
152: 		public string SSOApplication
153: 		{
154: 			get { return _SSOApplication; }
155: 			set { _SSOApplication = value; }
156: 		}
157: 		/// <summary>
158: 		/// Gets class ID of component for usage from unmanaged code.
159: 		/// </summary>
160: 		/// <param name="classid">Class ID of the component.</param>
161: 		public void GetClassID(out Guid classid)
162: 		{
163: 			classid = new System.Guid("FA7F9C55-6E8E-4855-8DAC-FA1BC8A499E2");
164: 		}	
165: 		/// <summary>
166: 		/// Not implemented.
167: 		/// </summary>
168: 		public void InitNew()
169: 		{
170: 		}	
171: 		public void Load(Microsoft.BizTalk.Component.Interop.IPropertyBag pb, Int32 errlog)
172: 		{
173: 			string val = (string)ReadPropertyBag(pb, "SSOApplication");
174: 			if (val != null) SSOApplication = val;
175: 		}		
176: 		/// <summary>
177: 		/// Saves the current component configuration into the property bag.
178: 		/// </summary>
179: 		/// <param name="pb">Configuration property bag.</param>
180: 		/// <param name="fClearDirty">Not used.</param>
181: 		/// <param name="fSaveAllProperties">Not used.</param>
182: 		public void Save(Microsoft.BizTalk.Component.Interop.IPropertyBag pb, Boolean fClearDirty, Boolean fSaveAllProperties)
183: 		{
184: 			object val = (object) SSOApplication;
185: 			WritePropertyBag(pb, "SSOApplication", val);            
186: 		}
187: 		/// <summary>
188: 		/// Reads property value from property bag.
189: 		/// </summary>
190: 		/// <param name="pb">Property bag.</param>
191: 		/// <param name="propName">Name of property.</param>
192: 		/// <returns>Value of the property.</returns>
193: 		private static object ReadPropertyBag(Microsoft.BizTalk.Component.Interop.IPropertyBag pb, string propName)
194: 		{
195: 			object val = null;
196: 			try
197: 			{
198: 				pb.Read(propName,out val,0);
199: 			}
200: 			catch(System.ArgumentException)
201: 			{
202: 				return val;
203: 			}
204: 			catch(Exception ex)
205: 			{
206: 				throw new ApplicationException( ex.Message);
207: 			}
208: 			return val;
209: 		}
210: 		private static void WritePropertyBag(Microsoft.BizTalk.Component.Interop.IPropertyBag pb, string propName, object val)
211: 		{
212: 			try
213: 			{
214: 				pb.Write(propName, ref val);
215: 			}
216: 			catch(Exception ex)
217: 			{
218: 				throw new ApplicationException( ex.Message);
219: 			}
220: 		}
221: 		#endregion
222: 		#region IComponentUI
223: 		/// <summary>
224: 		/// Component icon to use in BizTalk Editor.
225: 		/// </summary>
226: 		[Browsable(false)]
227: 		public IntPtr Icon
228: 		{
229: 			get	{	return IntPtr.Zero;	}
230: 		}
231: 		/// <summary>
232: 		/// The Validate method is called by the BizTalk Editor during the build 
233: 		/// of a BizTalk project.
234: 		/// </summary>
235: 		/// <param name="obj">Project system.</param>
236: 		/// <returns>
237: 		/// A list of error and/or warning messages encounter during validation
238: 		/// of this component.
239: 		/// </returns>
240: 		public IEnumerator Validate(object projectSystem)
241: 		{
242: 			if (projectSystem==null)
243: 				throw new System.ArgumentNullException("No project system");
244: 			IEnumerator enumerator = null;
245: 			ArrayList   strList  = new ArrayList();
246: 			try
247: 			{
248: 				//GetValidXsltPath(SSOApplication);
249: 			}
250: 			catch(Exception e)
251: 			{
252: 				strList.Add(e.Message);
253: 				enumerator = strList.GetEnumerator();
254: 			}
255: 			return enumerator;
256: 		}
257:         private string ReturnTargetNamespace(XmlDocument xdoc)
258:         {
259:             XmlNodeReader xmlndRdr = new XmlNodeReader(xdoc);
260:             string TargetNamespace = XElement.Load(xmlndRdr).GetDefaultNamespace().ToString();
261:             TargetNamespace = TargetNamespace + "#" + xdoc.DocumentElement.Name;
262:             return TargetNamespace;
263:         }
264: 		#endregion	
265: 	}
266: }
267: 

Once that is completed, simply place the dll in the pipeline components folder.

I created the pipeline that has the XSLTransform pipeline component and the XML Disassembler pipeline component and it works like a charm!

Now I can use the same component in multiple locations and can receive multiple messages in the same location and based on the application deployed to SSO, it will resolve where the xsl lives and transform it. Unlike the ESB dispatcher pipeline component, I actually don’t even need to deploy the map, I simply need to place it somewhere and I am ready to go.

Here is what the pipeline looks like