Udostępnij za pośrednictwem


WCF Extensibility - IServiceBehavior

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 first behavior covered in this series is the IServiceBehavior. It can be used to inspect or change the entire service description and runtime. For example, to collect information about all endpoints in the service, to apply some common behavior to all endpoints in the service, anything which affects the global service state, the service behavior is a good candidate. As you can see in the public implementations of the interface (and in the example later), they all apply to the service as a whole.

Public implementations in WCF

 

Interface declaration

  1. public interface IServiceBehavior
  2. {
  3.     void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase);
  4.     void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters);
  5.     void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase);
  6. }

As was mentioned in the post about Behaviors, you can use the Validate method to ensure that the service (or the host) doesn’t violate the validation logic on the behavior. One example is the AspNetCompatibilityRequirementsAttribute, which throws if the service requires ASP.NET compatibility, but it’s not there (i.e., in a self-hosted scenario), or if the behavior says that the compatibility mode is not allowed, but the hosting provides it.

AddBindingParameters is used to pass information to the binding elements themselves. For example, the security-related behaviors (ServiceAuthenticationBehavior, ServiceCredentials), add some security managers, which are used by the security binding elements when they’re creating their channels. This method in different than all others in all behaviors, in a sense that it can be called multiple times (all others are called only once) – it will be invoked once per endpoint defined in the service.

ApplyDispatchBehavior is the method which is most used. It’s called right after the runtime was initialized, so at that point the code has access to the listeners / dispatchers / runtime objects for the server, and can add / remove / modify those. Some examples are the help page added by the ServiceDebugBehavior (an extra channel listener is added to understand “GET” requests to the service base address), the wsdl page added by the ServiceMetadataBehavior (again, an extra listener), or the properties set by the ServiceThrottlingBehavior, which get propagated to all dispatchers in the service.

How to add a service behavior

Via the host Description property: if you have a reference to the host object, you can get a reference to the service behavior collection via its description property

  1. string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  2. ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
  3. host.Description.Behaviors.Add(new MyServiceBehavior());

Using attributes: If the service behavior is a class derived from System.Attribute, it can be applied directly to the service class, and it will be added to the description by WCF:

  1. public class MyServiceBehaviorAttribute : Attribute, IServiceBehavior
  2. {
  3.     // implementation of IServiceBehavior interface
  4. }
  5. [MyServiceBehavior]
  6. public class Service : IContract
  7. {
  8.     // implementation of the IContract interface
  9. }

Using configuration: if the service behavior has a configuration extension, then it can be applied directly in the configuration

  1. <system.serviceModel>
  2.     <extensions>
  3.         <behaviorExtensions>
  4.             <add name="myServiceBehavior" type="assembly-qualified-name-of-extension-class"/>
  5.         </behaviorExtensions>
  6.     </extensions>
  7.     <behaviors>
  8.         <serviceBehaviors>
  9.             <behavior name="UsingMyServiceBehavior">
  10.                 <myServiceBehavior/>
  11.             </behavior>
  12.         </serviceBehaviors>
  13.     </behaviors>
  14.     <services>
  15.         <service name="MyNamespace.MyService" behaviorConfiguration="UsingMyServiceBehavior">
  16.             <endpoint ... />
  17.         </service>
  18.     </services>
  19. </system.serviceModel>

 

Real-world scenario: a POCO Service Host

Last month I talked to a friend who said he was starting to use WCF and he actually liked it, but he didn’t like to have to add attributes all over to define the service. Last week I talked to a colleague who said that we should have thought at first of some convention-based definition for services (in addition to the current attribute-based, not as a replacement), just like we added during .NET Framework 3.5 (or 3.5 SP1, I don’t remember) the serialization support for POCO (plain old CLR objects) – types not decorated with [DataContract]/[DataMember] attributes. As I was looking for a good scenario for a behavior which applies to a whole service, this seemed like a good one – if not for something that we’d encourage people to use often (when I mentioned it to another colleague he said that anytime someone would use such a thing, a SOAP kitten would lose part of its soul), but it demonstrates some concepts about the service description and the service runtime which can be accomplished via this extensibility point.

The main challenge in using a non-[ServiceContract] type as a service interface is that WCF has some restrictions about the contracts for its endpoints – they have to be declared as such. So if you simply try to add an endpoint passing a non-[SC] interface (or class type), it will throw while opening the host, during the runtime initialization. We need instead to delay the addition of the (default) endpoint to the service only to after the service runtime is initialized – and that’s exactly where IServiceContract.ApplyDispatchBehavior is called.

