Thursday, 11 March 2010

Dynamically creating a ServiceHost for all ServiceContracts in an assembly

I’m currently shoehorning about 100 WCF services into an existing application that uses .NET Remoting. In order to be consistent with the Remoting code they’ll be exposed using wsHttpBinding. In a simple world this would mean that I could keep them completely separate from the existing code base by deploying them to IIS (the target environment is Windows 2003) as .svc files. Unfortunately IIS isn’t on the target environment required list and as this isn’t our product we can’t mandate that it should be. The upshot of this is that we have to self-host the services which is going to require 100 instances of ServiceHost (1 per service contract).

As I don’t fancy hard coding all the service contracts into the hosting code I need a quick way of setting up the hosting. On the plus side I’m leaving well alone as much of the existing code as possible, which means that I’m putting all the service contracts in a single separate assembly/class library. So what I’ve come up with is some helper code that can be pointed at an assembly to create a service host for each service contract in that assembly. The principle is pretty simple:

  • Reflect all classes in the assembly that implement interfaces with ServiceContractAttribute
  • Create a ServiceHost for each of these class and add to an array

I should highlight that although the methodology does facilitate a generic approach to life-cycle management, this is simply an elegant hack. No application should sensibly self-host this many services. As I understand it (and I’d be grateful for any corrections if I’m talking nonsense here) IIS uses a single listener and fires up the services as required, then keeps them alive for a finite time. Each instance of ServiceHost will have it’s own listener and keep the service loaded for the lifetime of the host even if no client ever calls it. So apart from the lack of scalability inherent in this, there is also a start up delay as all the ServiceHost instances are initialised.I can't vouch for the scalability of this - according to the 2nd edition of Programming WCF Services incoming calls are dispatched by monitoring threads to the I/O completion pool which has 1000 threads by default. It's not entirely clear whether the monitoring threads are pooled so I don't know how much more/less efficient this is than hosting in IIS.

This HostHelper class exposes 2 methods:

  • GetServiceTypes()

    Returns all the classes that can be exposed as WCF services (i.e. either implement an interface that has ServiceContractAttribute or have the attribute themselves)

  • GetServiceContractType()

    Returns the contract (interface) for the service class

I’ve designed it to sit in the same assembly as the service contracts (hence the use of GetExecutingAssembly() to self-reference), but one could just as easily pass in the assembly to be reflect as a parameter:

public static class HostHelper
{
   public static Type GetServiceContractType(Type type)
   {
       if (!type.IsClass)
       {
           throw new InvalidOperationException();
       }

       Type[] contractInterfaces = type.GetInterfaces();

       foreach (Type contractInterface in contractInterfaces)
       {
           if (contractInterface.GetCustomAttributes(
               typeof(ServiceContractAttribute), true).Length > 0)
           {
               return contractInterface;
           }
       }

       if (type.GetCustomAttributes(
           typeof(ServiceContractAttribute), true).Length > 0)
       {
           return type;
       }

       throw new InvalidOperationException();
   }

   public static List<Type> GetServiceTypes()
   {
       Assembly assembly = Assembly.GetExecutingAssembly();
       Debug.Assert(assembly != null);

       List<Type> serviceTypes = new List<Type>();
       Type[] types = assembly.GetTypes();

       foreach (Type type in types)
       {
           bool isWCFType = IsWCFType(type);

           if (isWCFType)
           {
               serviceTypes.Add(type);
           }
       }

       return serviceTypes;
   }

   private static bool IsWCFType(Type type)
   {
       if (type.IsClass)
       {
           if (type.GetCustomAttributes(typeof(ServiceContractAttribute), true).Length > 0)
           {
               return true;
           }
           else
           {
               Type[] classInterfaces = type.GetInterfaces();

               foreach (Type classInterface in classInterfaces)
               {
                   if (classInterface.GetCustomAttributes(
                       typeof(ServiceContractAttribute), true).Length > 0)
                   {
                       return true;
                   }
               }
           }
       }

       return false;
   }
}

The code that runs up the multiple service hosts can be put into a console application:

class Program
{
   private const string BASE_ADDRESS = "http://localhost:8889/";

   private static List<ServiceHost> m_Hosts;

   static void Main(string[] args)
   {
       List<Type> types = HostHelper.GetServiceTypes();

       if (types.Count > 0 && m_Hosts == null)
       {
           m_Hosts = new List<ServiceHost>();
       }

       foreach (Type type in types)
       {
           Type contract = HostHelper.GetServiceContractType(type);
           BindingElement bindingElement = new HttpTransportBindingElement();
           Binding binding = new CustomBinding(bindingElement);
           string fullBaseAddress = string.Concat(BASE_ADDRESS,type.Name);

           ServiceHost host = new ServiceHost(type, new Uri(fullBaseAddress));

           host.AddServiceEndpoint(contract, binding, "");

           ServiceMetadataBehavior metaDataBehavior = new ServiceMetadataBehavior();
           metaDataBehavior.HttpGetEnabled = false;
           host.Description.Behaviors.Add(metaDataBehavior);

           host.AddServiceEndpoint(typeof(IMetadataExchange), binding, "MEX");

           Console.WriteLine("{0}: Opening host for {1}", type.Name, DateTime.Now.ToString());
           host.Open();
           Console.WriteLine("{0}: Host for {1} is open", type.Name, DateTime.Now.ToString());

           m_Hosts.Add(host);
       }
      
       Console.Read();

       if (m_Hosts != null && m_Hosts.Count > 0)
       {
           foreach (ServiceHost host in m_Hosts)
           {
               if (host.State == CommunicationState.Opened)
               {
                   host.Close();
               }
           }
       }
   }
}

I’ve added metadata exchange endpoints so proxies could be generated using SvcUtil against http://localhost:8889/ActivityMeasureDataAccess, but basically all the service hosts are stored in the m_Hosts member variable.

This example could easily be ported to a Windows Service to operate like a poor man’s WAS, although it would need handling for the Faulted event to shore up the reliability:

static void host_Faulted(object sender, EventArgs e)
{
   ServiceHost host = sender as ServiceHost;
   Debug.Assert(host != null);

   Debug.Assert(host.State == CommunicationState.Faulted);

   m_Hosts.Remove(host);

   Type serviceType = host.Description.ServiceType;
   Debug.Assert(host.BaseAddresses.Count == 1);
   Uri baseAddress = host.BaseAddresses[0];
   host = new ServiceHost(serviceType, baseAddress);

   ⁄⁄ Other host setup code here

   m_Hosts.Add(host);
}

Which basically shuts down the faulted host, removes it from the global list, and replaces it with one that has the same settings.

1 comment:

  1. Many thanks for the post, I have the error that it couldn't convert the type to the contract.

    ReplyDelete