Developing with .NET Remoting in Mind

Using remote components requires some special considerations. These finer points are easily overlooked and can pose quite a bit of trouble for the novice .NET Remoting programmer. The next few sections consider several of them.

ByRef and ByVal Arguments

Microsoft Visual Basic .NET defaults to ByVal for all its arguments, which is different from the previous version of the language. That means the parameter is passed by value and copied into the new application domain. If it's a custom type and it's not marked <Serializable>, or if the remote object doesn't have a reference to the appropriate assembly, a SerializationException is thrown.

You also can mark arguments ByRef. In this case, you receive an exception unless the type in question is remotable (in other words, derives from MarshalByRefObject). If you pass an object that derives from MarshalByRef as a parameter, you actually end up passing a proxy to the object across the network. The recipient can then invoke the methods of this object, which will be executed on the computer where the object was created. Of course, for these methods to be executed successfully, the client that called the method and sent the remotable object must have an open channel listening for requests.

Finally, to make matters even more interesting, you can have a class that is both remotable and serializable. In this case, it's important that you choose the appropriate ByRef or ByVal identifier. Generally, ByVal is the behavior you want but it pays to be vigilant.

Exception Propagation

When you call a method of a remote object, any exceptions in that method are propagated back to your code. Therefore, you must be ready to handle errors such as SocketException, SerializationException, and RemotingException, as well as any other application-specific error condition. Ideally, the remote method will use exception handling to ensure that it catches all errors and throws a higher-level exception, such as ApplicationException, to inform the client of a problem.

You also need to think carefully about using custom exception classes. If you define these, both the server and the client need to reference an assembly where they are defined. Otherwise, the client won't recognize the specialized exception class. You must also ensure that all custom exceptions are marked with the <Serializable> attribute so they can be sent across the network.

Note

One way to avoid some of these exception propagation problems is to add a <OneWay> attribute (found in the System.Run­time.Remoting.Messaging namespace) to the method of a remotable object. This indicates that the method doesn't provide a return value and does not return any information to ByRef parameters. The client's method call will return immediately, and exceptions won't be propagated. This also means that the remote method will probably execute asynchronously to the client and that the client can't assume that the remote method has completed by the time the client call returns. Chapter 8 discusses one-way messages in detail.


Static Members

Static members are never remoted. In other words, if you execute a static member for a remote object, the code is executed locally in the client's application domain.

Another interesting point concerns the virtual methods that every object inherits from the base System.Object class. These methods, which include GetHashCode, ToString, Equals, and MemberwiseClone, will always execute locally unless they have been overridden with a custom implementation. This behavior is designed to ensure optimal performance if a remote call were required just to get the string representation of an object, it would result in an unnecessary slowdown in performance.

Private Methods

Private methods are never accessible anywhere outside the class in which they were created and are therefore not available in other application domains. Suppose, for example, that you have two MarshalByRefObject instances in two different application domains. Object A passes a delegate to object B, which points to a private method in object A. Even though object A is remotable, and even though object B has a reference to the metadata for object A, it won't be able to invoke the delegate.

Listing 11-1 shows the faulty code. Execution begins with ObjectA.Call­ObjectB and ends when ObjectB.DoSomething throws an unhandled exception.

Listing 11-1 Invalid delegate use with .NET Remoting
 Public Class ObjectA     Inherits MarshalByRefObject     Private ObjB As ObjectB     Public Sub CallObjectB()         ' Create a delegate.         Dim Ref As New MethodInvoker(AddressOf NonRemoteable)         ' Pass the delegate to a remote object.         ObjB.DoSomething(Ref)     End Sub     Private Sub NonRemoteable()         ' This method cannot be invoked remotely.     End Sub End Class Public Class ObjectB     Inherits MarshalByRefObject     Public Sub DoSomething(ByVal Ref As MethodInvoker)         ' The Ref parameter is received successfully.         ' However it cannot be invoked.         ' The next statement will fail.         Ref.Invoke()     End Sub End Class 

Public Members

If you create a public member variable, .NET automatically creates property get and property set methods. These methods execute remotely. If .NET didn't take this extra step, public member variables could lead to lost updates or inconsistent data.

A better approach is to use property procedures rather than public member variables with any object that will be exposed to other applications. And the best approach is to make server-side objects entirely stateless.

Versioning

Handling versioning issues in .NET Remoting can be a thorny problem because you need to consider the version of the assembly that exists on more than one computer. You not only have to worry about clients that try to activate older or invalid assemblies, but you might have additional headaches when remote objects attempt to exchange incompatible versions of a serializable object.

