Thursday, December 17, 2009

WCF Behavior To Remove Whitespace

After finding a bug in the way BizTalk executes body xpath expressions in WCF ports (details here) I came up with a custom behavior to remove the leading and trailing whitespace in an inbound xml message.

First the WCF behavior needs to be implemented, it looks like:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.ServiceModel.Description;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using My.Helpers.Xml;

namespace My.Helpers.WcfBehaviors
{
public class NormalizeMessageBehaviorExtension : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(NormalizeMessageEndpointBehavior); }
}

protected override object CreateBehavior()
{
return new NormalizeMessageEndpointBehavior();
}
}

public class NormalizeMessageEndpointBehavior : IEndpointBehavior
{
#region IEndpointBehavior Members

public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{ }

public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{ }

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(
new NormalizeMessageInspector());
}

public void Validate(ServiceEndpoint endpoint)
{ }

#endregion
}

public class NormalizeMessageInspector : IDispatchMessageInspector
{

#region IDispatchMessageInspector Members

public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
request
= NormalizeMessage(request);
return null;
}

public Message NormalizeMessage(Message inboundMessage)
{
Message normalizedMessage
= null;
MessageBuffer buffer
= inboundMessage.CreateBufferedCopy(Int32.MaxValue);
Message tempMessage
= buffer.CreateMessage();

using (var inputStream = new MemoryStream())
{
using (var writer = XmlDictionaryWriter.CreateTextWriter(inputStream, Encoding.UTF8))
{
tempMessage.WriteBodyContents(writer);
writer.Flush();

inputStream.Position
= 0;

using (MemoryStream outputStream = new MemoryStream())
{
XmlParser.Normalize(inputStream, outputStream);

using (var reader = XmlReader.Create(outputStream))
{
normalizedMessage
= Message.CreateMessage(inboundMessage.Version, null, reader);
normalizedMessage.Headers.CopyHeadersFrom(inboundMessage);
normalizedMessage.Properties.CopyProperties(inboundMessage.Properties);
buffer
= normalizedMessage.CreateBufferedCopy(Int32.MaxValue);
}
}
}
}

return buffer.CreateMessage();
}


public void BeforeSendReply(ref Message reply, object correlationState)
{ }

#endregion
}
}


Then the code for the normalize looks like this:



using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using System.Xml;
using My.Helpers.Utility;

namespace My.Helpers.Xml
{
[Serializable]
public class XmlParser
{

public static void Normalize(Stream inputStream, Stream outputStream)
{
try
{
XmlReaderSettings readerSettings
= new XmlReaderSettings() { ValidationType = ValidationType.None };

StreamReader tempReader
= new StreamReader(inputStream);
Encoding encoding
= tempReader.CurrentEncoding ?? Encoding.UTF8;

XmlReader reader
= XmlReader.Create(inputStream, readerSettings);

XmlWriterSettings writerSettings
= new XmlWriterSettings() { ConformanceLevel = ConformanceLevel.Auto, Encoding = encoding };
XmlWriter writer
= XmlWriter.Create(outputStream, writerSettings);

while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
if (reader.IsEmptyElement)
writer.WriteEndElement();
else
{
while (reader.MoveToNextAttribute())
{
if (reader.Value.Length > 0)
{
writer.WriteStartAttribute(reader.Prefix, reader.LocalName, reader.NamespaceURI);
writer.WriteString(NormalizeString(reader.Value));
writer.WriteEndAttribute();
}
else
{
writer.WriteStartAttribute(reader.Prefix, reader.LocalName, reader.NamespaceURI);
writer.WriteString(reader.Value);
writer.WriteEndAttribute();
}
}
}
break;
case XmlNodeType.Text:
string text = reader.Value;
if (text.Length > 0)
{
writer.WriteString(NormalizeString(reader.Value));
}
else
writer.WriteString(text);
break;

case XmlNodeType.CDATA:
writer.WriteCData(reader.Value);
break;
case XmlNodeType.EntityReference:
writer.WriteEntityRef(reader.Name);
break;
case XmlNodeType.XmlDeclaration:
case XmlNodeType.ProcessingInstruction:
writer.WriteProcessingInstruction(reader.Name, reader.Value);
break;
case XmlNodeType.Comment:
writer.WriteComment(reader.Value);
break;
case XmlNodeType.DocumentType:
writer.WriteDocType(
reader.Name,
reader.GetAttribute(Constants.XmlAttribute.Public),
reader.GetAttribute(Constants.XmlAttribute.System),
reader.Value);
break;
case XmlNodeType.Whitespace:
case XmlNodeType.SignificantWhitespace:
writer.WriteWhitespace(reader.Value);
break;
case XmlNodeType.EndElement:
writer.WriteFullEndElement();
break;
}
}

writer.Flush();
outputStream.Position
= 0;

}
catch (Exception ex)
{
throw ex;
}
}

