Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Hosting multiple services in separate AppDomains

4.61/5 (9 votes)
20 Jun 2007CPOL3 min read 1   871  
An article that discusses hosting multiple services in separate AppDomains

Introduction

When writing numerous little services, you may feel a bit guilty to know that you are wasting system resources where each little service occupies its own process. However, hosting your services in a common process is also problematic because a bug in any of your services can affect the other ones as well. Also, if you ever wanted to upgrade one service, you would have to shutdown, install new, and restart the entire service host.

The ideal solution for hosting multiple services is one where you can independently install each service without shutting down the service host and without affecting any other services. This article will show you how to accomplish that.

Methodology

The idea is simple. You want to develop your services as components separate from the service host. The services are compiled into class libraries -- perhaps even one component per class library -- and then individually deployed apart from the service host. When the service control manager starts a service, the service host will know how to load the component and run it. The service components are a kind of plug-in to the service host.

Any sort of plug-in architecture requires the creation of the component in a secondary AppDomain. This is because the CLR does not permit the unloading of assemblies in any way other than through unloading the secondary AppDomain.

Implementation

Because the service host must discover the location of the service components, load them into a secondary AppDomain and start the service. We need to agree on a minimal interface that will be declared in a shared assembly. The interface should at least provide a service's OnStart and OnStop methods. Here they are:

C#
public interface IServiceControl
{
    void OnStart(string[] args);
    void OnStop()
}

The service component must implement this interface so that the service host can use it. Creating a service host is quite straightforward. Here is its Main implementation:

C#
static void Main(string[] args)
{
    Service serviceOne = new Service("ServiceOne");
    Service serviceTwo = new Service("ServiceTwo");

    ServiceBase.Run(new ServiceBase[] { serviceOne, serviceTwo });
}

When you start or stop the service from the Services Control Panel, the service control manager will cause the OnStart or OnStop methods to be invoked. It is inside these methods where the secondary AppDomain is created and unloaded.

C#
AppDomain appDomain;
IServiceControl serviceCtrl;

// create the AppDomain, create and 
// start an instance of the service component
protected override void OnStart(string[] args)
{
    // create an AppDomain
    this.appDomain = CreateAppDomain( ...);
    
    // create the service control
    this.serviceCtrl = 
        (IServiceControl)appDomain.CreateInstanceAndUnwrap(...);
    
    // start the real service component
    this.serviceCtrl.OnStart(args);
}

// stop the service and unload the AppDomain
protected override void OnStop()
{
    this.serviceCtrl.OnStop();
    AppDomain.Unload(this.appDomain);
}

The service component must be a MarshalByReference type of component and implement the IServiceControl interface. This is what it needs to look like in principle:

C#
public class ServiceOne : MarshalByRefObject, IServiceControl
{
    public void OnStart(string[] args)
    {
        EventLog eventLog = new EventLog();
        eventLog.Source = "ServiceOneComponent";
        eventLog.WriteEntry("ServiceOneComponent version 2.1 has started.");
    }
    public void OnStop()
    {
        EventLog eventLog = new EventLog();
        eventLog.Source = "ServiceOneComponent";
        eventLog.WriteEntry("ServiceOneComponent version 2.1 has stopped.");
    }
}

You create this service component in a separate assembly. To make this assembly installable, you will need to add an installer class to it and also provide a separate set-up project. One important part is the implementation of a custom action to somehow register the service with the service host. There are four items of information that the service host needs to know in order to load and start the service. Let me show this in the form of a data structure.

C#
public class ServiceConfigInfo
{
    // here is where the service component is installed
    string applicationBase;
    public string ApplicationBase
    {
        get { return applicationBase; }
        set { applicationBase = value; }
    }

    // the component's configuration file
    string configFileName;
    public string ConfigFileName
    {
        get { return configFileName; }
        set { configFileName = value; }
    }

    // the component's assembly name
    string assemblyName;
    public string AssemblyName
    {
        get { return assemblyName; }
        set { assemblyName = value; }
    }

    // the component's fully qualified type name
    string typeName;
    public string TypeName
    {
        get { return typeName; }
        set { typeName = value; }
    }
}

