.NET Remoting

Design and Develop Seamless Distributed Applications for the Common Language Runtime

Dino Esposito

Code download available at:NetRemoting.exe(87 KB)

This article assumes you're familiar with Visual Basic .NET

Level of Difficulty123

SUMMARY

Prior to the advent of .NET, DCOM was the underlying technology for remote communications between Windows-based applications. But DCOM is quirky to set up and configure and not as interoperable as it should be. In .NET, XML Web Services and .NET Remoting are a seamless and effective answer to the demand for tools to build distributed applications.

This article provides a primer on .NET Remoting with insights into the internal plumbing. Important aspects of remoting, such as channels, object lifetime management, and clients for remote objects are discussed. In addition, some practical examples are provided.

Contents

A Short History of DCOM
What is .NET Remoting?
Isolated Units of Processing
Interprocess Communication
Channels and Formatters
Object Activation and Lifetime
Writing Remotable Objects
Remoting in Action
Writing Clients for Remote Objects
Final Thoughts

Today, one way to differentiate application models is by how tightly coupled the client and the server applications are. Loosely coupled applications usually can get by with a thin client and rely on HTTP and other open standards for physical connection and transportation. Tightly coupled applications work according to a two-tier, client/server pattern and don't necessarily have to rely on universally available protocols. Often, a tight coupling is a real necessity and these apps can't just be rewritten to use stateless, disconnected technologies like HTTP and SOAP.

Before the Microsoft® .NET Framework, DCOM was the underlying technology of choice for remote communication between Windows®-based applications. But DCOM was based on a proprietary binary protocol and suffered from a number of ailments, including a quirky setup and configuration process.

In this article, you'll find an in-depth look at effective mechanism for designing distributed applications for the common language runtime (CLR). The two technologies I'll examine are XML Web Services and .NET Remoting. Both let you expose services over the network and handle incoming calls. Both share a common software substrate especially for object serialization, data transportation, and underlying network protocols. But they shouldn't be considered alternate approaches to the same problem. Rather, .NET Remoting and Web Services are distinct technologies with a different set of goals and built-in features. For illustrative purposes, think of Web Services as a subset of the .NET Remoting infrastructure. Web Services are targeted to cross-platform communication and heterogeneous systems. While .NET Remoting could be utilized for cross-platform communication, it is optimized for communication between .NET-centric applications. It also takes in the best and the coolest aspects of DCOM and elegantly fixes the holes.

A Short History of DCOM

As users began asking for distributed applications capable of interoperating with a wide variety of remote systems, the natural answer was to take COM and attach a logical wire to both of its ends. DCOM became the network extension of COM, building a new infrastructure on the same successful component technology. Seamless integration, a flat learning curve, and the facility to maximize investments in COM-based applications and tools were the most compelling reasons to use DCOM.

Because it was a binary protocol, DCOM performance was considered to be good, especially when compared to text-based interaction like those taking place over HTTP. DCOM applications were fundamentally location-independent since the protocol infrastructure was flexible in the way it covered physical distance. For example, DCOM automatically created a pair of proxy/stub modules for any interprocess and intermachine communication and resolved the call within the boundary of the current process whenever possible. So what were the drawbacks?

In many Internet situations, the level of connectivity allowed between a client and a server is subject to a variety of restrictions. Proxy servers can get in the way. Firewalls might filter incoming Internet requests to protect the server components from unauthorized contact. The ultimate effect of such restrictions is that a DCOM client and a server could set up and carry out a conversation only through a narrow set of protocol and port combinations. DCOM dynamically selected a network port in the range of 1024 through 65535. Unfortunately, the selected port might be unreachable because system administrators normally prohibit inbound Internet traffic from passing through these ports. The DCOM security model was based on the assumption that developers and administrators would properly configure the security settings for each component. DCOM was transparent with respect to security, hiding all the security requirements of a component.

Over the years, DCOM was extended to work around these issues. In particular, the COM Internet Services (CIS) layer gave DCOM the ability to work over port 80 thanks to the Tunneling Transmission Control Protocol (TTCP). CIS worked as an ISAPI filter, requiring Microsoft Internet Information Services (IIS) 4.0 or higher to be up and running on the server machine. Even with this firewall avoidance technique, problems still arose. Not all firewalls will accept binary packets over HTTP. CIS did allow DCOM to find a port to enter through, but it affected the way DCOM components actually work; server components couldn't call back the client component to sink events or send notifications.

The advent of the .NET Framework supplanted most COM-related technology, and DCOM is no exception. The .NET architecture for remoting is a complete redesign of the concepts behind DCOM with two key goals: allowing for seamless and location-independent coding and allowing a fully operational method of interaction with restricted servers.

The .NET Remoting system is like a coin with two faces. On one side, you've got XML Web Services—an interoperability technology that you should already know about (unless you've spent the past two years in a cave). The second face of remoting is the classes of the .NET Remoting namespace. These classes promote optimized, effective communication between .NET-based applications. That said, the power and flexibility of Remoting is evident when used for communication between .NET-based applications. If you are looking for interoperability, ASP.NET Web Services are ideal. But if you just need to communicate between two .NET-centric applications, nothing is better than .NET Remoting.

