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

A .NET Remoting Component Server

4.20/5 (4 votes)
5 Jun 2006CPOL5 min read 3   448  
An article about a simple and installable component server.

Introduction

Not too long ago, I posted an article about marshalling a remote client request from one AppDomain to another. You can read about it here.

I have since had some very interesting responses. Some of the issues raised were about loading and unloading of an AppDomain at runtime. That has motivated me to write this article about a simple component server that supports the installation and re-installation of components at runtime. The purpose of this article is to demonstrate that you can replace a component at runtime without causing any interruptions to the clients that rely upon the component's services.

The installable component

To make your component installable, you must add an installer class to your component's project. You must override the OnAfterInstall method. Then, you must add a setup project to your Visual Studio solution and enable the custom actions. That having said, in a nutshell, I will explain how the installer co-operates with the component server to swap the components in and out while clients continue to make requests on that component.

The component server supports an interface for the installation and un-installation of a component.

C#
public interface IComponentServer
{
    void OnBeforeInstall(string objectUri, ComponentInstallInfo installInfo);
    void OnAfterInstall(string objectUri, ComponentInstallInfo installInfo);
    void OnBeforeUninstall(string objectUri);
    void OnAfterUninstall(string objectUri);
}

The interface methods resemble the overridable event methods of the installer class. Indeed, the installer connects to the component server via IPC, and tells the component server to install or swap out a component. The objectUri identifies the component, and the ComponentInstallInfo provides the necessary information for the component server to do its work. Here is some insight into how the installer class accomplishes it:

C#
IComponentServer server;
IComponentServer ComponentServer
{
    get
    {
        const string url = "ipc://IpcLocal/ComponentServer.rem";
        if (this.server == null)
            this.server = (IComponentServer)
              Activator.GetObject(typeof(IComponentServer), url);
        return this.server;
    }
}

ComponentInstallInfo installInfo;
ComponentInstallInfo InstallInfo
{
    get
    {
        if (this.installInfo == null)
        {
            this.installInfo = new ComponentInstallInfo();
            this.installInfo.TypeName = "ComponentServer.Component";
            this.installInfo.AssemblyName = "Component";
            this.installInfo.ConfigFileName = "Component.dll.config";
            this.installInfo.ApplicationBase = 
              System.IO.Path.GetDirectoryName(
              this.Context.Parameters["assemblyPath"]);
        }

        return this.installInfo;
    }
}

string objectUri = "Component.rem";

protected override void OnAfterInstall(System.Collections.IDictionary savedState)
{
    this.ComponentServer.OnAfterInstall(this.objectUri, this.InstallInfo);
}

Assuming that the component server is up and running, we can connect and communicate to it over the IPC channel. We pass the component's identifying objectUri and a ComponentInstallInfo. The data of the ComponentInstallInfo is the information required for the creation of an AppDomain and for the instantiation of the component.

You need to build your components to be enabled for marshalling across an AppDomain. You can read about it in my other article.

In the next section, I shall explain how the component server manages the components.

The component server

The component server maintains a ComponentsManager. The ComponentsManager maintains a list of installed components that is keyed off the component's objectUri. The CrossDomainMarhaller gets a reference for a component from the ComponentManager. You can find this in the CrossDomainMarshaller's MessageSink class.

C#
public IMessage SyncProcessMessage(IMessage msg)
{
    string objectUri = (string)msg.Properties["__Uri"];
    if (objectUri == null)
        return this.nextSink.SyncProcessMessage(msg);
    else
    {
        try
        {
            return ComponentsManager.Instance().GetComponent(objectUri).Marshal(msg);
        }
        catch (Exception e)
        {
            return new ReturnMessage(e, (IMethodCallMessage)msg);
        }
    }
}

The ComponentsManager's primary job is to install a component at runtime. I have designed it so that the creation of the AppDomain and the instantiation of the component is deferred to when a client's request is received for the first time. Here is the ComponentsManager.Install method:

