WCF Extensibility - IOperationBehavior
This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .
The last of the behavior interfaces in WCF is the IOperationBehavior. The scope of this one is limited to an individual operation, so one can have different operations in the same contract. For example, the OperationBehaviorAttribute can define that some specific operations in the contract will auto-complete transactions (if the operation returns successfully, i.e., does not throw an exception). Operation behaviors are typically used for those scenarios, when one wants fine-grained control over the operations in a service contract.
Public implementations in WCF
- DataContractSerializerOperationBehavior: Defines the runtime behavior for the DataContractSerializer used to serialize the parameters to the operation.
- XmlSerializerOperationBehavior: Defines the runtime behavior for the XmlSerializer used to serialize the parameters to the operation.
- OperationBehaviorAttribute: Defines some attributes which apply to the operation, such as transaction parameters or impersonation options.
- ReceiveContextEnabledAttribute: Defines that the operation controls the acknowledgement that the message was completely received. Used in queue scenarios.
- TransactionFlowAttribute: Defines whether the operation accepts incoming transactions from a client.
- AspNetCacheProfileAttribute: Defines the ASP.NET output cache profile for the operation.
- WebGetAttribute: Indicates that the operation (in REST services) is a retrieval operation, to be consumed via HTTP GET requests.
- WebInvokeAttribute: Indicates that the operation (in REST services) is an invoke operation, to be consumed via HTTP verbs other than GET.
Interface declaration
- public interface IOperationBehavior
- {
- void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters);
- void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation);
- void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation);
- void Validate(OperationDescription operationDescription);
- }
Again, like in the previous behavior interfaces, Validate is called first, with the operation description passed as a parameter. If the behavior finds the description violating some of its logic, it should throw an exception to abort the host (or the client) from opening. AddBindingParameters is called next, and again this is not commonly used.
ApplyDispatchBehavior and ApplyClientBehavior are called last, and they receive the operation description and the operation runtime (DispatchOperation for the server side, ClientOperation for client side). Among the properties in the operation runtime which are commonly used are the list of parameter inspectors (client / server) and the message formatter (client / server), which will be covered later in this series.
How to add an operation behavior
Via service / endpoint / contract description (server) : You can obtain a reference to the operation description via the contract description, and from there you can get a reference to the list of operation behaviors:
- string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
- ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
- ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new WSHttpBinding(), "");
- foreach (OperationDescription od in endpoint.Contract.Operations)
- {
- od.Behaviors.Add(new MyOperationBehavior());
- }
Via channel factory / generated proxy (client) : Like on the server side, once you get a reference to the endpoint, you can reference the contract, then the operation description collection. Notice that (just like in the service) you don’t need to enumerate all the operations, you can select by name to apply the behavior to specific operations.
- ChannelFactory<ITest> factory = new ChannelFactory<ITest>(binding, new EndpointAddress(address));
- factory.Endpoint.Contract.Operations.Find("Add").Behaviors.Add(new MyOperationBehavior());
Via attributes: If the operation behavior is derived from System.Attribute, it can be applied to the operation, either at the contract interface or at the actual implementation of the operation in the service code, and it will be added to the operation description by WCF. Adding it to both places is the same as adding it to a single one (the list of behaviors is a dictionary keyed by the behavior type, so a behavior collection cannot have more than one instance of each type).
- public class MyOperationBehaviorAttribute : Attribute, IOperationBehavior
- {
- // IOperationBehavior implementation
- }
- [ServiceContract]
- public interface ICalculator
- {
- [OperationContract]
- [MyOperationBehavior]
- int Add(int x, int y);
- [OperationContract]
- int Multiply(int x, int y);
- }
- public class CalculatorService : ICalculator
- {
- public int Add(int x, int y)
- {
- return x + y;
- }
- [MyOperationBehavior]
- public int Multiply(int x, int y)
- {
- return x * y;
- }
- }
Using configuration: Configuration is not an option for adding operation behaviors.
Real world scenario: a better parameter inspector
The IParameterInspector interface is a great way to analyze the inputs and outputs to operations in WCF services – it provides a callback where one can look, log, change, etc. the input and output parameters. It also lets the users see the return value of the operation, but doesn’t let the inspector change it. A few posts from the forums needed to actually change the return value as well. In one of them, the user Antonello Tesoro offered a great, simple suggestion on how to implement such inspector (wrapping an operation invoker), and I’ll implement his idea to show a real usage of operation behaviors.
And here goes the usual disclaimer – this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). Also, for simplicity sake it doesn’t have a lot of error handling which a production-level code would, and doesn’t support all contract types (asynchronous operations, for example, are only partially supported). Finally, this sample (as well as most other samples in this series) uses extensibility points other than the one for this post (e.g., operation invoker) which are necessary to get a realistic scenario going. I’ll briefly describe what they do, and leave to their specific entries a more detailed description of their behavior.
First, let’s define the new inspector interface. It’s almost a copy of IParameterInspector, with the only difference being the returnValue parameter to AfterCall being passed as reference:
- public interface IBetterParameterInspector
- {
- object BeforeCall(string operationName, object[] inputs);
- void AfterCall(string operationName, object[] outputs, ref object returnValue, object correlationState);
- }
On to the behavior. On the call to ApplyDispatchBehavior, the DispatchOperation object is already created and populated. We’ll then replace the existing operation invoker with our own implementation, passing the existing one to it:
- public class BetterParameterInspectorOperationBehavior : IOperationBehavior
- {
- IBetterParameterInspector inspector;
- public BetterParameterInspectorOperationBehavior(IBetterParameterInspector inspector)
- {
- if (inspector == null)
- {
- throw new ArgumentNullException("inspector");
- }
- this.inspector = inspector;
- }
- public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
- {
- }
- public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
- {
- }
- public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
- {
- dispatchOperation.Invoker = new BetterParameterInspectorOperationInvoker(operationDescription.Name, dispatchOperation.Invoker, this.inspector);
- }
- public void Validate(OperationDescription operationDescription)
- {
- }
- }
The new operation invoker will simply delegate all the calls to the original invoker, calling the inspector before and after completing each call. On synchronous method implementations, the implementation of the correlation state is trivial – simply store the return value of BeforeCall in a local variable, invoke the operation, then use that variable as the correlationState parameter to AfterCall. On asynchronous operations, however, it isn’t as simple. For this sample, the code throws if an inspector to an asynchronous operation returns anything other than null on BeforeCall. To fully implement correlation state for such operations, we’d need to chain the asynchronous operation as described in https://blogs.msdn.com/b/carlosfigueira/archive/2009/07/23/chaining-async-calls-in-wcf-or-in-net-in-general.aspx – too complex for this sample.
- class BetterParameterInspectorOperationInvoker : IOperationInvoker
- {
- string operationName;
- IOperationInvoker originalInvoker;
- IBetterParameterInspector inspector;
- public BetterParameterInspectorOperationInvoker(string operationName, IOperationInvoker originalInvoker, IBetterParameterInspector inspector)
- {
- this.operationName = operationName;
- this.originalInvoker = originalInvoker;
- this.inspector = inspector;
- }
- public object[] AllocateInputs()
- {
- return this.originalInvoker.AllocateInputs();
- }
- public object Invoke(object instance, object[] inputs, out object[] outputs)
- {
- object correlationState = this.inspector.BeforeCall(this.operationName, inputs);
- object result = this.originalInvoker.Invoke(instance, inputs, out outputs);
- this.inspector.AfterCall(this.operationName, outputs, ref result, correlationState);
- return result;
- }
- public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
- {
- object correlationState = this.inspector.BeforeCall(this.operationName, inputs);
- if (correlationState != null)
- {
- throw new InvalidOperationException("Correlation not implemented yet for asynchronous methods");
- }
- return this.originalInvoker.InvokeBegin(instance, inputs, callback, state);
- }
- public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
- {
- object operationResult = this.originalInvoker.InvokeEnd(instance, out outputs, result);
- this.inspector.AfterCall(this.operationName, outputs, ref operationResult, null);
- return operationResult;
- }
- public bool IsSynchronous
- {
- get { return this.originalInvoker.IsSynchronous; }
- }
- }
And that’s it. Now let’s put them to use. To test this scenario, I’ll define two service contract interfaces, one with synchronous operations, one with asynchronous ones.
- [ServiceContract]
- public interface ITest
- {
- [OperationContract]
- int Add(int x, int y);
- [OperationContract]
- int Subtract(int x, int y);
- [OperationContract]
- int Divide(int x, int y);
- [OperationContract]
- int Multiply(int x, int y);
- }
- [ServiceContract]
- public interface ITestWithAsyncOperation
- {
- [OperationContract(AsyncPattern = true)]
- IAsyncResult BeginEchoString(string text, AsyncCallback callback, object userState);
- string EndEchoString(IAsyncResult asyncResult);
- }
Their implementations are trivial (for the asynchronous one we delegate to the Func<T,TResult> delegate and let it handle the asynchronous behavior for us).
- public class Service : ITest, ITestWithAsyncOperation
- {
- public int Add(int x, int y)
- {
- int result = x + y;
- Console.WriteLine("{0} + {1} = {2}", x, y, result);
- return result;
- }
- public int Subtract(int x, int y)
- {
- int result = x - y;
- Console.WriteLine("{0} - {1} = {2}", x, y, result);
- return result;
- }
- public int Divide(int x, int y)
- {
- int result = x / y;
- Console.WriteLine("{0} / {1} = {2}", x, y, result);
- return result;
- }
- public int Multiply(int x, int y)
- {
- int result = x * y;
- Console.WriteLine("{0} * {1} = {2}", x, y, result);
- return result;
- }
- public IAsyncResult BeginEchoString(string text, AsyncCallback callback, object userState)
- {
- Func<string, string> func = this.EchoStringDoWork;
- return func.BeginInvoke(text, callback, userState);
- }
- public string EndEchoString(IAsyncResult asyncResult)
- {
- Func<string, string> func = ((System.Runtime.Remoting.Messaging.AsyncResult)asyncResult).AsyncDelegate as Func<string, string>;
- return func.EndInvoke(asyncResult);
- }
- private string EchoStringDoWork(string input) { return input; }
- }
Now for setting up the service. This operation behavior needs an instance of an inspector, so it cannot be defined as an attribute (attribute arguments cannot be user-defined objects). We’ll define two inspectors, one for the calculator (no negative numbers) and one for the asynchronous operations (no null return values):
- class NoNegativesInspector : IBetterParameterInspector
- {
- public object BeforeCall(string operationName, object[] inputs)
- {
- for (int i = 0; i < inputs.Length; i++)
- {
- inputs[i] = Math.Abs((int)inputs[i]);
- }
- return null;
- }
- public void AfterCall(string operationName, object[] outputs, ref object returnValue, object correlationState)
- {
- returnValue = Math.Abs((int)returnValue);
- if (outputs != null)
- {
- for (int i = 0; i < outputs.Length; i++)
- {
- outputs[i] = Math.Abs((int)outputs[i]);
- }
- }
- }
- }
- class NoNullReturnValue : IBetterParameterInspector
- {
- public object BeforeCall(string operationName, object[] inputs)
- {
- return null;
- }
- public void AfterCall(string operationName, object[] outputs, ref object returnValue, object correlationState)
- {
- if (returnValue == null)
- {
- returnValue = "<<null>>";
- }
- }
- }
Now for setting up the service itself (and calling it). In one of the endpoints the behavior is applied to all operations; in the other, the behavior is applied to one operation explicitly.
- static void Main(string[] args)
- {
- string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
- ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
- ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
- ServiceEndpoint asyncEndpoint = host.AddServiceEndpoint(typeof(ITestWithAsyncOperation), new BasicHttpBinding(), "async");
- foreach (OperationDescription od in endpoint.Contract.Operations)
- {
- od.Behaviors.Add(new BetterParameterInspectorOperationBehavior(new NoNegativesInspector()));
- }
- asyncEndpoint.Contract.Operations.Find("EchoString").Behaviors.Add(
- new BetterParameterInspectorOperationBehavior(new NoNullReturnValue()));
- host.Open();
- ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new BasicHttpBinding(), new EndpointAddress(baseAddress));
- ITest proxy = factory.CreateChannel();
- Console.WriteLine("Abs(Abs(33) + Abs(-44)) = {0}", proxy.Add(33, -44));
- Console.WriteLine("Abs(Abs(33) - Abs(-44)) = {0}", proxy.Subtract(33, -44));
- Console.WriteLine("Abs(Abs(33) * Abs(-44)) = {0}", proxy.Multiply(33, -44));
- ((IClientChannel)proxy).Close();
- factory.Close();
- ChannelFactory<ITestWithAsyncOperation> asyncFactory = new ChannelFactory<ITestWithAsyncOperation>(new BasicHttpBinding(), new EndpointAddress(baseAddress + "/async"));
- ITestWithAsyncOperation asyncProxy = asyncFactory.CreateChannel();
- Console.WriteLine(asyncProxy.EndEchoString(asyncProxy.BeginEchoString("hello", null, null)));
- Console.WriteLine(asyncProxy.EndEchoString(asyncProxy.BeginEchoString(null, null, null)));
- ((IClientChannel)asyncProxy).Close();
- asyncFactory.Close();
- }
That is it. This is an example on how an operation behavior can be used to implement this scenario. Notice that it only works for the server side, as there’s no invoker at the client side (for that side I think I’d use an IClientMessageFormatter). If there is enough demand I’ll expand that scenario in the upcoming post for that interface.
Coming up
We’ll look at the interfaces for the runtime, starting at the inspection interfaces (IClientMessageInspector / IDispatchMessageInspector / IParameterInspector).
Carlos Figueira
https://blogs.msdn.com/carlosfigueira
Twitter: @carlos_figueira https://twitter.com/carlos_figueira