What is .NET Remoting?

The entire set of services that enable .NET-centric applications to communicate with each other fall under the umbrella of .NET Remoting. Such applications can reside on the same computer, work on different computers in the same LAN, and even be scattered across the world in homogeneous networks and platforms. Of course, these homogeneous platforms and networks must host the CLR. Remoting is also a suitable tool for accessing objects running in different AppDomains or processes.

The .NET Remoting architecture provides a great deal of flexibility and enables you to use different transportation protocols, serialization formats, object lifetime schemes, and modes of object creation. In addition, programmers can plug directly into the flow of messages that each communication originates and can hook activities at various stages of the process.

As you've gathered by now, .NET Remoting is good at connecting applications under several different circumstances. However, looking at the technology in more depth, you realize that all forms of communication that are available through .NET Remoting are all special cases of just one particular form of communication. At the lowest level of abstraction .NET Remoting enables communication and data exchange between different AppDomains. So what's an AppDomain exactly?

Isolated Units of Processing

To understand AppDomains and their role in the overall remoting architecture, let's step back and think about the heart of .NET: the common language runtime. The CLR provides an execution environment for code that has been enriched with services like garbage collection, security, versioning, language-neutrality, threading, and many others. At this time, very few operating systems are capable of loading an executable built using .NET and running it within the context of a CLR instance. For this reason, .NET-based executables resort to an old trick. In Windows, when an executable doesn't match the system platform, a short piece of code, a stub, runs to send some text to the output console. In MS-DOS® this stub was used to send a message to inform users that the required runtime—Windows—was not running. Smart programmers usually changed that stub with another relatively standard piece of code that started Windows up with the recommendation of launching the application immediately. The same trick—a custom-made stub—is what allows you to execute any .NET-based executable in operating systems like Windows NT® and Windows 2000. Windows XP and newer systems, on the other hand, have a modified loader that looks directly into the source PE file for information that qualifies the file as managed code. The best resource on these runtime topics is Jeffrey Richter's book, Applied Microsoft .NET Framework Programming (Microsoft Press, 2002).

Today, a few system modules incorporate the ability to host the .NET CLR. The list includes Microsoft Internet Explorer 5.01 and higher, plus any user application that follows the instructions outlined by Steven Pratschner in his article "Microsoft .NET: Implement a Custom Common Language Runtime Host for Your Managed App" in the March 2001 issue of MSDN Magazine. Incidentally, the same CLR hosting mechanism will power one of the coolest features in the upcoming version of SQL Server™ (codenamed Yukon). In fact, a version of SQL Server that would host the common language runtime could support compiled stored procedures written in C# or Visual Basic® .NET.

After being initialized, in order to run the application's code, the instance of the CLR must obtain a pointer to an AppDomain. Normally, the CLR gets the first AppDomain defined within the process. This default AppDomain gets created during the CLR initialization. The host application can also create additional AppDomains and give them different settings for security, reference paths, and configuration files. At the end of the process shown in Figure 1, a managed executable is started up and begins processing its code within the boundaries of the (default) AppDomain.

Figure 1 AppDomain Creation

Figure 1** AppDomain Creation **

In .NET, AppDomains are separate units of processing that the CLR recognizes in a running process. AppDomains are separated and isolated from one another in a way that resembles process separation in Win32®. Unlike Win32 processes, though, AppDomains provide for a more lightweight mechanism of isolation between processing units.

Before it can run, managed code not only needs an application domain but may also go through a process to verify that the code is type-safe and cannot cause memory faults. In Win32, this was one of the reasons to have a physical separation between process memory contexts. The requirement to run type-safe code allows the CLR to provide a level of isolation that's as strong as a process boundary, yet more cost-effective because of its lighter weight. Unlike Win32 processes, you can have several AppDomains running within the boundaries of the same .NET-based application. Individual domains can be stopped without stopping the entire process, but AppDomains are an atomic unit of processing, so you can only unload them as a whole.

Managed code running in an AppDomain is executed by a particular thread. However, threads and AppDomains are orthogonal entities in the sense that you can have several threads actively running during the execution of the AppDomain's code, but a single thread is in no way confined to running only within the context of a given AppDomain.

The CLR enforces isolation by preventing direct calls between objects living in different AppDomains. However, a tailor-made set of system services allows you to access an object that resides in an external AppDomain. This set of system services is exactly what .NET Remoting refers to. From an application's standpoint, an external AppDomain can transparently be another AppDomain in the same process, the default AppDomain in another process on the same machine, or even on a physically distant machine.

Interprocess Communication

To some extent, you can customize the way in which you set up any interprocess and interdomain communication using .NET Remoting services. In particular, you can decide whether to marshal objects by value or by reference. You can also control the object's activation and lifetime and choose the most suitable communication channel for transporting messages to and from remote applications. In addition, you can make modifications to the formatter that will encode and decode messages as they go over, or come in from, a channel.

