Seamless Remoting

posted on 03/28/06 at 09:54:44 pm by Joel Ross

One of my clients just got word that they will be moving where they host their servers, and the new environment is much stricter than the old one. The public website will be in an outer DMZ and only able to communicate with an inner DMZ. The database server will be inside the firewall completely, meaning that the web server?can no longer communicate directly with the database server, and instead has to do so through an application server.

I'm sure this is a common scenario that people have to deal with. We discussed a few different options, and eventually settled on remoting. But, our number one concern when implementing this was to make it seamless - we didn't want to perform major surgery on the code base. Why? First, we didn't want to risk missing a spot and not catching it until later. Second, we only need to use remoting from the public website, and not the internal application that uses the same DLL. If we had to make a bunch of changes to the web code and then have to use two different coding styles for the internal app and the external app.

So I set out to find the least intrusive mechanism to get remoting running. I think we came up with a nice way to do it. Before I go into details, I should back up and point out our architecture. We used basic data entities - dummy objects that basically just hold data. Then we use static managers to manipulate the data.

So, with remoting, we had a few challenges. How do we perform surgery on a large code base without having to go back and touch every line of code? In my opinion, it's much harder to go in and touch every peice of existing code than it is to introduce a new layer that will make the necessary changes transparent to the rest of the application, so that's what we did.

Before we get into the solution, we need to touch on our architecture. We used static managers with "dumb" data classes. A "dumb" data class has no inherent ability to do anything. It holds data and that's pretty much it. They're easily serializable, but have no smarts. The Managers know nothing about state, so you pass in the data that it should act on. With that, here's how our code looks now, both from the website and in the managers. First, the website code (this would typically be in our controller):

   1:  Model.Customer customer = new CustomerManager.GetCustomer(customerId);
   2:  ?
   3:  customer.FirstName = "Joel";
   4:  customer.LastName = "Ross";
   5:  ?
   6:  CustomerManager.SaveCustomer(customer);

Now, the manager:

   1:  public static class CustomerManager
   2:  {
   3:      public static Model.Customer GetCustomer(int customerId) 
   4:      {
   5:          return Data.Customer.GetCustomer(customerId);
   6:      }
   7:  ?
   8:      public static void SaveCustomer(Model.Customer customer)
   9:      {
  10:          Data.Customer.SaveCustomer(customer);
  11:      }
  12:  }

Obviously, there's more. There's a UI on top of the controller, and there's a data layer below the managers, but since this is where our break will occur, this is the interesting part of the code. So how do we get "remoting goo" between these two? Well, first we have to have objects for remoting. Remoting uses singletons to manage passing data between the two systems. So, we have to have some objects that can be instantiated, but still act like static classes. What I mean there is that they shouldn't have internal data structures, since they will be shared across the application.

This is created in the application layer, and sits on top of the manager layer. Here's an example:

   1:  public class CustomerManagerObject : MarshalByRefObject
   2:  {
   3:      public Model.Customer GetCustomer(int customerId) 
   4:      { 
   5:          return CustomerManager.GetCustomer(customerId); 
   6:      }
   7:  ?
   8:      public Model.Customer SaveCustomer(Model.Customer customer) 
   9:      {
  10:          CustomerManager.SaveCustomer(customer);
  11:          return customer;
  12:      }
  13:  }

A couple things to note: First, this object has to inherit from MarshalByRefObject so it can be moved across the wire from the web server to the application server. The second note is that SaveCustomer now returns a Customer object. When a Customer object is passed over the wire, it's serialized, and is disconnected from the original object. Normally, the object would be passed by reference, so any changes to the object made by the manager layer or below will not be pushed back to the client automatically like they were in the past. By returning the object, we can get those changes back to the client easily.

So that's how the code looks from the application server. There's a couple more things?to do to expose the CustomerManagerObject to the world. First, we need a configuration file, which looks like this:

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:    <system.runtime.remoting>
   4:      <application>
   5:        <service>        
   6:          <wellknown mode="Singleton"  type="Application.Business.CustomerManagerObject, Application" objectUri="Application.Business.CustomerManagerObject.rem"/>
   7:        </service>
   8:        <channels>
   9:          <channel ref="http" port="8989"/>
  10:        </channels>
  11:      </application>
  12:    </system.runtime.remoting>
  13:  </configuration>

This, when loaded by the application, will tell the remoting server to listen for requests for Application.Business.CustomerManagerObject.rem on port 8989. The server then uses the app configuration file to fire up the remoting engine:

   1:  RemotingConfiguration.Configure("RemotingServer.exe.config");