Versioning with Server-Activated Objects

To begin with, server-activated objects (singleton and SingleCall activation types) have no form of version control. Even if you create these objects using strongly named assemblies and place multiple versions in the global assembly cache (GAC), the component host always activates the most recent version when it receives a request, regardless of the version that the client expects.

The best way to avoid this problem is to never update a remote object after it is in use. Instead, you should place new versions of the server-side object at a new URI endpoint and allow the clients to migrate by updating their configuration files. This technique works well, but it does force you to update the client-side configuration files. You might be able to automate this process using some of the dynamic registration techniques described at the end of this chapter.

So how do you create multiple endpoints for different versions of a single component? You have two choices. First, you can create and host an entirely new assembly. The second (and more common) choice is to use the strong-naming tool (sn.exe) described in Chapter 2 and install your server-side objects into the GAC. You can then modify the component host's configuration file to include version information in the type attribute.

Consider Listing 11-2, for example, which registers two versions of DBComponent at two different endpoints. Note that you need to include the exact version number and public key token (which is displayed in the Windows Explorer GAC plug-in).

Listing 11-2 Providing multiple versions of a server-side object
 <configuration>    <system.runtime.remoting>       <application name="SimpleServer">          <service>             <wellknown mode="SingleCall"               type="RemoteObjects.RemoteObject,               RemoteObjects, Version 1.0.0.1, Culture=neutral,               PublicKeyToken=8b5ed84fd25209e1"               objectUri="RemoteObject" /> 
             <wellknown mode="SingleCall"               type="RemoteObjects.RemoteObject,               RemoteObjects, Version 2.0.0.1, Culture=neutral,               PublicKeyToken=8b5ed84fd25209e1"               objectUri="RemoteObject_2" />          </service>          <channels>             <channel ref="tcp server" port="8080" />          </channels>       </application>    </system.runtime.remoting> </configuration> 

The client configuration file won't change (aside from updating the URI, if required). Keep in mind, however, that you can't specify version information for a server-activated object in the client configuration file. Doing so will have no effect.

Versioning with Client-Activated Objects

With a client-activated object, the rules differ completely. When the client creates the remote object, a message is passed that includes the version assembly information for the remote assembly, provided it has a strong name. The component host then creates the requested version of the assembly. Any version information in the server configuration file is ignored.

Remember that the client automatically uses the version of the assembly that it was compiled with, provided it has a strong name. You don't specify the version information in the configuration file. If you want to alter the binding version, however, the same rules apply as those used for local development. As described in Chapter 2, you can add the <bindingRedirect> tag to the client's configuration file to explicitly override the binding behavior and instruct it to use a different version.

Versioning with Serializable Objects

The rules change yet again for serializable objects that can be exchanged between clients. The .NET Remoting framework has two different ways to pass information. It can relax the rules and use a nonversioned system (somewhat like XML Web services), where missing information is ignored if possible. Alternatively, it can tighten the rules, send a strong assembly name with every serialized object, and require an exact version match. Otherwise, a SerializationException is thrown.

By default, the SOAP Formatter uses the relaxed rules and the Binary Formatter applies the strict verification. However, you can change these options by modifying the includeVersions attribute of the formatter tag. This setting applies to outgoing messages and can be applied to the server or client configuration file. Listing 11-3 shows an example that tightens requirements for the SOAP Formatter.

Listing 11-3 Modifying versioning rules
 <configuration>    <system.runtime.remoting>       <application name="SimpleServer">          <service>             <wellknown mode="SingleCall"               type="RemoteObjects.RemoteObject, RemoteObjects"                objectUri="RemoteObject" />          </service>          <channels>             <channel ref="http server" port="8080">                <formatter ref="soap" includeVersions="true" >             </channel>          </channels>       </application>    </system.runtime.remoting> </configuration> 

This is all well and good from a theoretical viewpoint, but you're probably wondering what the practical effects of strict and relaxed versioning are. As you might expect, using relaxed versioning can mean trading short-term convenience for long-term suffering. It works fairly well when you send a serialized object (either as a parameter or as a method return value). However, receiving a serialized object is more error-prone because the expected properties might not exist.

Some coding workarounds can defend against these problems. You can implement custom serialization in your serializable types, for example, and explicitly use error-handling logic when you attempt to retrieve information that might not exist in older versions. However, these workarounds are themselves subject to error and are time-consuming to implement and maintain. I don't recommend this approach. Now that the DLL is finally eradicated from the Windows world, there is no reason to reintroduce it with your own custom logic.

