Udostępnij za pośrednictwem


WCF Extensibility – IParameterInspector

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 message inspectors, described in the previous post of this series, allows you complete control over the message going through the WCF stack. They’re very powerful, but you have to know how to deal with the Message object, which is not the most desirable way of programming. If the service model in WCF hides all the messaging framework by allowing us to define our services in terms of strongly-typed operations (i.e., using nice primitive and user defined types), there should be a way of intercepting requests / responses after all the processing to extract those parameters from incoming messages (or before they’re packaged in outgoing messages) is done. The IParameterInspector is exactly that – before and after each call, the inspector gets a chance to inspect the operation inputs, outputs and return value, in the same types as defined by the operation contract, no conversion needed (the only thing needed is a cast, since the parameters are passed as objects).

Public implementations in WCF

None. There aren’t also any internal implementations of this interface in WCF, this is really a user (as opposed to system) extensibility point.

Interface declaration

  1. public interface IParameterInspector
  2. {
  3.     void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState);
  4.     object BeforeCall(string operationName, object[] inputs);
  5. }

Unlike the message inspector, the parameter inspector interface is the same for both client and server side. At the server side, right before the call is dispatched to the user code, BeforeCall is invoked, where the inspector can look at the inputs to the operation. When the operation returns, AfterCall is invoked to let the inspector look at the return value of the operation, and any out/ref parameters which the operation may have set. At the client side, right after the call on the proxy is made, and before it’s handed down to the WCF stack, BeforeCall is invoked to let the inspector look at the outgoing parameters before they’re packaged into a message object. Finally, when the call returns from the server and the result is about to be returned to the client (either by returning the synchronous invocation or in an asynchronous callback), AfterCall is called at the client side.

Like in the message inspector case, if the operation contract being called by the client is marked with IsOneWay=true, the call to AfterCall on a client inspector does not happen (it does happen at the server, though). And unlike the message inspector case, if an exception is thrown during the processing of the call (e.g., the server returned a fault), AfterCall will also not be called on the client – it makes sense, since there is no guarantees that the return value or any out/ref parameters have valid values at that point.

How to add parameter inspectors

At the server side: the list of parameter inspectors is available at the DispatchOperation object – each operation has its own list of parameter inspectors. It is typically accessed via an operation behavior in a call to ApplyDispatchBehavior:

  1. public class MyOperationBehavior : Attribute
  2. {
  3.     public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
  4.     {
  5.         dispatchOperation.ParameterInspectors.Add(new MyParameterInspector());
  6.     }
  7. }

At the client side: the list of client parameter inspectors is available at the ClientOperation object, and typically accessed via an operation behavior’s ApplyClientBehavior method..

  1. public class MyOperationBehavior : IOperationBehavior
  2. {
  3.     public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  4.     {
  5.         clientOperation.ParameterInspectors.Add(new MyParameterInspector());
  6.     }
  7. }

 

Real world scenario: simple profiler

One usage which I’ve seen a couple of times for the parameter inspector is to create a simple profiler / statistics page for WCF services. The parameter inspector interface methods are called really close to the actual method invocation (as can be seen in the example of the WCF Runtime post), both on the client and on the server, so they can fairly accurately record starting and ending times of arbitrary method calls. The example below is implemented as an endpoint behavior which adds the statistic collection logic for all the operations in the endpoint. In this example, we need to call a method on a class to show the statistics, but a more realistic scenario would save the profiling information in a file or database for further analysis.

And here comes 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. Also, as I mentioned before, the statistics are all stored in memory, a better profiler would save the profiling information in a file or database, and possibly even expose it as a service itself.

As with all of the runtime examples, it starts with a behavior. I’ll use an endpoint behavior here, but a contract (or even service) behavior would do.

  1. public class OperationProfilerEndpointBehavior : IEndpointBehavior
  2. {
  3.     private OperationProfilerManager manager;
  4.     public OperationProfilerEndpointBehavior(OperationProfilerManager manager)
  5.     {
  6.         this.manager = manager;
  7.     }
  8.  
  9.     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  10.     {
  11.     }
  12.  
  13.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  14.     {
  15.         foreach (ClientOperation operation in clientRuntime.Operations)
  16.         {
  17.             operation.ParameterInspectors.Add(new OperationProfilerParameterInspector(this.manager, operation.IsOneWay));
  18.         }
  19.     }
  20.  
  21.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  22.     {
  23.         foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
  24.         {
  25.             operation.ParameterInspectors.Add(new OperationProfilerParameterInspector(this.manager, operation.IsOneWay));
  26.         }
  27.     }
  28.  
  29.     public void Validate(ServiceEndpoint endpoint)
  30.     {
  31.     }
  32. }