You use ServiceConfigInfo when you override and implement the OnAfterInstall method of the service component's Installer class. The important part is to initialize and to persist it in a location where the service host can find it.

C#
protected override void OnAfterInstall(
    System.Collections.IDictionary savedState)
{
    // create and initialize the service config info
    ServiceConfigInfo configInfo = new ServiceConfigInfo();
    configInfo.ApplicationBase = 
        Path.GetDirectoryName(this.Context.Parameters["assemblyPath"]);
    configInfo.ConfigFileName = "ServiceOne.dll.config";
    configInfo.TypeName = typeof(ServiceOne).FullName;
    configInfo.AssemblyName = 
        Assembly.GetExecutingAssembly(
        ).FullName.Split(new char[] { ',' })[0];

    // construct the service config file path and 
    // create directory if neccessary
    string filePath = 
        Environment.GetFolderPath(
        Environment.SpecialFolder.CommonApplicationData);
    filePath = Path.Combine(filePath, "ServiceHost");
    Directory.CreateDirectory(filePath);
    filePath = Path.Combine(filePath, "ServiceOne.config.xml");

    // write the service configuration for this service
    StreamWriter writer = new StreamWriter(filePath);
    using (writer)
    {
        XmlSerializer serializer = 
            new XmlSerializer(typeof(ServiceConfigInfo));
        serializer.Serialize(writer, configInfo);
    }

    // restart the service if it is running
    ServiceController serviceController = 
        new ServiceController("ServiceOne");
    if (serviceController.Status == ServiceControllerStatus.Running)
    {
        serviceController.Stop();
        serviceController.WaitForStatus(ServiceControllerStatus.Stopped, 
            new TimeSpan(0, 0, 20));
        serviceController.Start();
    }
}

If you have previously installed the service host, the service component will be restarted with the newly updated component. Here is the complete implementation of the OnStart method:

C#
protected override void OnStart(string[] args)
{
    // load the service configuration for this service
    string configPath = 
        Environment.GetFolderPath(
        Environment.SpecialFolder.CommonApplicationData);
    configPath = 
        Path.Combine(configPath, 
        string.Format(@"ServiceHost\{0}.config.xml", base.ServiceName));
    if (!File.Exists(configPath))
    {
        string msg = 
            string.Format(
            "This service cannot start because no" + 
            " configuration data found at {0}", configPath);
        throw new Exception(msg);
    }

    ServiceConfigInfo configInfo = null;
    StreamReader reader = new StreamReader(configPath);
    using (reader)
    {
        XmlSerializer serializer = 
            new XmlSerializer(typeof(ServiceConfigInfo));
        configInfo = (ServiceConfigInfo)serializer.Deserialize(reader);
    }

    // create an AppDomain
    this.appDomain = CreateAppDomain(configInfo);
    // create the service control
    this.serviceCtrl = 
        (IServiceControl)appDomain.CreateInstanceAndUnwrap(
         configInfo.AssemblyName, configInfo.TypeName);
    // start the real service component
    this.serviceCtrl.OnStart(args);
}

Here is the method responsible for creating the AppDomain:

C#
AppDomain CreateAppDomain(ServiceConfigInfo configInfo)
{
    Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);

    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationName = base.ServiceName; // applicationName;
    setup.ShadowCopyFiles = "true";
    setup.CachePath = configInfo.ApplicationBase; // appDirectory;
    setup.ShadowCopyDirectories = configInfo.ApplicationBase; //appDirectory;
    setup.ApplicationBase = configInfo.ApplicationBase; //appDirectory;
    setup.ConfigurationFile = configInfo.ConfigFileName; // configFile;
    setup.PrivateBinPath = configInfo.ApplicationBase; //appDirectory;

    string domainName = this.GetType().Namespace + "." + base.ServiceName;
    return AppDomain.CreateDomain(domainName, evidence, setup);
}

Conclusion

This article was intended to show how you can host multiple services. Before you embark on saving system resources with the help of this article, you need to think about a host of other issues. For example, how would you manage unhandled exceptions? An unhandled exception handler can only be installed in the primary AppDomain. Regardless, I hope that you will find this article contribution useful. If you want to learn more about services and AppDomains, here are two articles that I recommend:

History

  • 20 June, 2007 -- Original version posted

License

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