Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Article

All you need to know about .NET Remoting

Rate me:
Please Sign up or sign in to vote.
4.89/5 (94 votes)
6 Oct 2008CPOL25 min read 173.5K   3.2K   253   27
.NET Remoting is available since beginning of .NET intoduction. It’s time to get to know it well eventually. I hope this topic will help you with this.

Author: Olexandr Malko
Date: 09/29/2008

Introduction

    .NET Remoting is available since beginning of .NET intoduction. It’s time to get to know it well eventually. I hope this topic will help you with this. This document has many samples attached. It was decided not to overload one project with all features at once. Even though only couple lines should be changed for switching from one final application to another, there will be a separate solution to avoid text like “//uncomment this to gain that”. All samples are introduced with Visual Studio 2003 solutions. So, you should be able to open them with VS2005 and VS2008.

    Sometimes objects on pictures won’t have numbers even though those will be referred as “second” or “fifth”. I will use such numbering with rules to count from top to bottom and from left to right.

Content

    1. What is .NET Remoting?
    2. Simple project to show .NET Remoting
    3. Configuration file and configuration in code
    4. Types of remote object activation
    4.1. Server Side Object Activation. Singleton
    4.2. Server Side Object Activation. SingleCall
    4.3. Client Side Object Activation
    5. What is Lease Time? How to control it?
    6. Hide Implementation from Client. Remoting via Interface exposure.
    7. Custom types as parameters and return values
    8. Custom exceptions through remoting channel
    9. Events in .NET Remoting
    10. Asynchronous calls
    11. Several Services in one Server. Several Server links from single Client app
    12. Summary

1. What is .NET Remoting?

    “.NET Remoting” are means in .NET Framework for 2 applications to interact over network (e.g. withing 1 PC, within LAN or even worldwide). Also, in .NET we have ability to run several Application Domains in one process. .NET Remoting is the way to interact between these Domains.

    There are 2 common types of protocols used in .NET Remoting: tcp for binary stream and http for SOAP stream. Here in this article all samples will use binary channels, tcp. It requires less traffic load and better performance as there is no overhead with XML parsing. For our production projects it is a big plus.

    As usual for distributed applications, there is a Server and a Client application. In .NET Remoting we can have as many clients as we want, and all those Client applications can use the same Server. .NET remoting is not just a socket with low level methods. It is framework where you can work remotely with classes with ability to invoke methods, to pass custom types as parameters and get them as return values, to throw Exceptions between processes, to pass Callback delegates and have them invoked later remotely, to do asynchronous calls.

2. Simple project to show .NET Remoting

    Remoting interaction requires:

        1) service type description that is available for both points on interaction
        2) point #1 – host (e.g. Server) that holds the instantiated remoting object of our service type
        3) point #2 – client application that can connect to Server and use remoting object

    Now, let’s take a look at picture below. You can see two separate processes. Server is holding a real instance of MyService. This instance can be used by other processes over .NET Remoting. Client process is not instantiating the instance of MyService. It just has some transparent proxy. When Client application invokes methods of MyService proxy, the proxy redirects those calls to .NET Remoting Layer in Client process. That remoting layer knows where to send such call – so, call goes over network (e.g. over .NET Remoting channel) right to our remoted Server process. After that Remoting layer on Server side knows if it should use already existing instance of MyService or create new one. It depends on type of activations. Activation types can be configured in *.xml config file or through code. All this will be described later in this article.

Image 1

    You may find “Simple Remoting” solution in downloads. It consists of three core projects. Almost all samples in this article will have them:

        1) ONXCmn - class library with definition of MyService type
        2) ONXServer – executable console application that hosts MyService service.
        3) ONXClient – executable console application that shows how to use remoted MyService sevice.

    You can start as many Client applications as you want. All of them will be served by single Server application. You cannot start several Servers at the same time though. This is because there is a port to listen for remote Client applications. You cannot initiate several socket listeners on the same network card and the same port.

    Also, I would like to pay your attention at Log and Utils classes. They will be used with all samples. You will find Log useful to print timestamp with each print out. Also, it prints id of current thread – so we can easily see if the same thread was used for group of actions or not. As for Utils class, it dumps information about all registered remoting service and client types. It helps you to catch some misconfiguration in case something is not working:

static void Utils.DumpAllInfoAboutRegisteredRemotingTypes()

public class MyService : MarshalByRefObject
{
  public MyService()
  {
    Log.Print("Instance of MyService is created");
  } 

  public string func1()
  {
    Log.Print("func1() is invoked");
    return "MyService.func1()";
  }
}

    Here we describe our remoting type – MyService. It must be derived from MarshalByRefObject. This parent class tells our MyService class not to be sent by value – it is referred by reference only. Our MyService has only one service method – “string func1()”. Whenever we invoke “func1()” we print log message and return value. As you may guess, we instantiate MyService object in Server application and use it from Client application. That is why we should expect log message to appear in Server console and not in Client one. The same about MyService() constructor. Log message about object creation should appear in Server console.

    Now, Server class:

class MyServer
{
  [STAThread]
  static void Main(string[] args)
  {
    RemotingConfiguration.Configure("ONXServer.exe.config");
    Utils.DumpAllInfoAboutRegisteredRemotingTypes(); 
    
    Log.WaitForEnter("Press EXIT to stop MyService host...");
  }
}

    It might surprise you if you really see .NET Remoting for first time. There is nothing specific and complex here. Why is it working? The “Utils.DumpAllInfoAboutRegisteredRemotingTypes()” is simply invoked to print registered .NET services. The “Log.WaitForEnter(..)” is just user prompt to press ENTER to close our console application. So, the only line of code that really turns our regular console application into .NET Remoting Server is “RemotingConfiguration.Configure("ONXServer.exe.config")”. This method reads *.xml file and has enough information from there to start socket listener on some port and to wait for requests from remote Client applications! This is nice approach as you can change behavior of your application without need to change and recompile our code. Now, let’s take a look at this configuration file:

<?xml version="1.0" encoding="utf-8" ?>
  <configuration>
    <system.runtime.remoting>
      <application>
        <service>
          <wellknown
            type="ONX.Cmn.MyService, ONXCmn"
            objectUri="MyServiceUri"
            mode="SingleCall" />
        </service>
      <channels>
        <channel ref="tcp" port="33000" />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

    Remoting is configured inside <configuration><system.runtime.remoting><application> section. This is true for both Server and Client configuration (yes, Client is also configured through *.xml file). For Server we have <service> section that might have one or more <wellknown> sections. This wellknown section is the place where you describe your service to be available for Client applications. There are 3 attributes for it:
        1) full type description – describes, what type to instantiate when we get request for this welknown type from Client. Full type value consists of type name with full namespace path and after comma there is the name of assembly where this type is.
        2) objectUri – this is unique name that Client application will be requesting by. Client application usually requests service by “URI” and not by direct type name. You will know why when you get to “6. Hide Implementation from Client. Remoting via Interface exposure” topic.
        3) This parameter may be either “SingleCall” or “Singleton”. In case of “SingleCall” every method call that comes from any Client is served by newly created instance of MyService. In “Singleton” configuration ALL calls from ALL client applications are served by single instance of MyService object.

    If you have several services that should be registered in our application, list “wellknown” sections one after another inside “service” section.

    Also, beside from “service” section there is “channels” section. Here we might have several channels defined. In our sample we have only “tcp” channel defined. It will be listening on port 33000.

    Now, let’s take a look at Client configuration:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown
          type="ONX.Cmn.MyService, ONXCmn"
          url="tcp://localhost:33000/MyServiceUri" />
      </client>
    </application>
  </system.runtime.remoting>
</configuration>

    You may notice pretty much similarity between Server and Client configurations. The difference is that in Client configuration we have “<client />” section instead of “<service />”. It makes application understand that when we create instance of MyService we actually want to request this class remotely. Also, wellknown section has “url” attribute that will connect to “localhost” machine to port 33000 and request named service with URI MyServiceUri. Attribute “type” says application to use this remoting whenever Client application code tries to instantiate the MyService object on client side. So, no actual instance of MyService is created in Client application. We only create Proxy that knows where to send our call requests whenever we call some method.

    And finally here is Client application:

class MyClient
{
  [STAThread]
  static void Main(string[] args)
  {
    RemotingConfiguration.Configure("ONXClient.exe.config");
    Utils.DumpAllInfoAboutRegisteredRemotingTypes();
    
    MyService myService = new ONX.Cmn.MyService();
    Log.Print("myService.func1() returned {0}", myService.func1()); 

    Log.WaitForEnter("Press ENTER to exit...");
  }
}

    As you see it is as simple as Server console application. You simply call “RemotingConfiguration.Configure("ONXClient.exe.config")” to register our MyService type correctly. Then you dump information about all remote types that were registered so far. After that you create “instance” on MyService. As you understand now, there will be only transparent proxy created. And then you call “MyService.func1()” method. This call will go to Server application, get return value from there, deliver it to Client application and print in our log on Client side.

    Here is what we get in Server and in Client consoles for our sample:

SERVER: 

[1812] [2008/10/05 21:30:15.595] ALL REGISTERED TYPES IN REMOTING -(BEGIN)---------
[1812] [2008/10/05 21:30:15.595] WellKnownServiceTypeEntry: type='ONX.Cmn.MyService, ONXCmn'; objectUri=MyServiceUri; mode=SingleCall
[1812] [2008/10/05 21:30:15.595] ALL REGISTERED TYPES IN REMOTING -(END) ---------
[1812] [2008/10/05 21:30:15.595] Press EXIT to stop MyService host...
[5068] [2008/10/05 21:30:20.876] Instance of MyService is created
[5068] [2008/10/05 21:30:20.876] func1() is invoked

CLIENT: 

[7388] [2008/10/05 21:30:20.736] ALL REGISTERED TYPES IN REMOTING -(BEGIN)---------
[7388] [2008/10/05 21:30:20.798] WellKnownClientTypeEntry: type='ONX.Cmn.MyService, ONXCmn'; url=tcp://localhost:33000/MyServiceUri
[7388] [2008/10/05 21:30:20.798] ALL REGISTERED TYPES IN REMOTING -(END) ---------
[7388] [2008/10/05 21:30:20.892] myService.func1() returned MyService.func1()

    You can see that Instance of Service is created in Server application even though we have “new MyService()” in Client application code!

3. Configuration file and configuration in code

    All configurations that were performed for our “Simple Remoting” solution can be done through code without need to have additional *.xml configuration file. Sometimes it is easier to have it in code, but it makes harder to do quick adjustments or modifications to configuration. That is why in our article I will continue to use *.xml files as this is easier to read also. But for security or any other reasons still you may store configuration in some files or in database, and then teach your application to read that configuration data and register remoting types inside of your code if you wish.

    As a brief example here is code that makes the same configuration as we have for our Client application in “Simple Remoting” sample in previous topic:

//RemotingConfiguration.Configure("ONXClient.exe.config");
RemotingConfiguration.RegisterWellKnownClientType(
  typeof(MyService),
  "tcp://localhost:33000/MyServiceUri");

    You may want to check MSDN to get more details on .NET Remoting configuration in code.

