Monday, 18 August 2008

Custom WCF Channel in Silverlight

Here's some code I use to create a custom channel in Silverlight. I use this to mimic the IMessageInspector behaviour available in the standard WCF stack (but alas, not in Silverlight).

Firstly you'll need a Custom BindingElement:

/// <summary>
/// Custom binding element - builds the inner channel
/// </summary>
public class ClientChannelBindingElement : BindingElement
{
#region Constructors
/// <summary>
/// Default constructor
/// </summary>
public ClientChannelBindingElement()
{
}
/// <summary>
/// Protected constructor
/// </summary>
/// <param name="other">Another binding element</param>
protected ClientChannelBindingElement(ClientChannelBindingElement other)
: base(other)
{
}
#endregion
#region Public methods
/// <summary>
/// Build a channel factory
/// </summary>
/// <typeparam name="TChannel">Channel of interest</typeparam>
/// <param name="context">Binding context</param>
/// <returns>A new channel factory</returns>
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
ClientChannelFactory<TChannel> factory = new ClientChannelFactory<TChannel>();
factory.InnerChannelFactory = context.BuildInnerChannelFactory<TChannel>();
return factory;
}
/// <summary>
/// Copy this object
/// </summary>
/// <returns>A copy of this</returns>
public override BindingElement Clone()
{
return new ClientChannelBindingElement(this);
}
/// <summary>
/// Gets an element property
/// </summary>
/// <typeparam name="T">Channel of interest</typeparam>
/// <param name="context">Binding context</param>
/// <returns>Property value</returns>
public override T GetProperty<T>(BindingContext context)
{
return context.GetInnerProperty<T>();
}
#endregion
}


Secondly, you'll need to reference this from a binding. I've made this binding look the same as a BasicHttpBinding, so that it may be used from Silverlight:

/// <summary>
/// Custom Silverlight binding (mimics BasicHttpBinding)
/// </summary>
public class ClientChannelBinding : CustomBinding
{
#region Constructor
/// <summary>
/// Default constructor
/// </summary>
public ClientChannelBinding()
{
BasicHttpBinding basic = new BasicHttpBinding();
this.Elements.Add(new ClientChannelBindingElement());
this.Elements.Add(new TextMessageEncodingBindingElement(basic.MessageVersion, basic.TextEncoding));
this.Elements.Add(new HttpTransportBindingElement());
}
#endregion
#region Public methods
/// <summary>
/// Builds the channel factory
/// </summary>
/// <typeparam name="TChannel">Channel of interest</typeparam>
/// <param name="parameters">Binding parameters</param>
/// <returns>An encapsulated channel factory</returns>
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingParameterCollection parameters)
{
ClientChannelFactory<TChannel> factory = new ClientChannelFactory<TChannel>();
BasicHttpBinding binding = new BasicHttpBinding();
factory.InnerChannelFactory = binding.BuildChannelFactory<TChannel>();
return factory;
}
#endregion
#region Internal methods
/// <summary>
/// Async Opened event handler
/// </summary>
/// <typeparam name="TChannel">Channel of interest</typeparam>
/// <param name="result">Async result</param>
internal void OnOpened<TChannel>(IAsyncResult result)
{
if (result.CompletedSynchronously)
{
return;
}
else
{
CompleteOpen<TChannel>(result);
}
}
/// <summary>
/// Async callback handler
/// </summary>
/// <typeparam name="TChannel">Channel of interest</typeparam>
/// <param name="result">Async result</param>
internal void CompleteOpen<TChannel>(IAsyncResult result)
{
IChannelFactory<TChannel> factory = (IChannelFactory<TChannel>)result.AsyncState;
factory.EndOpen(result);
}
#endregion
}


Next, you'll need a channel ...