You can invoke methods on a remote object via two basic techniques. But one way or another, the remote server objects must be reachable from the client. The first technique involves dynamically creating a full local copy of the remote server and accessing it on the local machine. The other possibility is to maintain an open channel with the remote machine and let parameters and return values travel back and forth to carry out the call. The first approach is referred to as marshal-by-value (MBV); the latter is known as marshal-by-reference (MBR).

Marshaling by value requires that the assembly defining that the server type be reachable by the client application. Clearly, this is not always the best approach, especially if you're working with large objects with dozens of methods. In fact, you risk consuming a significant portion of bandwidth and subjecting the client to a potentially lengthy wait only to execute one or two methods. With MBV, you cannot selectively decide what to download and cannot optimize the remote method call based on the needs of the application. MBV involves a kind of all-or-nothing approach that might not be suitable for all applications.

In addition, in order to be consumed by value, an object in .NET must be implemented as serializable, which is not always the case. For example, you cannot serialize an object representing or containing a database connection. This introduces an even more important reason to handle MBV scenarios with extreme care. Not all objects can be reasonably represented outside of their native environment. Not all of the information they hold makes sense once the object is transferred to the client. More often than not, in fact, the object has implicit dependencies and needs to access and use server-side resources that you cannot download to the client. For example, if one of the methods accesses a SQL Server table, how could you copy it to the client? Finally, MBV also raises some security concerns because of all the code information that is streamlined over a connection.

So when is MBV really a good option? When the object is not a large one, when you're going to make intensive use of it, when security and hacking are of no special concern, and, last but certainly not least, when the object has no dependencies on remote resources like files, databases, devices, or system resources. Otherwise, marshaling by reference is a more effective option.

When you marshal-by-reference, the client process receives a reference to the server object, rather than a copy. This means that any calls are resolved on the server within the native context of the object. The remoting infrastructure governs the call, collects all information about it, and sends it to the server process. On the server, the correct object is located and asked to execute the call using the client's arguments. When finished, results are packed and sent back to the client. MBR uses the network only for transmitting arguments and return values (see Figure 2).

Figure 2 Marshal by Reference

Figure 2** Marshal by Reference **

The .NET Remoting implementation of MBR provides for a proxy/stub pair and a physical channel for network transportation. The proxy represents the remote object to the client as it simply mirrors the same set of methods and properties. Each client invocation of a remote method actually hits the local proxy which, in turn, takes care of routing the call down to the server. A method invocation originates a message that travels on top of a channel and a transmission protocol. Each message passes through a chain of sinks on each side of the transport channel. Sinks are nearly identical to Windows hooks; by defining and registering a sink, the programmer can perform a specific operation at a specific stage of the remoting process. Since the creation of the proxy takes place automatically, programmers have to do very little work other than creating an instance of the target object and issuing the call. If the target object resides in an external AppDomain, the remoting infrastructure creates a transparent proxy and performs the requested operation.

But how can the code determine whether a given object is local, lives in a remote AppDomain, or just doesn't exist? In spite of the sophisticated code that makes up the remoting infrastructure, programming remote objects is mostly a matter of setup. Once the client has been properly configured (more on this later), you normally create a new instance of the remote class using the New operator, no matter what type of classes you are creating. Clients must declare to the CLR which classes are remote and provide information to connect. Remote objects, in turn, must be publicly available and bound to a given channel. I'll come back to this point later on.

Channels and Formatters

A channel is the element in the .NET Remoting architecture that physically moves bytes from one endpoint to the other. A channel takes a stream of bytes, creates a package according to a particular protocol, and routes it to the final destination across remoting boundaries. As I mentioned earlier, a channel can connect two AppDomains in the same process as well as two machines over a WAN.

A channel object listens for incoming messages and sends outbound messages. In both cases, the messages it handles can be made of packets written for a variety of protocols. In programming terms, a channel is a .NET class that implements the IChannel interface. The channel object is also required to implement IChannelReceiver and IChannelSender if it is expected to act as a receiver and/or a sender.

The .NET Framework comes with two predefined channel classes, TcpChannel and HttpChannel, both of which work as senders and receivers. TcpChannel uses a binary formatter to serialize data to a binary stream and transport it to the target object using the TCP protocol. HttpChannel transports messages to and from remote objects using the SOAP protocol.

The server object publishes the list of supported channels and, based on this list, the client decides how to actually perform the call. Servers must register at least one channel. Channels are registered on a per-AppDomain basis and must have unique names in that context. However, on any physical machine only one channel can listen to a given port. In other words, even though channels are AppDomain-specific, you can't have more than one channel registered to work on a given port on a given machine at one time. Figure 3 details the steps that take a method request down to the server.

Figure 3 Method Request to Server

Figure 3** Method Request to Server **

Let's review what really happens when the execution of a remote method is required, noting that I still owe you several details about the settings that are necessary to enable the client to call into remote servers.

A client enabled to make remote calls on a range of objects creates an instance of the desired class using the language-specific operator for instantiation. As an alternative, it can use the system-provided Activator object. For example, consider the following fragment:

Dim o As RemoteObject o = New RemoteObject() o.TheMethod(p1, p2)

When this code compiles, any calls to RemoteObject are resolved through an external reference added to the project much as you would with Web references. The underlying code that actually handles method invocation on RemoteObject is an instance of RealProxy, the abstract class that provides base functionality for .NET Remoting proxies. The proxy works as if it were an object of the same class as the remote object. It collects and packs all information about the method call (name/value pairs with the method name and the argument list) into an object that implements IMessage. (IMessage has just one property: a dictionary called Properties.) The collection of information about the method call is then passed to the proxy's Invoke method. The proxy knows about the channel that the client application has chosen to carry the conversation out. So it delegates Invoke to open the channel and sends data to the remote server. Opening a channel, though, is just an abstract representation of what really happens.

First, the proxy creates an instance of the channel object and begins traversing its sink chain. Both inbound and outbound messages pass through a sequence of channel sinks that actually implement all of the core channel functionality. The chain normally contains at least two standard sinks that open and close the chain: the formatter sink and the transportation sink. In between the two, programmers can define as many custom sinks as needed.

The first sink in the chain is the formatter, which is responsible for serializing the message—that is, name/value pairs describing the method call—into a stream. The stream is then passed down through any custom sinks you may have registered and finally reaches the client transport sink. At this point, the content of the message is sent out towards the destination port.

So who's listening on the server on the specified port? Any .NET remote object must have a host application listening to all the ports supported by the object. Unlike clients, remote objects do not hold channels but declare which ones they plan to support. The object-specific host application receives the inbound message and makes it go up through the server-side sink chain. The first sink to get a handle to the message is the transport sink that actually receives the packets. Custom sinks then get their turn and, finally, the formatter sink rebuilds the original message. At this point, the host application for the server AppDomain has all the information it needs to execute the call. It looks for the assembly in the remote object's configuration file, instantiates the object, and invokes the method. The object identification and activation policy deserves more attention, and I'll discuss it shortly. Any return value follows the reverse path and moves from the server to the client in a way that is nearly identical to a client-to-server method call.

The formatter sinks are of two basic types: SoapFormatter and BinaryFormatter. The SoapFormatter serializes and deserializes an object in SOAP format, whereas the BinaryFormatter does the same but in binary format.

Object Activation and Lifetime

The host app designates the server classes that will be registered to service any incoming call and, along with it, what protocol, port, and name must be used to call the service. All this data is stored in the host application's configuration file. Other important pieces of information that you'll find there are the object activation and lifetime policies.

An instance of an MBR remote class can be activated by either the server or client. Server-activated objects are created by the server only when the client invokes the first method through the local proxy. By contrast, client-activated objects are created on the server as soon as the client calls New or the Activator object. In addition, server-activated objects can be declared as Singleton or SingleCall objects. A Singleton object has exactly one instance to serve all possible clients. A SingleCall object requires that each incoming call is served by a new instance.

When the client instantiates a server-activated object using new or any other equivalent technique, only the local proxy is actually created. The proxy is made ready for use, but nothing has happened on the server yet. The message for the server is built and forwarded only when the client invokes the first method on that instance of the object.

What happens next on the server AppDomain? If the object is registered as a singleton, then the host application attempts to locate the global running instance of the object. If such an instance exists, the request for execution is processed. Otherwise, the host creates the unique global instance of the remote server and forwards the request to it. What happens if two requests arrive at the same time? The remoting subsystem provides for them to be automatically serviced by distinct threads. This requires singleton objects to be thread-safe. This is not a mandatory programming rule, but more of a practical guideline for real-world scenarios.

If the object is expected to work as a SingleCall server, then the host simply creates a new instance of the object, executes the method, and routes any return values back to the client. Let's go over some pros and cons for all of these options.

Server-activated objects are more efficient and flexible because they give you a chance to control the activation process and implement a laxer policy for instantiation. On the other hand, if the server object has a nondefault constructor that takes arguments, you can only use it through client activation. The reason for this difference is that for server-activated objects the instantation of the proxy and the actual object occurs at different times so the host application defaults to the standard constructor.

Server-activated objects have two working modes. You can have one instance to service all calls from all clients or one instance for each call. In the latter case, each method call causes the host application to instantiate the object, execute the method, and return. After that, the object instance is left to the garbage collector. Though not completely impossible, preserving state from one call to the next is a bit impractical for SingleCall objects. In this case, the lifetime of the object instance is short and barely covers the duration of the method call.

For singleton objects, state management is possible but must be coded in the body of the object in much the same way as you can do with ASP and ASP.NET pages and even ASP.NET Web Services. The idea is that you use a shared cache that all clients can access, unless you invent some sort of filtering mechanism. Figure 4 lists a comprehensive summary of the features and working modes available for MBR remote objects.

Figure 4 Marshal-by-Value Remote Objects

  Server-activated Client-activated