To enable such service to respond to client requests, we only needed to create a new dispatcher and route incoming messages to the appropriate service operation. In this implementation, however, I also update the service description, so that the service metadata behavior can produce metadata about this “new” endpoint and clients can use svcutil / Add Service Reference to create proxies to this POCO service, just like for any “normal” WCF service (and this is the only place I needed to access some internal property of WCF in this example).

This scenario is interesting because it also shows an example of the Validate method being used – we want to notify the user right away if the service class which is being used isn’t supported by our behavior. In the example, if the service is not a public class, does not have a default (public, parameter-less) constructor, or if it contains methods with ref or out parameters (implementation detail, it could be supported but I decided to omit them for simplicity sake), then it will throw on validation.

Before I go into the code a small disclaimer – this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few inputs and it worked, but I cannot guarantee that it will work for all services (please let me know if you find a bug or something missing). Also, for simplicity sake (it’s already quite large) it doesn’t have a lot of error handling which a production-level code would. 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, instance provider, etc.) 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.

To the code. The validation logic is straightforward, as I mentioned before (it can be made more complex, such as verifying whether the operation parameters are serializable, for example). On ApplyDispatchBehavior, the code first creates a description object (of type ServiceEndpoint) and adds it to the service description. As I mentioned before, if the endpoint wasn’t added to the description the service would still continue working, but its metadata would not be exposed.to runtime (on serviceHostBase) has already been initialized.

  1. public class PocoServiceBehavior : IServiceBehavior
  2. {
  3.     const string DefaultNamespace = "https://tempuri.org/";
  4.     static readonly Func<Binding> createBinding = () => new BasicHttpBinding();
  5.  
  6.     public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
  7.     {
  8.     }
  9.  
  10.     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  11.     {
  12.         Type serviceType = serviceDescription.ServiceType;
  13.         ContractDescription contractDescription = CreateContractDescription(serviceType);
  14.         string endpointAddress = serviceHostBase.BaseAddresses[0].AbsoluteUri;
  15.  
  16.         ServiceEndpoint endpoint = new ServiceEndpoint(contractDescription, createBinding(), new EndpointAddress(new Uri(endpointAddress)));
  17.         serviceDescription.Endpoints.Add(endpoint);
  18.  
  19.         ChannelDispatcher dispatcher = CreateChannelDispatcher(endpoint, serviceType);
  20.         serviceHostBase.ChannelDispatchers.Add(dispatcher);
  21.  
  22.         AssociateEndpointToDispatcher(endpoint, dispatcher);
  23.     }
  24.  
  25.     public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  26.     {
  27.         Type serviceType = serviceDescription.ServiceType;
  28.         ConstructorInfo ctor = serviceType.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[0], null);
  29.         if (ctor == null)
  30.         {
  31.             throw new InvalidOperationException("Service must have a parameterless, public constructor.");
  32.         }
  33.  
  34.         MethodInfo[] methods = serviceType.GetMethods(BindingFlags.Instance | BindingFlags.Public).Where(m => m.DeclaringType == serviceType).ToArray();
  35.         if (methods.Length == 0)
  36.         {
  37.             throw new InvalidOperationException("Service does not have any public methods.");
  38.         }
  39.  
  40.         foreach (MethodInfo method in methods)
  41.         {
  42.             foreach (ParameterInfo parameter in method.GetParameters())
  43.             {
  44.                 if (parameter.ParameterType.IsByRef)
  45.                 {
  46.                     throw new InvalidOperationException("This behavior does not support public methods with out/ref parameters.");
  47.                 }
  48.             }
  49.         }
  50.     }
  51. }