C#
public void InstallComponent(string objectUri, 
            ComponentInstallInfo installInfo)
{
    string key = "/" + objectUri;

    ComponentInfo componentInfo = null;
    if (!this.Components.ContainsKey(key))
    {
        // this is the first time a component is installed
        componentInfo = new ComponentInfo();
        componentInfo.InstallInfo = installInfo;
        componentInfo.CrossDomainMarhaller = new CrossDomainMarshaller();

        // defer the creation of the AppDomain to the time
        // when a client request is made for the first time
        this.Components.Add(key, componentInfo);

        // publish a CrossDomainMarshaller
        RemotingServices.Marshal(componentInfo.CrossDomainMarhaller, objectUri);
        Console.WriteLine("New component installed, {0}", objectUri);
        return;
    }

    lock (this.SyncRoot)
    {
        componentInfo = this.Components[key];
        if (componentInfo.AppDomain == null)
        {
            // a component for this objectUri was
            // previously installed but no AppDomain and 
            // component instance was ever created
            // we will just re-initialize the info
            // object with the install info
            componentInfo.InstallInfo = installInfo;
            Console.WriteLine("Ccomponent re-installed" + 
                " with new InstallInfo, {0}", objectUri);
            return;
        }
    }
   
    /////////////////////////////////////////////////
    // getting here means that a component
    // is alive and needs to be replaced
    /////////////////////////////////////////////////

    ComponentInfo newComponentInfo = new ComponentInfo();
    newComponentInfo.InstallInfo = installInfo;
    // create the new AppDomain
    newComponentInfo.AppDomain = CreateAppDomain(objectUri, 
       installInfo.ApplicationBase, installInfo.ConfigFileName);
    // create a component instance in that new AppDomain
    newComponentInfo.ComponentInstance = (ICrossDomainComponent)
      newComponentInfo.AppDomain.CreateInstanceAndUnwrap(
      installInfo.AssemblyName, installInfo.TypeName);
    // re-assign the CrossDomainMarshaller to new info structure
    newComponentInfo.CrossDomainMarhaller = 
         componentInfo.CrossDomainMarhaller;

    lock (SyncRoot)
    {
        // now replace the component
        this.Components[key] = newComponentInfo;
    }

    // we shall put the old component into
    // a separate thread for AppDomain unloading
    ThreadPool.QueueUserWorkItem(this.UnloadAppDomainThread, 
                                 componentInfo);

    Console.WriteLine("Component replaced, {0}", objectUri);
}

There is a private class, ComponentInfo, which serves only to contain various object references. Upon installing a component for the first time, a ComponentInfo object is created and added to a collection of components. The interesting point to note here is that we are creating a new instance of a CrossDomainMarshaller and publishing it to the component's objectUri. This will allow a client to connect to the component via Activator.GetObject(typeof(IComponent), "tcp://localhost:1234/Component.rem");. The reference of the CrossDomainMarshaller is also assigned to the ComponentInfo structure so that we can disconnect it properly when the component is uninstalled.

You may observe next that no AppDomain has been created yet. The AppDomain will be created only when a client connects for the first time. So, if you install the component one more time before the AppDomain is created, then only the install information is changed.

However, once a client has connected and made its first call into the component, then the install process is a replacement of the old component with the new one. We create a new AppDomain, instantiate the component in that new AppDomain, and assign everything to a new ComponentInfo structure. Finally, the old ComponentInfo structure is moved to be processed in a worker thread.

The big reason why the old ComponentInfo is processed in a separate thread is that the AppDomain must not be unloaded until all client calls have ended. Here is the worker thread procedure:

C#
void UnloadAppDomainThread(object item)
{
    ComponentInfo info = (ComponentInfo)item;

    while (info.ComponentInstance.Refcount > 0)
        Thread.Sleep(1000);

    AppDomain.Unload(info.AppDomain);
    Console.WriteLine("AppDomain unloaded.");
}

The thread loops until the Refcount decrements to zero. Implementing the component properly ensures that, for every client call, a reference count is incremented and decremented. You might want to download this article's source code and look at the implementation code.

Conclusion

What I have presented here is a component server for stateless components. I did not want to make the problem more complex than necessary. The model can support stateful components as well. That is a bit more complicated to implement because of the state transfer from the outgoing instance to the new instance. A component server for stateless installable components is nevertheless very useful.

In the interest of simplicity, I have omitted some important code. Firstly, the installer code should verify that the component server is running, before communicating with it. Secondly, when the component server exits, it should save the information about all installed components and read it back upon restart.

I hope that many of you will be inspired to develop something interesting along this concept of a component server for installable components. Please let me know about it and all the possible issues that you may encounter.

How to use the project

  • First compile the entire solution.
  • Start the component server.
  • Install the components.
  • Start the client.

Observe the console output of both the client and the server. While the client communicates with the server, modify the components so some different text can be printed to the Console. Now, install the modified components again. You should observe that the modified components take effect. And all of this without interrupting the client.

A note on the HTTP channel

One rather unexplained exception is thrown when the HTTP channel is used. It appears that the return message cannot be properly serialized by the SOAP formatter. It does work, however, when the HTTP channel is combined with the binary formatter.

License

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