4. Types of remote object activation

    There are 3 types of activation of remote objects: 2 types of Server Side Activation and 1 type of Client Side Activation:

        1) Server Side Singleton - Object is created on Server when first request comes from one of Client applications. Nothing happens on a Server when you "create" instance in your Client application. Server acts only when Client application invokes first method of remote object. In Singleton mode all Client applications share SINGLE instance of remote object that is created on Server. Even if you create several objects in Client application, still they use the same single object from Server application.

        2) Server Side SingleCall - Object is created for EACH method call. So, it does not matter how many Client applications are running. Every method call from any Client application has this life-cycle:

            - Server creates new instance of remote object
            - Server invokes requested method against newly created remote object
            - Server releases the remote object. So, now the remote object is available for Garbage Collection.

        3) Client Side Activation - Object is created in Server application with every "new" operator that is in Client application. Client application has full control over this remote object and does NOT share it with other Client applications. Also, if you create 2 or more remote objects in your Client application - yes, there will be created the exact number of remote objects in Server application. After that you may work with each instance individually as you would do without .NET remoting involved. The only issue here is Lease Time that might destroy your remote object on Server application earlier than you expect. See “5. What is Lease Time? How to control it?”

    For Server Activation Object you will need to register “well known type”. For Client Activation Object you will need to register “activated type”. Let take a look at each type of activation closer.

4.1. Server Side Object Activation. Singleton

    In this type of activation no object is created on a Server until first call comes from one of Clients. It does not matter how many calls are coming after object is created. It does not matter how many Client applications are trying to use our Server object – all such calls are directed to single remote object, e.g. “Instance of MyService” on a picture below.

Image 2

    Also, I would like to pay your attention that even though you request several instances of MyService in Client application (see myService1 and myService2 on picture) those 2 variables will still point to single TransparentProxy in Client process. This is because for “wellknown” type one proxy per process is enough with either “Server Activation Object” model.

    If Lease Time is expired, Singleton might be destroyed on Server. In this case with new request from Client application new Singleton is created and is used in the same way – e.g. Single object for all Clients requests. See “5. What is Lease Time? How to control it?”

    To use this type of activation you should configure server with well-known type like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown
          type="ONX.Cmn.MyService, ONXCmn"
          objectUri="MyServiceUri"
          mode="Singleton" />
          </service>
      <channels>
        <channel ref="tcp" port="33000"/>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

    And client configuration like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown
          type="ONX.Cmn.MyService, ONXCmn"
          url="tcp://localhost:33000/MyServiceUri" />
      </client>
    </application>
  </system.runtime.remoting>
</configuration>

    As for sample, locate “SAO Singleton” solution. With client code:

class MyClient
{
  [STAThread]
  static void Main(string[] args)
  {
    RemotingConfiguration.Configure("ONXClient.exe.config");
    Utils.DumpAllInfoAboutRegisteredRemotingTypes();
    string result; 

    //create myService1
    Log.WaitForEnter("1) Press ENTER to create Remote Service...");
    MyService myService1 = new MyService();
    Log.Print("myService1 created. Proxy? {0}",
      (RemotingServices.IsTransparentProxy(myService1)?"YES":"NO"));

    //query myService1.func1()
    Log.WaitForEnter("2) Press ENTER to query 1-st time...");
    result = myService1.func1();
    Log.Print("myService1.func1() returned {0}", result);

    //query myService1.func2()
    Log.WaitForEnter("3) Press ENTER to query 2-nd time...");
    result = myService1.func2();
    Log.Print("myService1.func2() returned {0}", result);

    //create myService2
    Log.WaitForEnter("4) Press ENTER to create another instance of Remote Service...");
    MyService myService2 = new MyService();
    Log.Print("myService2 created. Proxy? {0}",
        (RemotingServices.IsTransparentProxy(myService2)?"YES":"NO"));

    //query myService2.func1()
    Log.WaitForEnter("5) Press ENTER to query from our new Remote Service...");
    Log.Print("myService2.func1() returned {0}", myService2.func1());

    Log.WaitForEnter("Press ENTER to exit...");
  }
}

    We get

SERVER: 

[4424] [2008/10/05 22:31:52.369] Instance of MyService is created, MyService.id=1
[4424] [2008/10/05 22:31:52.369] func1() is invoked, MyService.id=1
[4424] [2008/10/05 22:31:53.056] func2() is invoked, MyService.id=1
[4424] [2008/10/05 22:31:54.556] func1() is invoked, MyService.id=1

CLIENT: 

>1) Press ENTER to create Remote Service...
[7076] [2008/10/05 22:31:51.416] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...
[7076] [2008/10/05 22:31:52.400] myService1.func1() returned MyService#1.func1()

3) Press ENTER to query 2-nd time...
[7076] [2008/10/05 22:31:53.056] myService1.func2() returned MyService#1.func2()

4) Press ENTER to create another instance of Remote Service...
[7076] [2008/10/05 22:31:53.650] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...
[7076] [2008/10/05 22:31:54.556] myService2.func1() returned MyService#1.func1()

    Here in this sample only 1 MyService instance was created on Server side. It served all 3 calls even though 2 calls came from myService1 and 1 call from myService2.

4.2. Server Side Object Activation. SingleCall

    As for creation of object on a Server side, we have the same situation – no object is created with “new MyService()” on a Client application. But as soon as you invoke ANY method in Client code, the invocation is directed to Server application. The .NET Remoting creates NEW instace for each such query. As you can see on a picture below, there were 5 invocations sent from 2 Client applications. It made .NET Remting create 5 instances of MyService. Each of instances was used only once – for single call. Pay attention that “Instance of MyService” #3 and #5 were for the same created with the same call of “myService1.func1()”, but still .NET Remoting created a separate instance for each call.

    Single Trasparent Proxy is created for all MyService objects in Client application (see second Client).

Image 3

    To use this type of activation you should configure server with well-known type like you did for SSA Singleton. The only difference is that mode should be set to “SingleCall”:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown
          type="ONX.Cmn.MyService, ONXCmn"
          objectUri="MyServiceUri"
          mode="SingleCall" />
      </service>
    <channels>
      <channel ref="tcp" port="33000"/>
    </channels>
  </application>
</system.runtime.remoting>
</configuration>

    Client configuration is absolutely the same as for SSA Singleton:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown
          type="ONX.Cmn.MyService, ONXCmn"
          url="tcp://localhost:33000/MyServiceUri" />
      </client>
    </application>
  </system.runtime.remoting>
</configuration>

    As for sample, locate “SAO SingleCall” solution.

SERVER: 