/// <summary>
/// Silverlight client channel class
/// </summary>
public class ClientChannel : IRequestChannel
{
#region Fields
/// <summary>
/// Internal channel
/// </summary>
private IRequestChannel innerChannel;
#endregion
#region Constructors
/// <summary>
/// Channel Constructor
/// </summary>
/// <param name="innerChannel">Inner channel</param>
public ClientChannel(IRequestChannel innerChannel)
{
this.innerChannel = innerChannel;
}
#endregion
#region Events
/// <summary>
/// Close event handler
/// </summary>
public event EventHandler Closed;
/// <summary>
/// Closing event handler
/// </summary>
public event EventHandler Closing;
/// <summary>
/// Faulted event handler
/// </summary>
public event EventHandler Faulted;
/// <summary>
/// Open event handler
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Opening event handler
/// </summary>
public event EventHandler Opening;
#endregion
#region Properties
/// <summary>
/// Gets the remote address of the channel
/// </summary>
public EndpointAddress RemoteAddress
{
get
{
return this.innerChannel.RemoteAddress;
}
}
/// <summary>
/// Gets the coomunication state object
/// </summary>
public CommunicationState State
{
get
{
return this.innerChannel.State;
}
}
/// <summary>
/// Gets the URI (from inner channel)
/// </summary>
public Uri Via
{
get
{
return this.innerChannel.Via;
}
}
#endregion
#region Methods
/// <summary>
/// Async begin request
/// </summary>
/// <param name="message">Message to send</param>
/// <param name="timeout">Timeout period</param>
/// <param name="callback">Callback method</param>
/// <param name="state">State information</param>
/// <returns>Asynchronous result</returns>
public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state)
{

// In Silverlight this is the place to add the header to the outgoing request
... ADD THE HEADER HERE ...
this.Decorate(message);
IAsyncResult result = this.innerChannel.BeginRequest(message, timeout, callback, state);
return result;
}
/// <summary>
/// Async begin request
/// </summary>
/// <param name="message">Message to send</param>
/// <param name="callback">Callback method</param>
/// <param name="state">State information</param>
/// <returns>Asynchronous result</returns>
public IAsyncResult BeginRequest(Message message, AsyncCallback callback, object state)
{

... ADD THE HEADER HERE ...
this.Decorate(message);
IAsyncResult result = this.innerChannel.BeginRequest(message, callback, state);
return result;
}
/// <summary>
/// End the request - parse the reply for the security header
/// </summary>
/// <param name="result">Async results object</param>
/// <returns><c>Message</c> reply</returns>
public Message EndRequest(IAsyncResult result)
{

// In Silverlight this is the place to read the header from the incoming reply
Message reply = this.innerChannel.EndRequest(result);

... READ THE HEADER HERE ...
return reply;
}
/// <summary>
/// Synchronous request
/// </summary>
/// <param name="message">Outgoing message</param>
/// <param name="timeout">Timeout period</param>
/// <returns>Modified message</returns>
public Message Request(Message message, TimeSpan timeout)
{
this.Decorate(message);
return this.innerChannel.Request(message, timeout);
}
/// <summary>
/// Synchronous request
/// </summary>
/// <param name="message">Outgoing message</param>
/// <returns>Modified message</returns>
public Message Request(Message message)
{
this.Decorate(message);
return this.innerChannel.Request(message);
}
/// <summary>
/// Gets a channel property
/// </summary>
/// <typeparam name="T">Type of property</typeparam>
/// <returns>Property result</returns>
public T GetProperty<T>() where T : class
{
return this.innerChannel.GetProperty<T>();
}
/// <summary>
/// Abort this channel
/// </summary>
public void Abort()
{
this.innerChannel.Abort();
}
/// <summary>
/// Asynchronous close
/// </summary>
/// <param name="timeout">Timeout period</param>
/// <param name="callback">Callback method</param>
/// <param name="state">State information</param>
/// <returns>Asynchronous result</returns>
public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback callback, object state)
{
return this.innerChannel.BeginClose(timeout, callback, state);
}
/// <summary>
/// Asynchronous close
/// </summary>
/// <param name="callback">Callback method</param>
/// <param name="state">State information</param>
/// <returns>Asynchronous result</returns>
public IAsyncResult BeginClose(AsyncCallback callback, object state)
{
return this.innerChannel.BeginClose(callback, state);
}
/// <summary>
/// Asynchronous open
/// </summary>
/// <param name="timeout">Timeout period</param>
/// <param name="callback">Callback method</param>
/// <param name="state">State information</param>
/// <returns>Asynchronous result</returns>
public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
{
return this.innerChannel.BeginOpen(timeout, callback, state);
}
/// <summary>
/// Asynchronous open
/// </summary>
/// <param name="callback">Callback method</param>
/// <param name="state">State information</param>
/// <returns>Asynchronous result</returns>
public IAsyncResult BeginOpen(AsyncCallback callback, object state)
{
return this.innerChannel.BeginOpen(callback, state);
}
/// <summary>
/// Synchronous close
/// </summary>
/// <param name="timeout">Timeout period</param>
public void Close(TimeSpan timeout)
{
this.innerChannel.Close(timeout);
}
/// <summary>
/// Synchronous close
/// </summary>
public void Close()
{
this.innerChannel.Close();
}
/// <summary>
/// Asynchronous callback - close
/// </summary>
/// <param name="result">Async result</param>
public void EndClose(IAsyncResult result)
{
this.innerChannel.EndClose(result);
}
/// <summary>
/// Asynchronous callback - open
/// </summary>
/// <param name="result">Async result</param>
public void EndOpen(IAsyncResult result)
{
this.innerChannel.EndOpen(result);
}
/// <summary>
/// Synchronous open
/// </summary>
/// <param name="timeout">Timeout period</param>
public void Open(TimeSpan timeout)
{
this.innerChannel.Open(timeout);
}
/// <summary>
/// Synchronous open
/// </summary>
public void Open()
{
this.innerChannel.Open();
}
#endregion
#region Protected Methods
/// <summary>
/// Decorate the message with the new headers
/// </summary>
/// <param name="message">Message to decorate</param>
protected virtual void Decorate(Message message)
{
message.Headers.Add(new SecurityHeader());
}
#endregion
}