Activity Singleton or SingleCall Each client is serviced by a personal instance of the object
State Management Hard to achieve for SingleCall objects; possible for Singleton objects Possible, because the personal instance of the server remains active until the proxy is destroyed
Lifetime Duration of a method call if SingleCall; controlled by the Lease Manager if Singleton Controlled by the Lease Manager
Instantiation Only default constructor Support any constructors

If you use client-activated objects, the object's activity follows different guidelines that fall somewhere between the two previous options. Basically, each client-activated instance of a remote class has a 1:1 mapping with a particular client. The relationship is established upon creation—when the client calls New. The duration of the object's lifetime is subject to other rules that I'll discuss in a moment. Since each client holds its own personal instance of the remote class, persisting state is straightforward and does not require any special coding or other contrivance.

With the sole exception of server-activated SingleCall objects, the object's lifetime is managed by a new module called the lease manager. As mentioned earlier, a SingleCall object has a very short lifetime which is managed automatically by the CLR. As soon as the method returns, the object goes out of scope and becomes ready for the garbage collector. For singleton and client-activated objects, you need to count object references to determine when it's time to destroy them. In COM, this issue was solved by implementing reference counting. In .NET Remoting, the lease manager (working on a per-AppDomain basis) allows objects to be released even though clients still hold a reference to them. Let's quickly review the trade-offs of the two approaches to understand the rationale behind the change.

Reference counting would require clients, including distributed clients, to communicate with the server each time they connect or disconnect. The object maintains the number of currently active client instances, and when the count goes to zero the object destroys itself. On an unreliable network, chances are good that some object's reference count may never go to zero. If this weren't bad enough to preclude reference counting, remember that the continual sequence of connect/disconnect calls would generate significant network traffic.

The idea behind leasing is that each instance is leased to the client for a given amount of time fixed by the manager. The lease starts when the object is created. By default, each singleton or client-activated object is given a five minute lease. When the interval expires, the object is marked for deletion. During the lifetime, though, any processed client call resets the lease time to a fixed value (by default, two minutes), increasing or decreasing the overall lease time as needed. Note that the leasing is exclusively managed on the server and doesn't require additional network traffic, aside from that needed for normal method execution. The initial lease time and the renewal period can be set both programmatically and declaratively.

Another tool that .NET Remoting uses to control the object's lifetime is called sponsorship. Both client and server objects can register with the AppDomain's lease manager to act as sponsors of a particular object. When the lease of an object expires, prior to releasing the reference the remoting infrastructure gives sponsors a chance to renew the lease. By implementing sponsors, you can control the lifetime of objects based on logical criteria rather than raw ticks of the clock.

It's clear from this discussion of leasing that nothing can guarantee that clients will always find their server objects up and running. When a client attempts to access a server that is no longer available, a RemotingException exception is thrown. One way to resolve the exception is to create a new instance of the remote object and repeat the operation that failed. Like conflicts in ADO.NET batch update, remoting exceptions require applications to support tailor-made, context-specific coding.

It's interesting to note that all creation and working modes discussed so far don't strictly require you to code client-activated objects differently from, say, other objects destined to work as singletons. All options can be set declaratively and, at least in theory, each object can be configured to work in different ways by simply changing a few entries in the server's config file. However, although intriguing as a possibility, such duality is not realistic in practice because a real-world object might need to delve into the specific features of a given working mode. In other words, you carefully choose the configuration options for your remote object and then stick to those as long as the user's requirements are stable. For example, a singleton object might need to implement a state management engine for its own purposes. At a later time, if you set this object up to work as SingleCall or client-activated, such an engine becomes redundant, to say the least.

Writing Remotable Objects

How do you write a remotable object—a .NET class that can be instantiated from external AppDomains? The elegance and consistency of the .NET Framework, of which the remoting subsystem is a key part, makes this a trivial task. A remotable class is a .NET class that inherits from MarshalByRefObject, and thus the object will be marshaled by reference. At the highest level of abstraction, writing remotable classes is not really different from how you write XML Web Services. One minor difference is that you can write Web Services classes from scratch without inheriting any base functionality, but you must declare an infrastructure to expose public methods over the Web.

If any methods on the remotable class need to return a user-defined object, mark it as serializable using the <serializable> attribute. Likewise, as return values you should use only classes in the .NET Framework that are serializable or that extend from MarshalByRefObject. For example, the DataSet and DataTable objects are the only ADO.NET objects that can be remoted because they inherit from MarshalByValueComponent and support the ISerializable interface.

The following code illustrates the typical creation of a remotable MBR class in Visual Basic:

Namespace MsdnMag Public Class NorthwindSalesManager Inherits MarshalByRefObject ••• End Class End Namespace

