Friday, March 12, 2010

Standard Endpoints in WCF 4.0

Sometimes you might want to add an endpoint to a service that doesn't exactly fit the normal WCF model. You might want to create something like what the IMetadataExchange interface does maybe or some sort of diagnostic endpoint. For the sake of discussion, let's call them infrastructure endpoints. Whatever the need, infrastructure endpoints usually have two main characteristics that differentiate them from your everyday service contract:

  • The implementation for the contract will be separate from the service class and,
  • You won't want metadata about the contract exposed from the service.

If you haven't thought about this before, you may be thinking "How hard could this be? Build up a ServiceEndpoint in a behaviour, set some magic property to make the runtime know it's different, add it to the host using AddServiceEndpoint and sit back and enjoy.". For a number of reasons (mainly that the 'magic' property doesn't really exist), the reality is that in .Net 3.5 this process is actually a lot more involved. A number of blogs are floating around on the subject (e.g Patric Fornasier's excellent post and this one by Tomas Restrepo) but essentially the process you'll end up following will go something like this (for simplicities sake we'll assume http transport):
  1. You probably won't want to use one of the existing endpoint bindings for the service so you'll need to create a new IChannelListener. You'll also find out around now that you'll have to set some internal properties on your transport element (notably
    InheritBaseAddressSettings) in order for it to work using reflection which will make you feel dirty.
  2. Once you've built your channel listener, you'll build your channel dispatcher.
  3. Once you've built your channel dispatcher, you can go ahead and build your endpoint dispatcher.
  4. You'll find that you have to provide an IInstanceProvider for the endpoint dispatcher's runtime. You'll discover that the only existing implementations for IInstanceProvider are internal to System.ServiceModel so you'll be writing that (or more likely, copying it out of reflector) yourself.
  5. You'll also find the same thing for IInstanceContextProvider - all implementations internal, better break out reflector again.
  6. You'll then have to create a DispatchOperation for each operation on your contract. WCF normally does this by using the super secret OperationInvokerBehavior, but that won't be an option for you as it's, yep - internal.
  7. You'll then have to create a IDispatchMessageFormatter for each DispatchOperation unless you only use untyped operation parameters(i.e. only accepting and returning Message types).
  8. Finally, you can put it all together and add your channel dispatcher to the host's ChannelDispatchers collection.


If you haven't scratched your own eyes out by now then you should have a reasonable starting place for implementing reusable endpoints, but let me ask you - didn't that seem like a lot of effort?


Why is this process so convoluted in .Net 3.5? Well, the main reason is that many of the classes that do this work on behalf of WCF are internal and we have to implement the functionality ourselves. This is fair enough. There is a limit on what size an API Microsoft can expose and support, unfortunately this doesn't make our job any easier. But the more fundamental issue is the one alluded to at the start of the post - we can't use AddServiceEndpoint. Unfortunately most of the work that we had to perform manually above is performed as a result of this invocation. If you try to pass in a ServiceEndpoint with a contract your service class does not implement, WCF will throw and helpfully tell you so. Well, what about the IMetadataExchange interface I hear you ask? Good question, and here's the kicker - WCF checks whether the implementation class implements your class but specifically excludes the IMetadataExchange interface from this check. There is no extension point to tell the runtime to treat your contract the same way - WCF is literally doing things like if(o is IMetadataExchange).... As a result, the only interface that can be implemented independently of your service AND that can be added to the service via AddServiceEndpoint is IMetadataExchange.

Thankfully, this situation has been largely addressed in WCF 4.0 through the introduction of standard endpoints. Standard endpoints are types that inherit from ServiceEndpoint that you can reference in the configuration file and that can be preconfigured to some degree in code and therefore don't need to be fully configured via the config. For example:

<service name="WcfService.Service">
     <endpoint name="ws" contract="WcfService.IService" binding="wsHttpBinding" />
     <endpoint kind="ping" address="ping" />
</service>

The "kind" attribute identifies the type of standard endpoint. In this case the standard endpoint uses a configurable address but the binding and contract are provided by the standard endpoint.

Standard endpoints are nice, but what's really nice is that the ServiceEndpoint base class now exposes the boolean property, IsSystemEndpoint that when set to true, tells the runtime that the implementation of our contract is provided independently of the service class. In other words, it's the missing 'magic' property mentioned above!

Enough babble. Here are the steps required to implement a simple standard endpoint that uses the REST Programming Model to allow GET callers to 'ping' a service and receive a plain text 'OK' in return. Not very useful, I know - but enough to demonstrate the basic concept. Our endpoint will sit at the address /ping under our service.

First, we define the interface and implementation:

[ServiceContract]
public interface IPing
{
     [WebGet(UriTemplate="")]
     Stream Hello();
}


public class Ping : IPing
{
     public Stream Hello()
     {
         var ms = new MemoryStream();
         var w = new StreamWriter(ms);
         w.Write("OK");
         w.Flush();
         ms.Position = 0;
         return ms;
     }
}

Pretty much nothing to see here, except that in WCF 4.0 you no longer need to specify the OperationContract attribute if you use the WebGet attribute. Otherwise this is all standard .Net 3.5 REST model stuff.

Next, we'll define the actual standard endpoint:

public class PingEndpoint:ServiceEndpoint
{
   public PingEndpoint()
      :base(ContractDescription.GetContract(typeof(IPing)))
   {
      var binding = new WebHttpBinding();
      binding.Name = "pingBinding";
      this.Binding = binding;
      this.Behaviors.Add(new WebHttpBehavior());
      this.IsSystemEndpoint = true;
   }
}