[Note: the SecurityHeader class is derived from the MessageHeader class]

... and an associated channel factory:


/// <summary>
/// Overriden custom channel factory
/// </summary>
/// <typeparam name="TChannel">Type of channel to create</typeparam>
public class ClientChannelFactory<TChannel> : ChannelFactoryBase<TChannel>
{
#region Fields
/// <summary>
/// Inner channel factory
/// </summary>
private IChannelFactory<TChannel> innerChannelFactory;
#endregion
#region Constructor
/// <summary>
/// Default constructor
/// </summary>
public ClientChannelFactory()
{
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the inner channel factory
/// </summary>
public IChannelFactory<TChannel> InnerChannelFactory
{
get
{
return this.innerChannelFactory;
}
set
{
this.innerChannelFactory = value;
}
}
#endregion
#region Protected methods
/// <summary>
/// Open event handler
/// </summary>
/// <param name="timeout">Timeout period</param>
protected override void OnOpen(TimeSpan timeout)
{
this.innerChannelFactory.Open(timeout);
}
/// <summary>
/// Create channel evebt handler
/// </summary>
/// <param name="to">Destination of channel</param>
/// <param name="via">Via this URI</param>
/// <returns>Newly created channel</returns>
protected override TChannel OnCreateChannel(EndpointAddress to, Uri via)
{
TChannel innerchannel = this.innerChannelFactory.CreateChannel(to, via);
if (innerchannel is IRequestChannel)
{
ClientChannel clientChannel = new ClientChannel((IRequestChannel)innerchannel);
return (TChannel)(object)clientChannel;
}
return innerchannel;
}
/// <summary>
/// End open async callback handler
/// </summary>
/// <param name="result">Async result</param>
protected override void OnEndOpen(IAsyncResult result)
{
this.innerChannelFactory.EndOpen(result);
}
/// <summary>
/// Begin open async handler
/// </summary>
/// <param name="span">Timeout span</param>
/// <param name="callback">Callback method</param>
/// <param name="asyncState">Async state object</param>
/// <returns>Async result</returns>
protected override IAsyncResult OnBeginOpen(TimeSpan span, AsyncCallback callback, object asyncState)
{
return this.innerChannelFactory.BeginOpen(callback, asyncState);
}
#endregion
}


Finally, you'll need to create the channel from your code (the client class is autogenerated when you import the service):


ClientChannelBinding binding = new ClientChannelBinding();
EndpointAddress endpoint = new EndpointAddress("http://localhost:3532/Authentication.svc"); ////TODO get this from config
this.client = new Authentication.AuthenticationClient(binding, endpoint);

Happy coding!

Graeme

3 comments:

Adrien said...

Hello !

Just a little comment to thank you for this code... It saves my life and is just awesome !! :)

Anonymous said...

Hi,

I just came accross your solution to custom SOAP headers in SL2 and I thought it would be worth mentioning that in SL2 RC0 we can now inject SOAP headers using OperationContext.Current.OutgoingMessageHeaders (the WCF way). But there is still a problem with reading the incoming SOAP headers. I posted the issue here: http://silverlight.net/forums/t/34952.aspx, hoping to get an official clarification. This post has a little code snippet for injecting a SOAP header too, which I tried it and worked.

In the meantime, as I suspect this is another shortcoming of SL2 support for enterprise applications, I'll use your fantastic code - thanks for it!

Regards,
Florin Neamtu

Unknown said...

Hi
I came across your solution when i was searching for the custom SOAP header in SilverLight 4. My need is to implement both http and Https binding. HTTP is used in development environment and HTTPs in customer environment. It should be easily shift from one configuration to another configuraton and configurable.

Please provide the sample code
Thanks
Vijay A