Those interested in Indigo would have probably seen by now the many introductory posts on how to write services, use bindings, use output/input channels, service/message/data contracts, etc. The Indigo community is doing a fantastic job to provide examples of how to use Microsoft‘s new technology for distributed computing.
As everyone else, as soon as I got Indigo, I too started to experiment with it. My aim is to move the SSDL tool to Indigo and start developing additional plugins for protocol validation and runtime support. The message-based service lifetime feature of Indigo is really cool and a great match for the multi-message SSDL contracts. I was talking to Jim today about some new ideas on protocol evolution and protocol lifetimes. Cool stuff but perhaps the subject of a different post.
(BTW… the reason I have been relatively silent lately was because I spent some time on writing a position paper for HPTS, learn Indigo, move the StarBrowser3D to the Avalon Match CTP, concentrate on some personal changes that may be on the way, and relax a bit.)
So, what I wanted to do as my first experiment with Indigo was to write a protocol handler. Something similar to implementing IOutputFilter/IInputFilter in WSE 2.0. Little I knew that this was going to be complicated.
DISCLAIMER
: There may be a much easier and simple way of doing this which I didn’t come across. I am sure the Indigo folks will correct me and give pointers.
The idea
I want to provide a class that adds a SOAP header to all outgoing messages. I know that I can use message contracts but I want the header to be added without the knowledge of the sender. Effectively, this is the equivalent of implementing a WS protocol handler.
The help
In my endeavours, Microsoft‘s Steve Maine patiently answered all my questions. Thanks Steve. You are great!
The code
The implementation of a message processing handler requires a new BindingElement to be created.
namespace Savas.Tests.Indigo { class MyBindingElement : BindingElement, IChannelBuilder { public override ChannelProtectionRequirements GetProtectionRequirements() { return null; }
public IChannelFactory BuildChannelFactory(ChannelBuildContext context) { MyChannelFactory factory = new MyChannelFactory(); factory.InnerChannelFactory = context.BuildInnerChannelFactory();
return factory; }
public IListenerFactory BuildListenerFactory(ChannelBuildContext context) { throw new Exception("The method or operation is not implemented."); } } }
I didn’t implement the BuildListenerFactory since I haven’t created the header removal at the receiving side. It’s not important for the purposes of this discussion.
The MyBindingElement is used with a CustomBinding to introduce yet another factory for creating new channels. The channel factories of all the binding elements are layered in relation to each other. This is the reason we have to build the inner factories and link them to our new factory.
Next, we define our channel factory which is created by the binding element above.
namespace Savas.Tests.Indigo { class MyChannelFactory : ChannelFactoryBase { public override bool CanCreateChannel<TChannel>() { return (typeof(TChannel) == typeof(IOutputChannel)) || this.InnerChannelFactory.CanCreateChannel<TChannel>(); }
protected override TChannel OnCreateChannel<TChannel>(EndpointAddress to) { TChannel channel = this.InnerChannelFactory.CreateChannel<TChannel>(to); return (TChannel)(IOutputChannel)newMyChannel(this, (IOutputChannel)channel); }
public override MessageVersion MessageVersion { get { return this.InnerChannelFactory.MessageVersion; } }
public override string Scheme { get { return this.InnerChannelFactory.Scheme; } } } }
The channel factory is responsible for creating channels. As it is the case with channel factories, channels are also layered. That’s why we need to create the channel down the chain and associate it with our new one.
namespace Savas.Tests.Indigo { class MyChannel: ChannelBase, IOutputChannel { private IOutputChannel innerChannel = null;
public MyChannel(ChannelManagerBase manager, IOutputChannel channel) : base(manager) { this.innerChannel = channel; }
protected override void OnAbort() { this.innerChannel.Abort(); }
protected override void OnClose() { this.innerChannel.Close(); }
protected override void OnOpen() { this.innerChannel.Open(); }
public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state) { throw new Exception("The method or operation is not implemented."); }
public void EndSend(IAsyncResult result) { throw new Exception("The method or operation is not implemented."); }
public EndpointAddress RemoteAddress { get { return this.innerChannel.RemoteAddress; }
}
public event EventHandler RemoteAddressChanged;
public void Send(Message message) { // Add header or do other things with the message // and then give it to the next channel in the chain this.innerChannel.Send(message); } } }
I found that this point was the most important and most difficult to figure out in the entire exercise. The MyChannel class delegates all the channel-related calls to the channel down the chain. Please note that I haven’t put any checking in there to see whether the inner channel indeed exists.
The Send(Message) method is where the magic happens. There, we can process the SOAP message and then pass it down the channel chain.
That’s it. We are ready to go.
namespace Savas.Tests.Indigo { class Program { static void Main(string[] args) { Uri serviceUri = newUri("http://localhost:8080/myservice"); ServiceHost<MyService> service = newServiceHost<MyService>();
BasicProfileBinding binding = newBasicProfileBinding(); binding.SecurityMode = BasicProfileSecurityMode.None; binding.SoapVersion = EnvelopeVersion.Soap11; service.AddEndpoint(typeof(MyService), binding, serviceUri);
// Start the service service.Open();
CustomBinding customBinding = newCustomBinding(); customBinding.Elements.Add(newMyBindingElement()); customBinding.Elements.Add(newTextMessageEncodingBindingElement( MessageVersion.Soap11Addressing1, System.Text.Encoding.UTF8)); customBinding.Elements.Add(newHttpTransportBindingElement()); IClient proxy = ChannelFactory.CreateChannel<IClient>(serviceUri, customBinding);
// Send message proxy.MyOperation();
Console.WriteLine("waiting..."); Console.ReadLine(); } } }
I’ve omitted the IClient and MyService implementations since those are trivial. I used the same program to start the service and also send the message (just to make life easier). You could, of course, have two separate programs; one to send and the other one to receive the message.
Above, the service starts listening at a specific endpoint. I could have configured the service’s binding information in the config file but I couldn’t do the same for the CustomBinding. This is because I haven’t yet implemented the necessary classes to allow for a MyBindingElementSection to appear in the configuration file. Perhaps the next step.
The sender part of the program above is the interesting one. A CustomBinding is created and three binding elements are added. One of them is the MyBindingElement which will introduce our required functionality into the Indigo message processing infrastructure. The other two bindings are necessary in order to match the binding at the service.
That’s it.
Conclusion
The Indigo infrastructure is really powerful but I feel that implementing and then adding into the message processing chain intermediaries could have been easier. As I said earlier, it could be the case that I have completely missed something obvious and simpler. If that’s the case, please do let me know.
I think we are going to have lots of fun with Indigo.
See "BrainExpanded - Introduction" for context on this post. Notes and links Over the years,…
This is the first post, in what I think is going to be a series,…
Back in February, I shared the results of some initial experimentation with a digital twin.…
I am embarking on a side project that involves memory and multimodal understanding for an…
I was in Toronto, Canada. I'm on the flight back home now. The trip was…