Setting filename using message contents

Often you will have to get some content from your messages, and use this to set the filename of your outgoing files, in our case we needed to use a sequencenumber. I will show a way to do this using a pipeline component in a streaming matter.

We start by creating helper methods to get the message stream and extract the data from this stream, by using an xpath query. Fortunatly, this can be done very easily by using the XPathDocument, which will do this nicely in a streaming way. Special thanks go out to Mohamed M Malek, whose code I adapted for this.

This code will get a stream from the incoming message, making sure it creates a readonly stream if the incoming data stream is not seekable.

/// <summary>
/// Get the message stream from a BizTalk message.
/// </summary>
internal static Stream GetMessageStream(Microsoft.BizTalk.Message.Interop.IBaseMessage msg, Microsoft.BizTalk.Component.Interop.IPipelineContext context)
{
	// Get the stream
	var stream = msg.BodyPart.GetOriginalDataStream();
 
	// Check if the stream is seekable
	if (stream.CanSeek)
	{
		return stream;
	}
 
	// If the stream is not seekable, we should create a read only stream
	var readStream = new ReadOnlySeekableStream(stream);
 
	// Check if we cann add the read only stream to the message's context
	if (context != null)
	{
		context.ResourceTracker.AddResource(readStream);
	}
 
	// Set the body of the message to the read only stream
	msg.BodyPart.Data = readStream;
	stream = readStream;
	return stream;
}

The next method will execute the xpath query, and will return the first result. Do notice, only simple xpath queries can be executed, f.e. indexes don’t work, and probably other filtering etc. will not work either.

/// <summary>
/// Extract the contents of a node selected by an xpath query.
/// The query does not support indexes, and probably other more "advanced" queries.
/// </summary>
internal static string ExtractDataValueXPath(Stream msgStream, string xpath)
{
	// Go to start of the stream
	msgStream.Seek(0, SeekOrigin.Begin);
 
	// Settings which will be used to create the XML reader used to read the stream
	var settings = new XmlReaderSettings
	{
		ConformanceLevel = ConformanceLevel.Document,
		ValidationType = ValidationType.None,
		IgnoreWhitespace = true,
		IgnoreProcessingInstructions = true,
		IgnoreComments = true,
		CloseInput = false
	};
 
	// Create the XML reader
	var reader = XmlReader.Create(msgStream, settings);
 
	// The value returned by the xpath query
	var xPathValue = string.Empty;
 
	// Check if a valid query was provided, and of the XML reader could be opened
	if (string.IsNullOrEmpty(xpath) || !reader.Read())
	{
		return xPathValue;
	}
 
	// Create an XPathDocument, which will be used to execute the query in a streaming manner
	var xPathDoc = new XPathDocument(reader);
 
	// Create the navigator, and execute the query
	var xNodes = xPathDoc.CreateNavigator().Select(xpath);
 
	// Check if any values were found, if so, return the first result
	if (xNodes.Count != 0 && xNodes.MoveNext())
	{
		xPathValue = xNodes.Current.Value;
	}
 
	// Reset the stream
	msgStream.Seek(0, SeekOrigin.Begin);
 
	return xPathValue;
}

Now we can use these methods to retrieve the wanted value from the incoming message, and write it into ReceivedFileName in the context. This way we can use it by using the %SourceFileName% macro on the send port. I have 1 property on my pipeline component, XPath, so we can set the query to be executed on the sendport. This can be used independantly off the adapter on the receiveport, f.e. in our scenario we received the message from a WCF service, and write our outgoing message to a SFTP location.

public string XPath { get; set; }
 
/// <summary>
/// Loads configuration properties for the component
/// </summary>
/// <param name="pb">Configuration property bag</param>
/// <param name="errlog">Error status</param>
public virtual void Load(IPropertyBag pb, int errlog)
{
	var val = ReadPropertyBag(pb, "xpath");
	if ((val != null))
	{
		XPath = ((string)(val));
	}
}
 
/// <summary>
/// Saves the current component configuration into the property bag
/// </summary>
/// <param name="pb">Configuration property bag</param>
/// <param name="fClearDirty">not used</param>
/// <param name="fSaveAllProperties">not used</param>
public virtual void Save(IPropertyBag pb, bool fClearDirty, bool fSaveAllProperties)
{
	WritePropertyBag(pb, "xpath", XPath);
}

Now in the execute method we will set ReceivedFileName in the context with the extracted value.

/// <summary>
/// Implements IComponent.Execute method.
/// </summary>
public IBaseMessage Execute(IPipelineContext pc, IBaseMessage inmsg)
{
	try
	{
		#region Validations
 
		if (String.IsNullOrEmpty(XPath))
		{
			throw new ArgumentException("SetFileNameUsingMessageContents - XPath query is missing");
		}
 
		#endregion
 
		// Get the value to be extracted using the xpath query
		var extractedValue = Helpers.ExtractDataValueXPath(Helpers.GetMessageStream(inmsg, pc), XPath);
 
		// Set ReceivedFileName, so we can use %SourceFileName% on the sendport
		inmsg.Context.Write("ReceivedFileName", "http://schemas.microsoft.com/BizTalk/2003/file-properties", extractedValue);
	}
	catch (Exception ex)
	{
		// Check if we can set error info on the message
		if (inmsg != null)
		{
			inmsg.SetErrorInfo(ex);
		}
 
		// Rethrow
		throw;
	}
 
	// Return the message with the new context property
	return inmsg;
}

Now after creating a pipeline with the new pipeline component, we can use the %SourceFileName% macro on the sendport, and it will use the value as it was found in the message.
Send_External_Charges

Leave a Reply

Your email address will not be published. Required fields are marked *