So now we have?a fully functional server that is listening for requests. How do we make requests? Well, remember our primary goal: minimal changes to the way the the website is written, and not just that, but minimal changes to the way it will be written going forward. But how do we do that? Remember, our code calls the managers directly, but those aren't exposed via the remoting server - objects sitting on top of those managers are.

Combine the above need with the desire to have the same data objects, and what we came up with was a model in our managers that uses compiler directives. This allowed us to use a modified version of the Application DLL in the web project that would provide us with the necessary "remoting goo" to make it work. I'll show the manager without the compiler directives in it for simplicity - you'll see exactly what the website would see. Later, I'll show the two mashed together. So, here's the manager from the website's perspective:

   1:  public static class CustomerManager
   2:  {
   3:      public static Model.Customer GetCustomer(int customerId) 
   4:      {
   5:          CustomerManagerObject customerManager = new CustomerManagerObject();
   6:          return customerManager.GetCustomer(customerId);
   7:      }
   8:  ?
   9:      public static void SaveCustomer(Model.Customer customer)
  10:      {
  11:          CustomerManagerObject customerManager = new CustomerManagerObject();
  12:          customer = customerManager.SaveCustomer(customer);
  13:      }
  14:  }

Now you can see how we avoided changing the website's existing code. Each call to the manager is "intercepted" by our alternate manager, and instead of doing direct calls, it instantiates the CustomerManagerObject and makes the calls that way. The remoting engine intercepts those calls and passes them over the wire to the application server. Of course, you have to configure that, so there's a configuration file on the web side too:

   1:  <?xml version="1.0"?>
   2:  <configuration>
   3:    <system.runtime.remoting>
   4:      <application>
   5:        <client>
   6:          <wellknown type="Application.Business.CustomerManagerObject, Application" url="http://localhost:8989/Application.Business.CustomerManagerObject.rem" />
   7:        </client>
   8:      </application>
   9:    </system.runtime.remoting>
  10:  </configuration>

This is in a seperate file. It doesn't work in the web.config, so we pulled it out of there. To fire up the remoting engine, you make one call in Application_OnStart in the Global.asax file:

   1:  System.Runtime.Remoting.RemotingConfiguration.Configure(HttpContext.Current.Server.MapPath("client.exe.config"), false);

Now, the remoting engine intercepts calls to the CustomerManagerObject and passes them over the wire. There's one other change that we made to ensure that the remoting service is working, and I'll show that in conjunction with the other changes we made to the CustomerManagerObject. Here's the final version of the object:

   1:  internal class CustomerManagerObject : MarshalByRefObject
   2:  {
   3:      public Model.Customer GetCustomer(int customerId) 
   4:      { 
   6:          throw new NotImplementedException(); 
   7:  #else
   8:          return CustomerManager.GetCustomer(customerId); 
   9:  #endif
  10:      }
  11:  ?
  12:      public Model.Customer SaveCustomer(Model.Customer customer) 
  13:      {
  15:          throw new NotImplementedException();
  16:  #else
  17:          CustomerManager.SaveCustomer(customer);
  18:          return customer;
  19:  #endif
  20:      }
  21:  }

We use the REMOTINGCLIENT compiler directive to get the alternate business object for the website, so if the remoting engine isn't started, we'll get a?NotImplementedException() whenever we make calls - which tells us something has gone wrong. The other interesting note is that because the managers are in the Application assembly, and they are the only class that calls the remoting objects, we can make them all internal. This ensures that all of the website interaction goes through the managers, meaning that if we got to a point where we didn't need the application server anymore, it would be easy to switch back to the old way of doing things.

For completeness, here's the actual CustomerManager, with the compiler directives in it, so you can see how it would look when developing:

   1:  public static class CustomerManager
   2:  {
   3:      public static Model.Customer GetCustomer(int customerId) 
   4:      {
   6:          CustomerManagerObject customerManager = new CustomerManagerObject();
   7:          return customerManager.GetCustomer(customerId);
   8:  #else
   9:          return Data.Customer.GetCustomer(customerId);
  10:  #endif
  11:      }
  12:  ?
  13:      public static void SaveCustomer(Model.Customer customer)
  14:      {
  16:          CustomerManagerObject customerManager = new CustomerManagerObject();
  17:          customer = customerManager.SaveCustomer(customer);
  18:  #else
  19:          Data.Customer.SaveCustomer(customer);
  20:  #endif
  21:      }
  22:  }

So, if you're using a Manager design, and have a need for remoting, here's one way to do it.

Categories: C#