Zurück

Sending a DataSet across the Wire using .NET Remoting

More Information on installing the .Net Framework click here.
Download
Visual Studio Project from this Article,


Overview

If you have ever worked with DCOM or CORBA, then you have an understanding of how to work with Remote Objects and how to deal with application communications across boundaries. .NET Remoting is Microsoft’s new infrastructure that provides a rich set of classes that allow developers to ignore most of the complexities of deploying and managing remote objects. In .NET Remoting, calling methods on remote objects is nearly identical to calling local methods.

Remoting is a framework built into the common language runtime (CLR) that can be used to build sophisticated distributed applications and network services. When a client creates an instance of a remote object, it receives a Proxy to the class instance on the server. All methods called on the Proxy will automatically be forwarded to the remote class and any results will be returned to the client. From the client's perspective, this process is no different than making a local call.

To use .NET remoting to build an application in which two components communicate directly across an application domain boundary, you need to build only the following:

  • A remotable object.
  • A host application domain to listen for requests for that object.
  • A client application domain that makes requests for that object.

Even in a complex, multiclient/multiserver application, .NET remoting can be thought of in this way. The host and the client application must also be configured with the remoting infrastructure and you must understand the lifetime and activation issues that the remoting infrastructure introduces.

  • Proxy objects. When a client creates an instance of a remote object, it receives a proxy to the class instance on the server. All methods called on the proxy will automatically be forwarded to the remote class and any results will be returned to the client. From the client's perspective, this process is no different than making a local call. Any exceptions thrown by the remote object will automatically be returned to the client. This enables the client to use normal try and catch blocks around sections of the code to trap and deal with exceptions.
  • Object passing. All objects created remotely are returned by reference and have to derive from MarshallByRefObject. Objects passed as parameters to a remote method call can be forwarded by value or by reference. The default behavior is pass by value provided the object in question is marked by the custom attribute [serializable]. Additionally, the object could implement the ISerializable interface, which provides flexibility in how the object should be serialized and deserialized. Objects that are not marshal by reference or marshal by value are not remotable.
  • Activation models. Remote objects can easily be created from a client by calling new. The framework contains enough "intelligence" to realize you are dealing with a remote object and will ensure an instance of the object gets created in the relevant remote application. Creating instances of remote objects is not limited to default constructors; you can even do this using a constructor that requires one or more parameters. The Activator class contains two methods, CreateInstance and GetObject, that can also be used to create an instance of remote objects. The former can be used in place of new to create an object instance while the latter is normally used to connect to an object at a specified URL.
  • Stateless and Stateful objects. The framework makes a provision for creating remote objects as stateless. When an object is configured as SingleCall, it will be created when a method is called on that object. The object processes the call, returns an optional result, and is then collected by the garbage collector. This way the client is always connected to a fresh object with each call. Configuring an object as a Singleton ensures that all clients will be connected to the same object whenever a call is made to that object. ClientActivated objects allow the client to pass parameters to the constructor of a remote object when it gets created. Each activation request for a client activated object (Activator.CreateInstance or new in combination with entries in the configuration file) on the client results in a new object on the server.
  • Channels and Serialization. When a client calls a method on a remote object, the remoting framework automatically serializes any data associated with the request and transports the data to the remote object using a channel. Some of the more popular channels supported are HTTP, TCP, and SMTP. In the case of HTTP, the framework uses the SOAP protocol to transport data in XML format from the client to the server and back. The default serialization formatter for HTTP is a SOAP formatter. Since programmers can create custom formatters for use with any channel, the remoting framework can be configured to work with any external .NET Framework on other platforms. The TCP channel uses plain sockets and Binary Serialization by default and can be used to communicate with any object on a remote server.

Example

The following example, we will show how to transfer DataSets between different computers. DataSets were built to work with .NET remoting capabilities. Not only are DataSets designed to be marshaled accross boundaries, but they also have capabilities specifically designed to make multi-tier development more efficient.

Creating the Server

This class needs to be in a DLL that can be called by the server and can also be referenced by the client (so that the client knows what methods are available to call). We create a class library called «MyServer», using Visual Studio. We then create a new class called «RemoteServer». It is important that the «RemoteServer» class is inherited from MarshalByRefObject which tells .NET how this class should be transfered over the network. An object can be passed from one place to another in two ways. When you pass something like a number or a string, you usually only want to pass the value, this is called marshaling by value. The other way of passing an object is by reference. Instead of just copying the data within an object, a reference to the object is passed. The receiver of the object can then call methods on the object directly. This is how most objects in C# are passed - if you pass a DataTable to a method, you are passing a reference to that DataTable, not making a copy of it. Behind the scenes, .NET automatically builds the proxy object to represent the object on the other machine you are calling.