>[3472] [2008/10/05 22:21:57.662] Instance of MyService is created, MyService.id=1
[3472] [2008/10/05 22:21:57.662] func1() is invoked, MyService.id=1
[3472] [2008/10/05 22:22:00.381] Instance of MyService is created, MyService.id=2
[3472] [2008/10/05 22:22:00.381] func2() is invoked, MyService.id=2
[3472] [2008/10/05 22:22:04.849] Instance of MyService is created, MyService.id=3
[3472] [2008/10/05 22:22:04.849] func1() is invoked, MyService.id=3

CLIENT: 

1) Press ENTER to create Remote Service...
[7252] [2008/10/05 22:21:54.209] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...
[7252] [2008/10/05 22:21:57.693] myService1.func1() returned MyService#1.func1()

3) Press ENTER to query 2-nd time...
[7252] [2008/10/05 22:22:00.381] myService1.func2() returned MyService#2.func2()

4) Press ENTER to create another instance of Remote Service...
[7252] [2008/10/05 22:22:02.756] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...
[7252] [2008/10/05 22:22:04.849] myService2.func1() returned MyService#3.func1()     

    In our sample the “id” is the unique id of each instance of MyService object that is created on Server side. As you can see, we have as many instances created in SERVER app as number of calls (e.g. 2 calls for myService1 and 1 call for myService2 – in sum we got 3).

    Also, according to timestamps you may conclude that MyService is created right with “func#()” call.

4.3. Client Side Activation

    This is pretty nice type of activation to have as it makes you to work with object like “there is no remoting at all”. You have distinct instance of object created for each of your “new” operator. Your instance is created remotely on a Server and it is never shared with other Client applications. So, for Client application this type of activation is very close to use case when you create object is a regular way, without .NET Remoting involved.

Image 4

    myService, myService1 and myService2 are real 3 objects that were instantiated on Server and transparently used by Client applications. Pay attention that among 3 types of activation described this is the only one where we have more than one proxy created for Client #2. This is because number of proxies will be equal to number of remote objects that your Client application has created so far.

    To use this type of activation you should configure server with “<activated />”section, not with well-known type:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <activated type="ONX.Cmn.MyService, ONXCmn" />
      </service>
      <channels>
        <channel ref="tcp" port="33000"/>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

    Client configuration also uses “<activated />”section:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <client url="tcp://localhost:33000">
        <activated type="ONX.Cmn.MyService, ONXCmn" />
      </client>
    </application>
  </system.runtime.remoting>
</configuration>

    Pay attention that “url” parameter is specified in “<client />” section with this type of activation. There is no need for objectURI here as .NET Remoting will know what type to use.

    Also, Leasing expiration is involved in this activation type. See

    As for sample, locate “CAO” solution. I won’t present text of Client code as it is the same as for 2 tests from above. The only change is configuration that controls the type of activation. Now, we get

SERVER: 

>[6956] [2008/10/05 22:38:47.075] Instance of MyService is created, MyService.id=3
[6956] [2008/10/05 22:38:49.918] func1() is invoked, MyService.id=3
[6956] [2008/10/05 22:38:52.559] func2() is invoked, MyService.id=3
[6956] [2008/10/05 22:38:54.965] Instance of MyService is created, MyService.id=4
[6956] [2008/10/05 22:38:57.231] func1() is invoked, MyService.id=4

CLIENT: 

1) Press ENTER to create Remote Service...
[2280] [2008/10/05 22:38:47.090] myService1 created. Proxy? YES

2) Press ENTER to query 1-st time...
[2280] [2008/10/05 22:38:49.918] myService1.func1() returned MyService#3.func1()

3) Press ENTER to query 2-nd time...
[2280] [2008/10/05 22:38:52.559] myService1.func2() returned MyService#3.func2()

4) Press ENTER to create another instance of Remote Service...
[2280] [2008/10/05 22:38:54.965] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...
[2280] [2008/10/05 22:38:57.231] myService2.func1() returned MyService#4.func1()

    In this sample MyService instances created on Server side right at the time that Client application code hits “new MyService()” command. You can see some delay in creation of myService1 (15 ms). This is because this was first call from our Client application to Server. It required establishing physical network connection between our applications and did all other hidden .NET Remoting handshakes. As for myService2 it was created right at the same millisecond. Also, as you can see, each of our myService# on Client side was served with corresponding MyService instance on Server side.

5. What is Lease Time? How to control it?

    In case of interprocess coordination Server does not know if Client is still going to use object or not. The easiest way for remoting object in Server application is to count how much time has passed since object was created or since last time when some Client used the object (e.g. made some method invocation).

    There are means to set lease time through configuration files (showed below) and through code:

using System;
using System.Runtime.Remoting.Lifetime; 
...
LifetimeServices.LeaseTime = TimeSpan.FromMinutes(30);
LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(30);
LifetimeServices.LeaseManagerPollTime = TimeSpan.FromMinutes(1);

    LeaseTime – is initial lease time span for AppDomain.
    RenewOnCallTime - the amount of time by which the lease is extended every time when call comes in on the server object.
    LeaseManagerPollTime - the time interval between each activation of the lease manager to clean up expired leases.

    See MSDN for details.

    Here is how it works. For each server object we can get CurrentLeaseTime time from Lease helper. This CurrentLeaseTime is how much time left for object to live. There is a .NET Remoting LeaseManager that wakes up periodically and checks every available server object in Server application. With each check it reduces the CurrentLeaseTime for each checked object. If object is expired then its reference is removed and that object is marked for GC to be collected. Every time when remote call comes for server object, this object’s CurrentLeaseTime is set to RenewOnCallTime time span.

    Take a look at “Lease Time” solution. As you can see it uses Server Activation in Singleton mode. It should make all Clients and all MyService objects in Clients’ application use the same instance of MyService that is on Server.

    But we configured lease time to be only 5 seconds:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      ...
      <lifetime 
        leaseTime="5S"
        renewOnCallTime="5S"
        leaseManagerPollTime="1S" />
    </application>
  </system.runtime.remoting>
