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:
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:
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.
AppDomain appDomain;
IServiceControl serviceCtrl;
protected override void OnStart(string[] args)
{
this.appDomain = CreateAppDomain( ...);
this.serviceCtrl =
(IServiceControl)appDomain.CreateInstanceAndUnwrap(...);
this.serviceCtrl.OnStart(args);
}
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:
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.
public class ServiceConfigInfo
{
string applicationBase;
public string ApplicationBase
{
get { return applicationBase; }
set { applicationBase = value; }
}
string configFileName;
public string ConfigFileName
{
get { return configFileName; }
set { configFileName = value; }
}
string assemblyName;
public string AssemblyName
{
get { return assemblyName; }
set { assemblyName = value; }
}
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.
protected override void OnAfterInstall(
System.Collections.IDictionary savedState)
{
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];
string filePath =
Environment.GetFolderPath(
Environment.SpecialFolder.CommonApplicationData);
filePath = Path.Combine(filePath, "ServiceHost");
Directory.CreateDirectory(filePath);
filePath = Path.Combine(filePath, "ServiceOne.config.xml");
StreamWriter writer = new StreamWriter(filePath);
using (writer)
{
XmlSerializer serializer =
new XmlSerializer(typeof(ServiceConfigInfo));
serializer.Serialize(writer, configInfo);
}
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:
protected override void OnStart(string[] args)
{
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);
}
this.appDomain = CreateAppDomain(configInfo);
this.serviceCtrl =
(IServiceControl)appDomain.CreateInstanceAndUnwrap(
configInfo.AssemblyName, configInfo.TypeName);
this.serviceCtrl.OnStart(args);
}
Here is the method responsible for creating the AppDomain
:
AppDomain CreateAppDomain(ServiceConfigInfo configInfo)
{
Evidence evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = base.ServiceName;
setup.ShadowCopyFiles = "true";
setup.CachePath = configInfo.ApplicationBase;
setup.ShadowCopyDirectories = configInfo.ApplicationBase;
setup.ApplicationBase = configInfo.ApplicationBase;
setup.ConfigurationFile = configInfo.ConfigFileName;
setup.PrivateBinPath = configInfo.ApplicationBase;
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