Saturday, December 5, 2009

Host WCF as Windows Service without Installing

I am working on a project that involves a centralized UI that then coordinates with agents to send out instructions. The agents will be WCF endpoints and to ease deployment/minimize the install footprint, they will install as windows services with a nice installer rather than hanging onto an IIS webspace directly.

In this post I want to walk you through the process of creating that WCF host programmatically, as well as a small little tweak that will allow you to run and test without having to install the service. It's actually quite easy to do but it's one of those things that can produce an "ah-hah!" moment if you haven't done it before.

To start with, let's create a simple service and operation contract. Create a new interface, call it IHelloWorldService, and add a method, like this:


[ServiceContract(Namespace="http://wintellect.com/sample")]
public interface IHelloWorldService
{
    [OperationContract]
    [FaultContract(typeof(Exception))]
    string Hi();
}

You'll have to add references and usings for System.ServiceModel.

Next, implement the interface:


public class HelloWorldService : IHelloWorldService
{
    const string REPLY = "Hello, world."; 

    public string Hi()
    {
        return REPLY; 
    }

}

That's all it takes to make your WCF service. Now we just need a way to host it. Add another reference to System.ServiceProcess. Create a new class called HelloServiceHost and derive it from ServiceBase. Now we'll get a little interesting:


public class HelloServiceHost : ServiceBase
{
    const string CONSOLE = "console";

    public const string NAME = "HelloWorldService"; 

    ServiceHost _serviceHost = null;

    public HelloServiceHost()
    {
        ServiceName = NAME; 
    }

    public static void Main(string[] args)
    {
        if (args.Length == 1 && args[0].Equals(CONSOLE))
        {
            new HelloServiceHost().ConsoleRun();
        }
        else
        {
            ServiceBase.Run(new HelloServiceHost());
        }
    }

    private void ConsoleRun()
    {
        Console.WriteLine(string.Format("{0}::starting...",GetType().FullName));

        OnStart(null);

        Console.WriteLine(string.Format("{0}::ready (ENTER to exit)", GetType().FullName));
        Console.ReadLine();

        OnStop();

        Console.WriteLine(string.Format("{0}::stopped", GetType().FullName));
    }

    protected override void OnStart(string[] args)
    {
        if (_serviceHost != null)
        {
            _serviceHost.Close();
        }

        _serviceHost = new ServiceHost(typeof(HelloWorldService));
        _serviceHost.Open();
    }

    protected override void OnStop()
    {
        if (_serviceHost != null)
        {
            _serviceHost.Close();
            _serviceHost = null;
        }
    }
}

Let's break it down. We're derived from service base, and need a service host to actually present the WCF end point. In the constructor, we set the service name. In the Main class, which is called for both console applications and service applications, we check the arguments. If you pass in "console" as an argument, it will create a new instance and run it directly. Otherwise, it calls into the ServiceBase to run as a Windows service.

The ConsoleRun is stubbed out to give you a nice message that it is starting, then manually call the start event and wait for a line to be entered. When you hit ENTER, it will close the service and shut down. This is what enables us to take the generated executable, and call it, like:
HelloWorldService.exe console
and then debug interactively without having to install it as an actual service.

The OnStart and OnStop overrides do basic housekeeping for registering the types of the endpoints that will be active (this one process could host several endpoints, if you so desired) and shutting down when finished.

Of course, you'll need to add an app.config file and come up with your end point name. It can run on existing IIS ports, but must have a unique URL. I set mine up like this:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="HelloWorldService.HelloWorldService" behaviorConfiguration="HelloWorldServiceBehavior">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:80/helloworld"/>
          </baseAddresses>
        </host>
        <endpoint address=""
                  binding="wsHttpBinding"
                  contract="HelloWorldService.IHelloWorldService"/>
        <endpoint address="mex"
                  binding="mexHttpBinding"
                  contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="HelloWorldServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="False"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Once this is in place, you should be able to compile, browse to the directory it is in, and run the program with the "console" argument (no dashes or anything special). Once it is waiting for a keypress, go to a web browser and navigate to http://localhost/helloworld?wsdl and you should see the WSDL for your service. At this point you could hook it up to a test harness, use SoapUI, or about any other method for testing and start doing things with the service.

Of course, eventually the goal is to allow it to be installed as a Windows service. For that, add a reference to System.Configuration.Install. Add a new class and call it ProjectInstaller. Derive the class from Installer. The class will look like this:


[RunInstaller(true)]
public class ProjectInstaller : Installer
{
    const string DESCRIPTION = "Hello world service host.";
    const string DISPLAY_NAME = "Hello World Service";

    private ServiceProcessInstaller _process;
    
    private ServiceInstaller _service;

    public ProjectInstaller()
    {
        _process = new ServiceProcessInstaller();
        _process.Account = ServiceAccount.LocalSystem;
        _service = new ServiceInstaller();
        _service.ServiceName = HelloWorldService.NAME;
        _service.Description = DESCRIPTION;
        _service.DisplayName = DISPLAY_NAME; 
        Installers.Add(_process);
        Installers.Add(_service);
    }
}

Now you can navigate to the command line and use installutil.exe to install it as a bona fide Windows service.

I'm not including a demo project because this should be fairly straightforward and I've included 100% of the code in this post ... I bet you might not have known hosting a WCF service outside of an actual web project was so simple!

Jeremy Likness