The behavior takes a “manager” as a parameter. The manager is the entity which will, in this example, store the call information for all the operations in the contract. It can then be invoked to display a summary of the statistics for the calls (I’ll omit most of the formatting methods here, you can find them in the code for this post, linked below).

  1. public class OperationProfilerManager
  2. {
  3.     Dictionary<string, List<double>> callsPerOperation;
  4.     public OperationProfilerManager()
  5.     {
  6.         this.callsPerOperation = new Dictionary<string, List<double>>();
  7.     }
  8.  
  9.     public void AddOneWayCall(string operationName)
  10.     {
  11.         List<double> callTimes;
  12.         if (this.callsPerOperation.ContainsKey(operationName))
  13.         {
  14.             callTimes = this.callsPerOperation[operationName];
  15.         }
  16.         else
  17.         {
  18.             callTimes = new List<double>();
  19.             this.callsPerOperation.Add(operationName, callTimes);
  20.         }
  21.  
  22.         callTimes.Add(-1);
  23.     }
  24.  
  25.     public void AddCall(string operationName, double duration)
  26.     {
  27.         List<double> callTimes;
  28.         if (this.callsPerOperation.ContainsKey(operationName))
  29.         {
  30.             callTimes = this.callsPerOperation[operationName];
  31.         }
  32.         else
  33.         {
  34.             callTimes = new List<double>();
  35.             this.callsPerOperation.Add(operationName, callTimes);
  36.         }
  37.  
  38.         callTimes.Add(duration);
  39.     }
  40. }

Notice that there are two methods to add information about an operation call. For “normal” operations, we provide the duration of the call, but for one-way operations, this information cannot be retrieved (esp. at the client side, since the control will return to the caller almost as if the call was made asynchronously). Next we’ll get to the inspector itself. It’s implementation is actually fairly simple, as it registers the time in which BeforeCall was invoked, and returns it as the correlation state to be passed to the AfterCall method (unless the operation is one-way, in which case we simply register such a call in the manager). There, we again register the time and take the difference between the two times to calculate the duration of the method, then store it in the manager.

And that’s it – this nothing else needed. Now for testing I’ll define a service with some operations which take a certain amount of time (simulated using a random generator), and some “fast” ones (again I’ll skip the definition of the data contracts used in the service, as they’re not relevant to the topic, but they can be found in the code for this post, linked below).

  1. [ServiceContract]
  2. public interface ITest
  3. {
  4.     [OperationContract]
  5.     int Add(int x, int y);
  6.     [OperationContract]
  7.     int ProcessOrder(Order order);
  8.     [OperationContract(IsOneWay = true)]
  9.     void ProcessOneWay(Order order);
  10. }
  11.  
  12. public class Service : ITest
  13. {
  14.     static Random rndGen = new Random(1);
  15.     public int Add(int x, int y)
  16.     {
  17.         return x + y;
  18.     }
  19.  
  20.     public int ProcessOrder(Order order)
  21.     {
  22.         Thread.Sleep(rndGen.Next(1, 100));
  23.         return order.Id;
  24.     }
  25.  
  26.     public void ProcessOneWay(Order order)
  27.     {
  28.         Thread.Sleep(rndGen.Next(1, 100));
  29.     }
  30. }

And finally, hosting the program to see the simple profiler in action:

  1. static void Main(string[] args)
  2. {
  3.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  4.     ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
  5.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
  6.     OperationProfilerManager serverProfilerManager = new OperationProfilerManager();
  7.     endpoint.Behaviors.Add(new OperationProfilerEndpointBehavior(serverProfilerManager));
  8.     Console.WriteLine("Opening the host");
  9.     host.Open();
  10.  
  11.     ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new BasicHttpBinding(), new EndpointAddress(baseAddress));
  12.     OperationProfilerManager clientProfilerManager = new OperationProfilerManager();
  13.     factory.Endpoint.Behaviors.Add(new OperationProfilerEndpointBehavior(clientProfilerManager));
  14.     ITest proxy = factory.CreateChannel();
  15.  
  16.     Order oneOrder = new Order
  17.     {
  18.         Id = 1,
  19.         Client = new Person
  20.         {
  21.             Name = "John Doe",
  22.             Address = "111 223th Ave",
  23.         },
  24.         Items = new List<OrderItem>
  25.         {
  26.             new OrderItem { Name = "bread", Unit = "un", UnitPrice = 0.56, Amount = 3 },
  27.             new OrderItem { Name = "milk", Unit = "gal", UnitPrice = 2.79, Amount = 1 },
  28.             new OrderItem { Name = "eggs", Unit = "doz", UnitPrice = 2.23, Amount = 1 },
  29.         }
  30.     };
  31.  
  32.     Console.WriteLine("Calling some operations...");
  33.  
  34.     for (int i = 0; i < 200; i++)
  35.     {
  36.         proxy.Add(i, i * i);
  37.         if ((i % 3) == 0)
  38.         {
  39.             proxy.ProcessOneWay(oneOrder);
  40.         }
  41.         else
  42.         {
  43.             proxy.ProcessOrder(oneOrder);
  44.         }
  45.     }
  46.  
  47.     Console.WriteLine("Now printing statistics (server)");
  48.     serverProfilerManager.PrintSummary();
  49.  
  50.     Console.WriteLine();
  51.  
  52.     Console.WriteLine("Now printing statistics (client)");
  53.     clientProfilerManager.PrintSummary();
  54.  
  55.     Console.WriteLine("Press ENTER to close");
  56.     Console.ReadLine();
  57.     host.Close();
  58.     Console.WriteLine("Host closed");
  59. }

Coming up

Client and server (dispatch) message formatters, with an example showing how to send / receive arbitrary data without using the WCF REST Raw programming model – this issue came up at the forums a few weeks back.

[Code for this post]

[Back to the index]

Carlos Figueira
https://blogs.msdn.com/carlosfigueira
Twitter: @carlos_figueira https://twitter.com/carlos_figueira