次の方法で共有


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).

  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.     endpointDispatcher.DispatchRuntime.InstanceProvider = new SimpleInstanceProvider(serviceType);
  23.     endpointDispatcher.DispatchRuntime.InstanceContextProvider = new SimpleInstanceContextProvider();
  24.     endpointDispatcher.AddressFilter = new EndpointAddressMessageFilter(endpoint.Address);
  25.     endpointDispatcher.ContractFilter = new ActionMessageFilter(endpoint.Contract.Operations.Select(op => op.Messages[0].Action).ToArray());
  26.     endpointDispatcher.FilterPriority = 1;
  27.  
  28.     channelDispatcher.Endpoints.Add(endpointDispatcher);
  29.     return channelDispatcher;
  30. }
  31.  
  32. // This is a workaround to the fact that the id of the dispatcher and endpoint must match
  33. // for the endpoint to be exposed in the service metadata. For simply using the service,
  34. // this step is not necessary.
  35. private void AssociateEndpointToDispatcher(ServiceEndpoint endpoint, ChannelDispatcher dispatcher)
  36. {
  37.     BindingFlags instanceBindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
  38.     PropertyInfo endpointIdProperty = typeof(ServiceEndpoint).GetProperty("Id", instanceBindingFlags);
  39.     PropertyInfo endpointDispatcherIdProperty = typeof(EndpointDispatcher).GetProperty("Id", instanceBindingFlags);
  40.     string endpointId = endpointIdProperty.GetValue(endpoint, null) as string;
  41.     foreach (EndpointDispatcher ed in dispatcher.Endpoints)
  42.     {
  43.         endpointDispatcherIdProperty.SetValue(ed, endpointId, null);
  44.     }
  45. }
  46.  
  47. // Simple invoker, simply delegates to the underlying MethodInfo object
  48. class OperationInvoker : IOperationBehavior, IOperationInvoker
  49. {
  50.     MethodInfo method;
  51.     public OperationInvoker(MethodInfo method)
  52.     {
  53.         this.method = method;
  54.     }
  55.  
  56.     public object[] AllocateInputs()
  57.     {
  58.         return new object[method.GetParameters().Length];
  59.     }
  60.  
  61.     public object Invoke(object instance, object[] inputs, out object[] outputs)
  62.     {
  63.         outputs = new object[0];
  64.         return this.method.Invoke(instance, inputs);
  65.     }
  66.  
  67.     public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
  68.     {
  69.         throw new NotImplementedException();
  70.     }
  71.  
  72.     public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
  73.     {
  74.         throw new NotSupportedException();
  75.     }
  76.  
  77.     public bool IsSynchronous
  78.     {
  79.         get { return true; }
  80.     }
  81.  
  82.     public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
  83.     {
  84.     }
  85.  
  86.     public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  87.     {
  88.     }
  89.  
  90.     public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
  91.     {
  92.         dispatchOperation.Invoker = this;
  93.     }
  94.  
  95.     public void Validate(OperationDescription operationDescription)
  96.     {
  97.     }
  98. }
  99.  
  100. // This provider implements a "PerCall" instance mode - a new instance of the service class is used
  101. // for each incoming message. Changing the provider (to implement a singleton pattern, for example)
  102. // is simple. More information later on the post about IInstanceProvider.
  103. class SimpleInstanceProvider : IInstanceProvider
  104. {
  105.     Type serviceType;
  106.     public SimpleInstanceProvider(Type serviceType)
  107.     {
  108.         this.serviceType = serviceType;
  109.     }
  110.  
  111.     public object GetInstance(InstanceContext instanceContext, Message message)
  112.     {
  113.         return Activator.CreateInstance(this.serviceType);
  114.     }
  115.  
  116.     public object GetInstance(InstanceContext instanceContext)
  117.     {
  118.         return Activator.CreateInstance(this.serviceType);
  119.     }
  120.  
  121.     public void ReleaseInstance(InstanceContext instanceContext, object instance)
  122.     {
  123.     }
  124. }
  125.  
  126. // This provider simply requests that WCF creates a new instance context for each incoming
  127. // message. More information later on the post about IInstanceContextProvider.
  128. class SimpleInstanceContextProvider : IInstanceContextProvider
  129. {
  130.     public InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel)
  131.     {
  132.         return null;
  133.     }
  134.  
  135.     public void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel)
  136.     {
  137.     }
  138.  
  139.     public bool IsIdle(InstanceContext instanceContext)
  140.     {
  141.         return true;
  142.     }
  143.  
  144.     public void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext)
  145.     {
  146.     }
  147. }

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:

  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.Add(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]

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