Implementing Broker with .NET Remoting Using Client-Activated ObjectsVersion 1.0.1 GotDotNet community for collaboration on this pattern Complete List of patterns & practices
Context You are building an application in .NET that requires the use of distributed objects, the lifetimes of which are controlled by the client. Your requirements include the ability to pass objects by value or reference, whether those objects reside on the same computer, on different computers in the same local area network (LAN), or on different computers in a wide area network (WAN). Implementation Strategy This pattern presents two implementations of client-activated objects in .NET remoting. The main difference between client-activated objects (CAO) and server-activated objects (SAO) is what controls the lifetime of the remote object. In the CAO scenario, the client controls the lifetime; in the SAO scenario, the server controls the lifetime. The example used here is similar in functionality to the example used in Implementing Broker in .NET Using Server-Activated Objects. The first implementation uses client activation as it is described in the .NET documentation and samples. This implementation demonstrates the capabilities of client-activated objects; however, they do have some drawbacks. The second implementation, known as the hybrid approach, resolves these problems. Client-Activated Object Implementation
The RecordingsManager class has a method named GetRecordings, which retrieves a list of recordings from a database and returns the result in DataSet. This class extends the MarshalByRefObject class to ensure that in a remoting scenario a proxy object is used instead of copying the object from the server to the client. The functionality described here is identical to that of the example described in Implementing Broker in .NET Using Server-Activated Objects.
RecordingsManager.cs
The following sample shows the RecordingsManager class, which is responsible for retrieving DataSet from the database:
using System; using System.Data; using System.Data.SqlClient; public class RecordingsManager : MarshalByRefObject { public DataSet GetRecordings() { String selectCmd = "select * from Recording"; SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Recording"); return ds; } } HttpServer.cs
The following code configures the server to allow for client-activated objects to be created using the new operator. Instead of actually registering an instance (as the SAO example demonstrates), this code configures the server with an application name and the type of the object that will be created. The URL for the remote object is http://localhost:8100/RecordingsServer. Behind the scenes, an SAO is automatically created by the framework on localhost. This SAO is responsible for accepting requests from clients and creating the objects when the client requests them.
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; public class HttpServer { static void Main(string[] args) { HttpChannel channel = new HttpChannel(8100); ChannelServices.RegisterChannel(channel); RemotingConfiguration.ApplicationName = "RecordingsServer"; RemotingConfiguration.RegisterActivatedServiceType( typeof(RecordingsManager)); Console.WriteLine("Recordings Server Started"); Console.ReadLine(); } } HttpClient.cs
To be able to the use the new operator and have the remoting framework create a remote object, as opposed to a local object, you must first associate the type of the remote object with the URL that was specified when the server set the ApplicationName property. This example defines ApplicationName as RecordingsServer and uses port 8100 on localhost.
using System; using System.Data; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; class HttpClient { [STAThread] static void Main(string[] args) { HttpChannel channel = new HttpChannel(); ChannelServices.RegisterChannel(channel); RemotingConfiguration.RegisterActivatedClientType( typeof(RecordingsManager), "http://localhost:8100/RecordingsServer"); RecordingsManager mgr = new RecordingsManager(); Console.WriteLine("Client.main(): Reference acquired"); DataSet ds = mgr.GetRecordings(); Console.WriteLine("Recordings Count: {0}", ds.Tables["recording"].Rows.Count); } }Registering the remote object associates the type of the object with the URL. After this occurs, the call to new creates a remote object on the server. This object looks like any other object in the code. This implementation allows for direct creation of remote objects under the control of the client. It also demonstrates that after the client is configured, object creation is identical to local object creation using the new operator. However, it has a major flaw. You cannot use the shared interface approach described in the SAO pattern. This means that you must ship the compiled objects to the client. For another alternative that uses SoapSuds, see Advanced .NET Remoting [Ingo02].
To address some of these issues, the following implementation describes a hybrid approach that uses an SAO to create objects. This approach provides the client with the ability to control the lifetime of the object without the server code having to be shipped to the client.
Hybrid Approach
The hybrid approach involves the RecordingsFactory SAO, which provides methods to create the RecordingsManager CAO. (If you are not familiar with the SAO examples, see Implementing Broker with .NET Remoting Using Server-Activated Objects.) The following class diagram describes the overall solution.
This implementation uses the shared interface approach described in the SAO examples. The two interfaces, IRecordingsManager and IRecordingsFactory, are in an assembly that is shared between the client and the server. IRecordingsFactory has a single Create method, which returns an object to implement the IRecordingsManager interface. This is an example of the AbstractFactory [Gamma95] pattern. Because the client only depends on the interfaces, there is no need to ship the server code. When a client needs a IRecordingsManager object, it calls the Create method on an instance of IRecordingsFactory. This allows the client to be in control of the lifetime of the IRecordingsManager object without needing its implementation. The two interfaces from the shared assembly are as follows.
IRecordingsManager.cs
The following sample shows the IRecordingsManager interface:
using System; using System.Data; public interface IRecordingsManager { DataSet GetRecordings(); } IRecordingsFactory.cs
The following sample shows the IRecordingsFactory interface:
using System; public interface IRecordingsFactory { IRecordingsManager Create(); }The server implementations of these objects, RecordingsFactory and RecordingsManager, are straightforward and are contained in their own assembly, named Server. RecordingsFactory.cs
This class extends MarshalByRefObject and implements the IRecordingsFactory interface:
using System; public class RecordingsFactory : MarshalByRefObject, IRecordingsFactory { public IRecordingsManager Create() { return new RecordingsManager(); } }The RecordingsFactory object is the server-activated object. This implementation simply calls new on the RecordingsManager type. This RecordingsManager object is created on the server and is returned, not as a RecordingsManager object, but as the IRecordingsManager interface. This mechanism allows the client to depend on the interface rather than the implementation. RecordingsManager.cs
The only change required in the RecordingsManager class is that it now implements the IRecordingsManager interface.
using System; using System.Reflection; using System.Data; using System.Data.SqlClient; public class RecordingsManager : MarshalByRefObject, IRecordingsManager { public DataSet GetRecordings() { Console.WriteLine("Assembly: {0} - filling a request", Assembly.GetEntryAssembly().GetName().Name); String selectCmd = "select * from Recording"; SqlConnection myConnection = new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds, "Recording"); return ds; } } HttpServer.cs
The server initialization code in the hybrid approach configures the remoting framework for a server-activated RecordingsFactory object. The activation scheme is independent of the channel and the protocol used, so they remain the same as before (HTTP protocol on port 8100).
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; public class HttpServer { static void Main(string[] args) { HttpChannel channel = new HttpChannel(8100); ChannelServices.RegisterChannel(channel); RemotingConfiguration.RegisterWellKnownServiceType( typeof(RecordingsFactory), "RecordingsFactory.soap", WellKnownObjectMode.Singleton); Console.ReadLine(); } }In this code, the RecordingsFactory type is associated with the URL: http://localhost:8100/RecordingsFactory.soap. HttpClient.cs
The client code demonstrates the hybrid nature of this approach. You first use the Activator.GetObject method to retrieve the IRecordingsFactory object from the server. Using this server-activated object, you then call the Create method to instantiate an IRecordingsManager object. This newly instantiated object is created on the server but is a remote object.
using System; using System.Data; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; public class HttpClient { [STAThread] static void Main(string[] args) { HttpChannel channel = new HttpChannel(); ChannelServices.RegisterChannel(channel); IRecordingsFactory factory = (IRecordingsFactory) Activator.GetObject(typeof(IRecordingsFactory), "http://localhost:8100/RecordingsFactory.soap"); Console.WriteLine("Client.main(): Factory acquired"); IRecordingsManager mgr = factory.Create(); DataSet ds = mgr.GetRecordings(); Console.WriteLine("Recordings Count: {0}", ds.Tables["recording"].Rows.Count); } } Resulting Context Using client-activated objects to implement Broker with .NET remoting results in the following benefits and liabilities: Benefits
Liabilities
Security Considerations To use the security features available with Microsoft Internet Information Services (IIS) (for example, standard HTTP authentication schemes include Basic, Digest, digital certificates, and even Microsoft .NET Passport) you must use an HTTP-based application hosted in IIS with ASP.NET. Using any other transport protocol or using the HttpChannel outside of IIS requires you to provide a security mechanism. Operational Considerations The following is a summary of a performance comparison that appears in the MSDN article, "Performance Comparison: .NET Remoting vs. ASP.NET Web Services" [Dhawan02]. The article concludes that you can achieve the highest performance by using the TCP channel and binary serialization with a Windows Service host. This configuration transmits binary data over raw TCP sockets, which is more efficient than HTTP. Performance is 60 percent faster than with the slowest approach, which is HttpChannel using SOAP serialization hosted in IIS with ASP.NET. Hosting in IIS is slower because it involves an extra process hop from IIS (Inetinfo.exe) to Aspnet_wp.exe. However, if you choose to host your channel without IIS and ASP.NET, you will need to provide your own mechanisms for authentication, authorization, and privacy. Related Patterns For more information, see the following related patterns: Acknowledgments [Ingo02] Rammer, Ingo. Advanced .NET Remoting. Apress, 2002. [Dhawan02] Dhawan, Priya. "Performance Comparison: .NET Remoting vs. ASP.NET Web Services." MSDN Library, September 2002. Available at: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/bdadotnetarch14.asp. |