In a real, evolving enterprise application, the only solution is to avoid incremental updates. If you need to update a serializable class, you should create a new version of the serializable class and put a new version of the server-activated object at a different URI. This allows a phased migration approach, which is always more successful than trying to handle multiple-client versions with a single-server version.

Interface-Based Programming

One of the most useful design techniques for .NET Remoting is interface-based programming. With interface-based programming, you define a contract that the client and server must follow. You also specify details for supplementary classes such as custom exceptions, EventArgs, and other information packages.

The goal is to concentrate all the shared information about all the remotable and serializable objects in a single assembly. You then distribute this assembly to the client and the server. The client can activate the remote type through the interface, without requiring a direct reference to the remote object's assembly. This simplifies distribution and improves security because there's no longer a need to copy the remote object assembly and distribute it to each client. It also enables you to develop the client application and server components independently after the interface has been defined. Without this technique, it becomes almost impossible to keep track of changing assemblies and update all the references appropriately.

Consider the CustomerDB and CustomerDetails example demonstrated in Chapter 2. To use interface-based programming, you create four separate projects:

  • A class library project that contains the interfaces and serializable classes (in this case, called CustomerInterfaces).

  • A client project that uses a local copy of the CustomerInterfaces assembly.

  • A remote object project that uses a local copy of the CustomerInterfaces assembly.

  • The component host project. This project still requires a local copy of the remote object assembly because it has the responsibility for creating the object. You can add a reference to this project or copy the assembly to the appropriate directory manually.

The complete Visual Studio .NET solution is included online with the code samples for this chapter.

The CustomerInterfaces Assembly

The CustomerInterfaces assembly includes all the information required by the client and the server-side component. To start with, this information includes the serializable CustomerDetails information package (as shown in Listing 11-4). (Alternatively, you can include just the interface for this class if it is subject to change. The remote object assembly will then implement the interface with a specific class.)

Listing 11-4 The serializable CustomerDetails
 <Serializable()> _ Public Class CustomerDetails     Private _ID As Integer     Private _Name As String     Private _Email As String     Private _Password As String     ' (Constructors and property procedures omitted.) End Class 

Next you need to define an interface for the CustomerDB component. This interface lists every public method and property (as shown in Listing 11-5).

Listing 11-5 The interface for CustomerDB
 Public Interface ICustomerDB     Function GetCustomerDetails(ByVal customerID As Integer) _      As CustomerDetails          Function GetCustomerDetails() As ArrayList          Sub AddCustomer(ByVal customer As CustomerDetails) _     Sub UpdateCustomer(ByVal customer As CustomerDetails)     Sub DeleteCustomer(ByVal customerID As Integer) End Interface 

In this case, the communication will be one-way (from the client to the CustomerDB object), so you don't need to define an interface for the client. If you were using bidirectional communication, you would create an IClient interface, which would allow the server to send messages to the client without having a copy of the client's assembly.

The Remote Object

The remote object changes very little. The CustomerDB class (shown in Listing 11-6) is identical except that it implements all the members of the interface and adds any private member variables.

Listing 11-6 The remotable CustomerDB
 Public Class CustomerDB     Inherits System.MarshalByRefObject     Implements CustomerDBInterfaces.ICustomerDB     Private Shared ConnectionString As String = _      "Data Source=MyServer;Initial Catalog=MyDb;" & _      "Integrated Security=SSPI"     Public Function GetCustomerDetails(ByVal customerID As Integer) _       As CustomerDetails Implements ICustomerDB.GetCustomerDetails         ' (Code omitted.)     End Function     Public Sub AddCustomer(ByVal customer As CustomerDetails) _       Implements ICustomerDB.AddCustomer         ' (Code omitted.)     End Sub     Public Sub UpdateCustomer(ByVal customer As CustomerDetails) _       Implements ICustomerDB.UpdateCustomer         ' (Code omitted.)     End Sub     Public Sub DeleteCustomer(ByVal customerID As Integer) _       Implements ICustomerDB.DeleteCustomer         ' (Code omitted.)     End Sub     Public Function GetCustomer(ByVal customerID As Integer) _       As CustomerDetails Implements ICustomerDB.GetCustomerDetails         ' (Code omitted.)     End Function     Public Function GetCustomers() As ArrayList _       Implements ICustomerDB.GetCustomerDetails         ' (Code omitted.)     End Function     ' (Private ExecuteNonQuery() method omitted.) End Class 
