Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Extending WCF Part I

4.80/5 (30 votes)
7 Nov 2009CPOL5 min read 87K   2.2K  
An article on using external configuration file at WCF client
Codes

Introduction

Microsoft WCF Framework (System.ServiceModel) provides several extension points from where the developer can extend the framework when it is required. In this Part I article, I'm going to cover two perspectives of extending WCF framework:

  1. Let's 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 the server before the 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 the 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 a 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 the behavior of service, endpoint and contract.

C#
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 the WCF Framework will use our implemented IErrorHandler.ProvideFault method when any exception is thrown from the service. Inside the method implementation, I used the NetDataContractSerializer to serialize the MessageFault which will be transmitted to the client.

C#
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<detail>
            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 <soap:Detail>
            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 the client end. At IClientMessageInspector.AfterReceiveReply method implementation, NetDataContractSerializer has been used to deserialize the fault exception and get the actual exception back again.

C#
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 handling, 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 the 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 to the ExtendingWCFPartI.WcfExtentions.dll to your service host application.

XML
<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.

XML
<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.

XML
<system.serviceModel>
    <services>
      <service behaviorConfiguration="CustomServiceBehavior" 
      	name="ExtendingWCFPartI.Service.CustomerService">
        ...
        ...
        Your service endpoint's configuration goes here.
      </service>
    </services>
</system.serviceModel>

That's all you need to do to host your WCF service which will have the two functionalities explained in the 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 the following:

XML
<?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 interface ExtendingWCFPartI.WcfExtentions.WcfClientHelper to get the instance of proxy.

C#
public static T GetProxy<T>(string externalConfigPath)
{
    var channelFactory = new ExtendedChannelFactory<T>(externalConfigPath);
    channelFactory.Open();
    return channelFactory.CreateChannel();
} 

At the client code, use this interface to call your service method:

C#
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 useful. 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 they want.

History

  • 7th November, 2009: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)