CreateContractDescription walks through the public methods of the service, and add an operation description to the contract. CreateOperationDescription will create the description of the incoming and outgoing messages which are necessary for the metadata extension to create the WSDL describing this service.

  1. private ContractDescription CreateContractDescription(Type serviceType)
  2. {
  3.     ContractDescription result = new ContractDescription(serviceType.Name, DefaultNamespace);
  4.     result.ContractType = serviceType;
  5.     foreach (MethodInfo method in serviceType.GetMethods(BindingFlags.Instance | BindingFlags.Public))
  6.     {
  7.         if (method.DeclaringType == serviceType)
  8.         {
  9.             result.Operations.Add(CreateOperationDescription(result, serviceType, method));
  10.         }
  11.     }
  12.  
  13.     return result;
  14. }
  15.  
  16. private OperationDescription CreateOperationDescription(ContractDescription contract, Type serviceType, MethodInfo method)
  17. {
  18.     OperationDescription result = new OperationDescription(method.Name, contract);
  19.     result.SyncMethod = method;
  20.  
  21.     MessageDescription inputMessage = new MessageDescription(DefaultNamespace + serviceType.Name + "/" + method.Name, MessageDirection.Input);
  22.     inputMessage.Body.WrapperNamespace = DefaultNamespace;
  23.     inputMessage.Body.WrapperName = method.Name;
  24.     ParameterInfo[] parameters = method.GetParameters();
  25.     for (int i = 0; i < parameters.Length; i++)
  26.     {
  27.         ParameterInfo parameter = parameters[i];
  28.         MessagePartDescription part = new MessagePartDescription(parameter.Name, DefaultNamespace);
  29.         part.Type = parameter.ParameterType;
  30.         part.Index = i;
  31.         inputMessage.Body.Parts.Add(part);
  32.     }
  33.  
  34.     result.Messages.Add(inputMessage);
  35.  
  36.     MessageDescription outputMessage = new MessageDescription(DefaultNamespace + serviceType.Name + "/" + method.Name + "Response", MessageDirection.Output);
  37.     outputMessage.Body.WrapperName = method.Name + "Response";
  38.     outputMessage.Body.WrapperNamespace = DefaultNamespace;
  39.     outputMessage.Body.ReturnValue = new MessagePartDescription(method.Name + "Result", DefaultNamespace);
  40.     outputMessage.Body.ReturnValue.Type = method.ReturnType;
  41.     result.Messages.Add(outputMessage);
  42.  
  43.     result.Behaviors.Add(new OperationInvoker(method));
  44.     result.Behaviors.Add(new OperationBehaviorAttribute());
  45.     result.Behaviors.Add(new DataContractSerializerOperationBehavior(result));
  46.  
  47.     return result;
  48. }

CreateChannelDispatcher is the method which sets up the listener which will accept client requests. The dispatcher is usually created by WCF itself during the runtime initialization, but since we’re adding a non-orthodox endpoint we’re creating it ourselves. The code below walks through the service description (which we created before) and sets up the runtime necessary to listen to incoming messages. AssociateEndpointToDispatcher will access an internal property of the dispatcher and the endpoint to ensure that they are “linked” to each other (otherwise the code generated by svcutil would contain the binding and contract information, but not the address). Finally, the three nested classes implement the logic for instancing (one new service instance per call), instance context (a new one for each new message) and operation invoking (dispatching via a MethodInfo).

[update from 2012/01/30] It turns out that we don’t need to set up a custom instance provider or instance context provider for this case, as long as the ServiceBehaviorAttribute is applied after our service behavior (it will set it up itself). Thanks to Youssef Moussaoui for pointing this up. I’ve updated the code gallery sample, and if you want to check the history, you can look at the GitHub project for this sample.

  1. private ChannelDispatcher CreateChannelDispatcher(ServiceEndpoint endpoint, Type serviceType)
  2. {
  3.     EndpointAddress address = endpoint.Address;
  4.     BindingParameterCollection bindingParameters = new BindingParameterCollection();
  5.     IChannelListener channelListener = endpoint.Binding.BuildChannelListener<IReplyChannel>(address.Uri, bindingParameters);
  6.     ChannelDispatcher channelDispatcher = new ChannelDispatcher(channelListener, endpoint.Binding.Namespace + ":" + endpoint.Binding.Name, endpoint.Binding);
  7.     channelDispatcher.MessageVersion = endpoint.Binding.MessageVersion;
  8.  
  9.     EndpointDispatcher endpointDispatcher = new EndpointDispatcher(address, endpoint.Contract.Name, endpoint.Contract.Namespace, false);
  10.     foreach (OperationDescription operation in endpoint.Contract.Operations)
  11.     {
  12.         string replyAction = operation.Messages.Count > 1 ? operation.Messages[1].Action : "";
  13.         DispatchOperation operationDispatcher = new DispatchOperation(endpointDispatcher.DispatchRuntime, operation.Name, operation.Messages[0].Action, replyAction);
  14.         foreach (IOperationBehavior operationBehavior in operation.Behaviors)
  15.         {
  16.             operationBehavior.ApplyDispatchBehavior(operation, operationDispatcher);
  17.         }
  18.  
  19.         endpointDispatcher.DispatchRuntime.Operations.Add(operationDispatcher);
  20.     }
  21.  
  22.     channelDispatcher.Endpoints.Add(endpointDispatcher);
  23.     return channelDispatcher;
  24. }
  25.  
  26. // Simple invoker, simply delegates to the underlying MethodInfo object
  27. class OperationInvoker : IOperationBehavior, IOperationInvoker
  28. {
  29.     MethodInfo method;
  30.     public OperationInvoker(MethodInfo method)
  31.     {
  32.         this.method = method;
  33.     }
  34.  
  35.     public object[] AllocateInputs()
  36.     {
  37.         return new object[method.GetParameters().Length];
  38.     }
  39.  
  40.     public object Invoke(object instance, object[] inputs, out object[] outputs)
  41.     {
  42.         outputs = new object[0];
  43.         return this.method.Invoke(instance, inputs);
  44.     }
  45.  
  46.     public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
  47.     {
  48.         throw new NotImplementedException();
  49.     }
  50.  
  51.     public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
  52.     {
  53.         throw new NotSupportedException();
  54.     }
  55.  
  56.     public bool IsSynchronous
  57.     {
  58.         get { return true; }
  59.     }
  60.  
  61.     public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
  62.     {
  63.     }
  64.  
  65.     public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  66.     {
  67.     }
  68.  
  69.     public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
  70.     {
  71.         dispatchOperation.Invoker = this;
  72.     }
  73.  
  74.     public void Validate(OperationDescription operationDescription)
  75.     {
  76.     }
  77. }