The Component Host

The component host uses exactly the same model as in Chapter 4. The CustomerDB object is referenced directly in the configuration file, not the interface because the component host needs to be able to instantiate the object. An interface is enough to communicate with an object once it exists, but not enough to create it.

The CustomerDB remote object is entirely stateless, so it is perfectly suited for a server-activated SingleCall object. Listing 11-7 shows the appropriate configuration file.

Listing 11-7 The component host configuration file
 <configuration>    <system.runtime.remoting>       <application name="SimpleServer">          <service>             <wellknown              mode="SingleCall"              type="DatabaseComponents.CustomerDB, DatabaseComponents"              objectUri="CustomerDB" />          </service>          <channels>             <channel ref="tcp server" port="8080" />          </channels>       </application>    </system.runtime.remoting> </configuration> 

Once again, the component host is designed as a Windows application (as shown in Listing 11-8). On startup, it just begins listening for client requests (as shown in Figure 11-1).

Listing 11-8 The component host form
 Imports System.Runtime.Remoting Public Class HostForm     Inherits System.Windows.Forms.Form     ' (Windows designer code omitted.)     Private Sub HostForm_Load(ByVal sender As System.Object,        ByVal e As System.EventArgs) Handles MyBase.Load         ' Configure the server channel.         RemotingConfiguration.Configure("ComponentHost.exe.config")         lblStatus.Text = "Listening for client requests ..."     End Sub End Class 
Figure 11-1. The component host

graphics/f11dp01.jpg

The Client

The client code needs a couple of small changes. Because it is activating the remote object through an interface, it can no longer create it directly by using the New keyword. Instead, it needs to use the System.Activator class, which provides a shared GetObject method.

Listing 11-9 shows the complete code. The remote object reference is created on startup. The GetCustomers method is invoked in response to a button click, and a DataGrid control is filled with the results (as shown in Figure 11-2).

Listing 11-9 Activating CustomerDB through an interface
 Imports System.Runtime.Remoting Imports CustomerDBInterfaces Public Class ClientForm     Inherits System.Windows.Forms.Form     ' (Windows designer code omitted.)     ' Holds the reference to the transparent CustomerDB proxy.     Private CustomerDB As ICustomerDB     Private Sub ClientForm_Load(ByVal sender As System.Object, _       ByVal e As System.EventArgs) Handles MyBase.Load         ' Configure client channel.         RemotingConfiguration.Configure("Client.exe.config")         ' Retrieve a reference to the remote object.         Dim RemoteObj As Object         RemoteObj = Activator.GetObject( _                     GetType(CustomerDBInterfaces.ICustomerDB), _                     "tcp://localhost:8080/SimpleServer/CustomerDB")         ' Access the remote object to the interface.         CustomerDB = CType(RemoteObj, CustomerDBInterfaces.ICustomerDB)         statusBar.Text = "Configuration successful" 
     End Sub     Private Sub cmdGetAll_Click(ByVal sender As System.Object, _       ByVal e As System.EventArgs) Handles cmdGetAll.Click         ' Access a remote method.         ' Because CustomerDB is a SingleCall object, this is the point         ' at which it will be created.         gridCustomers.DataSource = CustomerDB.GetCustomerDetails()     End Sub End Class 
Figure 11-2. The CustomerDB client

graphics/f11dp02.jpg

The configuration file no longer specifies the object URI, just the channel and formatter information (as shown in Listing 11-10).

Listing 11-10 The client configuration file
 <configuration>    <system.runtime.remoting>       <application name="SimpleClient">          <channels>             <channel ref="tcp client"/>          </channels>       </application>    </system.runtime.remoting> </configuration> 

Figure 11-3 shows the full set of four projects, and the required references, as a single Visual Studio .NET solution.

The solution is configured to start both the component host and the client project simultaneously (as shown in Figure 11-4), allowing easy debugging in the Visual Studio .NET IDE.

Figure 11-3. An interface-based CustomerDB solution

graphics/f11dp03.jpg

Note

Interfaces are particularly useful when you're using bidirectional communication because you can easily define all the interfaces in one assembly (such as IServer or IClient). This is the only assembly that needs to be distributed. The server references this assembly to gain the ability to talk to the client. The client also references this assembly to gain the ability to talk to the server. We'll use a similar approach to allow different parts of an application to communicate in Chapter 15.


