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]

Comments

  • Anonymous
    June 30, 2011
    Hi Carlos,                   I am trying to implement authorization for the Operation using the custom attribute which implements IOperationBehavior, IParameterInspector and i want to send  StatusCode Unauthorized with the StatusDescription "UnAuthorizedUser" back to client from BeforeCall if authorization fails. Is there a way to achieve this?Thanks
  • Anonymous
    July 01, 2011
    Not directly; you can't control the response message on IParameterInspector.BeforeCall. You can throw an exception there, and hook up an IErrorHandler in your service; in your handler you'd then convert that exception to a Message adding an HttpResponseMessageProperty with StatusCode.Unauthorized, so that it would be sent back to the client.
  • Anonymous
    July 06, 2011
    I added the code to handle the custom exception by hooking an IErrorHandler to the service as per the example shown in this post. but now i am receiving the status code 202 Accepted instead of  401 Unauthorized . Also i have not received the fault message as the response. Is there anything i need to do here.
  • Anonymous
    July 06, 2011
    I am using the WebScriptServiceHostFactory for the restful service and would like to send the status code as the response for unauthorized requests.
  • Anonymous
    July 06, 2011
    @Nayangowda, is the code on your error handler being reached (ProvideFault)? If so, are you creating a message which is compatible with the encoder you're using (i.e., since you're using a RESTful service, it needs to be of MessageVersion.None, and have one of the formats supported by the WebMessageEncodingBindingElement)
  • Anonymous
    July 10, 2011
    I am building a WCF rest service  using WebscriptServiceHostFactory to support both POX and Json response Message formats based on the endpoint configuration and implemented the custom attribute to handle the Authorization for the operations. I would like to send the Status code as response and end the request for unauthorized requests so i am throwing exception from custom attribute and handling in IErrorHandler. But i am not able send the status code to client.I am getting the status code as 202 instead of 401 Unauthorized. Is there anything wrong in the below code?[ServiceContract]public interface Irestservice{       [OperationContract]       [WebGet]       bool signin(string username, string password);}[ServiceBehavior(IncludeExceptionDetailInFaults = true, InstanceContextMode = InstanceContextMode.PerCall), AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]public class restservice : Irestservice{       [Authorization]       public bool signin(string username, string password)       {                     return true;                 }}public class AuthorizationAttribute : Attribute, IOperationBehavior, IParameterInspector{       public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)       {                     dispatchOperation.ParameterInspectors.Add(this);       }            public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)       {       }       public object BeforeCall(string operationName, object[] inputs)       {           string publicKey = WebOperationContext.Current.IncomingRequest.Headers["Authorization"];           bool flag = AuthorizationHelper.CheckPartnerAuthorization( publicKey);           if (!flag)           {               WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Unauthorized;               throw new LicensingException("PartnerUnauthorized");           }           return null;       }            }public class LicensingBehavior : IServiceBehavior{              public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)       {           foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)           {                             RestErrorHandler newHandler = new RestErrorHandler();               channelDispatcher.ErrorHandlers.Add(newHandler);                         }       }}class AppServiceHostFactory : WebScriptServiceHostFactory {     protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)     {          ServiceHost serviceHost =  base.CreateServiceHost(serviceType, baseAddresses);        serviceHost.Description.Behaviors.Add(new LicensingBehavior());        return serviceHost;     }     }public class RestErrorHandler:IErrorHandler{       #region IErrorHandler Members       public bool HandleError(Exception error)       {           return error is LicensingException;       }       public void ProvideFault(Exception error, MessageVersion version, ref Message fault)       {           LicensingException licensingException = error as LicensingException;           if (licensingException != null)           {               fault = Message.CreateMessage(version, null, new ValidationErrorBodyWriter(licensingException));               HttpResponseMessageProperty prop = new HttpResponseMessageProperty();               prop.StatusCode = HttpStatusCode.Unauthorized;               prop.Headers[HttpResponseHeader.ContentType] = "application/json; charset=utf-8";               fault.Properties.Add(HttpResponseMessageProperty.Name, prop);               fault.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Json));                         }                   }       class ValidationErrorBodyWriter : BodyWriter       {           private LicensingException licensingException;           Encoding utf8Encoding = new UTF8Encoding(false);           public ValidationErrorBodyWriter(LicensingException exception)               : base(true)           {               this.licensingException = exception;           }           protected override void OnWriteBodyContents(XmlDictionaryWriter writer)           {               writer.WriteStartElement("root");               writer.WriteAttributeString("type", "object");               writer.WriteStartElement("ErrorMessage");               writer.WriteAttributeString("type", "string");               writer.WriteString(this.licensingException.Message);               writer.WriteEndElement();               writer.WriteEndElement();           }       }   }
  • Anonymous
    July 26, 2011
    Nayangowda, the problem is that the ServiceHost instance returned by WebScriptServiceHostFactory will add an endpoint behavior (WebScriptEnablingBehavior) to the endpoint, and that behavior adds another IErrorHandler to the runtime. You error handler is creating the message correctly, but that other is overriding what you did.Do you really need to use the WebScriptServiceHostFactory (for interaction with the ASP.NET AJAX framework)? That factory isn't very extensible (the service host returned by it is internal, so you can't really override it. If all you need is the REST (XML + JSON) support, you can use the WebServiceHostFactory / WebServiceHost, and add your behavior only after the base host finished adding all of its behaviors itself, as shown below.   public class AppServiceHostFactory : WebServiceHostFactory   {       protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)       {           return new AppServiceHost(serviceType, baseAddresses);       }       class AppServiceHost : WebServiceHost       {           public AppServiceHost(Type serviceType, Uri[] baseAddresses)               : base(serviceType, baseAddresses)           {           }           protected override void OnOpening()           {               base.OnOpening();               this.Description.Behaviors.Add(new LicensingBehavior());           }       }   }
  • Anonymous
    August 01, 2011
    Hi Carlos,                    Thanks for the input. But still is persisting here is the configuration i am using. Is there any thing wrong here....<system.serviceModel>         <serviceHostingEnvironment aspNetCompatibilityEnabled="true"  />
    &lt;bindings&gt; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&lt;webHttpBinding&gt;     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;binding name=&quot;webBinding&quot;&gt;     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;/binding&gt;    &lt;/webHttpBinding&gt;&lt;/bindings&gt;&lt;behaviors&gt;               &lt;endpointBehaviors&gt;        &lt;behavior name=&quot;jsonBehavior&quot;&gt;            &lt;enableWebScript /&gt;        &lt;/behavior&gt;        &lt;behavior name=&quot;poxBehavior&quot;&gt;            &lt;webHttp/&gt;        &lt;/behavior&gt;    &lt;/endpointBehaviors&gt;    &lt;serviceBehaviors&gt;        &lt;behavior name=&quot;restserviceBehavior&quot; &gt;            &lt;serviceMetadata httpGetEnabled=&quot;true&quot;/&gt;            &lt;serviceDebug includeExceptionDetailInFaults=&quot;true&quot;/&gt;        &lt;/behavior&gt;    &lt;/serviceBehaviors&gt;&lt;/behaviors&gt;
    <services>           <service name="restservice" behaviorConfiguration="restserviceBehavior">                       <host>
     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&lt;baseAddresses&gt;     &nbsp; &nbsp; &nbsp; &nbsp;&lt;add baseAddress=&quot;http://localhost/LicensingAPIService/restservice.svc&quot;/&gt;
                                        </baseAddresses>
     &lt;/host&gt;
                         <endpoint address="json" binding="webHttpBinding"  bindingConfiguration="webBinding"  behaviorConfiguration="jsonBehavior" contract="Irestservice" />                      <endpoint address="pox" binding="webHttpBinding"  bindingConfiguration="webBinding"  behaviorConfiguration="poxBehavior"  contract="Irestservice"/>                 </service></services></system.serviceModel>
  • Anonymous
    August 01, 2011
    What is the problem that you have now? If you're using the factory approach, you shouldn't need to redefine the endpoints on web.config.
  • Anonymous
    August 02, 2011
    I trying to return XML or Json response based on these endpoints. So I have defined the end points on the web.config. I am not completely aware of the WCF pipeline and Factory approach. In the above code i am trying to authenticate the request and if it fails then throw a an exception and handle the exception by using the errorhandler  and send only the status code to client.But there is something i am missing here, due to which i am getting different status when client request are made to json and pox endpoints.
  • Anonymous
    August 02, 2011
    In the endpoint with the "json" address, you're using the <enableWebScript/> which as I mentioned before is not very extensible. You can return JSON with the <webHttp/> as well (it's just that XML is the default) - use <webHttp defaultOutgoingResponseFormat="Json"/> to change the default format. Then, if it's working for the "pox" endpoint, it will work with the "json" one as well.
  • Anonymous
    September 11, 2011
    Hi Carlos,                Thanks a lot for all your inputs. I am able to support the Pox and Json formats without using the <enableWebScript/>. Since i am using the framwork 3.5 . I had to workaround using the custom behaviour to support the defaultOutgoingResponseFormat in the config.                  Now i am having one issue for the Json requests even though i am sending the content type as application/json in errorhandler. I am gettign XML parsing error for the exceptions handled in the errorhandler. Is there any thing i am missing here.
  • Anonymous
    September 11, 2011
    DefaultOutgoingResponseFormat is supported since 3.5, you shouldn't need to use a custom behavior for that.
  • Anonymous
    September 21, 2011
    Hi,In an IIS hosted WCF application, how do you inject an instance of OperationProfilerManager in the constructor of the OperationProfilerEndpointBehavior ?This is how you do it in the Main(string[] args)   OperationProfilerManager serverProfilerManager = new OperationProfilerManager();   endpoint.Behaviors.Add(new OperationProfilerEndpointBehavior(serverProfilerManager));How to do the same in the web.config or programmatically ?E.
  • Anonymous
    September 22, 2011
    Hi Eric,If your service is hosted in IIS you have two options: you can either create a behavior element extension (more info at blogs.msdn.com/.../wcf-extensibility-behavior-configuration-extensions.aspx) in which can create the instancing of the OperationProfileEndpointBehavior and use that behavior in config; or you can use a ServiceHostFactory to set up your service (more info at blogs.msdn.com/.../wcf-extensibility-servicehostfactory.aspx) in which you can set up your endpoint via code, just as if it were in a self-hosted scenario.