Remoting with IIS Server and ASP.NET Client
By Peter A. Bromberg, Ph.D.
Printer - Friendly Version
Peter Bromberg

Recently a client of the company I work for asked us to implement a web - based application that allows customers to use the web to access private customer data. One of the requirements of the application architecture is that no calls to the customer database could be made from the "web tier". This basically left me with two choices - either make the database calls through a WebService on a middle tier, or use the .NET Remoting infrastructure.



After some deliberation, I chose the latter. The middle remoting tier, which I decided to host in IIS, then makes its database calls to databases located on other database tier machines in the network, passes the results back to the remoting tier machine, and then the results are proxied back to the web UI tier. A schematic diagram of the architecture follows:

While on the surface this requirement is probably a somewhat naive way to enforce the protection of sensitive customer data, it does accomplish one thing very well - unless somebody has a way to find out the difficult internals of making a remoting call to the middle tier, there's no way they are ever going to be able to compromise customer data. In addition, since I am hosting my remoting infrastructure on a middle - tier machine under IIS, it becomes very easy to implement standard IIS authentication and security or even SSL if desired.

What I present here is a "generic" version of this architecture, with a Remoting class library that accepts generic database calls consisting of the connection name, the stored procedure name, and an object array containing the stored procedure parameters. There is also a custom "fire and forget" method which is used to log customer page views and / or click-throughs for customer tracking purposes.

In the code following, I'll show you how to set up such a remoting class in IIS using the HTTP channel and the binary formatter, create a separate "Mirror" proxy class to handle the interface necessary for the client, create the required configuration files for both server and client to set up the remoting channel and URI, and a sample Web client that uses the infrastructure as a "proof of concept". You can run all of this on one machine if desired - in two separate IIS applications, one for the server and one for the client, or, with only a one-line minor change to the client configuration file, you can run the client on as many separate machines as desired.

First, let's take a look at my Remoting server class, which I've called "General.CustomerTracker":

