by Mosfiqur Rahman
Senior Software Engineer @Kaz
Introduction
Microsoft WCF Framework (System.ServiceModel) provides several extension points
from where developer can extend the framework when it requires. In this Part I article
I'm going to cover two perspective of extending WCF framework:
1. Lets think about a WPF desktop application or a WPf browser application or a
windows desktop application or a .net web application, built on service oriented
architecture and all the back end services are WCF services. Now for several reasons
you may need to change the server for your service which in turn changes your service
URL or you may need to change the configuration of service bindings/behaviors. If
you change any WCF configuration at the server you need to provide the same bindings/behaviors
configuration to the clients also to work the client-service interaction properly.
And this is very difficult in typical clients like XBAP (WPF Browser application)
clients. My ExtendingWCFPartI.WcfExtentions.dll component provides a solution to
this. You can have an external configuration file (e.g. by downloading it from server
before client application starts up) where service endpoints are configured properly
and use that external configuration file to create the WCF service proxy instances.
2. In many cases we want to get exactly the same exception thrown from WCF service
at WCF client. ExtendingWCFPartI.WcfExtentions.dll component also provides this.
Background
Extending ChannelFactory<T>
This extension is to serve the first introduction item.
In general System.ServiceModel.ChannelFactory<T> provides us the option to
get a proxy instance programmatically instead of using the proxy generated through
svcutil.exe (.NET Framework tool). I've extended this class to write my own ExtendingWCFPartI.WcfExtentions.ExtendedChannelFactory<T>
class to provide the external configuration file functionality.
The constructor argument "configurationPath" should contain the full path of the
external configuration file. Then I override the ChannelFactory<T>.CreateDescription()
method to apply the external configuration file. The external configuration file
looks exactly the same as conventional client's app.config or web.config file. For
sample external configuration, you can look at the ExtendingWCFPartI.Client.WcfClientConfiguration.xml
file.
Extending WCF Service and WCF client behavior
To provide the second introduction item, we need to
extend behavior of service, endpoint and contract.
public class ExtendedServiceErrorHandler:IErrorHandler
{
#region IErrorHandler Members
bool IErrorHandler.HandleError(Exception error)
{
if (error is FaultException)
{
return false; // Let WCF do normal processing
}
else
{
return true; // Fault message is already generated
}
}
void IErrorHandler.ProvideFault(Exception error,
MessageVersion version,
ref Message fault)
{
if (error is FaultException)
{
// Let WCF do normal processing
}
else
{
// Generate fault message manually
MessageFault messageFault = MessageFault.CreateFault(
new FaultCode("Sender"),
new FaultReason(error.Message),
error,
new NetDataContractSerializer());
fault = Message.CreateMessage(version, messageFault, null);
}
}
#endregion
}// end of class
ExtendedServiceErrorHandler should be injected into
ChannelDispatcher's error handler list. Then WCF Framework will use our
implemented IErrorHandler.ProvideFault method when any exception is thrown from
service. Inside the method implementation I used the NetDataContractSerializer to
serialize the MessageFault which will be trasmitted to client.
public class ExtendedClientMessageInspector:IClientMessageInspector
{
#region Methods
/// Reads the soap message to find the
/// node. If found than deserialize it using the
/// NetDataContractSerializer to construct the exception.
private static object ReadFaultDetail(Message reply)
{
const string detailElementName = "Detail";
using (var reader = reply.GetReaderAtBodyContents())
{
// Find
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.LocalName == detailElementName)
{
break;
}
}
// Did we find it?
if (reader.NodeType != XmlNodeType.Element || reader.LocalName != detailElementName)
{
return null;
}
// Move to the contents of
if (!reader.Read())
{
return null;
}
// Deserialize the fault
var serializer = new NetDataContractSerializer();
try
{
return serializer.ReadObject(reader);
}
catch (FileNotFoundException)
{
// Serializer was unable to find assembly where exception is defined
return null;
}
}
}
#endregion
#region IClientMessageInspector Members
/// Create a copy of the original reply to allow
/// default processing of the message. Then its reads
/// the copied reply to find Fault Detail using the ReadFaultDetail method
void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
{
if (reply.IsFault)
{
// Create a copy of the original reply to allow default processing of the message
var buffer = reply.CreateBufferedCopy(Int32.MaxValue);
var copy = buffer.CreateMessage(); // Create a copy to work with
reply = buffer.CreateMessage(); // Restore the original message
var faultDetail = ReadFaultDetail(copy);
var exception = faultDetail as Exception;
if (exception != null)
{
throw exception;
}
}
}
object IClientMessageInspector.BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
return null;
}
#endregion
}// end of class
Now we need to extend the IClientMessageInspector to get back the same exception
at client end. At IClientMessageInspector.AfterReceiveReply method implementation,
NetDataContractSerializer has been used to get desrialize the fault exception and
get the actual exception back again.
public class ExtendedServiceBehavior:Attribute, IServiceBehavior, IEndpointBehavior, IContractBehavior
{
private void ApplyDispatchBehavior(ChannelDispatcher dispatcher)
{
// Don't add an error handler if it already exists
foreach (IErrorHandler errorHandler in dispatcher.ErrorHandlers)
{
if (errorHandler is ExtendedServiceErrorHandler)
{
return;
}
}
dispatcher.ErrorHandlers.Add(new ExtendedServiceErrorHandler());
}
private void ApplyClientBehavior(ClientRuntime runtime)
{
// Don't add a message inspector if it already exists
foreach (IClientMessageInspector messageInspector in runtime.MessageInspectors)
{
if (messageInspector is ExtendedClientMessageInspector)
{
return;
}
}
runtime.MessageInspectors.Add(new ExtendedClientMessageInspector());
}
}
To tell the .NET runtime to use my ExtendedServiceErrorHandler at Service end error
handling and ExtendedClientMessageInspector at client error halding we need this
ExtendedServiceBehavior class which implemented service, endpoint and contract behavior.
All behaviors have an AddBindingParameters method, an ApplyDispatchBehavior method,
a Validate method, and an ApplyClientBehavior method with one exception: Because
IServiceBehavior cannot execute in a client, it does not implement ApplyClientBehavior.
For more details you can read the
msdn documentation.
Using the Code
For simplicity, I've hosted the WCF service at console. To run the sample code and
see it working, follow the steps below:
1. After opening the "ExtendingWCFPartI" application with VS, right click on the
ExtendingWCFPartI.Service project then click Debug -- > Start new instance. Doing
so, you have ensured that the service is running at console host.
2. Then run the ExtendingWCFPartI.Client project under debug mode.
ExtendingWCFPartI.WcfExtentions.dll is the reusable component which is going to
be used at both WCF client and WCF service. Also make sure that you wrote the custom
exceptions in a common assembly which will be used at both WCF client and WCF service.
Using the code at WCF Service
At first let me show you how we can use this component to host our WCF services. You
can host the WCF services at IIS or at Windows Service or at console, no matter
where you host it, add the following section at your app.config or web.config ServiceModel
section. Add a reference the ExtendingWCFPartI.WcfExtentions.dll to your service
host application
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="ExtendedServiceBehavior" type="ExtendingWCFPartI.WcfExtentions.ServiceBehaviorExtension, ExtendingWCFPartI.WcfExtentions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
</system.serviceModel>
ExtendingWCFPartI.WcfExtentions.ServiceBehaviorExtension has been used to tell WCF
framework to use ExtendedServiceBehavior at its CreateBehavior() method.
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="CustomServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceThrottling maxConcurrentCalls="16" maxConcurrentSessions="16" />
<ExtendedServiceBehavior />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
Now create a service behavior like the example above. <ExtendedServiceBehavior
/> ensures that this behavior configuration will use the ExtendedServiceBehavior.
<system.serviceModel>
<services>
<service behaviorConfiguration="CustomServiceBehavior" name="ExtendingWCFPartI.Service.CustomerService">
...
...
Your service endpoint's configuration goes here.
</service>
</services>
</system.serviceModel>
Thats all you need to do to host your WCF service which will have the two functionalities
explained at
introduction.
Using the code at WCF Client
Add a reference to the ExtendingWCFPartI.WcfExtentions.dll at your client application.
Now create the external configuration file. It may look like following:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="ExtendedServiceEndpointBehavior" type="ExtendingWCFPartI.WcfExtentions.ServiceBehaviorExtension, ExtendingWCFPartI.WcfExtentions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
<bindings>
.. tips: provide the same binding which has been used at service
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="CustomServiceEndpointBehavior">
<ExtendedServiceEndpointBehavior />
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="http://localhost:8036/Services/CustomerService"
binding="wsHttpBinding" bindingConfiguration="CustomWSHttpBinding"
behaviorConfiguration="CustomServiceEndpointBehavior"
contract="ExtendingWCFPartI.Common.Services.ICustomerService" name="CustomerServiceEndPoint" />
</client>
</system.serviceModel>
</configuration>
Now save this external configuration file at any location of your hard drive with
.xml or .config extension. e.g. "C:\Temp\MyAppServices.xml".
I've provided a very simple inerface ExtendingWCFPartI.WcfExtentions.WcfClientHelper
to get the instance of proxy.
public static T GetProxy<T>(string externalConfigPath)
{
var channelFactory = new ExtendedChannelFactory<T>(externalConfigPath);
channelFactory.Open();
return channelFactory.CreateChannel();
}
At client code use this interface to call your service method:
var externalConfigPath = @"C:\Temp\MyAppServices.xml";
var proxy = WcfClientHelper.GetProxy<IMyWCFService>(externalConfigPath);
proxy.CallServiceMethod();
Points of Interest
There are many more extension points at WCF framework which are very interesting
and usefull. I'll try my best to cover few more examples later in my next article.
In brief, I found the WCF framework so well designed that programmers have almost
all the options to do whatever he wants.