Section 10.5. Building a Distributed Application


10.5. Building a Distributed Application

Finally, after so many definitions and abstractions, it's time to put it all to use and see how to build a server, a host app domain to host the server object, and a client application to consume the remote object. Both the host and the client application need to indicate to .NET how they intend to use remoting. The host needs to register with .NET the channels and formats on which it's willing to accept remote calls, the remote types it's willing to export, their activation modes, and their URIs (if applicable). The client application needs to register with .NET the types it wants to access remotely, and any channels to be used for callbacks. There are two ways to achieve all this: programmatically and administratively. If you use programmatic configuration, you gain maximum flexibility because at any time you can change the activation modes, object locations, and channels used. Both the client and the host can use programmatic configuration. If you use administrative configuration, you save your remoting settings in a configuration file. Both the client and the server can use administrative configuration. You can also mix and match (i.e., have some settings in the configuration files and programmatically configure others), although normally you use one or the other of these methods, not both at the same time. Administrative configuration lets you change the settings and affect the way your distributed application behaves, even after deployment; it's the preferred way to handle remoting in most cases. This section demonstrates both techniques using the same sample application. I will explain programmatic configuration first and, armed with the understanding of the basic steps, will then examine the administrative configuration settings.

10.5.1. Programmatic Channel Registration