</configuration>

    It makes .NET Remoting mark remoted object for garbage collection until 5 seconds passed with no queries from any Client:

1) Press ENTER to create Remote Service...
[5044] [2008/10/01 14:17:47.442] myService1 created. Proxy? YES 

2) Press ENTER to query 1-st time...
[5044] [2008/10/01 14:17:48.552] myService1.func1() returned MyService#4.func1()

3) Press ENTER to query 2-nd time...
[5044] [2008/10/01 14:18:03.334] myService1.func2() returned MyService#5.func2()

4) Press ENTER to create another instance of Remote Service...
[5044] [2008/10/01 14:18:04.099] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...
[5044] [2008/10/01 14:18:04.990] myService2.func1() returned MyService#5.func1()

    See “3)” in output. As you can see, we were waiting too long (e.g. >5 seconds) before we invoked query 2-nd time. It made Server forget about MyService#4 and create new one – MyService#5. After that in “5)” we invoked func1() within 2 seconds and it was using MyService#5 as it was not expired yet on Server side.

    Here we start our Client application again and press ENTER continuously with no delays. As you can see, all three invokes use the same MyService instance:

1) Press ENTER to create Remote Service...
[5380] [2008/10/01 14:30:39.355] myService1 created. Proxy? YES 

2) Press ENTER to query 1-st time...
[5380] [2008/10/01 14:30:39.589] myService1.func1() returned MyService#6.func1()

3) Press ENTER to query 2-nd time...
[5380] [2008/10/01 14:30:39.652] myService1.func2() returned MyService#6.func2()

4) Press ENTER to create another instance of Remote Service...
[5380] [2008/10/01 14:30:39.808] myService2 created. Proxy? YES

5) Press ENTER to query from our new Remote Service...
[5380] [2008/10/01 14:30:39.980] myService2.func1() returned MyService#6.func1()    

    We can also make our remoting object never expire. In order to do so we will need to override one of the MarshalByRefObject methods and make it return “null”:

public class MyService : MarshalByRefObject
{
  ...

  public override object InitializeLifetimeService()
  {
    return null;
  }
}

If you add such override to LeaseTime solution, you will see that even though we waited too long and have <lifetime> parameter specified in configuration – our MyService is not expired and reused for all calls:

2) Press ENTER to query 1-st time...
[3056] [2008/10/01 14:36:51.455] myService1.func1() returned MyService#1.func1() 

>3) Press ENTER to query 2-nd time...
[3056] [2008/10/01 14:37:14.049] myService1.func2() returned MyService#1.func2()

    There is also “sponsoring” mechanism that allows customizing the lease time according application needs. You can read “sponsors” topic in MSDN to get more information on this.

6. Hide Implementation from Client. Remoting via Interface exposure

    It is not always a good idea to expose to the world the implementation of your remoting object. This is due to security reasons and due to size of assembly that has complex implementation. Also, implementation can use some other assemblies that you would not want to deploy to client computers. In this case it is a good idea to split our MyService class into:

        1) interface that we will expose to client
        2) and to the implementation itself.

    At this point we can put out types into separate assemblies and deliver only small part to client computer:

types_assemblies.PNG

    Then during delivery we need to put only small part of product on Client computers:

deployment.PNG

    You may find “Hide Implementation from Client app” solution to see how it is implemented. The idea is to request remote type by Uri and cast returned object to interface. On a server side such Uri request will instantiate our real implementation that is defined in ServerLib assembly.

    Server configuration:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown
          type="ONX.Server.MyService, ONXServerLib"
          objectUri="MyServiceUri"
          mode="Singleton" />
      </service>
      <channels>
        <channel ref="tcp" port="33000"/>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

    Client configuration (no need to define wellknown type here):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

    Client code to access MyService through IMyService:

IMyService myService1 = 
  Activator.GetObject(
    typeof(IMyService),
    "tcp://localhost:33000/MyServiceUri"
  ) as IMyService;

string result = myService1.func1();

    Note, that there is no way to use Client Activation Object if you decide to go with hiding implementation behind interface. This is because you need to _instantiate_ object of class in client side for Client Side activation. But you don’t have type information on client side – only interface. So, you can do this only with well known type definition (e.g. Server Activation Object).

7. Custom types as parameters and return values

    If you want to pass your own types as parameters to methods of remoted objects… If you want to get such types as results of functions… The only thing that you should do is to make your type serializable. This is easy – just add [Serailizable] attribute for your type description. Note, if your type has members of custom types, those included types should be also serializable. As for standard types like int, double, string, ArrayList and so on – most of them are already serializable.

    See “Custom Types” solution with example:

[Serializable]
public class MyContainer
{
  private string str_;
  private int num_; 

  public MyContainer(string str, int num)
  {
    str_ = str;
    num_ = num;
  }

  public string Str { get{ return str_;} }
  public int Num { get{ return num_;} }</p>

  public override string ToString()
  {
    return string.Format("MyContainer[str=\"{0}\",num={1}]", Str, Num);
  }
}

public class MyService : MarshalByRefObject
{
  public MyContainer func1(MyContainer param)
  {
    Log.Print("func1() is invoked, got {0}", param);
    return new MyContainer("abc", 123);
  }
}

    With this Client code

class MyClient
{
  [STAThread]
  static void Main(string[] args)
  {
    MyService myService = new MyService();
    Log.Print("myService created. Proxy? {0}",
      (RemotingServices.IsTransparentProxy(myService)?"YES":"NO")); 

    MyContainer container1 = new MyContainer("From Client", 555);

    MyContainer container2 = myService.func1(container1);
    Log.Print("myService.func1() returned {0}", container2);
  }
}

    it will give you such Server output:

[3660] [2008/10/03 10:05:27.970] func1() is invoked, got MyContainer[str="From Client",num=555]

    and such Client output:

[2696] [2008/10/03 10:05:27.892] myService created. Proxy? YES
[2696] [2008/10/03 10:05:27.970] myService.func1() returned MyContainer[str="abc",num=123]

8. Custom exceptions through remoting channel

    There are no limitations on throwing standard Exception class as it already has everything that is needed. As for custom exceptions here is the list of required TODOs:

        1) General rule: All custom exceptions should drive from Exception class or it’s descentants.
        2) It must have [Serializable] attribute for class
        3) It must have constructor

MyException(SerializationInfo info, StreamingContext context)

        4) It must override

void GetObjectData(SerializationInfo info, StreamingContext context)

        5) If your custom exception has some members, those should be taken care to write and read to/from stream.

    Here is our custom exception from “Exceptions” solution:

[Serializable]
public class MyException : ApplicationException
{
  private string additionalMessage_; 

  public MyException(string message, string additionalMessage)
    :base(message)
  {
    additionalMessage_ = additionalMessage;
  }

  public MyException(SerializationInfo info, StreamingContext context)
    :base(info, context)
  {
    additionalMessage_ = info.GetString("additionalMessage");
  }

  public override void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    base.GetObjectData (info, context);
    info.AddValue("additionalMessage", additionalMessage_);
  }

  public string AdditionalMessage { get{ return additionalMessage_;} }
}

    We save our member data in “GetObjectData(…)” method. During deserialization we restore this value in constructor with SerializationInfo as parameter.

    With this MyService implementation:

public class MyService : MarshalByRefObject
{
  public void func1()
  {
    throw new MyException("Main text for custom ex", "Additional text");
  } 
  
  public void func2()
  {
    throw new Exception("Main text for standard ex");
  }
}

    We simply try to throw both our custom exception and starndard one. Having such Client implementation:

class MyClient
{
  [STAThread]
  static void Main(string[] args)
  {
    RemotingConfiguration.Configure("ONXClient.exe.config");

    MyService myService = new MyService(); 
  
    try
    {
      myService.func1();
    }
    catch(MyException ex)
    {
      Log.Print("Caught MyException: message=\"{0}\", add.msg=\"{1}\"",
        ex.Message, ex.AdditionalMessage);
    }

    try
    {
      myService.func2();
    }
    catch(Exception ex)
    {
      Log.Print("Caught Exception: message=\"{0}\"",
        ex.Message);
    }

    Log.WaitForEnter("Press ENTER to exit...");
  }
}

    We get output (stripped):

[15:09:39.380] Caught MyException: message="Main text for custom ex", add.msg="Additional text"
[15:09:39.380] Caught Exception: message="Main text for standard ex"

    If we comment out saving and restoring of additionalMessage field in MyException class – after deserialization we will get default string value. So, no error will be generated but not full state restoring. If we comment out [Serializable] attribute, we will get runtime exception.

9. Events in .NET Remoting

    Imagine use case. Our remoting object is instantiated in Server. In regular use case Client applications use remoting object by invoking its methods. What if you want it to invoke some callback method that is resided inside Client application? You might prepare some information for Client and wait for client application to use polling mechanism and to call some remote object method periodically like “Information[] MyService.IsThereSomeInfoForMe()”. But actually we can use event mechanism. There are some refinements though:

        1) Server application should have runtime type information about type that holds callback method.
        2) This callback method should be public and cannot be static
        3) To avoid Server to wait and make sure that callback got recipient, we have to mark callback with [OneWay] attribute. It makes us unable to return some data neither through “return” value nor through “ref” of “out” parameters.
        4) As instance of this type will instantiated on Client side and will be used on Server side, it should derive from MarshalByRejObject class.

    Take a look at “Events” solution. All these limitations make us to introduce some even sink and define it in Cmn assembly so it is available for both Server and Client application:

public class EventSink : MarshalByRefObject
{
  public EventSink()
  {
  } 

  [System.Runtime.Remoting.Messaging.OneWay]
  public void EventHandlerCallback(string text)
  {
  }

  public void Register(MyService service)
  {
    service.EventHandler += new OnEventHandler(EventHandlerCallback);
  }

  public void Unregister(MyService service)
  {
     service.EventHandler -= new OnEventHandler(EventHandlerCallback);
  }
}

    As we want this sink to actually invoke our callback, we cannot use polymorphism and override some of methods in derived class that would be defined inside code of our Client application. This will violate rule #1 from above – Server will need to know our type. So we use delegation mechanism and pass our Client’s callback to EvenSink as constructor parameter. Here is full code for EventSink class:

public class EventSink : MarshalByRefObject
{
  private OnEventHandler handler_; 

  public EventSink(OnEventHandler handler)
  {
    handler_ = handler;
  }

  [System.Runtime.Remoting.Messaging.OneWay]
  public void EventHandlerCallback(string text)
  {
    if (handler_ != null)
    {
      handler_(text);
    }
  }

  public void Register(MyService service)
  {
    service.EventHandler += new OnEventHandler(EventHandlerCallback);
  }

  public void Unregister(MyService service)
  {
    service.EventHandler -= new OnEventHandler(EventHandlerCallback);
  }
}

    Also, since .NET Framwork v1.1 there are security restriction on deserialization of some types. In order to override default setting we need to set filterLevel to “Full”. Here is full Server config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown
          type="ONX.Cmn.MyService, ONXCmn"
          objectUri="MyServiceUri"
          mode="Singleton" />
      </service>
      <channels>
        <channel ref="tcp" port="33000">
          <serverProviders>
            <formatter ref="binary" typeFilterLevel="Full" />
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

    And Client configuration:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown
          type="ONX.Cmn.MyService, ONXCmn"
          url="tcp://localhost:33000/MyServiceUri" />
      </client>
      <channels>
        <channel ref="tcp" port="0">
          <clientProviders>
            <formatter ref="binary" />
          </clientProviders>
          <serverProviders>
            <formatter ref="binary" typeFilterLevel="Full" />
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

    It is also possible to configure this through code. See MSDN for details. Take a look at MyService class now.

public delegate void OnEventHandler(string message); 

