This article has been written based on a pre-PDC 2003 build of Microsoft Windows Code Name "Longhorn". This preview is far from the beta version and I am sure it will not work for some details. I will post its update ASAP. I included in this document, implementation of the logging classes, so if you don't have the PDC bits, just look at them here.
Contents
The Indigo (part of the next generation of MS Windows code - Longhorn) is a new message driven communication model for the Service Oriented Architecture that enables secure, reliable, transactional messaging across heterogeneous communication systems. The following figure shows its logical model:
The above high abstracted communication model is based on the Software Bus that allows to connect an application code via the Service connector. The Service layer encapsulates a connectivity in the business logical model, where the consumer of the service doesn't need to know where a physical business logic is located and how it is implemented. The Indigo represents a loosely coupled message pushing communication model based on the Message Exchange Patterns. The Indigo Message flows through the Software Bus to the destination Service in direct or forwarding manner.
The Software Bus standardizes a connectivity between the Services using the SoapMessage envelope abstracted into the Message, where:
- Headers represent the communication pattern (connectivity, message exchange, etc.) to process the message on the Software Bus.
- Content represents the business data passing to the CodeBehind via a Service connector. Note that in the special cases such as WS-Eventing, the message content can be also part of the communication pattern.
The Message on the Software Bus is a stateful object with predefined lifetime and behavior at the specified Service node. Based on the type of Headers, the Message travels with or without the Content. This behavior is controlled by the Service Managers as a logical conversation process between the Service and its consumer.
The Software Bus is implemented in the Indigo model by the System.MessageBus
namespace.
The CodeBehind (business logic) is connected to the Software Bus via the Service Connector. The connector encapsulates the three major parts of the Indigo infrastructure. The following picture shows their position in the message workflow:
Port represents the in/out gateway to the service. The Message flows through the send or/and receive pipeline stages in the logical manner to process a specific task based on the message headers. Using the base PortExtension
class, the message workflow in the port pipeline can be customized for additional purposes such as logging, custom conversation, etc. Note that the Port is connected to the wire (Software Bus) via a Transport adapter to handle an underlying wire protocol for a specific format such as HTTP or TCP (or custom, of course).
Channel represents a layer to perform a communication behavior and transferring a message pattern to/from the port. There are two kinds of channels in the service connector:
- Untyped channel represents a lower level untyped message pattern to receive, send, reverse and forward messages. This type of channel is implemented as a
PortExtension
.
- Typed channel is the highest layer of the strong message exchange pattern between the service and port. The Indigo infrastructure has built-in two typed Channels such as Datagram (no guarantees of message delivering) and Dialog channels for reliable and transacted exchange messages between the services.
Service represents a top-most layer in the Indigo infrastructure encapsulating a communication model from the business logic, using the strongly typed method invoking design pattern.
The communication complexity based on the application requirements is provided in the Indigo by its Service Managers. The Managers populate the Port pipeline using built-in PortExtension
s such as DialogPortExtension
, ReplyPortExtension
and etc. This "load" process can be done administratively or programmatically based on the application needs. Note that before the port has been opened, the configuration changes are acceptable. The Indigo has predefined a ServiceEnvironment
(known as "main" - see the machine.config file) template configuration, which can be adjusted in the app.config file or programmatically in the host process file.
The Indigo is a SoapMessage driven communication model using the pushing mechanism between the nodes (Ports). From the Service point of view, the message conversation between the Services are fully transparent, and without a logging mechanism it is very difficult for troubleshooting. This article describes a Tiny Logger of the SoapMessages published on the console host process. In many cases, like the MSDN Indigo Samples, the console program is a simple host process without any additional functionality specially on the server side, so why can it not be useful for logging messages from/to the Software Bus? Let's look at how it can be implemented in the Indigo.
The Tiny Indigo Logger has the following features:
- Logging incoming messages to the Port
- Logging outgoing messages from the Port
- Publishing Messages on the Console host process
- XML text formatted output layout
- Loosely coupled plug-in using the host process (or machine) config file
- Programmatically plug-into the Port
The following screen snippet shows the objects on the opened Port captured in the Indigo BothWayServiceMessageSession sample at the Server side:
The Message Logging concept is based on injecting the publisher handler in the port pipeline. The following picture shows capturing the Messages in the receive Port before the Spy stage (first PortExtension
handler).
The incoming SoapMessage from the Transporter flows to the ConsolePublisher (alias stage of the Spy) where it is captured and dumped on the console screen and then forwarded to the original Spy's stage handler and so on. The captured message is colored making them different from the messages driven by the application. Every message has its own unique ID based on the communication service pattern, for instance, MessageId
, RelatesTo
, SequenceId
, SequenceAckowledgementId
, etc. This unique ID is bolded on the console screen - see the above picture.
The included Tiny Logger solution has a hard coded position of the capturing messages in the port workflow, but it is very easy to change for another one or add more stages based on the configuration properties.
The Tiny Logger requires to have a Console host process, and PortExtensionLib
assembly installed into the GAC or in your application bin folder.
The ConsoleLoggingPort
extension can be injected into the Port using one of the following ways:
- Tightly coupled - creating the instance of the
ConsoleLoggingPort(Port port)
or ConsoleLoggingPort(Port port ConsoleLoggingSettings settings)
before the Port is opened in your host program.
- Loosely coupled - inserting into the
<serviceEnvironment>
section of the host process' app.config file - see the following config snippet from the Indigo Sample, element <rk.logging mode="on" color="on" details="true" setconsole="true"/>
.
<configuration>
<system.messagebus>
-->
<serviceEnvironments>
<serviceEnvironment name="main">
<port>
<identityRole>
soap.tcp://localhost:46001/HelloClient/</identityRole>
</port>
-->
<remove name="securityManager" />
<policyManager>
-->
-->
<areUntrustedPolicyAttachmentsAccepted>true
</areUntrustedPolicyAttachmentsAccepted>
<isPolicyReturned>true</isPolicyReturned>
</policyManager>
<rk.logging mode="on" color="on"
details="true" setconsole="true" />
</serviceEnvironment>
</serviceEnvironments>
</system.messagebus>
</configuration>
Note that the serviceEnvironmentItem
<rk.logging />
can be referenced locally like it's shown in the above commented section, or machine wide in the machine.config:
<add name="serviceEnvironmentItems">
<add name="dialogManager"
type="System.MessageBus.Configuration.DialogManagerConfigurationHandler,
System.MessageBus,
Version=1.2.3400.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
...
<add name="rk.logging"
type="RKiss.PortExtensionLib.ConsoleLoggingPortConfigHandler,
RKiss.PortExtensionLib,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e79b308cfc5975f" />
</add>
The Console Publisher can be configured by setting its properties:
NAME |
DEFAULT VALUE |
NOTE |
mode |
"on " |
Enable/Disable Logging |
send |
"true " |
Log outgoing messages |
receive |
"true " |
Log incoming messages |
details |
"false " |
Log headers |
color |
"on " |
Color on/off |
setconsole |
"true " |
Set-up the console buffer and windows |
Here is an example:
<rk.logging mode="on" color="on" details="true" setconsole="true" />
and its result shows the following screen snippet:
I am recommending the following configuration set-up of the Tiny Logger:
- installing the
PortExtensionLib
assembly into the GAC.
- adding the
ConsoleLoggingPortConfigHandler
into the machine.config file.
- adding the
<rk.logging />
section into the host process config file.
Note that the Tiny Logger doesn't have any run time user interface. The messages are written to the console circulated buffer and they are managed based on the standard features of the Console such as Find, Select, Copy, etc.
The Tiny Logger implementation is divided into three parts - files based on their functionalities:
- ConsoleLoggingSettings.cs
Basically, this class has only one responsibility - wire up the ConsoleLoggingPort
class to the Port. Before calling the WireUp
method, the Service Manager calls the Create
method to retrieve the configurable properties packaged in the ConsoleLoggingSettings
object (its type is obtained by method ServiceComponentType
). The following code snippet shows the implementation in detail:
public class ConsoleLoggingPortConfigHandler:
IServiceEnvironmentConfigurationHandler
{
public ConsoleLoggingPortConfigHandler()
{
Trace.WriteLine("ConsoleLoggingPortConfigHandler ctor");
}
public object Create(object parent, object context, XmlNode section)
{
ConsoleLoggingSettings settings = new ConsoleLoggingSettings();
if(section == null)
return settings;
XmlNode currentAttribute;
XmlAttributeCollection nodeAttributes = section.Attributes;
currentAttribute = nodeAttributes.RemoveNamedItem("mode");
if(currentAttribute != null &&
currentAttribute.Value.ToUpper(CultureInfo.InvariantCulture)
== "OFF")
{
settings.Mode = LoggingMode.Off;
}
currentAttribute = nodeAttributes.RemoveNamedItem("send");
if(currentAttribute != null)
{
settings.Send = Convert.ToBoolean(currentAttribute.Value);
}
return settings;
}
public void WireUp(object items, ServiceEnvironment se)
{
Port port = se[typeof(Port)] as Port;
ConsoleLoggingPort clp = new
ConsoleLoggingPort(port, items as ConsoleLoggingSettings);
Trace.WriteLine("ConsoleLoggingPortConfigHandler WireUp is done.");
}
public Type ServiceComponentType
{
get
{
return typeof(ConsoleLoggingSettings);
}
}
}
- ConsoleLoggingPortEx.cs
This class has the responsibility for injecting the ConsolePublisher into the proper stage of the Port pipeline using the Indigo pipeline stage strategy and the PortExtension
base class support. The ConsoleLoggingPort
class is derived from the PortExtension
class necessary for the Indigo plumbing pattern. We need to create two stages such as stage for our Console Publisher (logging) and the next one called as alias which represents the original stage (in our logger solution - the Transmit stage respectively the Spy stage). The Indigo Port pipeline model has an open stage infrastructure so we can create more stages, for instance, the stage after the Transmit stage and etc. Notice that the pipeline stage strategy needs to have a valid handler for each known stage, otherwise the message workflow will have an undefined behavior.
public class ConsoleLoggingPort : PortExtension
{
ConsoleLoggingSettings settings;
static Stage aliasedStage;
static Stage loggingStage;
#region Stages used for send pipeline.
StageAlias[] sendAliases;
Stage[] sendStages;
IMessageHandler[] sendHandlers;
#endregion
#region Stages used for receive pipeline.
StageAlias[] receiveAliases;
Stage[] receiveStages;
IMessageHandler[] receiveHandlers;
#endregion
#region Constructor
internal ConsoleLoggingPort() : this(null) {}
public ConsoleLoggingPort(Port port) : this(port, null) {}
public ConsoleLoggingPort(Port port, ConsoleLoggingSettings settings)
{
this.settings = settings == null ?
new ConsoleLoggingSettings() : settings;
string strGuid = Guid.NewGuid().ToString();
loggingStage = new
Stage(new Uri(string.Format("uuid:{0};id=0", strGuid)));
aliasedStage = new
Stage(new Uri(string.Format("uuid:{0};id=1", strGuid)));
if(this.settings.Mode == LoggingMode.On)
{
if(this.settings.Send)
{
#region Message workflow stages for Send handlers
Stage[] aliasedSendStages = new
Stage[] { loggingStage, aliasedStage };
sendStages = new Stage[] { loggingStage };
sendAliases = new StageAlias[]
{ new StageAlias(PortSendStages.Transmit,
aliasedStage, aliasedSendStages) };
#endregion
#region attach the ConsolePublisher
sendHandlers = new IMessageHandler[]
{new ConsolePublisher("SEND", this.settings)};
#endregion
}
if(this.settings.Receive)
{
#region Message workflow stages for Receive handlers
Stage[] aliasedReceiveStages =
new Stage[] { loggingStage, aliasedStage };
receiveStages = new Stage[] { loggingStage };
this.receiveAliases =
new StageAlias[] {new StageAlias(PortReceiveStages.Spy,
aliasedStage, aliasedReceiveStages) };
#endregion
#region attach the ConsolePublisher
receiveHandlers = new IMessageHandler[]
{ new ConsolePublisher("RECEIVE",
this.settings) };
#endregion
}
#region remove and add our extension from the port pipeline
if(port.Extensions.Contains(this))
port.Extensions.Remove(this);
else
port.Extensions.Add(this);
#endregion
}
}
#endregion
#region PortExtension - overrides
public override IMessageHandler[]
CreateReceiveHandlers() { return receiveHandlers; }
public override IMessageHandler[]
CreateSendHandlers() { return sendHandlers; }
public override StageAlias[]
GetSendAliases() { return sendAliases; }
public override Stage[]
GetSendStages() { return sendStages; }
public override StageAlias[]
GetReceiveAliases() { return receiveAliases; }
public override Stage[]
GetReceiveStages() { return receiveStages; }
public override void OnOpening()
{
#region Logo
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("********************" +
"****************************************");
Console.WriteLine(" Tiny Console Logger Version" +
" 0.9, created by Roman Kiss ");
Console.WriteLine("********************" +
"****************************************");
Console.ResetColor();
#endregion
#region Setup Screen
if(settings.SetConsole)
{
Console.SetBufferSize(300, 5000);
Console.SetWindowSize(75, 50);
}
#endregion
#region dump all port collections
Console.ForegroundColor = settings.Color == ColorMode.On ?
ConsoleColor.Cyan : Console.ForegroundColor;
Console.WriteLine(">>> The Port has been opened");
Console.ForegroundColor = settings.Color == ColorMode.On ?
ConsoleColor.DarkCyan : Console.ForegroundColor;
Console.WriteLine("PortExtensions:");
foreach(object obj in this.Port.Extensions)
Console.WriteLine(" " + obj.ToString());
Console.WriteLine("PortFormatters:");
foreach(object obj in this.Port.Formatters)
Console.WriteLine(" " + obj.ToString());
Console.WriteLine("PortHeaderTypes:");
foreach(object obj in this.Port.HeaderTypes)
Console.WriteLine(" " + obj.ToString());
Console.WriteLine("PortTransportAddresses:");
foreach(object obj in this.Port.TransportAddresses)
Console.WriteLine(" " + obj.ToString());
Console.WriteLine("PortTransports:");
foreach(object obj in this.Port.Transports)
Console.WriteLine(" " + obj.ToString());
Console.WriteLine("==========================" +
"================================");
Console.WriteLine("");
Console.ResetColor();
#endregion
}
public override void OnClosed()
{
Console.WriteLine("The Port has been closed");
}
#endregion
}
- ConsolePublisher.cs
This endpoint class is our Message processing handler. It must be derived from the SyncMessageHandler
base class to be part of the pipeline message workflow. Its implementation is straightforward to write the Message envelope (such as Headers and Body) on the Console screen using the XML text formatted layout.
#region Console Publisher
class ConsolePublisher : SyncMessageHandler
{
#region Private Members
private string prompt;
#endregion
#region Constructor
ConsoleLoggingSettings settings;
public ConsolePublisher(string prompt,
ConsoleLoggingSettings settings) : base()
{
this.prompt = prompt;
this.settings = settings == null ?
new ConsoleLoggingSettings() : settings;
}
#endregion
#region ProcessMessage
public override bool ProcessMessage(Message msg)
{
#region validation
if(msg == null)
throw new ArgumentNullException("message");
if(msg.Encoding == null)
throw new ArgumentException("message");
#endregion
lock(this)
{
#region Capture message
Message message = msg.Clone();
try
{
#region Prompt Line
MessageIdHeader mih =
message.Headers[typeof(MessageIdHeader)]
as MessageIdHeader;
RelatesToHeader rth =
message.Headers[typeof(RelatesToHeader)]
as RelatesToHeader;
MessageHeader WsrmSeq = message.Headers["Sequence",
@"http://schemas.xmlsoap.org/ws/2003/02/RM"]
as MessageHeader;
MessageHeader WsrmSeqAck =
message.Headers["SequenceAcknowledgement",
@"http://schemas.xmlsoap.org/ws/2003/02/RM"]
as MessageHeader;
string strId = "Unknown Message Id";
if(mih != null)
{
strId = mih.MessageId.ToString();
}
else if(rth != null)
{
strId = string.Concat("RelatesTo: "
+ rth.RelatesTo.ToString());
}
else if(WsrmSeq != null)
{
strId = string.Concat("Sequence: ",
WsrmSeq.Element.InnerText.Substring(
WsrmSeq.Element.InnerText.LastIndexOf('/') + 1));
}
else if(WsrmSeqAck != null)
{
strId = string.Concat("SequenceACK: ",
WsrmSeqAck.Element.InnerText.Substring(
WsrmSeqAck.Element.InnerText.LastIndexOf('/') + 1));
}
Console.WriteLine("");
Console.ForegroundColor = settings.Color == ColorMode.On ?
ConsoleColor.Yellow : Console.ForegroundColor;
Console.WriteLine(string.Format(">>> {0} {1}",
prompt, strId));
Console.ForegroundColor = settings.Color == ColorMode.On ?
ConsoleColor.DarkYellow : Console.ForegroundColor;
#endregion
#region Message headers
foreach(MessageHeader mh in message.Headers)
{
try
{
if(settings.Details)
{
XmlNode node = mh.Element;
if(node != null)
Console.WriteLine(OutputXmlLayout(node.OuterXml));
}
else
Console.WriteLine(mh.Name + ": " + mh.StringValue);
}
catch(Exception ex)
{
Console.WriteLine(string.Format("[{0}={1}],
{2}", mh.Name, mh.StringValue, ex.Message));
}
}
#endregion
#region Message Content
Console.WriteLine("-----------" +
"--<Message.Content>---------------------------");
XmlReader reader = message.Content.Reader;
XmlDocument document = new XmlDocument(reader.NameTable);
while(reader.MoveToContent() == XmlNodeType.Element)
{
try
{
XmlNode node = document.ReadNode(reader);
if(node != null)
Console.WriteLine(OutputXmlLayout(node.OuterXml));
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Console.WriteLine("===========" +
"==============================================");
Console.WriteLine("");
#endregion
}
catch(Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Logging Internal Error: {0}",
ex.Message);
}
finally
{
Console.ResetColor();
message.Close();
}
#endregion
}
return true;
}
#endregion
#region Helpers
private string OutputXmlLayout(string strSource)
{
StringBuilder sb = new StringBuilder();
using(StringWriter sw = new StringWriter(sb))
{
XmlTextWriter tw = new XmlTextWriter(sw);
tw.Indentation = 3;
tw.Formatting = Formatting.Indented;
XmlDocument doc = new XmlDocument();
doc.LoadXml(strSource);
doc.Save(tw);
tw.Flush();
tw.Close();
}
return sb.ToString();
}
#endregion
}
#endregion
In this article, I showed you how can Indigo Port pipeline be extended for the custom stage. It is based on the PortExtension sample from the MSDN Indigo Samples. Usually, for test purpose, often used is the Console process (see MSDN Indigo Samples) hosting your services and/or remotable objects. Simply plug-in the Tiny Logger to the host process configuration, you can see the Indigo conversation pattern between the service and its consumer on the Console screen. I hope you will enjoy it.