.NET Remoting - Part III/IV...
Part III/IV: Remoting Applied: configuration issues and interface assemblies.
Knowledge assumed for series: VB.NET, VS.NET, Parts I and II
Remoting provides a very flexible environment for distributed applications in the .NET arena. In part one I introduced the background to distributed applications and the .NET remoting architecture that aims to support distributed applications. In part two we looked at some examples of the application of this theory, in particular we created a remotable class, server activated objects and a client activated object. In this third part we continue to look at the practicalities with an examination of related configuration issues and the use of interface assemblies. In part four we'll look at the role of IIS and the topic of asynchronous remoting.
In the examples up until this point the remoting framework has been configured programmatically, i.e. the channel and remote object have been registered. There is the additional option of declarative configuration via an XML-based configuration file. This has the distinct advantage that any configuration changes are automatically picked up - there is no need to recompile code to effect changes.
The files utilised are those standard to .NET – machine.config for machine level configuration and web.config for application specific configuration. The section of interest here is the <system.runtime.remoting> section and it has the following child elements:
| Child Elements | |
| Element | Description |
| <application> | Contains information about remote objects the application consumes and exposes. Can occur once in the <system.runtime.remoting> element. |
| <channels> | Contains channel templates. Can occur once in the <system.runtime.remoting> element. |
| <channelSinkProviders> | Contains providers of channel sinks to be inserted in the channel sink chain. Can occur once in the <system.runtime.remoting> element. |
| <debug> | Requires the .NET Remoting system to load all types when the application starts, to make it easy to catch typing errors in the configuration file. Can occur once in the <system.runtime.remoting> element. |
Looking at the application element:
|
<application name="AppName"> <lifetime/> <channels/> <service/> <client/> <soapInterop/> </application> |
| Optional Attributes | |
| Attribute | Description |
| name | Names the application. You cannot use this attribute when hosting a remote type in Internet Information Services (IIS). In other hosting scenarios, the name becomes a part of the activation URL. For details, see Activation URLs. |
| Child Elements | |
| Element | Description |
| <lifetime> | Contains information about the lifetime of all remotable objects. Can occur once in the <application> element. |
| <service> | Contains objects the application exposes. Can occur one or more times in the <application> element. |
| <client> | Contains objects the application consumes. Can occur one or more times in the <application> element. |
| <channels> | Contains channels that the application uses to communicate with remote objects. Can occur once in the <application> element. |
| <soapInterop> | Contains type mappings used with SOAP. Can occur once in the <application> element. |
and an example might be:
|
<configuration> <system.runtime.remoting> <application> <lifetime leaseTime="10S" sponsorshipTimeout="0S" renewOnCallTime="5S" leaseManagerPollTime="5S" /> <service> <wellknown type="ServerActivatedType, RemoteAssembly" objectUri="ServerType.rem" mode="Singleton" /> </service> <channels> <channel port="8080" ref="http"/> </channels> </application> <debug loadTypes="true"/> </system.runtime.remoting> </configuration> |
Most of this relates to what we've done programmatically earlier. You can see this is a Singleton SAO. For a remoting client you would specify a <client> section rather than a <service> section.
As an example we'll create a singleton SAO similar to the SAOs that we implemented in the last article (in fact we implemented a singlecall SAO – I'm choosing the very similar singleton here for completeness).
In this example we'll be using a configuration file. Add another console application project to the solution. I’ve called it DBConnectSingletonServerConfig. Add references to the remotable class assembly and to System.Runtime.Remoting, rename the module to DbConnectSingletonServer and add the following code:
|
Imports System.Runtime.Remoting Module DbConnectSingletonServer Sub Main() 'load remoting configuration"> RemotingConfiguration.Configure("DBConnectSingletonServer.exe.config") Console.WriteLine("Started server in the Singleton mode") Console.WriteLine("Press <ENTER> to terminate server...") Console.ReadLine() End Sub End Module |
Add a new XML file to the project naming it DBConnectSingletonServer.exe.config.
Modify this file to contain the following code:
|
<configuration> <system.runtime.remoting> <application> <service> <!-- set the activation mode, remotable object and its URL --> <wellknown mode="Singleton" type= "RemotingDB.DbConnect, RemotingDB" objectUri="DbConnect" /> </service> <channels> <!-- set the channel and port --> <channel ref="tcp" port="54321" /> </channels> </application> </system.runtime.remoting> </configuration> |
This should be self-explanatory, particularly if you compare with the remoting servers we created in the previous article – it does exactly the same except declaratively rather than programmatically.
Move the file to the projects bin folder. Build the project.
You've just created a remoting server capable of registering the DbConnect class for remote invocation using the Singleton activation mode via the settings in the config file.
Now configure your solution to use this remoting server, i.e. set DbConnectSingletonServer to be the startup project and specify that the project should be started without debugging. Do the same for the remoting client - you can use the same client as previously: DbConnectClientSAO. Make sure they are configured in this order. Now start the solution without debugging.
The configuration of a remoting client is similar to that of a remoting server. You configure the <client> element of the config file rather than the <service> element. I'll leave this for the reader to explore.
As introduced in the previous article in this series, interface assemblies are a preferable way of facilitating compilation of remoting clients without the need for the client project to have the assembly implementation available. An interface does not contain implementation detail, only information concerning members exposed by the class.
In this section we'll look at how to replace the assemblies used by client projects with their equivalent interfaces. Also introduced shall be the soapsuds tool, which can automatically create the interface for a remotable class.
The steps we'll need to take to produce a compete solution using SAOs on this basis are:
The majority of the code presented won't be significantly different from that already presented based on the assembly itself. Taking each step in turn:
1. The interface assembly
Create a new blank solution and add a VB.NET class library project named RemotingDBInterface; rename the default class library file to IDBConnect.vb. Replace the code with:
|
Imports System Imports System.Data Public Interface IDbConnect Function ExecuteQuery(ByVal strQuery As String) As DataSet End Interface |
Build the project. An assembly that contains the definition of the IDBConnect interface has been created.
2. The remotable object
The next step is to implement the IDBConnect interface in a class named DBConnect which must adhere to the contract defined by the interface.
Add a new class library project to the solution and call it RemotingDB. Add references to System.Runtime.Remoting and RemotingDBInterface. Rename the default class to DBConnect.vb and replace the code therein with:
|
Imports System Imports System.Data Imports System.Data.SqlClient Imports RemotingDBInterface ' Marshal-by-Reference Remotable Object Public Class DbConnect Inherits MarshalByRefObject ' Implement the IDbConnect interface Implements IDBConnect Private sqlconn As SqlConnection ' Default constructor connects to the Northwind database Public Sub New() sqlconn = New SqlConnection("data source=(local); initial catalog=Northwind;" & _ "integrated security=SSPI") Console.WriteLine("Created a new connection to the Northwind database") End Sub ' Parameterized constructor connects to the specified database Public Sub New(ByVal DbName As String) sqlconn = New SqlConnection("data source=(local); initial catalog=" & DbName & ";" & _ "integrated security=SSPI") Console.WriteLine("Created a new connection to the " & DbName & " database") End Sub Public Function ExecuteQuery(ByVal strQuery As String) As DataSet _ Implements IDbConnect.ExecuteQuery Console.Write("Starting to execute the query...") ' Create a SqlCommand to represent the query Dim sqlcmd As SqlCommand = sqlconn.CreateCommand() sqlcmd.CommandType = CommandType.Text sqlcmd.CommandText = strQuery ' Create a SqlDataAdapter object ' to talk to the database Dim sqlda As SqlDataAdapter = New SqlDataAdapter sqlda.SelectCommand = sqlcmd ' Create a DataSet to hold the results Dim ds As DataSet = New DataSet Try ' Fill the DataSet sqlda.Fill(ds, "Results") Catch ex As Exception Console.WriteLine(ex.Message, "Error executing query") End Try Console.WriteLine("Done.") ExecuteQuery = ds End Function End Class |
Build the project.
This is very similar to our earlier remotable object with the addition that the class is also implementing the IDBConnect interface in addition to deriving from the MarshalByRefObject class.
This remotable object can be exposed to clients via the remoting framework exactly as per earlier examples as the interface usage is encapsulated in the object implementation.
3. The remoting server
See article 2, Creating a SAO- SingleCall SAO. The implementation is exactly the same. As before the remoting server will register the remotable object. In this case however, the remotable object implements an interface assembly.
In this instance name the console application DBConnectSingleCallServer. Building the project then creates a remoting server capable of registering RemotingDB.DBConnect, the remotable object that implements the RemotingDBInterface.IDBConnect interface for remote invocation using the singlecall activation mode.
4. The remoting client
Now we want to create a remoting client that uses an interface instead of the actual implementation. This is achieved by simply including a reference to the interface assembly rather than that of the implementation assembly at the client side but there are a couple of further changes to be made in the code necessary because of the use of an interface assembly. The client will then extract the necessary type information and metadata for compiling and running the program from the interface.
Add a new VB.NET Windows application named DBConnectClientSAOIf to the solution. Add references to System.Runtime.Remoting and RemotingDBInterface. Add a Windows form to the project naming it DBConnectClient, setting it as the startup object for the project. The user interface implementation is as per article 2 (LINK), Creating a SAO- SingleCall SAO, the code behind differs slightly:
|
Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Imports System.Runtime.Remoting.Channels.Tcp Imports RemotingDBInterface Public Class DbConnectClient Inherits System.Windows.Forms.Form 'declare a remote object Dim dbc As IDbConnect Private Sub btnExecute_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnExecute.Click Try 'invoke a method on the remote object Me.dgResults.DataSource = dbc.ExecuteQuery(Me.txtQuery.Text) dgResults.DataMember = "Results" Catch ex As Exception MessageBox.Show(ex.Message, "Query Execution Error") End Try End Sub Private Sub DbConnectClient_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load 'create and register a TCP client channel Dim channel As TcpClientChannel = New TcpClientChannel ChannelServices.RegisterChannel(channel) 'instantiate the remote class dbc = CType(Activator.GetObject(GetType(IDbConnect), _ "tcp://localhost:54321/DBConnect"), IDbConnect) End Sub End Class |
Build the project. Set up the solution to run as previously. Run the solution and check it is working as you expect – exactly as per the earlier example but now using an interface assembly.
The main difference apparent in the code is the way the remote object is instantiated via the Activator class. This class contains methods to create types of objects locally or remotely, or obtain references to existing remote objects. Key methods are:
Activator.GetObject, which creates a proxy for a currently running remote object, server-activated well-known object, or XML Web service
CreateInstance, which creates an instance of the specified type using the constructor that best matches the specified parameters. This method is useful for activating client-activated objects as we’ll see shortly.
We've just introduced the use of interface assemblies which have the advantage that you do not have to distribute the actual implementation to clients using your remotable objects. However, using this approach you still have the overhead of having to distribute the interface assembly to.
The soapsuds tools will, given the URL of a remotable object, generate an interface assembly for the remote object. This saves considerable resources from having to distribute the interface to all of the clients. A typical usage might be:
soapsuds –NoWrappedProxy
–url:http://MyServer.com/DbConnect?wsdl
-outputAssemblyFile:RemotingDBInterface.dll
The NoWrappedProxy switch instructs SoapSuds to generate an unwrapped proxy.
Here the URL switch specifies the URL of the remote object. You normally have to append the ?wsdl to the URL to allow soapsuds to generate the metadata from the URL.
The outputAssemblyFile switch specifies the name of the file in which you want the output assembly to be created.
By default soapsuds generates wrapped proxies. These proxies store extra information like channel formatting and the URL within the proxy itself. This can save implementing these details but it also means that you cannot write code to specify this information at a later date so is not recommended ... hence the use of unwrapped proxies.
I'll leave as an exercise for the reader the task of using soapsuds to generate an interface assembly for our application. Here a couple of pointers however:
Neither of the options described thus far will work with CAOs due to the fact that they can use non-default constructors and an interface cannot contain the declaration of a constructor as it does with a method or property. This problem is generally solved using the technique of 'abstract factory patterns' where you declare as many methods as there are constructors in the remotable class. Each of these methods is responsible for creating an object in a way defined by its corresponding constructor. 'Factory' because the technique allows you to create or 'manufacture' objects in different ways.
We won't go into significant detail of the implementation here but the process involves the following:
Then when a method is invoked at the client that corresponds to a constructor on the remotable object the return value of the 'constructor method' will be a remotable object of type DBConnect.
As you can this is a couple of degrees up in complexity involving extra steps and a significant change to the remoting server implementation as well as minor changes to the client implementation.
In this article we took a look at two main remoting topics: declarative configuration and interface assemblies. In the final article in this series we complete our look at remoting with a consideration of the use of IIS as an activation agent as well as asynchronous remoting.
.NET SDK
Developing XML WebServices and Server Components with VB.NET and the .NET Framework
Mike Gunderloy
Que