public class MyService : MarshalByRefObject
{
  public event OnEventHandler EventHandler;

  public string func1()
  {
    PublishEventAnfScheduleOneMore("Event from Server: func1() is invoked");
    return "MyService.func1()";
  }

  private void PublishEvent(string message)
  {
    if (EventHandler != null)
    {
      EventHandler(message);
    }
  }

  private void PublishEventAnfScheduleOneMore(string text)
  {
    PublishEvent(text);
    Thread t = new Thread(new ThreadStart(PublishEventIn5Seconds));
    t.Start();
  }

  private void PublishEventIn5Seconds()
  {
    Thread.Sleep(5000);
    PublishEvent("5 seconds passed from one of method calls");
  }
}

    As you can see we invoke callback immediately when some Client called “MyService.func()” and also we do it one more time from separate thread after 5 seconds timeframe. It was done for testing purposes to show that events can be invoked at any time (e.g. not even to answer on call invocation). We span a separate thread and return control to Client that invoked “func1()”. And then, after 5 seconds our spanned thread will raise event for all registered event handlers. Once Client registers its event handler - it will get ALL events from Server.

    Here is stripped code for our Client application. Full version is available in “Events” solution:

class MyClient
{
  private MyService myService_;
  private EventSink sink_; 

  public MyClient()
  {
    //create proxy to remote MyService
    myService_ = new ONX.Cmn.MyService();</p>

    //create event sink that can be invoked by MyService
    sink_ = new EventSink(new OnEventHandler(MyEventHandlerCallback));

    //register event handler with our event sink
    //(after that event sink will invoke our callback)
    sink_.Register(myService_);
  }

  public void MyEventHandlerCallback(string text)
  {
    Log.Print("Got text through callback! {0}", text);
  }

  public void Test()
  {
    Log.Print("myService.func1() returned {0}", myService_.func1());
  }

  [STAThread]
  static void Main(string[] args)
  {
    RemotingConfiguration.Configure("ONXClient.exe.config");

    MyClient c = new MyClient();
    c.Test();

    Log.WaitForEnter("Press ENTER to exit...");
  }
}

    And here is stripped output from one of test runs:

[5412] [09:43:55] myService.func1() returned MyService.func1()
[5412] [09:43:55] Press ENTER to exit...
[7724] [09:43:55] Got … callback! Event from Server: func1() is invoked
[7724] [09:44:00] Got … callback! 5 seconds passed from one of method calls

    As you can see we got initial event right after call to “func1()” and then one more after 5 seconds. Pay attention that callback functions were invoked on a separate thread. So, if you need to synchronize some data access, beware.

10. Asynchronous calls

    This topic does not differ much from simple asynchronous calls without remoting. Let’s analyze “Async Calls” solution. It has simple implementation of MyService:

public class MyService : MarshalByRefObject
{
  public string func1(string text)
  {
    Log.Print("func1(\"{0}\") is invoked", text);
    return text+DateTime.Now.ToString("HH:mm:ss.fff");
  }
}

    And here is the sample of how it is used in Client application:

delegate string GetStringHandler(string arg);

class MyClient
{
  private const int NUMBER_OF_INVOCATIONS = 5;</p>

  private static void OnCallEnded(IAsyncResult ar)
  {
    GetStringHandler handler = ((AsyncResult)ar).AsyncDelegate as GetStringHandler;
    int index = (int)ar.AsyncState;</p>

    string result = handler.EndInvoke(ar);

    Log.Print("myService.func1() #{0} is done. Result is \"{1}\"",
      index, result);
  }

  [STAThread]
  static void Main(string[] args)
  {
    RemotingConfiguration.Configure("ONXClient.exe.config");

    MyService myService = new MyService();
    Log.Print("myService created. Proxy? {0}",
      (RemotingServices.IsTransparentProxy(myService)?"YES":"NO"));

    for(int index=1;index<=NUMBER_OF_INVOCATIONS;++index)
    {
      Log.Print("Invoking myService.func1() #{0}...", index);
      GetStringHandler handler = new GetStringHandler(myService.func1);
      handler.BeginInvoke("from Client", new AsyncCallback(OnCallEnded), index);
    }

    Log.WaitForEnter("Press ENTER to exit...");
  }
}

    As you can see we loop 5 times in “for”. With every iteration we create delegate that corresponds to prototype of “MyService.func1” method and make asynchronous call with “BeginInvoke(…)”. As we passed our “OnCallEnded” method as callback, when asynchronous invocation is done, we get control in our OnCallEnded method. There we get reference to our delegate and get result by calling “EndInvoke(ar)”.

    Here is example out output of Client application:

[0216] [2008/10/03 16:39:45.243] myService created. Proxy? YES
[0216] [2008/10/03 16:39:45.243] Invoking myService.func1() #1...
[0216] [2008/10/03 16:39:45.274] Invoking myService.func1() #2...
[0216] [2008/10/03 16:39:45.274] Invoking myService.func1() #3...
[0216] [2008/10/03 16:39:45.290] Invoking myService.func1() #4...
[0216] [2008/10/03 16:39:45.290] Invoking myService.func1() #5...

[0216] [2008/10/03 16:39:45.290] Press ENTER to exit...
[2248] [2008/10/03 16:39:45.290] myService.func1() #2 is done. Result is "from Client16:39:45.274"
[3868] [2008/10/03 16:39:45.290] myService.func1() #1 is done. Result is "from Client16:39:45.274"
[2248] [2008/10/03 16:39:45.290] myService.func1() #3 is done. Result is "from Client16:39:45.290"
[2248] [2008/10/03 16:39:45.290] myService.func1() #5 is done. Result is "from Client16:39:45.290"
[3868] [2008/10/03 16:39:45.290] myService.func1() #4 is done. Result is "from Client16:39:45.290"

    We were even lucky to get our 5 calls in order, that is different from original – call #2 ends earlier than call #1. The same about calls #4 and #5.

    Also notice, that not all calls were running in the same thread. And all of them are different from thread where we initiated our 5 calls.