Option Explicit On 
Option Strict On
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Messaging
Imports Microsoft.ApplicationBlocks.Data
Imports System.IO
Imports System.Collections.Specialized
<Serializable()> _
Public Class CustomerTracker
 Inherits MarshalByRefObject
 Public DebugMode As Boolean = False
 Private Settings As NameValueCollection
 Public Sub New()
  If DebugMode Then WriteInfoToFile("Started " _
            & System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt")
  Settings = System.Configuration.ConfigurationSettings.AppSettings
  DebugMode = Convert.ToBoolean(Settings("debugMode"))
 End Sub
 <OneWay()> _
 Public Function InsertPageView(ByVal UserIPAddress As String, _
ByVal UserLoginId As String, ByVal SourceUrl As String, _ ByVal BrowseType As String, ByVal ClickThruId As String) As Integer Dim strConn As String = Convert.ToString(Settings("sqlConn")) Dim cmd As New SqlCommand Dim cn As New SqlConnection(strConn) cmd.CommandType = CommandType.StoredProcedure cmd.CommandText = "usp_InsertUserVisitData" cmd.Connection = cn cmd.Parameters.Add(New SqlParameter("@OriginIPAddress", UserIPAddress)) cmd.Parameters.Add(New SqlParameter("@UserId", UserLoginId)) cmd.Parameters.Add(New SqlParameter("@SourceUrl", SourceUrl)) cmd.Parameters.Add(New SqlParameter("@BrowserType", BrowseType)) cmd.Parameters.Add(New SqlParameter("@ClickThruID", ClickThruId)) cn.Open() Dim retval As Integer = cmd.ExecuteNonQuery() cn.Close() cmd.Dispose() If DebugMode Then WriteInfoToFile("Did page insert " _ & System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt") Return retval End Function Public Function GenericSpNonQuery(ByVal connectionName As String, _
ByVal spName As String, ByVal spParams As Object()) As Integer Dim strConn As String = Convert.ToString(Settings("sqlConn")) Dim retval As Integer = SqlHelper.ExecuteNonQuery(strConn, spName, spParams) If DebugMode Then WriteInfoToFile("Did GenericSpNonQuery insert" _ & System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt") Return retval End Function Public Function GenericSpReturnDataSet(ByVal connectionName As String, _
ByVal spName As String, ByVal spParams As Object()) As DataSet Dim strConn As String = Convert.ToString(Settings("sqlConn")) Dim ds As DataSet = SqlHelper.ExecuteDataset(strConn, spName, spParams) If DebugMode Then WriteInfoToFile("Did SpReturnDataSet" _ & System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt") Return ds End Function Public Function GenericSQLReturnDataSet(ByVal connectionName As String, _ ByVal strSQL As String) As DataSet Dim strConn As String = Convert.ToString(Settings("sqlConn")) Dim ds As DataSet = SqlHelper.ExecuteDataset(strConn, CommandType.Text, strSQL) If DebugMode Then WriteInfoToFile("Did SQLReturnDataSet" _ & System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt") Return ds End Function <OneWay()> _ Public Sub WriteInfoToFile(ByVal strData As String, ByVal strFileName As String) If strFileName = "" Then strFileName = "Log.txt" Try Dim strPath As String = System.AppDomain.CurrentDomain.BaseDirectory & "\" & strFileName Dim writer As StreamWriter = New StreamWriter(strPath, True) ' true for Append writer.Write(strData & System.DateTime.Now.ToLongTimeString) writer.Close() Catch Throw End Try End Sub End Class

Note first off, that this class derives from MarshalByRefObj, which is required for the remoting infrastructure. Everything else in this class should be pretty much self-explanatory for most developers. You also see the OneWay attribute on two of the methods; this can be used for methods that are basically "Fire and forget" which do not need to return any values, and saves the Remoting infrastructure from having to set up the plumbing to marshal objects back to the client. It's pretty much the same as making an asynchronous remoting call without the need to set up delegates.

To complete the picture for the remoting server, let's take a look at the web.config file that will go into the IIS Vroot where this will be hosted:

<configuration>
<appSettings>
<add key="sqlConn" value="Server=(local);DataBase=Usertracking;User id=sa;Password=;" />
<add key="debugMode" value="True" />
</appSettings>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown mode="Singleton" 
                   type="General.CustomerTracker, General" 
                   objectUri="CustomerTracker.soap" />
      </service>
          <channels>
            <channel ref="http"/>
     <serverProviders>
     <formatter ref="binary" />
     </serverProviders>
         </channels>
     </application>
  </system.runtime.remoting>
</configuration>  

As can be seen above. I've added an appSettings section to hold my debugMode Boolean (which controls whether to write log events to the log file for testing purposes) and a "sqlConn" connection string. The client will pass this connection string name as one of the parameters to its method calls, which enables us to have as many connection strings as our application needs, and be able to simply refer to them by name from the client method call.

Note also that I've set up a Singleton service with an objectUri of "CustomerTracker.soap", using the HTTP Channel, and the Binary Formatter. When this is deployed to IIS, you should be able to make a browser request to Http://<servername>/<vrootname>/CustomerTracker.soap?WSDL and see the generated WSDL in the browser as a test to make sure your server is correctly deployed.

In the downloadable solution, you'll also see that the Microsoft Application Block "SqlHelper" is also deployed to the server to make database calls easy. I use this extensively in my work, it makes remoting calls much easier because its various methods have overloads that accept a simple Object array containing the Sql parameter values for the particular stored procedure being called. This uses the DeriveParameters method of the SqlCommand class to discover and "fill in" the parameter details. It does involve a separate call to the database, but in practice I think you'll find this all happens so fast that you will never notice the difference.

The General.dll and Sqlhelper.dll asemblies will be placed in the /bin folder of a new IIS Application Vroot with a physical name of "Vroot" aliased in IIS as "IISImageHandler", and the web.config will be placed in the root of this folder just above the /bin folder. That is all that is necessary to set up a remoting server in IIS and have it begin operating immediately. The web.config and DLLS are not locked by IIS, so that redeployment is as simple as copying over the new files through your network.

Now let's move to the client, which will be an ASP.NET application on a completely different machine (or for testing purposes, on a different IIS Application in the same machine as the server). This is a little trickier, so follow closely as I explain the setup and the logic.

There are actually two "pages" in the sample client app, one to handle calls to an Image in the page so that the customer tracking info can be written to the database, and another "main page" that actually shows the images, has a DataGrid that displays the contents of the customer tracking log table, and also a button that allows us to clear the log entries from the table. For simplicity, I'm only going to show the main page, because the concept is the same for both.

First, the "main code block" for the main page:

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels.Http
Imports System.Runtime.Remoting.Channels
Imports System.Diagnostics
Public Class WebForm1
 Inherits System.Web.UI.Page
 Protected WithEvents Button1 As System.Web.UI.WebControls.Button
 Protected WithEvents lblMessage As System.Web.UI.WebControls.Label
 Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
#Region " Web Form Designer Generated Code "

 'This call is required by the Web Form Designer.
 <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()

 End Sub

 Private Sub Page_Init(ByVal sender As System.Object,  _
ByVal e As System.EventArgs) Handles MyBase.Init 'CODEGEN: This method call is required by the Web Form Designer 'Do not modify it using the code editor. InitializeComponent() End Sub #End Region Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load 'Set a fake "user" for the tracking call Session("loginid") = "TestUser" ' Make the remoting call to get the DataSet of log records Dim mgr As General.CustomerTracker mgr = New General.CustomerTracker Dim ds As DataSet = mgr.GenericSpReturnDataSet("sqlConn", "usp_GetLogRecords", Nothing) DataGrid1.DataSource = ds.Tables(0) DataGrid1.DataBind() End Sub Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click ' Do the Delete Button call--- Dim mgr As New General.CustomerTracker Dim retval As Integer = mgr.GenericSpNonQuery("sqlConn", "usp_UserTrackingDelete", Nothing) lblMessage.Text = retval.ToString & " items deleted." End Sub End Class

At first, it seems that there isn't much going on with regard to Remoting, doesn't it? However, there are a couple of important things going on "under the hood" here. First, in my Global.asax, I have this method call in my Application_Start event handler:

RemotingConfiguration.Configure(HttpContext.Current.Server.MapPath("Client.exe.config"))

The client.exe.config file is a Remoting "client style" config file. You can't put the information in this into a regular web.config file. It won't throw an exception if you do, but the remoting infrastructure won't be set up properly. So we have a separate "Client.exe.config" file that looks like this:

<system.runtime.remoting>
    <application>
    <channels>
            <channel ref="http" useDefaultCredentials="true" port="0">
               <clientProviders>
                  <formatter 
                     ref="binary"
                  />
               </clientProviders>
            </channel>
         </channels>
      <client>
        <wellknown type="General.CustomerTracker, General"  
                   url="http://localhost/IISImageHandler/CustomerTracker.soap" />
      </client>
     </application>
  </system.runtime.remoting>
</configuration>

Note that I've set up the HTTP channel with the Binary Formatter to match the server, and the client element provides the wellknown directive that specifies the namespace and class, and the assembly name, along with the actual URL that points to the server as above.

There is one other thing we need to do, however to allow the client to successfully remote calls to the server. The client must have the metadata to "know" what the server class "looks like" in order to do its Proxy thing and send method calls to the server. Typically you might use the SOAPSuds utility to generate a metadata proxy class to include in your client project, but in this case I am using the BinaryFormatter with which this will not work. So instead, I have a separate project (not included in the main Solution because of namespace collisions) that essentially "Duplicates' the exact namespace, class name and Assembly name as the "General.CustomerTracker" class that 's on my Remoting server. The only difference is, it only contains the method signatures needed to "Mirror" the server class, but no implementation except lines in each method to throw a NotSupportedException if the class is mistakenly actually called locally because the application hasn't been set up correctly:

Public Function InsertPageView(ByVal UserIPAddress As String, ByVal UserLoginId As String, ByVal SourceUrl As String, _
ByVal BrowseType As String, ByVal ClickThruId As String) As Integer
Throw New NotSupportedException("Cannot run method locally")
End Function

By compiling this assembly separately with the exact same Assembly name as the server class, and dropping it into the /bin folder of the web client application, we have completed the picture, providing the client with all the metadata it needs to make successful remoting calls.

In the downloadable solution I've provided all the referenced classes and projects (including the separate "General Proxy" project that will need to be compiled separately, as well as a SQL Script that will set up your CustomerTracking test database in SQL Server. To run this on a single machine, you'll need to perform the following steps:

1) Unzip the downloaded file into a folder under your wwwroot named "ImageHandler"

2) Mark the "WebClient" folder under this as an IIS Application.

3) Mark the "Vroot" folder (this is the server) as an IIS Application with a Virtual Directory name of "IISImageHandler" Note that this is applied to the physical folder "vroot" and that the main folder "ImageHandler" is not an IIS application at all, just a "holder" for all our "stuff".

4) Run the Sql Script to create your database and stored procedures.

5) If it's not already there, you will need to separately load and compile the "GeneralProxy" project, and copy the General.dll assembly into the /bin folder of your WebCleint application.

To run the client from a separate machine, you only need to change the URL in the Client.exe.config file to point to the correct URL of the actual machine that's hosting the server.

Download the Source Code that accompanies this article

 

 

Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.