Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / IIS

How to host multiple isolated WCF services within a single Windows service with zero App.config

4.40/5 (6 votes)
18 Mar 2011CPOL6 min read 43.9K  
How to host multiple isolated WCF services within a single Windows service with zero App.config

Note: The code featured in this blog posting was put together whilst working with David Marsh on the Tranquility.Net (WCF App Server), an open source .NET app server available on CodePlex.

CodePlex project: http://tranquilitydotnet.codeplex.com/

The source code for this blog entry can be found on the CodePlex project (using the link above), click on the Source Code tab and download changeset 2671 (Initial commit of source).  

[EDIT 19th MARCH 2011: For the latest version of Tranquility I recommend checking out the latest CodePlex change set.  Lots of new functionality has been added with some refactoring so I'll be updating the article shortly to reflect these changes]  

Ok, hands up who love IIS? Nope, me neither. Tired of it hogging RAM and eating up your server resources when hosting your lightweight WCF services? Why not host all your services within a single Windows service on your server. But app pooling in IIS is pretty cool right, we can’t do without that – no problem, we’ll just wrap each of our services within its own AppDomain so we can recycle as required.

For an added bonus, I’ve removed the WCF configuration a.k.a. the A, B, C’s (Address, Binding and Contract’s) from the app.Config so you can dynamically load them at runtime. This means we could retrieve this information from a central configuration service or from a database, etc.

Ooh, that all makes sense but it sounds hard? Nope, it’s easy.

Overview of Classes

Overview-of-classes

The Classes Explained

Program.cs

The main entry point of the program, on startup of Windows the service this class just creates and runs a ServiceContainer.

ServiceContainer.cs, ConfigService.cs, WcfServiceConfig.cs and WcfService.cs:

ServiceContainer-and-Config-class

When the ServiceContainer starts, it makes a call to the ConfigService to get the WCF service information (assembly, service and contract names) along with the endpoint Uri address e.g., net.tcp://localhost:8323/WcfServiceLibrary1/Service1. From this information, the WcfAppServer can create the A. B, C for the WCF configuration. The address is provided, the binding can be inferred from the start of the address and the contract and service types are read using reflection on the assembly.

Note: The service DLLs do not need to be referenced (within Visual Studio) by the WcfAppServer but the files will need to be placed in the same folder as the WcfAppServer.exe. This allows reflection to retrieve all the required type information.

You can replace the ConfigService code for a call to your own config service or database. For this demonstration, the ConfigService just returns hard coded information (see further down for code).

Once the ServiceContainer has its list of WcfServiceConfigs, it loops through each item first creating, then opening an IsolatedServiceHost for each service. A list of IsolatedServiceHosts is stored by the ServiceContainer.

ServiceContainer_OnStart

IsolatedServiceHost.cs

This class isolates each service host by creating a ServiceHostWrapper object within a new AppDomain. This ensures one service faulting will not affect any of the other services.

IsolatedServiceHost

ServiceHostWrapper.cs

This class simply serves as a wrapper around the generic .NET ServiceHost with the added bonus of being derived from MarshalByRefObject which allows for cross AppDomain communication. This enables our service container to send commands to our ServiceHost like Open, Close and Abort – which is pretty sweet! A couple of methods to easily setup the WCF config have also been included.

ServiceHostWrapper

WcfServiceInstaller.cs

installer

This code allows the Windows Service to be installed from either a setup deployment project (.msi) or by using the InstallUtil command. For this example, we’ll be using the InstallUtil command. The AfterInstall event will startup the service on our behalf.

WcfHelper.cs

WcfHelper

This class infers and creates the binding from the start of an endpoint address. For example:

net.tcp://localhost:7834/Assembly/Service requires the net tcp binding

http://localhost:7834/Assembly/Service requires the HTTP binding

Right, it’s….. SHOWTIME!

Ok, so you are ready for some code? Here’s how it’s done.

Note: This code was created using Visual Studio 2010 Ultimate edition.

Create a Windows Service from Visual Studio, called “WcfAppServer”:

Create-windows-service

Once project has been created, remove the default Service1.cs file from your project.

As we’ll be dealing with WCF services, we’ll want a reference to the System.ServiceModel and System.ServiceProcess frameworks. Just right click on References and select Add Reference…, then scroll down and double click on System.ServiceModel and System.ServiceProcess.

A third reference System.Configuration.Install is also required by the service installer class.

Add-reference

In order to be able to build the project whilst we add each file, just to make sure we haven’t entered a typo, you’ll want to comment out the reference to recently removed Service1 class.

Program.cs
C#
using System.ServiceProcess;

namespace WcfAppServer
{
  static class Program
  {
      /// <summary>
      /// The main entry point for the application.
      /// </summary>
      static void Main()
      {
          ServiceBase[] ServicesToRun;
          ServicesToRun = new ServiceBase[]
   {
    //new Service1()
   };
          ServiceBase.Run(ServicesToRun);
      }
  }
}

Next up, we’ll need to add our files, you can cut and paste from the code below or download these files from the source code links at the bottom of this blog.

WcfHelper.cs
C#
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;

namespace WcfAppServer
{
  public static class WcfHelper
  {
      public static Binding GetBinding(string address, bool isMexBinding)
      {
          if (isMexBinding)
          {
              if (address.StartsWith("net.tcp"))
                  return CreateMexNetTcpBinding();

              if (address.StartsWith("http://"))
                  return CreateMexHttpBinding();
          }
          else
          {
              if (address.StartsWith("net.tcp"))
                  return CreateTcpBinding();

              if (address.StartsWith("http://"))
                  return CreateWsHttpBinding();
          }
          throw new Exception("Cannot infer binding from address, 
		please use net.tcp or http:// address");
      }

      private static Binding CreateTcpBinding()
      {
          var binding = new NetTcpBinding()
          {
              MaxReceivedMessageSize = 25 * 1024 * 1024,
              ReceiveTimeout = new TimeSpan(0, 2, 0)
          };
          return binding;
      }

      private static Binding CreateWsHttpBinding()
      {
          Binding binding = new WSHttpBinding();
          return binding;
      }

      private static Binding CreateMexHttpBinding()
      {
          return MetadataExchangeBindings.CreateMexHttpBinding();
      }

      private static Binding CreateMexNetTcpBinding()
      {
          return MetadataExchangeBindings.CreateMexTcpBinding();
      }
  }
}
ServiceHostWrapper.cs
C#
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;

namespace WcfAppServer
{
  public class ServiceHostWrapper : MarshalByRefObject
  {
      private ServiceHost _serviceHost;
      public void CreateHost(Type serviceType, Type implementedContract, 
		Uri address, bool useDebugSettings)
      {
          _serviceHost = new ServiceHost(serviceType, address);

          Binding binding = WcfHelper.GetBinding(address.ToString(), false);

          _serviceHost.AddServiceEndpoint(implementedContract, binding, address);

          if (useDebugSettings)
              SetupMexAndDebugBehaviour(address);
      }

      private void SetupMexAndDebugBehaviour(Uri address)
      {
          var serviceMetadataBehavior = 
		_serviceHost.Description.Behaviors.Find<ServiceMetadataBehavior>();

          if (serviceMetadataBehavior == null)
          {
              serviceMetadataBehavior = new ServiceMetadataBehavior();
              _serviceHost.Description.Behaviors.Add(serviceMetadataBehavior);
          }

          _serviceHost.AddServiceEndpoint(typeof(IMetadataExchange), 
		WcfHelper.GetBinding(address.ToString(), true), "MEX");

          var serviceDebugBehavior = 
		_serviceHost.Description.Behaviors.Find<ServiceDebugBehavior>();

          if (serviceDebugBehavior == null)
          {
              serviceDebugBehavior = new ServiceDebugBehavior()
              {
                  IncludeExceptionDetailInFaults = true
              };
              _serviceHost.Description.Behaviors.Add(serviceDebugBehavior);
          }
      }

      public void Open()
      {
          _serviceHost.Open();
      }

      public void Close()
      {
          _serviceHost.Close();
      }

      public void Abort()
      {
          _serviceHost.Abort();
      }
  }
}
IsolatedServiceHost.cs
C#
using System;
using System.Reflection;
using System.ServiceModel;

namespace WcfAppServer
{
  public class IsolatedServiceHost : IDisposable
  {
      public string ServiceId { get; set; }

      ServiceHostWrapper _serviceHostWrapper;

      public event EventHandler Closed = delegate { };
      public event EventHandler Closing = delegate { };
      public event EventHandler Opened = delegate { };
      public event EventHandler Opening = delegate { };

      public CommunicationState State { get; set; }

      public IsolatedServiceHost(string serviceId, Type serviceType, 
		Type implementedContract, Uri address, bool useDebugSettings)
      {
          State = CommunicationState.Faulted;

          this.ServiceId = serviceId;

          var appDomain = AppDomain.CreateDomain(serviceId);

          string assemblyName = Assembly.GetAssembly(typeof(ServiceHostWrapper)).FullName;

          _serviceHostWrapper = appDomain.CreateInstanceAndUnwrap
	(assemblyName, typeof(ServiceHostWrapper).ToString()) as ServiceHostWrapper;

          if (_serviceHostWrapper == null)
              throw new Exception(string.Format("Exception creating instance 
		'{0}' from appdomain '{1}'", serviceType, appDomain.FriendlyName));

          _serviceHostWrapper.CreateHost(serviceType, implementedContract, 
		address, useDebugSettings);

          State = CommunicationState.Created;
      }

      public void Open()
      {
          if (State != CommunicationState.Created)
              return;

          try
          {
              Opening(this, EventArgs.Empty);

              _serviceHostWrapper.Open();

              State = CommunicationState.Opened;

              Opened(this, EventArgs.Empty);
          }
          finally
          {
              State = CommunicationState.Faulted;
          }
      }

      public void Close()
      {
          if (State != CommunicationState.Opened)
              return;

          try
          {
              Closing(this, EventArgs.Empty);

              _serviceHostWrapper.Close();

              State = CommunicationState.Closed;

              Closed(this, EventArgs.Empty);
          }
          finally
          {
              State = CommunicationState.Faulted;
          }
      }

      public void Abort()
      {
          try
          {
              _serviceHostWrapper.Abort();
          }
          finally
          {
              State = CommunicationState.Faulted;
          }
      }

      void IDisposable.Dispose()
      {
          Close();
      }
  }
}
WcfService.cs
C#
namespace WcfAppServer
{
  public class WcfService
  {
      public string Namespace { get; set; }
      public string ServiceAssemblyName { get; set; }
      public string ServiceClassName { get; set; }
      public string ContractAssemblyName { get; set; }
      public string ContractClassName { get; set; }

      public WcfService()
      {
      }

      public WcfService(string serviceAssemblyName, string serviceClassName, 
		string contractAssemblyName, string contractClassName)
      {
          ServiceAssemblyName = serviceAssemblyName;
          ServiceClassName = serviceClassName;
          ContractAssemblyName = contractAssemblyName;
          ContractClassName = contractClassName;
      }
  }
}
WcfServiceConfig.cs
C#
using System;

namespace WcfAppServer
{
  public class WcfServiceConfig
  {
      public WcfService WcfService { get; set; }
      public string Endpoint { get; set; }
  }
}
ConfigService.cs
C#
using System.Collections.Generic;

namespace WcfAppServer
{
  public class ConfigService
  {
      public static List<WcfServiceConfig> GetWcfServiceConfigs()
      {
          return new List<WcfServiceConfig>(2)
              {
                  new WcfServiceConfig()
                      {
                          Endpoint  = "net.tcp://localhost:8732/WcfServiceLibrary1/Service1",
                          WcfService = new WcfService("WcfServiceLibrary1", 
				"Service1", "WcfServiceLibrary1", "IService1")
                      },
                  new WcfServiceConfig()
                      {
                          Endpoint  = "net.tcp://localhost:8733/WcfServiceLibrary2/Service1",
                          WcfService = new WcfService("WcfServiceLibrary2", 
				"Service1", "WcfServiceLibrary2", "IService1")
                      }
              };
      }
  }
}
ServiceContainer.cs
C#
using System;
using System.Collections.Generic;
using System.Reflection;
using System.ServiceModel;
using System.ServiceProcess;

namespace WcfAppServer
{
  public class ServiceContainer : ServiceBase
  {
      public List<IsolatedServiceHost> IsolatedServiceHosts { get; set; }

      protected override void OnStart(string[] args)
      {
          var configs = ConfigService.GetWcfServiceConfigs();
          IsolatedServiceHosts = new List<IsolatedServiceHost>(configs.Count);

          foreach (var config in configs)
          {
              var isolatedServiceHost = CreateIsolatedServiceHost(config);

              isolatedServiceHost.Open();

              IsolatedServiceHosts.Add(isolatedServiceHost);
          }
      }

      private IsolatedServiceHost CreateIsolatedServiceHost(WcfServiceConfig config)
      {
          var service = config.WcfService;
          var assembly = Assembly.ReflectionOnlyLoad(service.ServiceAssemblyName);
          var serviceType = assembly.GetType(string.Format("{0}.{1}", 
		service.ServiceAssemblyName, service.ServiceClassName));
          var implementedContractType = assembly.GetType(string.Format("{0}.{1}", 
		service.ServiceAssemblyName, service.ContractClassName));

          var appDomainName = string.Format("{0}.{1}", 
		serviceType.AssemblyQualifiedName, config.Endpoint);

          return new IsolatedServiceHost(appDomainName,
                                         serviceType,
                                         implementedContractType,
                                         new Uri(config.Address),
                                         true);
      }

      protected override void OnStop()
      {
          foreach (var isolatedServiceHost in IsolatedServiceHosts)
          {
              if (isolatedServiceHost == null)
                  continue;

              if (isolatedServiceHost.State == CommunicationState.Opened)
                  isolatedServiceHost.Close();
          }
      }
  }
}

So we can install our Windows Service we’ll need to add an installer class.

WcfAppServerInstaller.cs
C#
using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;

namespace WcfAppServer
{
  [RunInstaller(true)]
  public class WcfApplicationServerInstaller : System.Configuration.Install.Installer
  {
      public WcfApplicationServerInstaller()
      {
          var serviceProcessInstaller = new ServiceProcessInstaller()
          {
              Account = ServiceAccount.LocalSystem,
              Password = null,
              Username = null
          };

          var serviceInstaller = new ServiceInstaller()
          {
              DisplayName = "Wcf App Server",
              ServiceName = "ServiceContainer",
              StartType = ServiceStartMode.Automatic
          };

          Installers.AddRange(new System.Configuration.Install.Installer[] {
                                  serviceProcessInstaller,
                                  serviceInstaller});

          AfterInstall += ServiceInstaller_AfterInstall;
      }

      void ServiceInstaller_AfterInstall(object sender, InstallEventArgs e)
      {
          var serviceController = new ServiceController("Wcf App Server");
          serviceController.Start();
      }
  }
}

Finally, update the Program.cs file to instantiate our ServiceContainer class on start up:

C#
using System.ServiceProcess;

namespace WcfAppServer
{
  static class Program
  {
      /// <summary>
      /// The main entry point for the application.
      /// </summary>
      static void Main()
      {
          ServiceBase[] ServicesToRun;
          ServicesToRun = new ServiceBase[]
   {
    new ServiceContainer()
   };
          ServiceBase.Run(ServicesToRun);
      }
  }
}

We now need to add our two WCF Service Libraries to the project which will act as sample libraries for our demo. From the Visual Studio main menu, select File > Add > Add New Project, and then select WCF > WCF Service Library, accepting the default name and location for the project.

Add-new-wcf-project-1

Repeat the process again to add a second WcfServiceLibrary, this time with the default name WcfServiceLibrary2.

To ensure each WCF Service Library returns a unique message (so we can tell them apart during the demo), update each service to return a relevant message.

WcfServiceLibrary1.Service1.cs
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace WcfServiceLibrary1
{
  // NOTE: You can use the "Rename" command on the "Refactor" menu 
  // to change the class name "Service1" in both code and config file together.
  public class Service1 : IService1
  {
      public string GetData(int value)
      {
          return string.Format("Using Service Library 1, you entered: {0}", value);
      }

      public CompositeType GetDataUsingDataContract(CompositeType composite)
      {
          if (composite == null)
          {
              throw new ArgumentNullException("composite");
          }
          if (composite.BoolValue)
          {
              composite.StringValue += "Suffix";
          }
          return composite;
      }
  }
}

And repeat for WcfServiceLibrary2.Service1.cs.

Now the next bit is optional and I’ll explain why. The WcfAppServer can load and host any WCF Service classes from a .NET assembly. All you need to do is drop the DLL into the same folder as the WcfAppServer.exe and using reflection, the service will load them from our “config service”.

For this demo, I’m going to include the project references to save having to build and copy across the DLL files manually. To add the references to our WcfAppServer project, right click on the WcfAppServer project > Add Reference…, then select Projects and double click on each of our new projects:

Add-project-reference

So with a quick “CTRL + SHIFT + B” to build the project in debug mode, we are now ready to install our new Windows Service. Just run the following commands from the Visual Studio Command Prompt:

cd C:\Projects\WcfAppServer\WcfAppServer\bin\Debug

installutil WcfAppServer.exe

install-service

Note: The Visual Studio Command Prompt can be found in the Visual Studio tools shortcut folder (Start > Programs > Microsoft Visual Studio 2010 > Visual Studio Tools > Visual Studio Command Prompt (2010)).

Now we can start up our WCF App Server Windows Service and test our hosting.

Open the Services mmc management window (Start > Control Panel > Administration Tools > Services).

Start-Service

Now that our Windows Service is up and running, we can test them using the WcfTestClient. To add this Tool to Visual Studio, from the main menu in Visual Studio, select Tools > External Tools, then enter the following details:

WcfTestClient

The WcfTestClient.exe can be found within C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\ folder.

Note: This tool is installed relative to your Visual Studio installation folder (64 bit machines will be different from the above address).

You can now run the tool from within Visual Studio. From the main menu Tools > WcfTestClient.

You can now add each of our services to the test client by right clicking on My Service Projects > Add Service…

add-service

Once you’ve added both the WCF endpoints, you’re ready to start test driving your services!

Testing WcfServiceLibrary1

testing-nettcp-1

Testing WcfServiceLibrary2

testing-nettcp-2

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)