To tie it all up, we have a new service host class, PocoServiceHost, which is simply a subclass of ServiceHost which adds the PocoServiceBehavior to the services. And as mentioned in the update, our service behavior needs to be inserted before the ServiceBehaviorAttribute, which is added by default to the service description.

  1. public class PocoServiceHost : ServiceHost
  2. {
  3.     public PocoServiceHost(Type serviceType, Uri baseAddress)
  4.         : base(serviceType, baseAddress)
  5.     {
  6.     }
  7.  
  8.     protected override void InitializeRuntime()
  9.     {
  10.         this.Description.Behaviors.Insert(0, new PocoServiceBehavior());
  11.         this.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
  12.         base.InitializeRuntime();
  13.     }
  14. }

And an example of this host being used:

  1. static void Main(string[] args)
  2. {
  3.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  4.     PocoServiceHost host = new PocoServiceHost(typeof(Service), new Uri(baseAddress));
  5.     host.Open();
  6.  
  7.     Console.WriteLine("Press ENTER to close");
  8.     Console.ReadLine();
  9.     host.Close();
  10. }

Coming up

Other behavior interfaces.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    March 22, 2011
    The comment has been removed
  • Anonymous
    March 22, 2011
    The comment has been removed
  • Anonymous
    March 22, 2011
    Are you saying that this is what causes the Error in WF/WCF configuration in IIS? The VS message is annoying but harmless.The IIS error, OTOH, prevents saving WF Monitoring settings, so it's not harmless (although there's a workaround -- temporarily delete "myServiceBehavior" from the file while configuring monitoring). As it happens, the one machine where I've seen this DOES have VS on it.On other machines, IIS doesn't complain about this.
  • Anonymous
    March 23, 2011
    Oops, sorry I thought you only mentioned the VS message; let me check with the WF team about this issue, and I'll post back if they have a fix / workaround / solution. Just to be clear, is this monitoring configuration you're talking about the management console added to IIS by Windows Server AppFabric? Thanks!
  • Anonymous
    March 23, 2011
    Carlos, yes, that's it. Right-click the application, "Manage WCF and WF Services", Configure, choose "Workflow Persistence".On ONE MACHINE (which also has VS) we see this error if we then choose "SQL Server Workflow Persistence" + a store and click apply. On other machines (with or without VS) there is no error. On the machine with the error we can work around it by temporarily modifying the app's web.config to remove "myServiceBehavior" -- although obviously we have to put it back before we run the application.
  • Anonymous
    March 31, 2011
    The comment has been removed
  • Anonymous
    December 12, 2012
    Hi, great article however i'm having trouble with it when i change binding to netnamedpipe. I am not sure how to handle listener creation for the dispatcher. Leaving it as it is throws argument exception saying that i cannot use IReplyChannel with NetNamedPipeBinding at endpoint.Binding.BuildChannelListener<IReplyChannel> ...