| 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 |