Just as you want to aspect out the functionality around the client calling a service, so you may also want to aspect out the functionality around a service receiving a call from a client. WCF already has something built in for this: the OperationInvoker. Although the OperationInvoker is an Operation level construct I wanted to be able to take it a step further and be able to turn these aspects on or off via the config file. Furthermore I wanted to be able to apply these behaviors at either the endpoint or the service level depending on what I was debugging. Specifically when I encountered an issue in the service not behaving correctly I wanted to be able to turn on logging or possibly performance counters to see what the problem was.
- As I have mentioned before: the three steps to WCF extensibility are here:
- Implement the interface you are trying to plug in (in my case IOperationInvoker)
- Author a behavior to replace the WCF functionality with your functionality written in step 1
- (optional) If you are plugging into the config file you will need to author a class that describes your config element
- (optional) If you are applying the behaviors programmatically consider creating a custom service host that automatically applies your behaviors
OK, so on to step 1:
class LoggingOperationInvoker : IOperationInvoker { private static readonly ILog log = LogManager.GetLogger(typeof(LoggingOperationInvoker)); readonly IOperationInvoker innerOperationInvoker; private readonly string methodName; private readonly bool writeInput; private readonly bool writeOutput; private readonly bool ignored; public LoggingOperationInvoker(IOperationInvoker innerOperationInvoker, string methodName, bool writeInput, bool writeOutput, bool ignored) { this.innerOperationInvoker = innerOperationInvoker; this.methodName = methodName; this.writeInput = writeInput; this.writeOutput = writeOutput; this.ignored = ignored; } public object[] AllocateInputs() { return innerOperationInvoker.AllocateInputs(); } public object Invoke(object instance, object[] inputs, out object[] outputs) { if (!ignored && writeInput) LogUtils.LogMethodCall(log, methodName, inputs); // Invoke the operation using the inner operation invoker. object result; try { result = innerOperationInvoker.Invoke(instance, inputs, out outputs); } catch (Exception ex) { if (!ignored && writeOutput) LogUtils.LogMethodThrow(log, methodName, ex); throw; } if (!ignored && writeOutput) LogUtils.LogMethodReturn(log, methodName, result); return result; } public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state) { if (!ignored && writeInput) LogUtils.LogMethodCall(log, methodName, inputs); return innerOperationInvoker.InvokeBegin(instance, inputs, callback, state); } public object InvokeEnd(object instance, out object[] outputs, IAsyncResult asyncResult) { // Finish invoking the operation using the inner operation invoker. object result; try { result = innerOperationInvoker.InvokeEnd(instance, out outputs, asyncResult); } catch (Exception ex) { if (!ignored && writeOutput) LogUtils.LogMethodThrow(log, methodName, ex); throw; } if (!ignored && writeOutput) LogUtils.LogMethodReturn(log, methodName, result); return result; } public bool IsSynchronous { get { return innerOperationInvoker.IsSynchronous; } } }Notice that there are lots of variables in the constructor which let this particular operation invoker know what to log. We will talk about how all of this gets set in just a minute.
In order to get that plugged in to WCF we need to to implement a behavior (here an operation behavior).
public class LoggingOperationBehavior : Attribute, IOperationBehavior { private readonly bool writeInput; private readonly bool writeOutput; private readonly ICollection<string> ignoredMethodNames; public LoggingOperationBehavior(bool writeInput, bool writeOutput, ICollection<string> ignoredMethodNames) { this.writeInput = writeInput; this.writeOutput = writeOutput; this.ignoredMethodNames = ignoredMethodNames; } public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { } public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) { bool ignored = ignoredMethodNames.Contains(dispatchOperation.Name); dispatchOperation.Invoker = new LoggingOperationInvoker(dispatchOperation.Invoker, dispatchOperation.Name, writeInput, writeOutput, ignored); } public void Validate(OperationDescription operationDescription) { } }This step may be the easiest of the lot. We simply pass along the things we are constructed with and set the invoker.
On to step 3. I want to be able to plug this in to the config file at both the endpoint behavior and the service behavior level. Let’s start at the endpoint level. In order to plug into the config file at the endpoint level we need a endpoint behavior extension element. But the extension element is responsible for creating a behavior, and we don’t yet have an endpoint behavior. So we will have to create both, ugh!
public class LoggingEndpointBehavior : IEndpointBehavior { private readonly bool writeInput; private readonly bool writeOutput; private readonly ICollection<string> ignoredMethodNames; public LoggingEndpointBehavior(bool writeInput, bool writeOutput, ICollection<string> ignoredMethodNames) { this.writeInput = writeInput; this.writeOutput = writeOutput; this.ignoredMethodNames = ignoredMethodNames; } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { foreach (DispatchOperation dispatchOp in endpointDispatcher.DispatchRuntime.Operations) { bool ignored = ignoredMethodNames.Contains(dispatchOp.Name); dispatchOp.Invoker = new LoggingOperationInvoker(dispatchOp.Invoker, dispatchOp.Name, writeInput, writeOutput, ignored); } } public void Validate(ServiceEndpoint endpoint) { } } public class LoggingEndpointBehaviorExtensionElement : BehaviorExtensionElement { private const string WriteInputPropertyName = "writeInput"; private const string WriteOutputPropertyName = "writeOutput"; private const string IgnoredMethodNamesPropertyName = "ignoredMethodNames"; public override Type BehaviorType { get { return typeof(LoggingEndpointBehavior); } } protected override object CreateBehavior() { var listIgnoredMethods = (ICollection<string>)IgnoredMethodNames.Split(','); return new LoggingEndpointBehavior(WriteInput, WriteOutput, listIgnoredMethods); } [ConfigurationProperty(WriteInputPropertyName, DefaultValue = true)] public bool WriteInput { get { return (bool)base[WriteInputPropertyName]; } set { base[WriteInputPropertyName] = value; } } [ConfigurationProperty(WriteOutputPropertyName, DefaultValue = true)] public bool WriteOutput { get { return (bool)base[WriteOutputPropertyName]; } set { base[WriteOutputPropertyName] = value; } } [ConfigurationProperty(IgnoredMethodNamesPropertyName, DefaultValue = "")] public string IgnoredMethodNames { get { return (string)base[IgnoredMethodNamesPropertyName]; } set { base[IgnoredMethodNamesPropertyName] = value; } } }You can see that the EndpointBehavior is basically the same as the OperationBehavior, but because we start at a higher level (the endpoint) we have to dig a little deeper to get to the individual operations. The LoggingEndpointBehaviorExtensionElement allows the WriteInput, WriteOutput and IgrnoredMethods names be set correctly in the config file.
I will leave it as an exercise to the reader to implement the service behavior, but it is almost exactly like the other two.
The last thing that I want to do is show you how to turn these things on and off from the config file. Here is an excerpt from the system.serviceModel section
<extensions> <behaviorExtensions> <add name="logEndpointCalls" type="Interface.LoggingEndpointBehaviorExtensionElement, Interface, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/> </behaviorExtensions> </extensions> <services> <service name="Service.MyService"> <endpoint address="" behaviorConfiguration="log" binding="netTcpBinding" contract="Interface.IService" /> </service> </services> <behaviors> <endpointBehaviors> <behavior name="log"> <logEndpointCalls /> </behavior> </endpointBehaviors> </behaviors>That’s it, happy WCFing…