#region Normalize Helper Methods

private static string NormalizeString(string input)
{
StringBuilder sb
= new StringBuilder();
string[] parts = input.Split(
new char[] {
Constants.Char.Space,
Constants.Char.NewLine,
Constants.Char.Tab,
Constants.Char.CarriageReturn,
Constants.Char.FormFeed,
Constants.Char.VerticalTab
},
StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < parts.Length; i++)
sb.AppendFormat(Constants.GeneralFormat.AppendSpace, parts[i]);
return sb.ToString().Trim();
}

#endregion

}

internal class Constants
{
internal class Char
{
internal const char CarriageReturn = '\r';
internal const char Comma = ',';
internal const char FormFeed = '\f';
internal const char NewLine = '\n';
internal const char Pipe = '|';
internal const char Space = ' ';
internal const char Tab = '\t';
internal const char VerticalTab = '\v';
}

internal class ContextProperty
{
internal const string SchemaStrongName = "SchemaStrongName";
}

internal class ErrorMessageFormat
{
internal const string LeadingWhiteSpaceInString = "Leading whitespace in string field {0}.";
internal const string TrailingWhiteSpaceInString = "Trailing whitespace in string field {0}.";
internal const string ConsecutiveSpacesInString = "Consecutive spaces in string field {0}.";
internal const string CRLFOrTabInString = "Carriage return, line feed or tab in string field {0}.";
}

internal class GeneralFormat
{
internal const string AppendSpace = "{0} ";
}

internal class GuidString
{
internal const string StringNormalizerClassId = "233553fa-b648-40ad-af26-d749d06ce443";
}

internal class PropertyName
{
internal const string ErrorOnNonNormalString = "ErrorOnNonNormalString";
internal const string NormalizeStrings = "NormalizeStrings";
}

internal class ResourceNames
{
internal const string StringNormalizer = "My.BizTalk.PipelineComponents.StringNormalizer";
}

internal class ResourceStrings
{
internal const string ComponentDescription = "COMPONENTDESCRIPTION";
internal const string ComponentIcon = "COMPONENTICON";
internal const string ComponentName = "COMPONENTNAME";
internal const string ComponentVersion = "COMPONENTVERSION";
}

internal class XmlAttribute
{
internal const string Public = "PUBLIC";
internal const string System = "SYSTEM";
}

internal class XmlNamespace
{
internal const string BizTalkSystemProperties = "http://schemas.microsoft.com/BizTalk/2003/system-properties";
}
}
}


To use the behavior in BizTalk just add the assembly to the GAC, and register it in the machine.config file with the rest of the behaviors that ship with .net. Then add the behavior in your WCF port configuration, and the issues with whitespace are solved!

40 comments:

  1. 目標是什麼不重要,目標能產生什麼樣的效果才重要..............................

    ReplyDelete
  2. Unable to give you a heart. so have a reply to push up your post. ........................................

    ReplyDelete
  3. 若對自己誠實,日積月累,就無法對別人不忠了。........................................

    ReplyDelete
  4. 向著星球長驅直進的人,反比踟躕在峽路上的人,更容易達到目的。............................................................

    ReplyDelete
  5. 成功多屬於那些很快做出決定,卻又不輕易變更的人。而失敗也經常屬於那些很難做出決定,卻又經常變更的人.................................................................

    ReplyDelete
  6. 旁觀自己的悲傷是解脫,主觀自己的悲傷是更加悲傷................................................

    ReplyDelete
  7. 教育的目的,不在應該思考什麼,而是教吾人怎樣思考............................................................

    ReplyDelete
  8. 人應該做自己認為對的事,而不是一味跟著群眾的建議走。..................................................

    ReplyDelete
  9. 一棵樹除非在春天開了花,否則難望在秋天結果。..................................................

    ReplyDelete
  10. 生存乃是不斷地在內心與靈魂交戰;寫作是坐著審判自己。..................................................

    ReplyDelete
  11. 真正仁慈的人,會忘記他們做過的善行,他們全心投入現在的工作,過去的事已被遺忘。.................................................

    ReplyDelete
  12. 人類的聰明,並非以經驗為依歸,而是以接受經驗的行程為依歸。..................................................

    ReplyDelete