Figure 11-4. CustomerDB solution settings

graphics/f11dp04.jpg

Problems with Interface-Based Remoting

Interface-based programming isn't without a few quirks. The client can no longer use the simple approach of creating a remote object by using the New keyword because the client does not have a reference to the full assembly. An interface can't be instantiated directly because it is just an object definition, not an object. The solution using the Activator class is not ideal because it bypasses the configuration file. You can still use a configuration file and the RemotingConfiguration.Configure method to apply channel and formatter settings. However, you have to specify the object URI in your code. This means that if the object is moved to another computer or exposed through another port, you need to recompile the code.

You can solve this problem in several ways. One approach is just to create your own custom configuration setting in the custom <appSettings> section of the client configuration file, as shown in Listing 11-11.

Listing 11-11 Putting the object URI in the configuration file
 <configuration>   <appSettings>     <add key="CustomerDB"          value="tcp://localhost:8080/SimpleServer/CustomerDB" />     </appSettings>    <system.runtime.remoting>       <application name="SimpleClient"> 
          <channels>             <channel ref="tcp client"/>          </channels>       </application>    </system.runtime.remoting>     </configuration> 

You can then retrieve this setting using the ConfigurationSettings.AppSettings collection:

 Dim RemoteObj As Object RemoteObj = Activator.GetObject( _             GetType(CustomerDBInterfaces.ICustomerDB), _             ConfigurationSettings.AppSettings("CustomerDB")) 

To simplify life even more, you can create a custom factory class. A factory is a dedicated class that abstracts the object-creation process. It allows your client code to remain blissfully unaware of whether the returned object is a remote object created through the Activator class or a class that's instantiated locally. In Listing 11-12, the factory uses a shared method so that the client won't need to create an instance of the factory class before using it.

Listing 11-12 Using a factory
 Public Class CustomerDBFactory     Public Shared Function Create() As ICustomerDB         Dim RemoteObj As Object         RemoteObj = Activator.GetObject( _             GetType(CustomerDBInterfaces.ICustomerDB), _             ConfigurationSettings.AppSettings("CustomerDB"))         Return CType(RemoteObj, CustomerDBInterfaces.ICustomerDB)     End Function End Class 

The client now needs only a single line of code to create the object:

 CustomerDB = CustomerDBFactory.Create() 

If you want, you can expand the intelligence of the CustomerDBFactory. For example, it might read a configuration file or contact an XML Web service to determine whether it should create a local or remote ICustomerDB instance.

Stubs

You can solve the assembly distribution problem in another way. You can create a separate "stub" class that represents the remote object instead of using an interface. A stub is a bare skeleton of class. It contains all the public members, but it doesn't define any implementation. Listing 11-13 shows a stub for the CustomerDB class.

Listing 11-13 A CustomerDB stub
 Public Class CustomerDB     Inherits System.MarshalByRefObject     Public Function GetCustomerDetails(ByVal customerID As Integer)       As CustomerDetails     End Function     Public Sub AddCustomer(ByVal customer As CustomerDetails)     End Sub     Public Sub UpdateCustomer(ByVal customer As CustomerDetails)     End Sub     Public Sub DeleteCustomer(ByVal customerID As Integer)     End Sub     Public Function GetCustomer(ByVal customerID As Integer) _       As CustomerDetails     End Function     Public Function GetCustomers() As ArrayList     End Function End Class 

The client can use this stub to create the remote CustomerDB object, as long as the fully qualified class name of the stub matches the fully qualified class name of the remote class. Visual Studio .NET won't raise a compile-time error because it has the metadata it needs to validate your client code:

 ' Configure the client. RemotingConfiguration.Configure("SimpleClient.exe.config") ' Create the object using the stub. Dim CustomerDB As New CustomerDB.CustomerDB() 

The stub approach is really a crude way to simulate an interface. Generally, interfaced-based programming is preferable. One potential problem with the stub approach is that the client might accidentally create a local copy of the stub rather than the remote object. This occurs if the stub namespace differs even slightly from the remote object namespace. To defend against this possibility, you should add code to the constructor that throws an exception, or just make the default constructor private. Listing 11-4 shows the first approach.

Listing 11-14 A stub that cannot be instantiated
 Public Class CustomerDB     Inherits System.MarshalByRefObject     Public Sub New()         Throw New ApplicationException("Stub cannot be instantiated.")     End Sub     ' (Remainder of code omitted.) End Class 


Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

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