Figure 5 contains the full source code of a sample remotable class called MsdnMag.NorthwindSalesManager. The project (available with this month's source code) produces an assembly called nwsales.dll. The class shows off one method, GetSalesReport, with two overloads: one returns all sales for a given year and one provides a sales report based on year and employee. As you can see, the object needs to query a back-end database (Northwind) to do its job. This inevitably leads to discounting the option of making the object available by value.

Figure 5 MsdnMag.NorthwindSalesManager

' Imports Imports System.Data.SqlClient Imports System.Text ' Define the remotable object "MsdnMag.NorthwindSalesManager", Assembly: ' NWSalesMan.dll Namespace MsdnMag Public Class NorthwindSalesManager Inherits MarshalByRefObject '————————————————————————————————————————————— ' Constants '————————————————————————————————————————————— Private Const m_connectionString As String = _ "DATABASE=northwind;SERVER=localhost;UID=sa;" '————————————————————————————————————————————— ' Method: GetSalesReport() ' Returns amount of sales for the given year and all employees '————————————————————————————————————————————— Public Function GetSalesReport(ByVal theYear As Integer) As DataSet Dim ds As DataSet = New DataSet("Sales" & theYear.ToString()) Dim dt As DataTable dt = ExecuteQuery(theYear, -1) ds.Tables.Add(dt) Return ds End Function '————————————————————————————————————————————— ' Method: GetSalesReport() ' Returns amount of sales for the given year and a given employee '————————————————————————————————————————————— Public Function GetSalesReport(ByVal theYear As Integer, ByVal _ theEmp As Integer) As Single Dim dt As DataTable dt = ExecuteQuery(theYear, theEmp) Return Convert.ToSingle(dt.Rows(0)("Sales")) End Function '————————————————————————————————————————————— ' //////////// PRIVATE METHODS '————————————————————————————————————————————— Private Function ExecuteQuery(ByVal theYear As Integer, _ ByVal empID As Integer) As DataTable ' Prepare the connection object Dim conn As SqlConnection = New _ SqlConnection(m_connectionString) ' SELECT e.lastname AS Employee, SUM(price) AS Sales FROM ' (SELECT o.employeeid, od.orderid, ' SUM(od.quantity*od.unitprice) AS price FROM Orders o, [Order ' Details] od ' WHERE Year(o.orderdate)=@TheYear AND od.orderid=o.orderid ' GROUP BY o.employeeid, od.orderid ) AS t1 ' INNER JOIN Employees e ON t1.employeeid=e.employeeid ' GROUP BY t1.employeeid, e.lastname ' Prepare the SELECT command object Dim sb As StringBuilder = New StringBuilder("") sb.Append("SELECT e.lastname AS Employee, SUM(price) _ AS Sales FROM ") sb.Append("(SELECT o.employeeid, od.orderid, _ SUM(od.quantity*od.unitprice) AS price ") sb.Append(" FROM Orders o, [Order Details] od") sb.Append(" WHERE Year(o.orderdate)=@TheYear AND _ od.orderid=o.orderid ") If empID > 0 Then sb.Append(" AND o.employeeid=@theEmp ") End If sb.Append("GROUP BY o.employeeid, od.orderid ) AS t1 ") sb.Append("INNER JOIN Employees e ON _ t1.employeeid=e.employeeid ") sb.Append("GROUP BY t1.employeeid, e.lastname") Dim cmd As SqlCommand = New SqlCommand(sb.ToString(), conn) ' Add the needed parameters Dim p1 As SqlParameter = New SqlParameter() p1.ParameterName = "@TheYear" p1.SqlDbType = SqlDbType.Int p1.Direction = ParameterDirection.Input p1.Value = theYear cmd.Parameters.Add(p1) If empID > 0 Then Dim p2 As SqlParameter = New SqlParameter() p2.ParameterName = "@TheEmp" p2.SqlDbType = SqlDbType.Int p2.Direction = ParameterDirection.Input p2.Value = empID cmd.Parameters.Add(p2) End If ' Prepare the Data Adapter Dim da As SqlDataAdapter = New SqlDataAdapter() da.SelectCommand = cmd Dim dt As DataTable = New DataTable("NorthwindSales") da.Fill(dt) Return dt End Function End Class End Namespace

As an MBR entity, the class can be client-activated or server-activated. It doesn't need to have a nondefault constructor, so both client- and server-activated modes are fine. The object is expected to work as a one-off service and has no need to maintain state for each client. In light of this, you can discard the client-activated option and go for the server-driven activation.

So which mode setting is better now, Singleton or SingleCall? SingleCall—a short-lived instance that serves the request and dies—is certainly a fair choice. However, if you choose to use the object as a singleton, you can architect more efficient code and avoid querying SQL Server every time a request comes in. Singletons have a single instance running all the time until the lease expires. This fact, along with the behavior of the object, makes it suitable to cache results in memory as long as chances are good that multiple clients ask for reports regarding the same employee and the same year. The remoting architecture has much flexibility, but any form of optimization that you plan narrows the range of feasible options. So in real-world scenarios, switching from one working mode to another typically requires changes to the code.

Let's look at a server-activated object working with single calls. Once you have compiled the class and decided how it has to work, you must publish some information about the remote object for the clients to successfully connect. In particular, the server must define a host application to listen to incoming calls and process the request. In addition, the server object must list the channels and the ports to be used and define a resource name (URI) that clients will use to uniquely identify the remote class.

The host application can be a custom program (such as a console application) written by the same class author as well as IIS or dllhost.exe if you're accessing COM+ objects in Windows XP. If you use a custom host, then you must make sure it is up and running prior to issuing remote calls. With IIS, of course, this is not a realistic concern. Figure 6 contains the source code for a simple yet effective class for remotable objects. The source is named host.vb and when it's compiled it generates host.exe. The key statement is this:

RemotingConfiguration.Configure("Host.exe.config")

Figure 6 New Class

' Host.vb Imports System Imports System.Runtime.Remoting Public Class MyApp Public Shared Sub Main() RemotingConfiguration.Configure("Host.exe.config") Console.WriteLine("Press Enter to terminate...") Console.ReadLine() End Sub End Class

The host program reads the given config file and organizes itself to listen on the specified channels, formatters, and ports for calls destined for the remote object it governs. The config file contains information about the class name, the assembly that contains it, the required activation mode (client, singleton, singlecall), any URI, and leasing parameters. A typical config file for the remote object you saw in Figure 5 is shown in Figure 7.

Figure 7 Config File

<configuration> <system.runtime.remoting> <application> <service> <wellknown mode="SingleCall" type="MsdnMag.NorthwindSalesManager, NWSalesMan" objectUri="NWSalesMan.rem" /> </service> <channels> <channel ref="http" /> <channel ref="tcp" port="3412" /> </channels> </application> </system.runtime.remoting> </configuration>

The object can be activated through both HTTP and TCP channels. If it's TCP, the port must be 3412. The <service> tag describes how the server must be activated. If the object is server-activated, it's said to be a well-known object and described through the <wellknown> tag. If the object is client-activated then you must use the <activated> tag instead. The <wellknown> tag has a Mode attribute that you set to SingleCall or Singleton, as needed. The Type attribute is a comma-separated string in which the former token is the fully qualified name of the class and the latter is the name of the assembly containing the class. Finally, ObjectUri indicates the universal name of the object for server-activated objects. Client-activated objects don't need it. The object's URI is the endpoint of the URL with which the client identifies the target object location. Figure 8 shows the server configuration of the same object when client activated.

Figure 8 Client-activated Object's Config File

<configuration> <system.runtime.remoting> <application> <service> <activated type="MsdnMag.NorthwindSalesManager, NWSalesMan" /> </service> <channels> <channel ref="http" /> <channel ref="tcp" port="3412" /> </channels> </application> </system.runtime.remoting> </configuration>

Remoting in Action

To round out the configuration of the object in Figure 5, let's walk through the steps necessary to use IIS, rather than a custom host, as the activation agent. First, create a new virtual directory (say, NWSales) and copy the object's assembly in its BIN subdirectory. In this case, the config file must be called web.config and resides in the virtual directory's root.

If you choose to use IIS as the activation agent, you must be aware of a few things. In the web.config file you cannot use the <debug> section. Only the HTTP channel is supported and any other channel you indicate is simply ignored. Since the host is a system process (inetinfo.exe), you cannot programmatically exercise any control on the activation process as you would do with a custom host. As a result, either you use a static config file or you arrange for a global.asax file to be in the virtual folder. In global.asax you can execute some custom code in the Application_Start event handler. Figure 9 shows how to proceed.

Figure 9 Application_Start

Sub Application_Start() ' Create the HTTP channel Dim props As IDictionary = New HashTable() props("name") = "TheChannel" props("priority") = "100" ' Use binary formatters to reduce packets Dim channel As HttpChannel channel = New HttpChannel(props, _ New BinaryClientFormatterSinkProvider(), _ New BinaryServerFormatterSinkProvider()) ' Equivalent to <wellknown> Dim wko As WellKnownServiceTypeEntry wko = New WellKnownServiceTypeEntry( _ GetType(MsdnMag.NorthwindSalesManager, _ "NWSalesMan.rem", _ WellKnownObjectMode.SingleCall) RemotingConfiguration.RegisterWellKnownServiceType(wko) End Sub

Unlike DCOM, .NET Remoting never takes care of starting any host for the server object, so being able to use a service or IIS is a viable option. This choice has its pros and cons, however. IIS forces you to use HTTP channels and takes care of ports. The inevitable use of SOAP as the underlying protocol increases the average size of network packets. The impact of this, though, can be lessened by using binary formatters at both ends of the channel (see Figure 9). One of the advantages of using IIS is that you can exploit its authentication engine and work over SSL (HTTPS), building transport-level security in your app.

So much for the server side of remoting. Let's see what's needed for a client to invoke a remotable class.

Writing Clients for Remote Objects

A client application doesn't have to be very complicated to call a remote object. Let's create a Windows Forms application in Visual Basic that calls the NorthwindSalesManager object to get sales reports for a given year.

At the project level, you must accomplish two basic tasks, the first of which is quite obvious: make sure that the remoting assembly is referenced correctly. The second task is to add a reference to the remote object assembly. This procedure is nearly identical to what happens with Web Services proxies that require you to add a Web reference. In this case, all you have to do is browse through the network, locate the assembly, and add a reference to the project. For IIS-activated objects, the reference must point to the virtual directory's bin subfolder. Such a reference is needed to enable the client application to know about the types defined in the assembly.

The code of the client application has one more requirement to fulfill. The remote object must be registered with the application before you can use it. This can be accomplished either through a config file or programmatically by calling the RegisterWellKnownClientType method on the RemoteConfiguration object. Of course, you'll use this method only for server-activated objects:

RemotingConfiguration.RegisterWellKnownClientType( _ GetType(MsdnMag.NorthwindSalesManager), _ "https://localhost/NWSales/NWSalesMan.rem")

The method takes the type of the remote object and a URL that includes both the HTTP path to the remote server and port and the object's URI. In the previous sample code, NWSales is the virtual directory you defined when you decided to use IIS as the activation agent. Of course, you replace localhost with the name of the Web server where the object resides. Pay attention to the structure of the object's URI. When the host is IIS, the URI must have a .rem or .soap extension to be treated as such by the Web server filters. This is not required if the host is a custom application.

An alternative approach for remote objects registration is based on a config file. In Figure 10 you can see a typical client script that is triggered by the following statement:

RemotingConfiguration.Configure("MyClient.exe.config")

Figure 10 Client Script

<configuration> <system.runtime.remoting> <application name="TestClient" > <client> <wellknown type="MsdnMag.NorthwindSalesManager, NWSalesMan" url="https://localhost/NWSales/NWSalesMan.rem" /> </client> <channels> <channel ref="http" /> <channel ref="tcp" /> </channels> </application> </system.runtime.remoting> </configuration>

The initialization step serves the purpose of letting the CLR know about the location of the remote class. That class, in fact, is not part of the .NET Framework nor does it belong to a referenced assembly. Once the application is properly configured, instantiating the remote object is a matter of calling the New operator.

Dim o As MsdnMag.NorthwindSalesManager o = New MsdnMag.NorthwindSalesManager() Dim ds As DataSet ds = o.GetSalesReport(1997)

As an alternative to using New, you can have the System.Activator class instantiate a class instance. System.Activator has two methods, GetObject and CreateInstance, whose overall functionality is nearly identical to the New operator. Using either one is fine. There's just one significant difference between New and Activator's methods. Activator's methods are capable of creating instances of objects even when you have minimal information about them. A URL and one interface that the object implements is enough to return a working, functional reference to your client application:

o = Activator.GetObject( _ GetType(MsdnMag.NorthwindSalesManager), _ "https://localhost/NWSales")

GetObject is used to create instances of server-activated objects whereas CreateInstance is used to instantiate client-activated objects. Figure 11 shows some of the source code of the client application in Figure 12. The bar charts in the figure are drawn using GDI+ managed classes.

Figure 12 Bar Chart from GDI+ Managed Classes

Figure 11 Using Remoting

Imports System.Runtime.Remoting Imports System.Runtime.Remoting.Channels Imports System.Data Public Class Form1 Inherits System.Windows.Forms.Form #Region " Windows Form Designer generated code " ••• #End Region ' ********************************************************************* ' Form Loading: initialize remoting and controls Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles MyBase.Load RemotingConfiguration.RegisterWellKnownClientType( _ GetType(MsdnMag.NorthwindSalesManager), _ "https://localhost/NWSales/NWSalesMan.rem") ' Alternative approach 'RemotingConfiguration.Configure("TestClient.exe.config") ' Fill the years combo cboYears.Items.Add("1998") cboYears.Items.Add("1997") cboYears.Items.Add("1996") cboYears.SelectedIndex = 1 End Sub ' ********************************************************************* ' ********************************************************************* ' Fill the grid: invoke the remote object and populate the form's UI Private Sub btnLoad_Click(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles btnLoad.Click Dim o As MsdnMag.NorthwindSalesManager o = New MsdnMag.NorthwindSalesManager() 'o = Activator.GetObject(GetType(MsdnMag.NorthwindSalesManager), _ ' "https://localhost/NWSales") Dim ds As DataSet ds = o.GetSalesReport(cboYears.Text) DataGrid1.CaptionText = ds.Tables(0).TableName DataGrid1.DataSource = ds.Tables(0) End Sub ' ********************************************************************* End Class

Final Thoughts

This article just scratches the surface of .NET Remoting. However, it should be enough to enable you to start designing, configuring, and calling remote objects. In the sample code accompanying this article (see the link at the top of this article) calls take place synchronously on a distinct thread and there's no way for the server and the client to communicate other than through the calls. The remoting architecture provides events and asynchronous calls as a way to improve the interaction between endpoints as well as a technique to optimize the client's performance.

For related articles see:
.NET Framework Samples: Remoting Basic
.NET Framework Samples: Remoting Advanced
Microsoft .NET Remoting: A Technical Overview
.NET Remoting Overview

Dino Espositois an instructor and consultant based in Rome, Italy. He is the author of Building Web Solutions with ASP.NET and ADO.NET and the upcoming Applied XML Programming for .NET, both from Microsoft Press. Reach Dino at dinoe@wintellect.com.