A remoting channel is any component that implements the IChannel interface, defined in the System.Runtime.Remoting.Channels namespace. You rarely need to interact with a channel object directlyall you have to do is register them. Out of the box, .NET provides three implementations of the IChannel interface: the TcpChannel, HttpChannel, and IpcChannel classes, defined in the System.Runtime.Remoting.Channels.Tcp, System.Runtime.Remoting.Channels.Http, and System.Runtime.Remoting.Channels.Ipc namespaces, respectively. The host application needs to register which channels it wishes to use, using the static method RegisterChannel( ) of the ChannelServices class:

     public sealed class ChannelServices     {        public static void RegisterChannel(IChannel channel);        //Other methods     } 

Typically, the host will put the channel registration code in its Main( ) method, but it can register the channels anywhere else, as long as the registration takes place before remote calls are issued. Note that you can register the same channel type only once per app domain, unless you explicitly assign it a different name, as described later.

10.5.1.1 Host channel registration

The host must register at least one channel if it wants to export objects. To register a TCP or HTTP channel, the host first creates a new channel object, providing as a construction parameter the port number associated with this channel. Next, the host registers the new channel. For example:

     using System.Runtime.Remoting.Channels;     using System.Runtime.Remoting.Channels.Tcp;            //Registering TCP channel     IChannel channel = new TcpChannel(8005);     ChannelServices.RegisterChannel(channel); 

When a new remote call is accepted, the channel grabs a thread from the thread pool and lets it execute the call, while the channel continues to monitor the port. This way, .NET can serve incoming calls as soon as they come off the channel. Note that the number of concurrent calls .NET remoting can service is subject to the thread-pool limitation. Once the pool is exhausted, new requests are queued until requests in progress are complete.

To register an IPC channel, the host provides the IpcChannel constructor with the pipe's name:

     using System.Runtime.Remoting.Channels;     using System.Runtime.Remoting.Channels.Ipc;     //Registering IPC channel     IChannel ipcChannel = new IpcChannel("MyHost");     ChannelServices.RegisterChannel(ipcChannel); 

The named pipe is a global resource on the host machine, and therefore it must be unique machine-wide. No other host on the same machine can open a named pipe with the same name; doing so will yield a RemotingException.

The host can register multiple channels, like so:

     using System.Runtime.Remoting.Channels;     using System.Runtime.Remoting.Channels.Tcp;     using System.Runtime.Remoting.Channels.Http;     using System.Runtime.Remoting.Channels.Ipc;            //Registering TCP channel     IChannel tcpChannel = new TcpChannel(8005);     ChannelServices.RegisterChannel(tcpChannel);            //Registering http channel     IChannel httpChannel = new HttpChannel(8006);     ChannelServices.RegisterChannel (httpChannel);     //Registering IPC channel     IChannel ipcChannel = new IpcChannel("MyHost");     ChannelServices.RegisterChannel(ipcChannel); 

When the host instantiates a channel, .NET creates a background thread to open a socket and listen to activation requests on the port. As a result, you can run blocking operations after creating and registering a channel, because you won't affect the thread monitoring the channel. For example, this is valid host-side registration code:

     static void Main( )     {        //Registering TCP channel        IChannel channel = new TcpChannel(8005);        ChannelServices.RegisterChannel(channel);               Thread.Sleep(Timeout.Infinite);     } 

An inherent limitation of network programming is that on a given machine you can open a given port only once. Consequently, you can't open multiple channels on the same port on a given machine. For example, you can't register channels this way:

     //You can't register multiple channels on the same port     IChannel tcpChannel = new TcpChannel(8005);     ChannelServices.RegisterChannel(tcpChannel);            IChannel httpChannel = new HttpChannel(8005);     ChannelServices.RegisterChannel(httpChannel); 

Registering multiple channels targeting the same port number causes an exception to be thrown at runtime. In addition, you can register a channel type only once, even if you use different ports:

     //You can only register a channel once, so this will not work:            IChannel tcpChannel1 = new TcpChannel(8005);     ChannelServices.RegisterChannel(tcpChannel1);            IChannel tcpChannel2 = new TcpChannel(8007);     ChannelServices.RegisterChannel(tcpChannel2);//Throws RemotingException 

The same is true for the IPC channelthe host can register only a single IPC channel per app domain.

When the host application shuts down, .NET automatically frees the port (or the named pipe) so other hosts on the machine can use it. However, it's customary that as soon as you no longer need the channels, you unregister them explicitly using the static method UnregisterChannel( ) of the ChannelServices class:

     IChannel channel = new TcpChannel(8005);     ChannelServices.RegisterChannel(channel);            /* Accept remote calls here */            //When doneunregister channel(s):     ChannelServices.UnregisterChannel(channel); 

10.5.1.2 Channels and formats

The default constructors shown so far automatically select the appropriate default formatter. By default, the TcpChannel and IpcChannel classes use the binary formatter to format messages between the client and the host. The HttpChannel class uses the SOAP formatter by default. However, as stated previously, you can combine any channel with any format. The channel classes provide the following constructors:

     public class TcpChannel : <base types>     {        public TcpChannel(IDictionary properties,                          IClientChannelSinkProvider clientSinkProvider,                          IServerChannelSinkProvider serverSinkProvider);         /* Other constructors and methods  */     }            public class HttpChannel : <base types>     {         public HttpChannel(IDictionary properties,                            IClientChannelSinkProvider clientSinkProvider,                            IServerChannelSinkProvider serverSinkProvider);         /* Other constructors and methods  */     }     public class IpcChannel : <base types>     {        public IpcChannel(IDictionary properties,                          IClientChannelSinkProvider clientSinkProvider,                          IServerChannelSinkProvider serverSinkProvider);         /* Other constructors and methods  */     } 

These constructors accept a collection of key/value pairs and two sink interfaces. The collection is a dictionary of predetermined channel-configuration properties, such as the new channel's name and the port number. The two sink interfaces are where you can provide a formatter instead of accepting the default. The clientSinkProvider parameter registers a channel on the client's side (when client-side registration takes place for callbacks); the serverSinkProvider parameter registers a channel on the host's side. The available formatters for the host are SoapServerFormatterSinkProvider and BinaryServerFormatterSinkProvider, implementing the IServerChannelSinkProvider interface. The available formatters are SoapClientFormatterSinkProvider and BinaryClientFormatterSinkProvider, implementing the IClientChannelSinkProvider interface. The details of these interfaces and format-providing classes are immaterial; the important thing is that you can use one to explicitly force a message format.

Refer to the MSDN Library for more information on the configuration parameters and the way they affect the channels.


Here is how to register a SOAP formatter using a TCP channel on the host side:

     IServerChannelSinkProvider formatter;     formatter = new SoapServerFormatterSinkProvider( );            IDictionary channelProperties = new Hashtable( );     channelProperties["name"] = "MyServerTCPChannel";     channelProperties["port"] = 8005;            IChannel channel = new TcpChannel(channelProperties,null,formatter);     ChannelServices.RegisterChannel(channel); 

Note that the second construction parameter is ignored. When doing the same on the client side, you need not provide a port number, and you provide the formatter as the second (instead of the third) parameter:

     IClientChannelSinkProvider formatter;     formatter = new SoapClientFormatterSinkProvider( );            IDictionary channelProperties = new Hashtable( );     channelProperties["name"] = "MyClientTCPChannel";            IChannel channel = new TcpChannel(channelProperties,formatter,null);     ChannelServices.RegisterChannel(channel); 

10.5.2. Programmatic Type Registration

The host must indicate to .NET which objects it's willing to expose as client-activated objects and which to expose as server-activated objects (and in what mode). The client can also indicate to .NET which objects it wants to access remotely. Both host and client register these types using the static methods of the RemotingConfiguration class. Configuration can be done only once per app domain, for both the host and the client.

10.5.2.1 Host type registration

To register the instances of the MyClass type as well-known server-activated objects, the host uses the static method RegisterWellKnownServiceType( ) of the RemotingConfiguration class:

     public static void RegisterWellKnownServiceType(Type type,                                                     string objectUri,                                                     WellKnownObjectMode mode); 

The host needs to provide the server type, a URI, and the desired server-activation mode: singleton or single-call. The mode parameter is of the enum type WellKnownObjectMode, defined as:

     public enum WellKnownObjectMode {SingleCall,Singleton} 

For example, to register the type MyClass, defined as:

     public class MyClass : MarshalByRefObject     {...} 

as a single-call object, the host writes:

     Type serverType = typeof(MyClass);            RemotingConfiguration.RegisterWellKnownServiceType(serverType,                                                        "MyRemoteServer",                                                       WellKnownObjectMode.SingleCall); 

The host can also register generic remote classes. The host is required to provide a specific type parameter, such as:

     public class MyClass<T> : MarshalByRefObject     {...}     Type serverType = typeof(MyClass<int>);     RemotingConfiguration.RegisterWellKnownServiceType(serverType,                                                        "MyRemoteServer",                                                       WellKnownObjectMode.SingleCall); 

Note that the specific type parameter used must be a marshalable typethat is, either serializable or derived from MarshalByRefObject. Consequently, a generic remote type will typically place a derivation constraint from MarshalByRefObject on its generic type parameters when expecting reference type parameters:

     public class MyClass<T> : MarshalByRefObject where T : MarshalByRefObject     {...} 

If the object's URL contains an application name section, the host can prefix the URI with the application name:

     RemotingConfiguration.RegisterWellKnownServiceType(serverType,                                                        "MyApp/MyRemoteServer",                                                       WellKnownObjectMode.SingleCall); 

The host can't associate multiple URIs with the same type, even it uses different activation modes for each URI, because the more recent registration overrides the previous one:

     RemotingConfiguration.RegisterWellKnownServiceType(serverType,                                                        "MyRemoteServer1",                                                       WellKnownObjectMode.SingleCall);            //Last registration wins     RemotingConfiguration.RegisterWellKnownServiceType(serverType,                                                        "MyRemoteServer2",                                                       WellKnownObjectMode.Singleton); 

However, the host can use the same URI with multiple types:

     Type serverType1 = typeof(MyClass);     Type serverType2 = typeof(MyOtherClass);            RemotingConfiguration.RegisterWellKnownServiceType(serverType1,                                                        "MyRemoteServer",                                                       WellKnownObjectMode.SingleCall);            RemotingConfiguration.RegisterWellKnownServiceType(serverType2,                                                        "MyRemoteServer",                                                       WellKnownObjectMode.SingleCall); 

When registering a type on the host's side as a client-activated object, there is no need to provide a URI. Registering a type as a client-activated object is done using the static method RegisterActivatedServiceType( ):

     Type serverType = typeof(MyClass);            RemotingConfiguration.RegisterActivatedServiceType(serverType); 

If the client-activated object's URL contains an application name part, the host must register an application name via the ApplicationName static property:

     RemotingConfiguration.ApplicationName = "MyApp"; 

If the object's URL doesn't contain an application name, setting the ApplicationName property has no effect on client-activated objects. Setting the ApplicationName property always affects server-activated objects, making it mandatory to use the application name as part of these URLs.

10.5.2.2 Client-side type registration

.NET provides clients with a number of ways to activate remote objects. One of them is the same as for local objectsusing new. Another is by using the static methods of the Activator class, GetObject( ), and CreateInstance( ). The Activator class (discussed later in this chapter) provides quite a few overloaded versions for each method. If you use new or some version of the CreateInstance( ) method, you need to register the object on the client side as a remote object. In essence, client-side registration associates a type with a URL and URI, so that when the client tries to create an instance of that type, .NET knows it should create the object at the location specified, rather than as a local object. If you use the GetObject( ) method, you need not register the type on the client side, because the parameters to GetObject( ) contain the object's URI and URL. However, you can only use GetObject( ) for server-activated objects, so in general, type registration is a required step on the client side as well.

To register a type as a client-activated object, the client uses the RegisterActivatedClientType( ) method, providing the type and the URL:

     Type serverType = typeof(MyClass);     string url = "tcp://localhost:8005";            RemotingConfiguration.RegisterActivatedClientType(serverType,url); 

When using IPC, the URL needs to contain only the pipe's name:

     string url = "ipc://MyHost"; 

To register a type as a server-activated object, the client uses the RegisterWellKnownClientType( ) method, providing the type, the URL, and the well-known object's URI:

     Type serverType = typeof(MyClass);     string url = "tcp://localhost:8005/MyRemoteServer";            RemotingConfiguration.RegisterWellKnownClientType(serverType,url); 

When using IPC, simply append the URI to the URL:

     string url = "ipc://MyHost/MyRemoteServer"; 

Remember that the client decides whether it wants a client- or a server-activated object, and the host decides which kind of server-activated object to serve the client. This is why the client doesn't need to specify the activation mode in the call to RegisterWellKnownClientType( ).

Once registration is done on the client side, any new activation requests in the client app domain are redirected to the remote host:

     using RemoteServer;            MyClass obj = new MyClass ( );     obj.SomeMethod( ); 

10.5.3. Programmatic Configuration Example

Instead of fragmented code samples, it's now time for a more comprehensive example showing how the different steps required for programmatic configuration fit together, on both the client and the host side. As mentioned previously, since both the host and the client require the server's metadata, it's best if the server is in a class library. This section walks you through Example 10-5a fully functional distributed application. The source code accompanying this book contains the RemotingDemo (Programmatic) solution, with three projects: the ServerAssembly class library, the RemoteServerHost EXE assembly, and the Client EXE assembly. Both EXE assemblies are Windows Forms applications that allow you to select channels and activation modes. The class MyClass provides a parameter-less constructor to be used in all activation modes. The constructor brings up a message box so that you can tell when a new object is created, which is especially useful when experimenting with single-call or singleton objects. The single public method of MyClass is Count( ), which pops up a message box showing the incremented value of a counter. The counter indicates the state lifecycle in the different activation modes. The message boxes have as their captions the name of the current app domain. The RemoteServerHost application is a dialog-based application. Its Main( ) method registers TCP, IPC, and HTTP channels and then displays the dialog shown in Figure 10-11.

Displaying a form by calling Application.Run( ) is a blocking operation, and control returns to the Main( ) method only when the dialog is closed. This, of course, has no effect on the channels registered, because worker threads are used to monitor the channels. When the dialog is closed, the Main( ) method unregisters the channels and exits. The Server Host dialog lets you check how the host registers the MyClass type it exports: as client-activated, server-activated, or both. If you select Server Activated, you

Figure 10-11. Server Host lets you programmatically decide which activation modes to register


need to choose between Single Call and Singleton, using the radio buttons. When you click the Register Object button, the OnRegister( ) method is called (see Example 10-5). OnRegister( ) simply registers the object based on the user-interface selections. Note that you can register the object both as client- and server-activated.

The Client application displays the dialog shown in Figure 10-12. You can select either Client Activated or Server Activated (but not both), and then register the type with the OnRegister( ) method. The interesting part of the client application is the way it constructs the activation URL for the object registration. The helper method GetActivationURL( ) constructs a URL based on the channel selected (starts with tcp://, http://, or ipc://) and appends the URI only if Server Activated mode is selected. OnRegister( ) calls GetActivationURL( ) and then registers the type accordingly. When you click "new" in the Client of Remote Object dialog, it simply uses new to create a new instance of MyClass and calls Count( ) twice, either remotely (if you registered) or locally (if no registration took place).

Figure 10-12. Client lets you choose a channel and an activation mode


Revisiting SingletonApp

Chapter 8 introduced the class SingletonApp, used to produce a singleton Windows Forms application. While the code shown in Example 8-9 works fine, it does lack a featureif the singleton application is minimized and the user tries to launch a new instance, it would have been nice to restore the minimized app to the normal state. You can achieve that easily using an IPC channel. The first time the application is launched, in addition to creating the named mutex, it will do the following:

  • Save a copy of the activated form in a static variable

  • Register an IPC channel

  • Register a single-call object that monitors activation requests

The second time the application is launched, after detecting that there is already a running instance, it will connect to the first instance over the IPC channel and ask it to activate its main window. The first instance will check if the main window is available and is minimized, and if so will restore it (or just bring it to the foreground). When the singleton application shuts down, it unregisters the IPC channel. The source code available with this book contains this revised version of SingletonApp.


Example 10-5. Programmatic remoting configuration
 /////////////////////   ServerAssembly class library  //////////////////////////// namespace RemoteServer {    public class MyClass : MarshalByRefObject    {       int m_Counter = 0;       public MyClass( )       {          string appName = AppDomain.CurrentDomain.FriendlyName;          MessageBox.Show("Constructor",appName);       }       public void Count( )       {          m_Counter++;          string appName = AppDomain.CurrentDomain.FriendlyName;          MessageBox.Show("Counter value is " + m_Counter,appName);       }    } } //////////////////////  RemoteServerHost EXE assembly  /////////////////////////// using RemoteServer;    partial class ServerHostDialog : Form {    Button RegisterButton;    RadioButton m_SingletonRadio;    RadioButton m_SingleCallRadio;    CheckBox m_ClientActivatedCheckBox;    CheckBox m_ServerActivatedCheckBox;    GroupBox m_GroupActivationMode;       public ServerHostDialog( )    {       InitializeComponent( );    }    void InitializeComponent( )    {...}       static void Main( )    {       //Registering TCP channel       IChannel tcpChannel = new TcpChannel(8005);       ChannelServices.RegisterChannel(tcpChannel);          //Registering http channel       IChannel httpChannel = new HttpChannel(8006);       ChannelServices.RegisterChannel(httpChannel);                   //Registering IPC channel       IChannel ipcChannel = new IpcChannel("MyHost");       ChannelServices.RegisterChannel(ipcChannel);       Application.Run(new ServerHostDialog( ));          //Do not have to, but cleaner:       ChannelServices.UnregisterChannel(tcpChannel);       ChannelServices.UnregisterChannel(httpChannel);       ChannelServices.UnregisterChannel(ipcChannel);    }    void OnRegister(object sender,EventArgs e)    {       Type serverType = typeof(MyClass);          //if the client activated checkbox is checked:       if(m_ClientActivatedCheckBox.Checked)       {          RemotingConfiguration.RegisterActivatedServiceType(serverType);       }          //if the server activated checkbox is checked:       if(m_ServerActivatedCheckBox.Checked)       {          if(m_SingleCallRadio.Checked)          {             //Allow Server activation, single call mode:             RemotingConfiguration.RegisterWellKnownServiceType(serverType,                                                                "MyRemoteServer",                                                   WellKnownObjectMode.SingleCall);             }             else             {                //Allow Server activation, singleton mode:                RemotingConfiguration.RegisterWellKnownServiceType(serverType,                                                                  "MyRemoteServer",                                                    WellKnownObjectMode.Singleton);             }          }       }    } } //////////////////////////  Client EXE assembly  ///////////////////////////////// using RemoteServer;    partial class ClientForm : Form {    RadioButton m_HttpRadio;    RadioButton m_TCPRadio;    RadioButton m_ServerRadio;    RadioButton m_ClientRadio;    GroupBox m_ChannelGroup;    GroupBox m_ActivationGroup;    Button m_NewButton;    Button m_RegisterButton;       public ClientForm( )    {       InitializeComponent( );    }    void InitializeComponent( )    {...}    static void Main( )    {       Application.Run(new ClientForm( ));    }    string GetActivationURL( )    {       if(m_TCPRadio.Checked)       {          if(m_ServerRadio.Checked)          {             //Server activation over TCP. Note object URI             return "tcp://localhost:8005/MyRemoteServer";          }          else          {             //Client activation over TCP             return "tcp://localhost:8005";          }       }       if(m_HttpRadio.Checked)//http channel       {          if(m_ServerRadio.Checked)          {             //Server activation over http. Note object URI             return "http://localhost:8006/MyRemoteServer";          }          else          {             //Client activation over http             return "http://localhost:8006";          }       }       else//IPC channel       {          if(m_ServerRadio.Checked)          {             //Server activation over IPC. Note object URI             return "ipc://MyHost/MyRemoteServer";          }          else          {             //Client activation over IPC             return "ipc://MyHost";          }       }    }    void OnRegister(object sender,EventArgs e)    {       Type serverType = typeof(MyClass);       string url = GetActivationURL( );          if(m_ServerRadio.Checked)       {          RemotingConfiguration.RegisterWellKnownClientType(serverType,url);       }       else //Client activation mode       {          //Register just once:          RemotingConfiguration.RegisterActivatedClientType(serverType,url);       }    }    void OnNew(object sender,EventArgs e)    {       MyClass obj;       obj = new MyClass( );       obj.Count( );       obj.Count( );    } } 

10.5.4. Administrative Configuration

Both client and host applications can take advantage of a configuration file to specify their remoting settings instead of making programmatic calls. Although you can use whatever name you want for the configuration file, the convention is to give it the same name as the application, but suffixed with .config, like so: <app name>.exe.config. For example, if the host assembly is called RemoteServerHost.exe, the configuration file will be called RemoteServerHost.exe.config. The configuration file is an XML file, and it can be the same configuration file that captures custom version-binding policies, as discussed in Chapter 5. You'll find the remoting configuration section in the <application> tag, under the <system.runtime.remoting> tag:

     <?xml version="1.0" encoding="utf-8"?>     <configuration>        <system.runtime.remoting>           <application>                 <!--Remoting configuration setting goes here -->           </application>        </system.runtime.remoting>     </configuration> 

Both the client and the host load the configuration file using the static Configure( ) method of the RemotingConfiguration class, providing the name of the configuration file:

     public static void Configure(string filename); 

Based on the instructions in the configuration file, .NET programmatically registers the channels and the objects, so that you don't need to write appropriate code. If at runtime .NET can't locate the specified file, an exception of type RemotingException is thrown. Typically, both the client and the host application call the Configure( ) method in their Main( ) methods, but you can call it anywhere else, as long as you call it before making any remote calls:

     static void Main( )     {        RemotingConfiguration.Configure("RemoteServerHost.exe.config");        //Rest of Main( )     } 

All the design directives and limitations described in the context of programmatic configuration also apply to administrative configuration.


10.5.4.1 Visual Studio 2005 and configuration files

Visual Studio 2005 can automatically generate a configuration file prefixed with your application name. Bring up the Add New Item dialog, and select the Application Configuration File template, as shown in Figure 10-13. Name the file App.config. The name of the file does matterit must be exactly App.config.

Figure 10-13. Adding the App.config file


Visual Studio 2005 will add the App.config file to the project (with a build action set to None). Open the file and add the remoting settings to it. After every successful build, Visual Studio 2005 will copy the App.config file to the project output folder and rename it <app name>.exe.config. The old copy of the config file is erased.

If you already have a configuration file in the format of <app name>.exe.config, but not the App.config file, Visual Studio 2005 will erase it (not substitute it) and you will lose your settings.


If you adopt the convention of always naming your configuration files in the format of <app name>.exe.config, and you always place the file in the application directory, you can improve on RemotingConfiguration.Configure( ). Example 10-6 shows the static helper class RemotingConfigurationEx, with a parameter-less Configure( ) method.

Example 10-6. The RemotingConfigurationEx helper class
 public static class RemotingConfigurationEx {    public static void Configure( )    {       string fileName = AppDomain.CurrentDomain.FriendlyName + ".config";       RemotingConfiguration.Configure(fileName);    } } static void Main( ) {    RemotingConfigurationEx.Configure( ); //Automatically loads                                         //the correct file    //Rest of Main( ) } 

Recall that the Main( ) method runs in the default app domain, whose name matches that of the application EXE assembly. RemotingConfigurationEx retrieves the friendly name of the app domain, appends .config to it, and passes that as the filename for RemotingConfiguration.Configure( ). In most cases, using RemotingConfiguration.Configure( ) avoids hardcoding the configuration filename.

10.5.5. Administrative Channel Registration

The machine-wide configuration file Machine.config contains global channel definitions and configurations, under the <system.runtime.remoting> tag. In particular, it contains definitions for the HTTP, TCP, and IPC channels and points to where the types implementing them reside. You can take advantage of these definitions and reference them in your configuration file.

10.5.5.1 Host channels registration

The host needs to add a <channel> tag for each channel it wishes to register. The channels are added under the <channels> tag. For example, to register a TCP channel on port 8005, you would write:

     <?xml version="1.0" encoding="utf-8"?>     <configuration>        <system.runtime.remoting>           <application>                   <channels>                 <channel ref="tcp"  port="8005"/>              </channels>           </application>        </system.runtime.remoting>     </configuration> 

The host uses the ref attribute to refer to the predefined tcp channel. You can register different channels this way, as long as they use different ports:

     <channels>        <channel ref="tcp"  port="8005"/>        <channel ref="http" port="8006"/>        <channel ref="ipc"  portName="MyHost"/>     </channels> 

If you want to register custom channels, you can either add their definitions to the Machine.Config file and simply reference them, or include the type name and the assembly identity (including strong name) in the <channel> tag. See the MSDN Library for more information on using custom channels.


10.5.5.2 Channels and formats

When the host configures a channel as just shown, .NET uses the default formatters associated with each transport protocol. However, you can configure it to use a different formatter, using the <clientProviders> tag. For example, here are the settings required to configure the HTTP channel to use the binary formatter:

     <configuration>        <system.runtime.remoting>           <application>              <channels>                 <channel ref="http">                         <clientProviders>                       <formatter ref="binary"/>                     </clientProviders>                 </channel>              </channels>           </application>        </system.runtime.remoting>     </configuration> 

10.5.6. Administrative Type Registration

As with programmatic type registration, the host configuration file can contain a list of the objects it's willing to expose, either as client-activated objects or as server-activated objects, and in what mode. The client configuration file can contain a list of the types it wants to access remotely and their URLs and URIs, if required. When the client or host references a type, it must specify a fully qualified name (type name and its namespace) as well as the type's assembly. Any misspellings or namespaces incompatibilities will be discovered only at runtime.

10.5.6.1 Host type registration

The host uses the <service> tag to contain a list of the types it exposes. Each type has an entry indicating its activation mode and URI, if required. For example, here is how the host registers the type MyClass from the RemoteServer namespace in the ServerAssembly assembly as a client-activated object:

     <application>        <service>             <activated  type="RemoteServer.MyClass,ServerAssembly"/>         </service>      </application> 

The host can also expose the type MyClass as a server-activated object and specify the activation mode and the URI:

     <application>        <service>           <activated  type="RemoteServer.MyClass,ServerAssembly"/>           <wellknown  type="RemoteServer.MyClass,ServerAssembly"                       mode="SingleCall" objectUri="MyRemoteServer"/>        </service>      </application> 

With administrative configuration, the host is subjected to the same URI constraints as with programmatic configuration. The host can also specify an application name with the same semantics as assigning a name programmatically:

     <application name="MyApp">         ...      </application> 

To register a generic type, provide the type parameter in double square brackets. For example, to register the class MyClass<T> with an integer, you should write:

     <service>        <wellknown type="RemoteServer.MyClass[[System.Int32]],ServerAssembly"                   mode="SingleCall" objectUri="MyRemoteServer"/>     </service> 

The double square brackets are required in case you need to specify multiple generic type parameters, in which case each type parameter would be encased in a separate pair of brackets, separated by a comma. For example, to register the class MyClass<T,U> with an integer and a string, you would write:

     <service>       <wellknown type="RemoteServer.MyClass[[System.Int32],[System.String]],                          ServerAssembly" mode="SingleCall" objectUri="MyRemoteServer"/>     </service>  

10.5.6.2 Client-side type registration

The client uses the <client> tag to contain a list of the types it wants to consume remotely. It must provide the objects' URLs and URIs, if required. The client can register a given type only once, just as when registering programmatically.

To register a type as a client-activated object, the client uses the <activated> tag, providing the type's name and assembly:

     <application>        <client url="tcp://localhost:8005">           <activated  type="RemoteServer.MyClass,ServerAssembly"/>        </client>      </application> 

The object's URL is provided as an attribute of the <client> tag. To register a type as a server-activated object, the client uses the <wellknown> tag:

     <application>             <client>           <wellknown type="RemoteServer.MyClass,ServerAssembly"                       url="tcp://localhost:8005/MyRemoteServer"/>        </client>     </application> 

Note that when you use the <wellknown> tag, the URL and URI are specified as attributes of the <wellknown> tag, rather than as attributes of the <client> tag.

If you need to register multiple remote types on the client's side, you need to use multiple <client> tags:

     <application>        <client url="tcp://localhost:8005">           <activated  type="RemoteServer.MyClass,ServerAssembly"/>        </client>        <client>           <wellknown  type="RemoteServer.MyOtherClass,ServerAssembly"           url="tcp://localhost:8005/MyRemoteServer"/>        </client>      </application> 

To register remote generic types on the client side, use the same syntax for describing the type parameters as the host.

The .NET Configuration tool has what looks like visual administrative support for managing the remoting part of an application configuration file. Unfortunately, that support leaves much to be desired: the tool doesn't generate a blank configuration file, and its ability to edit existing files with existing remoting attributes is partial at best.


10.5.7. Administrative Configuration Example

Example 10-7 demonstrates a host and client using a configuration file to provide remote access to the same object as in Example 10-5. The source code accompanying this book contains the RemotingDemo (Administrative) solution, with three projects: the ServerAssembly class library, the RemoteServerHost EXE assembly, and the Client EXE assembly. Both EXE assemblies are Windows Forms applications, but this time there are no settings to select because they are all defined in the configuration file. The ServerAssembly class library is the same class library from Example 10-5, providing the class MyClass. The host configuration file exposes the MyClass type both as a client-activated object and as a single-call object, and it registers both TCP and HTTP as transport channels. The host calls RemotingConfigurationEx.Configure( ) in its Main( ) method and then displays a blank dialog. The client configuration file registers the type MyClass as a client-activated object (the client can associate the type with only a single activation mode). The client configuration file provides the type's URL, using TCP as the transport channel. The client application calls RemotingConfigurationEx.Configure( ) in its Main( ) method and then displays the client dialog. The client dialog has a single button on it, allowing you to create new remote objects.

Example 10-7. Administrative remoting configuration using configuration files
 /////////////  RemoteServerHost.exe.config : the host configuration file  //////// <?xml version="1.0" encoding="utf-8"?> <configuration>    <system.runtime.remoting>       <application>          <service>             <activated  type="RemoteServer.MyClass,ServerAssembly"/>             <wellknown  type="RemoteServer.MyClass,ServerAssembly"                         mode="SingleCall" objectUri="MyRemoteServer"/>          </service>          <channels>             <channel ref="tcp"  port="8005"/>             <channel ref="http" port="8006"/>          </channels>       </application>    </system.runtime.remoting> </configuration> /////////////////////  RemoteServerHost EXE assembly  //////////////////////////// partial class ServerHostDialog : Form {    public ServerHostDialog( )    {       InitializeComponent( );    }    void InitializeComponent( )    {...}       static void Main( )    {        RemotingConfigurationEx.Configure( );       Application.Run(new ServerHostDialog( ));    } } ////////////////  Client.exe.config: the client configuration file  ////////////// <?xml version="1.0" encoding="utf-8"?> <configuration>    <system.runtime.remoting>       <application>          <client url="tcp://localhost:8005">             <activated  type="RemoteServer.MyClass,ServerAssembly"/>          </client>       </application>    </system.runtime.remoting> </configuration> ///////////////////////////  Client EXE assembly  //////////////////////////////// using RemoteServer; partial class ClientForm : Form {    Button m_NewButton;       public ClientForm( )    {       InitializeComponent( );    }       void InitializeComponent( )    {...}       static void Main( )    {        RemotingConfigurationEx.Configure( );       Application.Run(new ClientForm( ));    }    void OnNew(object sender,EventArgs e)    {       MyClass obj;       obj = new MyClass( );       obj.Count( );       obj.Count( );    } } 

10.5.8. Creating Remote Objects

If the client has registered a type as a remote type (either programmatically or administratively), the client can use the plain new operator to create any kind of remote type, be it client- or server-activated. However, as mentioned already, .NET provides clients with several ways to connect to remote objects. These options differ in their need for pre-registration of the type and in their ability to connect to client-activated objects.

10.5.8.1 RemotingServices.Connect( )

The client can choose to connect explicitly to a remote object using the static method Connect( ) of the RemotingServices class:

     public static object Connect(Type classToProxy, string url); 

Calling Connect( ) explicitly creates a proxy to the remote object on the client side. You can use Connect( ) only to connect to server-activated objects, and you can use only the default constructor. Using the same definitions as in Example 10-5, here is how to create a remote server-activated object using Connect( ):

     Type serverType = typeof(MyClass);     string url = "tcp://localhost:8005/MyRemoteServer";            MyClass obj;                    obj = (MyClass)RemotingServices.Connect(serverType,url);     obj.Count( ); 

Connect( ) doesn't affect other attempts of the client to create instances of the type, meaning that if no type registration takes place, after a call to Connect( ), calls to new create the type locally in the client's app domain. In fact, you can even register the type, associate it with one location, and then have Connect( ) activate an instance at another location.

10.5.8.2 Activator.GetObject( )

The Activator class provides the static method GetObject( ):

     public static object GetObject(Type type,string url); 

GetObject( ) works like RemotingServices.Connect( ) does, allowing the client to connect to a server-activated object:

     Type serverType = typeof(MyClass);     string url = "tcp://localhost:8005/MyRemoteServer";            MyClass obj;                    obj = (MyClass)Activator.GetObject(serverType,url);     obj.Count( ); 

GetObject( ) doesn't require type registration on the client side, and it doesn't affect registration of the type.

10.5.8.3 Activator.CreateInstance( )

The Activator class provides many overloaded versions of the static method CreateInstance( ). CreateInstance( ) lets you create any object type (client- or server-activated) and lets you have fine-grained control over the creation process. Some of the versions require type registration before invocation. This version:

     public static object CreateInstance(Type type); 

is no different from new and creates the instance at the remote location registered:

     Type serverType = typeof(MyClass);     MyClass obj;                    obj = (MyClass)Activator.CreateInstance(serverType);     obj.Count( ); 

CreateInstance( ) also offers a generic version:

     public static T CreateInstance<T>( ); 

The generic version is, of course, superior to the object-based version, as it is type-safe:

     MyClass obj = Activator.CreateInstance<MyClass>( );     obj.Count( ); 

The following version allows you to pass in construction parameters for client-activated objects in the form of an array of objects:

     public static object CreateInstance(Type type, object[] args); 

CreateInstance( ) chooses the best-fitting constructor, similarly to how the compiler chooses a constructor. If you want to create a remote instance without registering the type beforehand, use a version that accepts an array of activation attributes:

     public static object CreateInstance(Type type,object[] args,                                                        object[] activationAttributes); 

You can pass in as an activation attribute an instance of UrlAttribute, defined in the System.Runtime.Remoting.Activation namespace:

     using System.Runtime.Remoting.Activation;     using RemoteServer;            object[] attArray = {new UrlAttribute("tcp://localhost:8005")};     Type serverType = typeof(MyClass);            //No registration is required:     MyClass obj = (MyClass)Activator.CreateInstance(serverType,null,attArray);     obj.Count( ); 

CreateInstance( ) is available for advanced creation scenarios. This version:

     public static ObjectHandle CreateInstance(string assemblyName,string typeName); 

creates an instance of the specified type and delays setting up a proxy and loading the server assembly until the object handle is unwrapped:

     using RemoteServer;            ObjectHandle handle;     handle = Activator.CreateInstance("ServerAssembly","RemoteServer.MyClass");            //Proxy is only set up here, and assembly is loaded locally     MyClass obj = (MyClass)handle.Unwrap( );     obj.Count( ); 

Note that you have to specify the fully qualified type name. Other versions of CreateInstance( ) allow you to specify locale and security information.

Table 10-1 compares the various options available when creating or activating remote objects.

Table 10-1. Creating and activating remote object options

Creating option

Requires registration

Client-activated objects

Server-activated objects

new

Yes

Yes

Yes

RemotingServices.Connect( )

No

No

Yes

Activator.GetObject( )

No

No

Yes

Activator.CreateInstance( )

Depends

Yes

Yes


10.5.8.4 Creating remote generic objects

You can use the Activator class to create instances of generic remote objects, such as this one:

     public class MyServer<T> : MarshalByRefObject     {...} 

You need to provide the specific type parameters to use, and you must provide the specific types when explicitly casting the returned object:

     string url = "tcp://localhost:8005/MyRemoteServer";     Type serverType = typeof(MyServer<int>);     MyServer<int>obj;     obj = (MyServer<int>)Activator.GetObject(serverType,url); 

You can, of course, also use the generic CreateInstance( ) method to create generic types:

     MyServer<int> obj;     obj = Activator.CreateInstance<MyServer<int>>( ); 

It would have been useful if GetObject( ) were to offer a generic version, like this:

     public static T GetObject<T>(string url); 


10.5.9. Remote Callbacks

Callbacks are just as useful in distributed applications as they are in local applications. A client can pass in as a method parameter a reference to a client-side marshal-by-reference object to be used by a remote object. A client can also provide a remote server with a delegate targeting a client-side method, so that the remote server can raise an event or simply call the client-side method. The difference in the case of remote callbacks is that the roles are reversed: the remote server object becomes the client, and the client (or client-side object) becomes the server. In fact, as far as the server is concerned, the client (or the target object) is essentially a client-activated object, because the server has no URI associated with the target object and cannot treat it as a well-known object.

To receive a remote callback, the client needs to register a port and a channel and have .NET listen on that port for remote callbacks. The problem is, how does the remote server know about that port? And for that matter, how does the remote server object know what URL to use to connect to the client? The answer is built into the remoting architecture. As mentioned already, whenever a reference to an object is marshaled across an app domain boundary, the object reference contains information about the location of the remote object and the channels the host has registered. The object reference is part of the proxy. When the server calls the proxy, the proxy knows where to marshal the call and which channel and port to use. In essence, this is also how delegates work across remoting. The client can create a delegate that targets a method on an object on the client's side and then add that delegate to a public delegate or an event maintained by the server object. Recall from Chapter 7 that every delegate definition is actually compiled to a class. The delegate class is a serializable class and is marshaled by value, which includes a serialization of the internal invocation list of the delegate. Each delegate in the list has a reference to its target object (available via the Target property of the delegate). If the target object is derived from MarshalByRefObject, the serialized delegate will contain only a proxy to the target object.

10.5.9.1 Registering callback channels

The client must register the channels on which it would like to receive remote callbacks. The client provides a port number to the channel constructor, just like the host application does:

     //Registering a channel with a specific port number on the client side,     //to enable callbacks:     IChannel channel = new TcpChannel(9005);     ChannelServices.RegisterChannel(channel); 

When the client invokes a call on the server, the client needs to know in advance which ports the host is listening on. This isn't the case when the server makes a callback to the client, because the proxy on the server side already knows the client-side port number. Consequently, the client doesn't really have to use a pre-designated port for the callback; any available port will do. To instruct .NET to select an available port automatically and listen on that port for callbacks, the client simply needs to register the channels with port 0:

     //Instructing .NET to select any available port on the client side     IChannel channel = new TcpChannel(0);     ChannelServices.RegisterChannel(channel); 

Or, if you're using a configuration file:

     <channels>        <channel ref="tcp"  port="0"/>     </channels> 

The client can also register an IPC channel for the callbacks:

     //Registering an IPC callback channel     IChannel ipcChannel = new IpcChannel("MyCallback");     ChannelServices.RegisterChannel(ipcChannel); 

An interesting scenario is when the client registers multiple channels for callbacks. The client can assign a priority to each channel, using a named property called priority as part of a collection of named properties provided to the channel constructor (similar to explicitly specifying a formatter). The client can also assign priorities to channels in the configuration file:

     <application>        <channels>           <channel ref="tcp"   port="0" priority="1"/>           <channel ref="http"  port="0" priority="2"/>        </channels>     </application> 

The channels' priority information is captured by the object reference. The remote server tries to use the channels according to their priority levels. If the client registers multiple channels but doesn't assign priorities, the host selects a channel for the call.

10.5.9.2 Remote callbacks and type filtering

Every remoting channel has a filter associated with it. The filter controls the kinds of types the channel is willing to serialize across. In a distributed application the server is inherently more susceptible to attacks than the client, and it is particularly susceptible to being handed a harmful callback object by a malicious client. To protect against such attacks, the default level of the filter is set to Low. Low-level type filtering allows only a limited set of types to be passed in as parameters to remote methods as callback objects. Types allowed under low type filtering include remoting infrastructure types and simple compositions of reference types out of primitive types. Full type filtering allows all marshalable types to be passed in as callback objects. Setting the filter level to Full does not eliminate the security threat; it just makes it your explicit decision to take the risk, rather than Microsoft's decision.

To enable callbacks, the host must set its type-filtering level to Full. In most practical scenarios the client must elevate its type filtering to Full as well, unless only very simple callback objects are involved. Changing the type filtering is done on a per-formatter-per-channel basis. You can set the type filtering to Full both programmatically and administratively. To set it programmatically, supply a set of properties to the channel, and set the TypeFilterLevel property of the channel formatter to Full. TypeFilterLevel is of the enum type TypeFilterLevel, defined as:

     public enum TypeFilterLevel     {         Full,         Low     } 

For example, here is how you programmatically set the type filtering of the binary formatter using a TCP channel on the host side:

     BinaryServerFormatterSinkProvider formatter;     formatter = new BinaryServerFormatterSinkProvider( );     formatter.TypeFilterLevel = TypeFilterLevel.Full;     IDictionary channelProperties = new Hashtable( );     channelProperties["name"] = "FullHostTCPChannel";     channelProperties["port"] = 8005;     IChannel channel = new TcpChannel(channelProperties,null,formatter);     ChannelServices.RegisterChannel(channel); 

When using an IPC callback channel, instead of port number you will need to provide the pipe's name in the portName property:

     channelProperties["portName"] = "MyClientCallback"; 

To set the filter level administratively, use the serverProviders tag under each channel, and set the filter level for each formatter. For example, to set type filtering to Full for a TCP channel on the host side, provide this configuration file:

     <channels>        <channel ref="tcp" port="8005">           <serverProviders>              <formatter ref="soap"   typeFilterLevel="Full"/>              <formatter ref="binary" typeFilterLevel="Full"/>           </serverProviders>        </channel>     </channels>  

Here is the matching client-side configuration file:

     <channels>        <channel ref="tcp" port="0">           <serverProviders>              <formatter ref="soap"   typeFilterLevel="Full"/>              <formatter ref="binary" typeFilterLevel="Full"/>           </serverProviders>        </channel>     </channels> 

Note the use of port 0 on the client side, which tells .NET to automatically select any available port for the incoming callback.

10.5.9.3 Remote callbacks and metadata

Another side effect of reversing the roles of the client and server when dealing with remote callbacks has to do with the client's metadata. At runtime, the host must be able to build a proxy to the client-side object, and therefore the host needs to have access to the object's metadata. As a result, you typically need to package the client-side callback objects (or event subscribers) in class libraries and have the host reference those assemblies. You can use the technique shown in Chapter 6 for sinking interfaces with remote event subscribers. The host needs to have access only to the metadata describing the interface, not to the actual subscribers.

10.5.9.4 Remote callbacks and error handling

On top of the usual things that can go wrong when invoking a callback, with remote callbacks there is also the potential for network problems and other wire-related issues. This is a particular concern in the case of remote event publishers, since they have to try to reach every subscriber and may have to wait a considerable amount of time for each because of network latency. However, because a publisher/subscriber relationship is by its very nature a looser relationship than that of a client and server, often the publisher doesn't need to concern itself with whether the subscriber managed to process the event successfully, or even if the event was delivered at all. If that is the case with your application, it's better if you don't publish events simply by calling the delegate. Instead, publish the events using the asynchronous event-publishing technique that uses the EventsHelper class, introduced in Chapter 7 and refined in Chapter 8.

There is something you can do on the subscriber's side to make the life of the remote publisher easier. The OneWay attribute, defined in the System.Runtime.Remoting.Messaging, makes any remote method call a fire-and-forget asynchronous call. If you designate a subscriber's event-handling method as a one-way method, the remoting infrastructure only dispatches the callback and doesn't wait for a reply or for completion. As a result, even if you publish an event by calling a delegate directly, the event publishing will be asynchronous and concurrent: it's asynchronous because the publisher doesn't wait for the subscribers to process the event, and it's concurrent because every remote subscriber is served on an impendent worker thread (remote calls use threads from the thread pool). In addition, any errors on the subscriber's side don't propagate to the publisher's side, so the publisher doesn't need to program to catch and handle exceptions raised by the event subscribers.

Because static members and methods aren't remotable (no object reference is possible), you can't subscribe to a remote static event, and you can't provide a static method as a target for a remote publisher. You can only pass a remote object a delegate targeting an instance method on the client side. If you pass a remote publisher a delegate targeting a static method, the event is delivered to a static method on the remote host side.


10.5.9.5 Remote callback example

Example 10-8 shows a publisher firing events on a remote subscriber. The source code accompanying this book contains the Remote Events solution, with three projects: the ServerAssembly class library, the RemoteServerHost EXE assembly, and the Client EXE assembly. These projects are a variation of the projects presented in Example 10-7. The host is identical to the one in Example 10-7; the only changes are in the host configuration file. The host exposes the type RemoteServer.MyPublisher as a client-activated object and as a server-activated object. The host also elevates type filtering to Full on its channels. The ServerAssembly class library contains both the subscriber and the publisher classes. Recall that this is required so that both the client and the host can gain access to these types' metadata. Note that both the publisher and the subscriber are derived from MarshalByRefObject. The publisher uses GenericEventHandler by aliasing it to NumberChangedEventHandler:

     using NumberChangedEventHandler = GenericEventHandler<int>; 

The publisher publishes to the subscribers in the FireEvent( ) method using EventsHelper. The subscriber is the MySubscriber class. The subscriber's event-handling method is OnNumberChanged( ), which pops up a message box with the value of the event's argument:

          [OneWay]     public void OnNumberChanged(int number)     {        MessageBox.Show("New Value: " + number);     } 

The interesting part of OnNumberChanged( ) is that it's decorated with the OneWay attribute. As a result, the publisher's FireEvent( ) method actually fires the event asynchronously and concurrently to the various subscribers. The client configuration file is the same as in Example 10-7, except this time the client registers the port 0 with the channel, which allows the client to receive remote callbacks. Interestingly enough, the subscriber is simple enough to pass on the client channel even with client-side type filtering set to Low. The client configuration file registers the publisher as a remote object. The client creates a local instance of the subscriber and a remote instance of the publisher, and saves them as class members. The client is a Windows Forms dialog. The dialog allows the user to subscribe to or unsubscribe from the publisher, and to fire the event. The dialog reflects the event's argument in a text box. Subscribing to and unsubscribing from the event are done using the conventional += and -= operators, as in the local case.

Example 10-8. Remote events
 /////////////  RemoteServerHost.exe.config : the host configuration file  //////// <?xml version="1.0" encoding="utf-8"?> <configuration>   <system.runtime.remoting>     <application>       <service>          <activated  type="RemoteServer.MyPublisher,ServerAssembly"/>          <wellknown  type="RemoteServer.MyPublisher,ServerAssembly"                      mode="SingleCall" objectUri="MyRemotePublisher"/>       </service>       <channels>          <channel ref="tcp" port="8005">             <serverProviders>                <formatter ref="soap"   typeFilterLevel="Full"/>                <formatter ref="binary" typeFilterLevel="Full"/>             </serverProviders>          </channel>          <channel ref="http" port="8006">             <serverProviders>                <formatter ref="soap"   typeFilterLevel="Full"/>                <formatter ref="binary" typeFilterLevel="Full"/>                </serverProviders>          </channel>       </channels>     </application>   </system.runtime.remoting> </configuration> /////////////////////   ServerAssembly class library  //////////////////////////// using NumberChangedEventHandler = GenericEventHandler<int>; namespace RemoteServer {    public class MyPublisher : MarshalByRefObject    {       public event NumberChangedEventHandler NumberChanged;       public void FireEvent(int number)       {          EventsHelper(NumberChanged,number);       }    }    public class MySubscriber : MarshalByRefObject    {       [OneWay]       public void OnNumberChanged(int number)       {          MessageBox.Show("New Value: " + number);       }    } } ////////////////  Client.exe.config: the client configuration file  ////////////// <?xml version="1.0" encoding="utf-8"?> <configuration>    <system.runtime.remoting>       <application>          <client url="tcp://localhost:8005">             <activated  type="RemoteServer.MyPublisher,ServerAssembly"/>          </client>          <channels>             <channel ref="tcp"  port="0"/>          </channels>       </application>    </system.runtime.remoting> </configuration> ///////////////////////////  Client EXE assembly  //////////////////////////////// using RemoteServer;   partial class SubscriberForm : Form {    Button m_FireButton;    Button m_SubscribeButton;    Button m_UnsubscribeButton;    TextBox m_NumberValue;       MyPublisher  m_Publisher;    MySubscriber m_Subscriber;       public SubscriberForm( )    {       InitializeComponent( );          m_Publisher  = new MyPublisher( );       m_Subscriber = new MySubscriber( );    }    void InitializeComponent( )    {...}      static void Main( )    {       RemotingConfigurationEx.Configure( );          Application.Run(new SubscriberForm( ));    }    void OnFire(object sender,EventArgs e)    {       int number = Convert.ToInt32(m_NumberValue.Text);       m_Publisher.FireEvent(number);    }    void OnUnsubscribe(object sender,EventArgs e)    {       m_Publisher.NumberChanged -= m_Subscriber.OnNumberChanged;        }    void OnSubscribe(object sender,EventArgs e)    {       m_Publisher.NumberChanged += m_Subscriber.OnNumberChanged;    } } 

10.5.10. Separating the Server Code from its Metadata

As explained previously, both the client and the host require the server assembly. The client requires the server's metadata to compile against and to build a proxy at runtime. At runtime, the host requires the server's IL to host the components and the metadata for call-marshaling purposes. If the host is doing programmatic registration, the host requires the metadata at compile time as well.

You can reduce the client application's dependency on the server class library by allowing access to the server objects via interfaces only and by splitting the server into two class libraries: one with the interface definitions only and one with the actual interface implementation. You can then deploy on the client's side only the interfaces assembly, not the assembly with the actual code, which remains on the server. The only issue now is how the client will instantiate the types that support the interfaces. In fact, there are a number of options:

  • Name the interfaces and implementation assemblies the same, including the strong name. Make sure both have the same version number. Add to the interfaces assembly the definitions of the classes implementing them, with stubbed-out implementation; this allows the client to compile. Using type registration, redirect the types to the host.

  • Use a class factory to instantiate the objects and have the factory return interfaces only. The class factory will be in a separate class library assembly altogether. The factory assembly will require access to the assembly implementing the interfaces, but no other clients will need that assembly.

  • If the remote object is server-activated, use Activator.GetObject( ) or RemotingServices.Connect( ) to create an instance of the interface. This works because the host instantiates the type behind the interface and returns only the interface. This technique doesn't work with new because it won't compile, and Activator.CreateInstance( ) will fail at runtime.

To avoid deploying assemblies with code on the client's side, you can use the SoapSuds.exe command-line utility. SoapSuds.exe extracts the metadata of the types in a server assembly and generates wrapper classes that point to the server assembly's remote location. For each public class that derives from MarshalByRefObject, SoapSuds.exe generates a class with a matching name that derives from the abstract class RemotingClientProxy.

Use command-line switches to provide the input assembly and the remote host URL:

     soapsuds -inputassemblyfile:ServerAssembly              -outputassemblyfile:ClientProxy.dll              -serviceendpoint:http://localhost:8006/MyRemoteServer 

Next, add a reference in the client's assembly to the wrapper classes assembly instead of the actual server assembly. In that respect, the wrapper classes act as a proxy to the real proxy.

The main advantage of SoapSuds.exe is that you can use it to generate wrapper classes even if the server assembly is an EXE assembly (i.e., the host and the server are the same assembly) and avoid adding a reference to the host directly. However, SoapSuds.exe has a number of limitations:

  • It hardcodes the channel and host location into the wrapper classes. If you want the client to use them to connect to other hosts, you have to programmatically set the Url property of the wrapper class.

  • The wrapper classes work only if the host is listening on HTTP channels and using the SOAP format.

  • You can use the wrapper classes to connect only to server-activated objects.

  • It's cumbersome to create wrapper classes if the host uses multiple URIs associated with the types in the input assembly.

10.5.11. Providing a Host as a System Service

Implementing a host requires only a few lines of code, as shown in Example 10-7. The downside isn't the amount of work required to provide a host; it's that the host has to be running before remote calls are issued. As a result, you are likely to provide your host in the form of a system service. .NET makes implementing a service straightforward, as shown in Example 10-9. Add to an EXE assembly a class derived from ServiceBase, which is found in the System.ServiceProcess namespace. In the Main( ) method of the assembly, run the service. Override the OnStart( ) method of ServiceBase, and either register channels and objects or load a configuration file. You will also need to include in the assembly a class derived from Installer (defined in the System.Configuration.Install namespace) to install the EXE as a system service. The Installer-derived class captures various service parameters, such as the startup mode and the account under which to run the service. Next, you need to install the service using the InstallUtil.exe command-line utility. Visual Studio 2005 can automate many of these phases, including the installer class, and it can even generate a service setup project to install the service. Please refer to the MSDN documentation for additional information on developing system services.

Example 10-9. Providing a host as a system service
 using System.ComponentModel; using System.ServiceProcess; using System.Configuration.Install;    public class MyHostService : ServiceBase {    static void Main( )    {       ServiceBase.Run(new MyHostService( ));    }    protected override void OnStart(string[] args)    {       RemotingConfigurationEx.Configure( );    } }    [RunInstaller(true)] public class HostInstaller : Installer {    ServiceProcessInstaller m_ServiceProcessInstaller;    ServiceInstaller m_ServiceInstaller;       public HostInstaller( )    {       InitializeComponent( );    }    void InitializeComponent( )    {       m_ServiceProcessInstaller = new ServiceProcessInstaller( );       m_ServiceInstaller = new ServiceInstaller( );       m_ServiceProcessInstaller.Account = ServiceAccount.LocalSystem;       m_ServiceProcessInstaller.Password = null;       m_ServiceProcessInstaller.Username = null;             m_ServiceInstaller.DisplayName = "MyHostService";       m_ServiceInstaller.ServiceName = "MyHostService";       m_ServiceInstaller.StartType   = ServiceStartMode.Automatic;          Installer[]installers = {m_ServiceProcessInstaller,m_ServiceInstaller};       Installers.AddRange(installers);    } } 

10.5.11.1 Hosting with IIS

Instead of providing your own service, you can host your remote components in the worker process that the Internet Information Server (IIS) uses to host web applications. There are a few reasons why you might want to host your components with IIS. The first is that the worker process is a service, so it will always be running when client requests are sent. The second reason is security: you can take advantage of IIS's built-in security to provide call authentication. The third is that your remote objects will also be available as web services. To host with IIS, follow these guidelines:

  • Create a new virtual root under IIS.

  • The server assembly must be in a known location. You can put it either in the GAC, or in a \bin folder under the root.

  • Server-activated objects' URIs must end with either .rem or .soap, such as MyServer.rem.

  • Avoid registering channels. IIS requires you to use HTTP, over port 80 by default. If you need a different port, configure it using the IIS snap-in. The only reason to register a channel would be to use the binary format instead of the default SOAP.

  • Don't register an application name. IIS will use the virtual root as the app name.

  • If you use a configuration file:

    • Place it in the root.

    • The configuration filename must be web.config.

You can host both client- and server-activated objects in IIS.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net