using System;
using System.Data;
using System.Data.SqlClient;

namespace Akadia.MyServer
{
    // This class represents the Server Object, which can be
    // accessed remotly using the Proxy Object.

    public class RemoteServer : MarshalByRefObject
    {
        // Method to be called by client that returns a DataSet
        // across the wire.

        public DataSet GetDataSet()
        {
            DataSet ds = null;

            string strConnect = "server=XEON;database=Northwind;
                                 user id=sa;password=manager";
            SqlConnection conn = new SqlConnection(strConnect);
            try
            {
                conn.Open();
                string strSQL = "SELECT CustomerID,ContactName FROM Customers";
                SqlDataAdapter sda = new SqlDataAdapter(strSQL,conn);
                ds = new DataSet();
                sda.Fill(ds,"Customers");
            }
            finally
            {
                conn.Close();
            }
            return ds;
        }
    }
}

Hosting the Server Object

We now have an object designed to be called from remote, it is however not enough. We cannot just compile the code and wait for .NET to find it. For our purposes, though, we will build a small server that sits on the computer Focus and waits for requests. Our server has two parts: a small console application and a configuration file. Lets look at the application first. We create a new console application called «TestApp». Once the application is created, we add a reference to the «MyServer» DLL, so that the server can create the appropriate object - the «RemoteServer».

   





Add a Reference to the
«MyServer» DLL from the Test Application «TestApp»

using System;
using System.Runtime.Remoting;
using System.IO;
using System.Reflection;

namespace Akadia.TestApp
{
    // Set up a fully functional server listening for requests.
    // It is important that the TestApp has a reference to the
    // assembly containing the server code (MyServer). The Server
    // can be tested with thw following URL from any Web-Browser
    // http://<ServerHost>:8086/MyServer/RemoteServer.soap?WSDL

    class TestApp
    {
        static void Main(string[] args)
        {
            // Get the Application Directory
            string strAppDir = Path.GetDirectoryName(
                Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName);

            // Use the Configuration File to configure and start the Server
            string strConfigFile = strAppDir + "\\TestApp.exe.Config";
            RemotingConfiguration.Configure(strConfigFile);

             // Keep the Server alive to respond to requests
            Console.WriteLine("Server running and waiting for requests ...");
            Console.WriteLine("Press [ENTER] to shut down Server");
            Console.ReadLine();
        }
    }
}

The Configuration File

The Configuration File contains the heart of what the server is supposed to do. The configuration file's name follows the remoting convention - the executable name followed by .Config - TextApp.exe.Config.

<configuration>
    <system.runtime.remoting>
        <application name="MyServer">
            <service>
                <wellknown mode="Singleton"
                type="Akadia.MyServer.RemoteServer, MyServer"
                objectUri="RemoteServer.soap"/>

            </service>
            <channels>
                <channel ref="http" port="8086"/>
            </channels>
        </application>
    </system.runtime.remoting>
</configuration>



(1)

(2)




(3)



 

(1) - The Name of the application that will be exposed.

(2) - The first attribute mode indicates how the object should be accessed, the activation model

There are 2 activation models for Remote Objects:

Server Side Activation

The Server Side Activation (you can also call this method “Well Known” activation) is when the Remote object is created and executed totally on the server side while the client creates a proxy to trick it into thinking that the Object is available on the client side. Even in this activation model we have 2 separate ways to deal with the state fullness of the Object:

  • Single Call
The SingleCall flag notifies the server that each remote method call into the server will create a new instance of the object on the server which means there will be no state kept for that object on the server between method calls.
  • Singleton
The Singleton flag notifies the server that remote method calls into the server do not destroy the instance of the remote object on the server after the method returns which means subsequent calls from the client can take advantage of the state of the object made from previous calls.

Client Side Activation

The second activation model is the Client Side Activation, and that is when the client is allowed to pass parameters to the constructor of a remote object when it gets created. The trick here is that the Object in question has to have the [Serializable] attribute set.

As you can see in the example, the object is a singleton - we want to have only one server running.

The next attribute, type, identifies the type of object to create. The first part of the argument (before the comma) is the classname (RemoteServer), along with its namespace (Akadia.MyServer). The second part of the argument (after the comma) is the name of the assembly that contains the class. In this example, the assembly and the DLL are one and the same, so the assembly has the same name as the DLL (MyServer).

The objectURI specifies how the object will be referenced, this value will become part of the URL, that will be used to find and communicate with the server (http://focus:8086/MyServer/RemoteServer.soap)

(3) - This information says that the server will be available via HTTP, using port 8086.

Testing the Server

Although we do not yet have a client, we now have everything we need to run the server. Of course, we should test to make sure the server is working correctly. Fortunately, we habe a generic client we can use that already knows how to talk HTTP, the web browser.

The remoting services automatically create a special call for describing the interface of a remote application. The format for the description is called WSDL (Web Service Description Language), which is XML based. You can access it directly from a web browser - of course you must start the server application first.

Start the Server:

Test the Server from any web browser:

Although you can go through the output to determine information about the server, the important thing is taht, by getting any response back, we know the server is running, click here for the full output.

Creating the client

Now that we have proven that the server is working, the last step is to build a client application to talk to the server. We do this with yet another console application. It needs to have a reference to the MyServer DLL so that it knows what methods are available.

  




Add a Reference to the «MyServer» and System.Runtime.Remoting DLL from the Client Application «MyClient»

using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Data;
using Akadia.MyServer;

namespace Akadia.MyClient
{
    class ShowDataSet
    {
        // Remoting client
        // You must add a reference to MyServer, and to System.Runtime.Remoting

        [STAThread]
        static void Main(string[] args)
        {
            // Register a channel
            HttpChannel ch = new HttpChannel(8088);
            ChannelServices.RegisterChannel(ch);

            try
            {
                // Get a proxy to the server object
                RemoteServer dss =
                    (RemoteServer)Activator.GetObject(typeof(RemoteServer),
                    "http://focus:8086/MyServer/RemoteServer.soap");

                // Call the method on the proxy
                DataSet ds = dss.GetDataSet();

                // Write out the results
                PrintDataTable(ds.Tables[0]);
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            Console.WriteLine("\r\nPress enter to exit");
            Console.ReadLine();
        }

        // Writes out the passed DataTable
        public static void PrintDataTable(DataTable dt)
        {
            foreach(DataRow dr in dt.Rows)
            {
                // Step through each column in the row and write it out
                foreach(DataColumn dc in dt.Columns)
                {
                    if (dc.Ordinal > 0)
                        Console.Write(", ");
                    if (dr.RowState == DataRowState.Deleted)
                    {
                        // For Deleted rows, need to access the original value
                        Console.Write(dr[dc,DataRowVersion.Original].ToString().Trim());
                    }
                    else
                    {
                        // Otherwise, just write out the regular version
                        Console.Write(dr[dc].ToString().Trim());
                    }
                }
                Console.WriteLine("");
            }
        }
    }
}

Use appropriate namespaces

using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Data;
using Akadia.MyServer;

There are a bunch of namespaces here. Channels contains classes for setting up a channel - a channel is used to transport messages. The Http namespace has the specific channel capabilities for using HTTP, as you would expect.

Register a channel

HttpChannel ch = new HttpChannel(8088);
ChannelServices.RegisterChannel(ch);

This code creates a new HTTP channel and registers it for use. The argument to the HttpChannel's constructor is the port on which communication should occur. You may have noticed that this port is not the same as the one we used for the server. That difference is deliberate - messages go out on one port and return on another. You cannot use the same port for both sides on the same machine, although you can use the same port on multiple machines - therefor we used different ports.

Get a reference from the server

RemoteServer dss =
   (RemoteServer)Activator.GetObject(typeof(RemoteServer),
   "http://focus:8086/MyServer/RemoteServer.soap");

This is the real magic line. The call to the Activator GetObject() method creates a proxy for a currently running remote object, server-activated well-known object, or XML Web service. It really just setup to call the object remotely. The information we are providing includes the type of the object we want and the location of the real object. The Activator does the rest for us. We must typecast the result to be a RemoteServer, though, because GetObject() returns a regular object.

One interesting point here is - even though this code has created the proxy for us, it has in no way connected to the server or confirmed that the real object exists. Doing so woulkd be inefficient, because it would require a round-trip. The connection won't be tested until we call a method on the object.

Call object via proxy

DataSet ds = dss.GetDataSet();
PrintDataTable(ds.Tables[0]);

Even though we know the reference returned to us is not the RemoteServer, but is, in fact, a proxy, we can treat it as though it really were the object. The code simply calls the methods on the object as though it were local !

In this case case, the server is asked to generate a DataSet and return it. The client, with no code of its own create a DataSet, nonetheless is handed one. This code would work if there were no way for the client to talk to the database, because the client never talks to the database. The PrintDataTable() method simply prints out the DataSet content.

Testing the Client

After all, we have to install the client code on any machine. First you have to install the Microsoft .NET Framework Version 1.1 Redistributable Package. The package includes everything you need to run applications developed using the .NET Framework. You can download it from Microsofts Download Site.

Conclusion

After all that, the output from the client is not very exiting - it simply shows the output from the table Customers on one of our SQL-Servers. However, the example doesn't matter as much as the power behind the concepts. With very little code is was possible to create a distributed application.