The critical thing to note here is the use of the IsSystemEndpoint property. This is what tells WCF that our implementation of IPing will not be found within the service implementation. Set this to false and you will get the same InvalidOperationException that you would see in .Net 3.5 had you added an unimplemented contract via AddServiceEndpoint. Setting this value to true also prevents the service from emitting any metadata about the endpoint.

That's most of the hard work out of the way but sadly, Microsoft has not been kind enough to expose everything for us. We still need to write our own implementations of IInstanceProvider and IInstanceContextProvider. Below are a couple of implementations of these based on what you can find in reflector that employ a singleton instance:

internal class InstanceProvider : IInstanceProvider
{
   private object _instance;


   internal InstanceProvider(object instance)
   {
      if (instance == null)
      {
         throw new ArgumentNullException("instance");
      }
      _instance = instance;
   }


   public object GetInstance(InstanceContext instanceContext)
   {
      return _instance;
   }


   public object GetInstance(InstanceContext instanceContext, Message message)
   {
      return _instance;
   }


public void ReleaseInstance(InstanceContext instanceContext, object instance)
   {
      IDisposable disposable = instance as IDisposable;
      if (disposable != null)
      {
         disposable.Dispose();
      }
   }
}

internal class SingletonInstanceContextProvider : IInstanceContextProvider
{
   private InstanceContext _instanceContext;
   private object _syncRoot = new object();
   private ManualResetEvent _isInitialised;


   public InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel)
   {
      if (_instanceContext != null)
      {
         _instanceContext = WaitForInstanceContext();
      }


      return _instanceContext;
   }


   public void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel)
   {
      lock (_syncRoot)
      {
         this._instanceContext = instanceContext;
         if (_isInitialised != null)
            _isInitialised.Set();
      }
   }


   public bool IsIdle(InstanceContext instanceContext)
   {
      return false;
   }


   public void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext) { }


   internal InstanceContext WaitForInstanceContext()
   {
      bool wait = false;


      if (_instanceContext == null)
      {
         lock (_syncRoot)
         {
            if (_instanceContext == null)
            {
               if (_isInitialised == null)
                  _isInitialised = new ManualResetEvent(false);
               wait = true;
            }
         }
      }


      if (wait)
      {
         _isInitialised.WaitOne();
         _isInitialised.Close();
      }

      return _instanceContext;
   }
}


Once that's done, you'll need a behaviour to attach your instances of these types to the runtime like so:

public class PingBehavior : IServiceBehavior
{
   public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
   {
      foreach (ChannelDispatcherBase baseDispatcher in serviceHostBase.ChannelDispatchers)
      {
         ChannelDispatcher dispatcher = baseDispatcher as ChannelDispatcher;
         if (dispatcher != null && isPingDispatcher(dispatcher))
         {
            var dispatchRuntime = dispatcher.Endpoints[0].DispatchRuntime;
            var pingService = new Ping();
            dispatchRuntime.InstanceProvider = new InstanceProvider(pingService);
            dispatchRuntime.InstanceContextProvider = new SingletonInstanceContextProvider();
         }
      }
   }


   private bool isPingDispatcher(ChannelDispatcher dispatcher)
   {
      return dispatcher.Endpoints[0].ContractName == "IPing";
   }
...

We're now pretty much done. All that remains is configuration support. I'll assume you know how to hook up the service behaviour above and just focus on what's required for the endpoint. First, we derive a class from StandardEndpointElement specific to our standard endpoint:

public class PingEndpointElement : StandardEndpointElement
{
   protected override Type EndpointType
   {
      get { return typeof(PingEndpoint); }
   }


   protected override ServiceEndpoint CreateServiceEndpoint(ContractDescription contract)
   {
      return new PingEndpoint();
   }


   protected override void OnApplyConfiguration(ServiceEndpoint endpoint, ServiceEndpointElement serviceEndpointElement)
   {
      PingEndpoint customEndpoint = (PingEndpoint)endpoint;
   }


   protected override void OnApplyConfiguration(ServiceEndpoint endpoint, ChannelEndpointElement channelEndpointElement)
   {
      PingEndpoint customEndpoint = (PingEndpoint)endpoint;
   }


   protected override void OnInitializeAndValidate(ServiceEndpointElement serviceEndpointElement)
   {
   }


   protected override void OnInitializeAndValidate(ChannelEndpointElement channelEndpointElement)
   {
   }
}

The last class we need is a subclass of StandardEndpointCollectionElement. It only needs to be declared, not implemented at this stage:

public class PingEndpointCollectionElement : StandardEndpointCollectionElement

{
}

We're finally there. We add the above type to the new endpointExtension configuration section:

<system.serviceModel>
   <extensions>
      <endpointExtensions>
         <add name="ping" type="WcfService1.PingEndpointCollectionElement, WcfService1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </endpointExtensions>
...

Assuming you've added the service behaviour we defined above to the behaviours collection either programmatically or via configuration you should now be able to add the endpoint to the service configuration just like above:

<service name="WcfService.Service">
   <endpoint name="ws" contract="WcfService.IService" binding="wsHttpBinding" />
      <endpoint kind="ping" address="ping" />

   </service>

OK, it still requires some effort but adding these sort of endpoints is a lot more straightforward now in WCF 4.0 than it was with 3.5.

2 comments: