Introduction
WCF provides a flexible and extensible architecture for the developer. The most common situation is to customize the extension of behavior. It is not complex, but some issues should be noticed. This article discusses how to extend the behavior in WCF.
On the service side, if we want to extend the behavior, we need to extend the DispatchRuntime
and DispatchOperation
. The points of extension include inspecting parameters, messages and invoker of operations. The corresponding interfaces are IParameterInspector
(to inspect parameters), IDispatchMessageInspector
(to inspect messages) and IOperationInvoker
(to invoke the operation). On the client side, we should extend the ClientRuntime
and ClientOperation
, and the points of extension include inspecting parameters and messages. The corresponding interfaces are IParameterInspector
and IClientMessageInspector
. All interfaces are placed in System.ServiceModel.Dispatcher
namespace. Please note that IParameterInspector can be applied to both service side and client side.
It seems like implementation of AOP (Aspect Oriented Programming) to implement these interfaces. We can inject some additional logic before and after invoking the related methods, so we call these extensions “Listener”. For example, there are some methods in IParameterInspector
interface as shown below:
void AfterCall(string operationName, object[] outputs, object returnValue,
object correlationState);
object BeforeCall(string operationName, object[] inputs);
BeforeCall()
method will be invoked before we invoke the target method of the service object, and AfterCall()
method will occur after the target method is invoked. For instance, we can validate if the value of parameter is less than zero before the method is invoked. If yes, it will throw an exception:
public class CalculatorParameterInspector : IParameterInspector
{
public void BeforeCall(string operationName, object[] inputs)
{
int x = inputs[0] as int;
int y = inputs[1] as int;
if (x < 0 || y < 0)
{
throw new FaultException("The number can not be less than zero.");
}
return null;
}
public void AfterCall(string operationName, object[] outputs, object returnValue,
object correlationState)
{
}
}
It distinguishes between the service and client side to inspect the parameter, and the methods of interface are quite converse to the order of messaging (Note: IDispatchMessageInspector
interface includes BeforeSendReply()
and AfterReceiveRequest();
and IClientMessageInspector
interface includes BeforeSendRequest()
and AfterReceiveReply()
). We might handle the message through by the methods of this interface, for example, printing the message header:
public class PrintMessageInterceptor : IDispatchMessageInspector
{
#region IDispatchMessageInspector Members
public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request,
IClientChannel channel, InstanceContext instanceContext)
{
MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
request = buffer.CreateMessage();
Console.WriteLine("After Receive Request:");
foreach (MessageHeader header in request.Headers)
{
Console.WriteLine(header);
}
Console.WriteLine(new string(‘*’, 20));
return null;
}
public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply,
object correlationState)
{
MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
reply = buffer.CreateMessage();
Console.WriteLine("Before Send Request:");
foreach (MessageHeader header in reply.Headers)
{
Console.WriteLine(header);
}
Console.WriteLine(new string(‘*’, 20));
}
#endregion
}
There are four different kinds of behaviors including Service Behavior, Endpoint Behavior, Contract Behavior and Operation Behavior. Their corresponding interfaces are IServiceBehavior
, IEndpointBehavior
, IContractBehavior
and IOperationBehavior
. Although they are different interfaces by nature, their methods are almost similar including: AddBindingParameters()
, ApplyClientBehavior()
and ApplyDispatchBehavior()
.
Note: Because IServiceBehavior
is only used on the service side, it has no ApplyClientBehavior()
method.
We can customize our class to implement these interfaces, but some key elements should be underlined:
- The scope of the behavior. Table 1 describes all situations:
Behavior Type
| Interface
| Scope
|
| | Service
| Endpoint
| Contract
| Operation
|
Service
| IServiceBehavior
| Y
| Y
| Y
| Y
|
Endpoint
| IEndpointBehavior
|
| Y
| Y
| Y
|
Contract
| IContractBehavior
|
|
| Y
| Y
|
Operation
| IOperationBehavior
|
|
|
| Y
|
- We can add the extension of service behavior, contract behavior and operation behavior by applying custom attribute, but cannot add the extension of endpoint behavior in this way. We can add the extension of service behavior and endpoint behavior by using config file, but cannot add the extension of contract behavior and operation behavior in this way. All behaviors can be added by
ServiceDescription
.
To add the extended behavior by applying custom attribute, we can let the custom behavior be derived from Attribute
class. Then we can apply it on service, contract or operation:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)]
public class MyServiceBehavior:Attribute, IServiceBehavior
{}
[MyServiceBehavior]
public interface IService
{ }
If you want to add the extended behavior by using config file, you must define a class derived from BehaviorExtensionElement
(it belongs to System.ServiceModel.Configuration
namespace) class, then override the BehaviorType
property and CreateBehavior()
method. BehaviorType
property returns the type of extended behavior, and CreateBehavior()
is responsible for creating the instance of the extended behavior:
public class MyBehaviorExtensionElement : BehaviorExtensionElement
{
public MyBehaviorExtensionElement() { }
public override Type BehaviorType
{
get { return typeof(MyServiceBehavior); }
}
protected override object CreateBehavior()
{
return new MyServiceBehavior();
}
}
If the element which should be configured adds the new property, we must apply the ConfigurationPropertyAttribute
on this new one:
[ConfigurationProperty("providerName", IsRequired = true)]
public virtual string ProviderName
{
get
{
return this["ProviderName"] as string;
}
set
{
this["ProviderName"] = value;
}
}
The details of config file are like this:
<configuration>
<system.serviceModel>
<services>
<service name="MessageInspectorDemo.Calculator">
<endpoint behaviorConfiguration="messageInspectorBehavior"
address="http://localhost:801/Calculator"
binding="basicHttpBinding"
contract="MessageInspectorDemo.ICalculator"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="messageInspectorBehavior">
<myBehaviorExtensionElement providerName="Test"/>
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="myBehaviorExtensionElement"
type="MessageInspectorDemo.MyBehaviorExtensionElement,
MessageInspectorDemo,
Version=1.0.0.0,
Culture=neutral,
PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
</system.serviceModel>
</configuration>
Please note the contents that are in bold. <myBehaviorExtensionElement>
is our extended behavior, and providerName
is the new property of MyBehaviorExtensionElement
. If you extended IEndpointBehavior
, <serviceBehaviors>
section should be replaced with <endpointBehaviors>
. The extensions of custom behaviors will be placed in the <extensions></extensions>
section. The value of name
attribute must match the configuration of <behavior>
section, both are “myBehaviorExtensionElement
”.
The value of type inside the <behaviorExtensions>
section you want to add must be the full name of type. The first part of the full name is the full type name, and the second part of the name is the namespace. Version
, Culture
and PublicKeyToken
are also indispensable elements. The string
of type name uses the comma as a splitter. After the comma, it must leave a space, otherwise we cannot add the configuration of extended behavior normally. Why does it give the awful constraint here? Because the value is prepared for reflect technology. I agree that it is a defect. I hope Microsoft will solve this problem in the next release of WCF.
In the body of related methods, we need to add the extensions of checking parameters, messages and operation invoker. The relationship between the extensions of them exist here. For checking parameters, the logic of extensions might be added in ApplyClientBehavior()
and ApplyDispatchBehavior()
of IOperationBehavior
interface. For example, we can define a CalculatorParameterValidation
class for CalculatorParameterInspector
:
public class CalculatorParameterValidation : Attribute, IOperationBehavior
{
#region IOperationBehavior Members
public void AddBindingParameters(OperationDescription operationDescription,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(OperationDescription operationDescription,
ClientOperation clientOperation)
{
CalculatorParameterInspector inspector = new CalculatorParameterInspector();
clientOperation.ParameterInspectors.Add(inspector);
}
public void ApplyDispatchBehavior(OperationDescription operationDescription,
DispatchOperation dispatchOperation)
{
CalculatorParameterInspector inspector = new CalculatorParameterInspector();
dispatchOperation.ParameterInspectors.Add(inspector);
}
public void Validate(OperationDescription operationDescription)
{
}
#endregion
}
If it is not necessary to separate the inspector from the extended behavior, a better solution is to let a custom class implement both IParameterInspector
and IOperationBehavior
. For example:
public class CalculatorParameterValidation : Attribute, IParameterInspector,
IOperationBehavior
{
#region IParameterInspector Members
public void BeforeCall(string operationName, object[] inputs)
{
int x = inputs[0] as int;
int y = inputs[1] as int;
if (x < 0 || y < 0)
{
throw new FaultException("The number can not be less than zero.");
}
return null;
}
public void AfterCall(string operationName, object[] outputs, object returnValue,
object correlationState)
{
}
#endregion
#region IOperationBehavior Members
public void AddBindingParameters(OperationDescription operationDescription,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(OperationDescription operationDescription,
ClientOperation clientOperation)
{
CalculatorParameterInspector inspector = new CalculatorParameterInspector();
clientOperation.ParameterInspectors.Add(this);
}
public void ApplyDispatchBehavior(OperationDescription operationDescription,
DispatchOperation dispatchOperation)
{
CalculatorParameterInspector inspector = new CalculatorParameterInspector();
dispatchOperation.ParameterInspectors.Add(this);
}
public void Validate(OperationDescription operationDescription)
{
}
#endregion
}
While operation invoker is associated with IOperationBehavior
, in fact, it will do with Invoker
property of DispatchOperation
. Assume that we have defined a MyOperationInvoker
class which implements the IOperationInvoker
interface, the solution is:
public class MyOperationInvoker : IOperationInvoker
{
}
public class MyOperationInvokerBehavior : Attribute, IOperationBehavior
{
#region IOperationBehavior Members
public void AddBindingParameters(OperationDescription operationDescription,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(OperationDescription operationDescription,
ClientOperation clientOperation)
{
}
public void ApplyDispatchBehavior(OperationDescription operationDescription,
DispatchOperation dispatchOperation)
{
dispatchOperation.Invoker = new MyOperationInvoker(dispatchOperation.Invoker);
}
public void Validate(OperationDescription operationDescription)
{
}
#endregion
}
As far as message inspecting with Dispatch
are concerned, we can add it using MessageInspectors
property in DispatchRuntime
owned by IServiceBehavior
, or IEndpointBehavior
, or IContractBehavior
. For message inspecting with Client
, we can add in using MessageInspectors
property in ClientRuntime
owned by IEndpointBehavior
or IContractBehavior
(IServiceBehavior
cannot be used on the client side, so it’s not IServiceBehavior
’s business). For example:
public class PrintMessageInspectorBehavior : IDispatchMessageInspector,
IEndpointBehavior
{
#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior
(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(this);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
public void Validate(ServiceEndpoint endpoint)
{
}
#endregion
}
If our behavior implements the IServiceBehavior
, we must iterate the ServiceHostBase
object in the ApplyDispatchBehavior()
method:
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
}
}
History
- 10th June, 2009: Initial post
- 18 Sep, 2014