11. Several Services in one Server. Several Server links from single Client app

    All the samples in MSDN and internet that I reviewed were showing single Remoting Object type in Server application. I was wonder how do we introduce several services in single Server. And how do we use several Servers in single Client application. It appeared to be not so hard, but still it better to see than to guess.

    Let us analyze the case with 2 wellknown types on Server side:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown
          type="ONX.Cmn.MyService1, ONXCmn"
          objectUri="MyService1Uri"
          mode="SingleCall" />
        <wellknown
          type="ONX.Cmn.MyService2, ONXCmn"
          objectUri="MyService2Uri"
          mode="SingleCall" />
      </service>
      <channels>
        <channel ref="tcp" port="33000"/>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

    You cannot:

        1) Have several channels with the same protocol (e.g. “ref” parameter). Otherwise you will get exception that such protocol is already registered. But you can specify several channels if they are for different protocols
        2) Each known type should have unique objectUri. Otherwise definition of types will be overlapped and only on of types will be available

    For configuration from above our helper “Utils.DumpAllInfoAboutRegisteredRemotingTypes();” method gives us:

[7496] [2008/10/05 00:01:04.047] ALL REGISTERED TYPES IN REMOTING -(BEGIN)---------
[7496] [2008/10/05 00:01:04.047] WellKnownServiceTypeEntry: type='ONX.Cmn.MyService2, ONXCmn'; objectUri=MyService2Uri; mode=SingleCall
[7496] [2008/10/05 00:01:04.047] WellKnownServiceTypeEntry: type='ONX.Cmn.MyService1, ONXCmn'; objectUri=MyService1Uri; mode=SingleCall
[7496] [2008/10/05 00:01:04.047] ALL REGISTERED TYPES IN REMOTING -(END) ---------

    In our case Client configuration looks like:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.runtime.remoting>
    <application>
      <client>
        <wellknown
          type="ONX.Cmn.MyService1, ONXCmn"
          url="tcp://localhost:33000/MyService1Uri" />
        <wellknown
          type="ONX.Cmn.MyService2, ONXCmn"
          url="tcp://localhost:33000/MyService2Uri" />
      </client>
    </application>
  </system.runtime.remoting>
</configuration>

    If we would want to use Services from different Servers, each Server would listen on different port. So, there would be different port in each “<wellknown/>” section.

    There is “Two Services in single Server” solution if you would like to try it for yourself.

12. Summary

    Thank you for your time. I hope it was spent with use. Any comments are welcome. I will try to adjust this article as soon as I have some comments and time. Happy remoting!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow to host the .net remoting in IIS Pin
bhavin chheda11-Aug-17 0:13
bhavin chheda11-Aug-17 0:13 
QuestionGood Job Pin
Kushan Randima14-Jun-15 20:07
Kushan Randima14-Jun-15 20:07 
QuestionVery good Article. My Vote of 5 Pin
Ali_IND17-Dec-12 23:34
Ali_IND17-Dec-12 23:34 
GeneralMy vote of 5 Pin
Member 92209444-Aug-12 4:22
Member 92209444-Aug-12 4:22 
QuestionI was looking for it Pin
Saumitra Kumar Paul20-Aug-11 8:55
Saumitra Kumar Paul20-Aug-11 8:55 
GeneralMy vote of 5 Pin
James Schuldiner20-Mar-11 14:14
James Schuldiner20-Mar-11 14:14 
GeneralMy vote of 5 Pin
programist1234-Mar-11 3:06
programist1234-Mar-11 3:06 
GeneralMy vote of 5 Pin
Liwei Xu24-Feb-11 12:50
Liwei Xu24-Feb-11 12:50 
Question.net remoting with CruiseControl.net Pin
mcraig8831-Jan-11 12:06
mcraig8831-Jan-11 12:06 
GeneralClient and Server in the same application Pin
Sergiu M12-Jan-11 3:50
Sergiu M12-Jan-11 3:50 
GeneralMy vote of 5 Pin
Kais19776-Nov-10 5:51
Kais19776-Nov-10 5:51 
GeneralMy vote of 5 Pin
thatraja24-Sep-10 23:47
professionalthatraja24-Sep-10 23:47 
GeneralMy vote of 5 Pin
akiner0015-Jul-10 15:36
akiner0015-Jul-10 15:36 
Questionprblem remting wirh 2 ethernet Pin
Member 371594516-Nov-09 1:12
Member 371594516-Nov-09 1:12 
GeneralQuestion Pin
Alireza_136227-Oct-09 4:27
Alireza_136227-Oct-09 4:27 
GeneralISponsor implementation Pin
Kurkin Dmitry5-Aug-09 18:39
Kurkin Dmitry5-Aug-09 18:39 
GeneralGood article but little lengthy! Pin
VenkataSirish16-Jul-09 8:58
VenkataSirish16-Jul-09 8:58 
Generalcan't make it work in VB.net Pin
Kerroux6-Jan-09 10:15
Kerroux6-Jan-09 10:15 
GeneralRegisterWellKnownClientType Pin
[Neno]30-Oct-08 22:35
[Neno]30-Oct-08 22:35 
Generaldo's and don'ts Pin
ervegter8-Oct-08 1:03
ervegter8-Oct-08 1:03 
QuestionWhy? Pin
Roger Jakobsson7-Oct-08 11:33
professionalRoger Jakobsson7-Oct-08 11:33 
AnswerRe: Why? Pin
Olexandr Malko7-Oct-08 11:53
Olexandr Malko7-Oct-08 11:53 
GeneralRe: Why? Pin
thebossedgar7-Oct-08 14:13
thebossedgar7-Oct-08 14:13 
GeneralRe: Why? Pin
AndyKEnZ8-Oct-08 0:17
AndyKEnZ8-Oct-08 0:17 
GeneralRe: Why? Pin
ervegter8-Oct-08 1:14
ervegter8-Oct-08 1:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.