You are on page 1of 9

Data Access Layer Page 1 of 9

One Data Access Layer: (, w/explanation)

Author: Terry Voss
Assistant: Minglong Wu, reviewer/critic/tester

Demo the dal DataAccessLayer:

Download the zip with 10 files:

dal.aspx, dal.aspx.vb, customer.vb, consumers.vb, dalrequest.vb, AbstractProvider.vb, dalfactory.vb, sqlprovider.vb, xmlsettings.vb, dal.xml
(extract 10 files into new web project named dal or whatever, set dal.aspx as startup, press F5)

After reading "Professional Design Patterns in VB .NET, Building Adaptable Applications", Johnny Papa's articles, Steven Smith's seminar on DAL, and then using
LLBLGen Data Access Layer, plus other lesser inputs, I felt I needed 5 things from a DAL.
1) The automatic generation of data manipulation classes and stored procedures for each table in your database like LLBLGen

2) The ability to easily add another Data Provider even within the same application
3) Shortcut interfaces for commands that can be used from the middle tier without being specific to one Data Provider
4) The ability to receive back various types of classes like DataReader, DataSet, DataTable, DataRow, and custom classes.
5) and various miscellaneous features such as stored procedures vs text commands, built in error checking, security levels, etc.

Since I couldn't find this thing laying around, I began finding time to work on it and ran into many interesting OOP concepts that I hoped that I already understood,
but came to find out no I didn't until struggling with the code for a while. For example, even though I've read many books entries on Factory Design Pattern I couldn't
grasp what it was that was really gained from it.

In following diagram note that middle classes have Data Provider specific code in them (eg Sql Server). This is to show you the difference between my DAL and
using LLBLGen in the strict way it was made to be used. Classes on left will not have any Data Provider specific code in them. On the right we have Stored
Procedures. I want to compare what I am doing with using the LLBLGen in the standard way. I still generate with LLBLGen the classes and SPs for my database,
but then I modify the generated classes to be Data Provider non-specific Middle-Tier classes. I then often modify some of the SPs to be more specifically practical
for the current Application. 05/13/2010
Data Access Layer Page 2 of 9

I will have a detailed code example that I am currently using for SQL Server 2000 that you may want to use or easily modify for Oracle or any provider like OleDb,
available, but I will be focusing mostly on the different ways of going about keeping this process simple and maintainable and easy to use from different types of
classes and why I made the choices I have. The main goal is making a DAL that anyone can maintain.

From the requirements list, the DataProvider variation will be handled by different subclasses of one AbstractDataProvider class. You copy my SqlProvider class in
sqlprovider.vb to a file called oracleprovider.vb and edit away for an hour at most and you have your new concrete data provider subclass. To handle
CommandType variation we will use a DALRequest class with default settings to our favorite. I will be calling the output variation consumers to contrast with the
usage of provider. To handle the variation of consumers, I will vary the input signature of overloaded method called Execute of the SqlProvider subclass. To handle
the variation of Commands I will overload Execute and vary the code from overload to overload very slightly so it is easy to understand and change. 05/13/2010
Data Access Layer Page 3 of 9

Below is diagram of Class Design showing all the main files in this download, with client-tier on bottom, business-tier as customer, and rest is DAL. 05/13/2010
Data Access Layer Page 4 of 9

Now if I could just get a generator to compile so that the classes are ready to use this DAL, that would be a great savings of coding time, (maybe a next article?).

Let's look at each class individually to see what we can learn about the whole simple process starting with the front end. I am not delivering any Stored Procedures
with this because you can test all of this with just a few tweaks to Northwind's SPs or place some text commands very easily with this once you understand the
structure. It might look complex, but once you see the design things fall easily into place. First in dal.aspx there is a datagrid and a textbox and 3 buttons: getorders,
delete one record, and insert a customer.

Looking at the code for these button in the codebehind: dal.aspx.vb,,, we see that we are merely instantiating a class from the middle tier and calling one of its
methods. This is all I allow in this tier's responses to events. Never any data provider specific code. Some may say that a datareader is specific, or dataset, but they
may be made as abstract as you want with wrapper classes as we will see was necessary with the datareader class. You will see that datareader is not the
sqldatareader, but a custom wrapper class. I feel that a dataset class is not specific to sql or oracle or oledb, so I did not choose to make a wrapper as I could have
for the dataset or datatable. The main thing to notice here is that if I want to receive back a datareader for datagrid binding, then I simply input to the middle-tier
method, an new and empty datareader object. I could here, input only a type of datareader and and an instantiation of nothing and get my DAL to work fine and
have been lighter weight in what I passed, but inputing the non null instantiation that is empty saves me code in my sqlProvider class as you will see so I decided on
this course. If I want to receive a dataset back, I simply input a new dataset object, etc This works for any custom class that you want to define also which is very
easy to do, but this only works for consumers that you have handled in the DAL. So, it is a 1 or 2 word change of code to get any type of consumer object back for
binding or whatever use.

  Private Sub btnGetOrders_Click(ByVal  sender As System.Object, ByVal  e As System.EventArgs) Handles btnGetOrders.Click 

    Dim customer As  New  customer() 
    Dim consumer As  New  DataReader() 
    consumer = customer.getOrders(txtCustomerID.Text, consumer) 
    dgOrders.DataSource = consumer.ReturnedDataReader 
  End Sub 
  Private Sub deletetop_Click(ByVal  sender As System.Object, ByVal  e As System.EventArgs) Handles deletetop.Click 
    Dim customer As  New  customer() 
  End Sub 
  Private Sub Button1_Click(ByVal  sender As System.Object, ByVal  e As System.EventArgs) Handles Button1.Click 
    Dim customer As  New  customer() 
    txtCustomerID.Text = customer.AddCustomer(txtCustomerID.Text, 1) 
  End Sub 

First notice the declaration of dataProvider object. AbstractProvider, which we will be looking at soon is an abstract class that is non-specific as regards data
providing, so that if your middle-tier is composed of 100 classes like customer, you won't have to go to 100 places to change to Oracle. You go to the default
constructor of the DALRequest class and change the default from sql to oracle as the default. Also if a client has all data on Oracle, but their customer table on Sql
Server, you can just change one property of the DALRequest class each time you need to switch to sql for a request. The DALFactory class uses the DALRequest
Provider property/field to get the proper concrete provider class instantiated. Also note that to execute a selected stored procedure, I need set the command
property/field, clearallparameters, code one line for each input and output parameter, and then code one line for execution. One line must be added to switch to
using text commands versus stored procs the default. Also note that adding a customer requires the return of the primary key identity that I use so I must input an
integer to tell the concrete data provider which overload to use to return that integer information. In the customer dispose method I close the dataprovider
connection later than for the other consumers in the case of a datareader. (calling close an extra time is safe)

Public Class  customer 
  Dim dataProvider As  AbstractProvider = DALFactory.GetProvider(DalRequest.Provider) 
  Public Function getOrders(ByVal id As  String, ByVal consumer As DataReader) As DataReader 
    DalRequest.Command = "pr_orders_GetOrders"  
    dataProvider.AddParameter("@customerID", ssenumSqlDataTypes.ssSDT_String, 8, id, dataProvider.Direction.input) 
    Return dataProvider.Execute(consumer) 
  End Function  
  Public Function DeleteTopOrder(ByVal id As String) 
    DalRequest.Command = "pr_orders_DeleteTopOrder" 
    dataProvider.AddParameter("@customerID", ssenumSqlDataTypes.ssSDT_String, 8, id, 1) 
  End Function  
  Public Function AddCustomer(ByVal customerID As String, ByVal  consumer As Integer) As Integer 
    dataProvider.AddParameter("@customerid", ssenumSqlDataTypes.ssSDT_String, 5, customerID, dataProvider.Direction.Input) 
    dataProvider.AddParameter("@id" , ssenumSqlDataTypes.ssSDT_Integer, 4, 0, dataProvider.Direction.Output) 
    DalRequest.Command = "pr_customers_Insert"  
    Return dataProvider.Execute(1) 
  End Function  
  Sub dispose() ' needed only if consumer is datareader 
  End Sub 
End Class    

Notice that in the consumer.vb file there are only 3 classes and 1 enumerator defined. That is because dataset doesn't necessarily need one since it is a class that
is already abstracted from being a Sql class or an Oracle class etc. There is no abstracted datareader class except the one we've defined here. There is only
sqldatareader or oledbdatareader, etc. So DataReader is a simple wrapper that will be used to carry the specific thing. Its use is to isolate the specific data provider
code from the codebehind or middle-tier. Order is a custom class that could have as many properties as we need. A very interesting thing to consider is why the
connection and direction constructions are necessary to isolate. Without these constructions I could not find anyway to isolate even though there may surely be
some other way. These are interesting kinds of questions when you are designing a data access layer. If I use the built in ParameterDirection enumerator isolation 05/13/2010
Data Access Layer Page 5 of 9

is corrupted and when I want to change the code for the next client I find I have to go to 100 places and change code instead of one place. One interesting thing I
did not expect is that in customer when I declare DataProvider as type AbstractProvider, and then set it equal to a specific output of the DalFactory if the abstract
factory did not have exactly the same direction property as in the specific concrete provider, what showed up was the abstract direction, not the specific. This forces
the last two classes here. This class library could grow over time. The connection class was necessitated because I needed to close a connection from the middle
tier in the case of the datareader consumer. The direction enumerator copies the ParameterDirection enumerator of the namespace. I'd rather not have
a structure in my abstract provider class, so I created my own. One of these latter is required in order to instantiate the capability of creating both input
and output parameters for stored procedures.

Imports System.Data.SqlClient 
Public Class  DataReader 
  Public ReturnedDataReader As  IDataReader 
End Class  
Public Class  Order 
  Public OrderDate As  String 
End Class  
Public Class  Connection 
  Public ConSql As  SqlConnection 
  ' add another property here for Oracle etc 
End Class  
Public Enum Direction 
  input  = 1 
  output = 2 
  both = 3 
  returnitem = 6 
End Enum      

This class is very useful for controlling defaults and making it easy to refer to any current properties requested as it is shared. SqlDataProvider relies on this. As you
can see I like fields tied to enumerators versus method properties unless the method properties will be used for greater control. Sub New provides the default values
that an application will use mainly. Only one property has to be set each time, Command either as text command or stored procedure name as string. UserType is
linked to dal.xml read/write config file that is accessed via xmlsetting shared class. Each UserType has a different userid and password in dal.xml. These could be in
your employee or customer table as well and make programming more safe as certain kinds of users would not be able to execute certain commands on your data.

Public Class  DalRequest 
  Public Shared Provider As ProviderType 
  Public Shared RoleObject As UserType 
  Public Shared Role As String 
  Public Shared CommandType As CommandType 
  Public Shared Command As  String 
  Public Shared Transaction As Boolean  
  Public Shared Locking As Boolean  
  Public Shared ParamCache As Boolean  
  Public Shared TableName As String 
  Shared Sub New() 
    Provider = ProviderType.Sql 
    RoleObject = New UserType() 
    Role = RoleObject.Admin 
    CommandType = CommandType.StoredProcedure 
    Command = "" 
    Transaction = False  
    Locking = False  
    ParamCache = False  
    TableName = "data" 
  End Sub 
End Class  
Public Enum ProviderType 
End Enum 
Public Class  UserType 
  Public Shared External As String 
  Public Shared Internal As String 
  Public Shared SuperUser As String 
  Public Shared Admin As String 
  Sub New() 
    External = "external" 
    Internal = "internal" 
    SuperUser = "superuser"  
    Admin = "admin" 
  End Sub 
End Class        

This abstract class enforces a minimum interface on all its subclasses. You can add more functionality to a concrete provider that has such available, but you must
add the enfiorced interface. Instead of using an abstract class I could have used an Interface instead, which is how I actually first attempted this. I found that the
complexity of defining the connection and direction properties and implementing them was more difficult than using the abstract class and that books with significant
coverage on Interfaces was not available to me even though I have many books supposedly covering this area. Coverage was very spotty. It would be nice if
someone would do an entire book on Interfaces. This is one value of the factory pattern which is comprised of 2 classes here: AbstractProvider class and the
DalFactory class. 05/13/2010
Data Access Layer Page 6 of 9

  Public Connection As  Connection 
  Public Direction As  Direction 
  Public MustOverride  Sub  Execute() 
  Public MustOverride  Function Execute(ByVal  consumer As Integer) As Integer 
  Public MustOverride  Function Execute(ByVal  consumer As DataReader) As  DataReader 
  Public MustOverride  Function Execute(ByVal  consumer As DataSet) As  DataSet 
  Public MustOverride  Function Execute(ByVal  consumer As DataTable) As  DataTable 
  Public MustOverride  Function Execute(ByVal  consumer As DataRow) As  DataRow 
  Public MustOverride  Function Execute(ByVal  consumer As Order) As  Order 
  Public MustOverride  Sub  ClearAllParameters() 
  Public MustOverride  Sub  AddParameter(ByVal parameterName As  String, ByVal dataType As ssenumSqlDataTypes, _ 
    ByVal  size As Integer, ByVal value As  String, ByVal direction As Integer) 
End Class  

The other value of the Factory Design Pattern is that the DalFactory class will choose the proper concrete class for us in a way that is not specific to the
DataProvider so that we can do this instantiation in hundreds of places and never have to go and change this declaration and instantiation code ever again, just add
to the code in the DalFactory, and the default or current value of the DalRequest.provider property.

Public Class  DALFactory 
  Public Shared Function GetProvider(ByVal  provider As Integer) As AbstractProvider 
    Select Case provider 
    Case ProviderType.Sql 
      Return New SQLProvider() 
    Case ProviderType.Oracle 
      'Return New OracleProvider() 
    Case ProviderType.Oledb 
      'Return New OledbProvider() 
    End Select 
  End Function  
End Class  

At the highest level of overview, there are imports, 1 enumeration of sqlDataTypes, a Parameter class for purposes of wrapping the specific parameters of specific
data providers like the sqlParameters class, and then the main SqlProvider which is inheriting the AbstractProvider class. System.IO, and System.Text are needed
for logging exceptions. Microsoft.VisualBasic.ControlChars is for using crlf, and then System.Data and System.Data.Client are necessary to manipulate the Sql
Server database in the most efficient manner currently existing. We will need to be able to convert our abstract parameters into sqlparameters and thus the
SqlDataTypes enumerator is needed.

Imports System.IO 
Imports System.Text 
Imports System.Data 
Imports System.Data.SqlClient 
Imports Microsoft.VisualBasic.ControlChars 
Public Enum ssenumSqlDataTypes  
<Serializable()> Public Class Parameter 
<Serializable()> Public Class SQLProvider : Inherits AbstractProvider 

With the parameter class and the methods of the SqlDataProvider below we can easily handle stored procedure parameters and make stored procedures as easy
as Sql text commands. The reason we need an abstract parameter class and then convert to sql parameters inside the concrete data provider class is that we must
be abstract in the middle tier and specific inside the data provider. I know I am repeating myself, but if you don't get this stuff it takes some thought time and
repetition helps. I have collected these somewhat disparate pieces here to help you get that this parameter stuff is not that complex. I had a little trouble with it at
first. When you decide to create your OracleDataProvider, you will have to consider how to change the convertParametertoOracleParameter method. You will also
have to make a few changes to the datatype enumerator also. Now on to more interesting things.

<Serializable()> Public Class Parameter 
  Public DataType As  SqlDbType '//--- The datatype of the parameter  
  Public Direction As  ParameterDirection '//--- The direction of the parameter  
  Public ParameterName As  String '//--- The Name of the parameter  
  Public Size As  Integer  '//--- The size in bytes of the parameter  
  Public Value As  String '//--- The value of the parameter  
  Sub New(ByVal  sParameterName As String, ByVal lDataType As SqlDbType, ByVal iSize As Integer, _ 
    ByVal sValue As  String, ByVal iDirection As Integer ) 
    ParameterName = sParameterName 
    DataType = lDataType 
    Size = iSize 
    Value = sValue 
    Direction = iDirection 
  End Sub 
End Class  
Private m_oParmList As ArrayList = New  ArrayList() ' holds parameters for a stored procedure 
Public Overloads  Overrides Sub ClearAllParameters() 
  m_oParmList.Clear () 
End Sub 
Public Overloads  Overrides Sub AddParameter(ByVal sParameterName As String, _ 
  ByVal lSqlType As ssenumSqlDataTypes, ByVal iSize As Integer, ByVal  sValue As String, ByVal iDirection As Integer) 
  Dim eDataType As  SqlDbType 
  Dim oParam As  Parameter = Nothing  
  Select Case lSqlType 
  Case ssenumSqlDataTypes.ssSDT_String 
    eDataType = SqlDbType.VarChar 05/13/2010
Data Access Layer Page 7 of 9

  Case ssenumSqlDataTypes.ssSDT_Integer 
    eDataType = SqlDbType.Int 
  Case ssenumSqlDataTypes.ssSDT_DateTime 
    eDataType = SqlDbType.DateTime 
  Case ssenumSqlDataTypes.ssSDT_Bit 
    eDataType = SqlDbType.Bit 
  Case ssenumSqlDataTypes.ssSDT_Decimal 
    eDataType = SqlDbType.Decimal 
  Case ssenumSqlDataTypes.ssSDT_Money 
    eDataType = SqlDbType.Money 
  End Select 
  oParam = New Parameter(sParameterName, eDataType, iSize, sValue, iDirection) 
End Sub 
Private Function  ConvertParameterToSqlParameter(ByVal oP As  Parameter) As SqlParameter 
  Dim oSqlParameter As  SqlParameter = New  SqlParameter(oP.ParameterName, oP.DataType, oP.Size) 
  With oSqlParameter 
    .Value = oP.Value 
    .Direction = oP.Direction 
  End With 
  Return oSqlParameter 
End Function  

Below you can see the overview that shows the detail of the sqlDataTypes, the shadowing of the connection and direction properties over the abstract versions in
AbstractProvider, the sub new, the location of the parameter methods, and finally the 7 overloads of the Execute method that does the main work. The 7 overloads
are for the 7 consumers I was interested in. 1-updates/deletes which I don't want to return anything. I could have unified updates/deletes/inserts by having them all
return integers/how many rows affected, but what if your primary key is not integer like mine? 2-inserts, 3-datareader, 4-dataset, 5-datatable, 6-datarow, 7-custom
class order. Actually as you will see, the code difference between the overloads is very small number of lines and so therefore you could easily unify all overloads
into one Execute method. I chose not to because I like the method of overloading using one input object that makes it so easy to remember which overload does
what for me! Also, this overloading makes it very easy to add features in the future since the length of each Execute method is so small. It is very easy to add a new

Imports System.IO 
Imports System.Text 
Imports System.Data 
Imports System.Data.SqlClient 
Imports Microsoft.VisualBasic.ControlChars 
Public Enum ssenumSqlDataTypes ssSDT_Bit 
End Enum <Serializable()> Public Class  Parameter 
<Serializable()> Public Class SQLProvider : Inherits AbstractProvider 
Public Shadows Direction As  New  Direction() 
Public Shadows Connection As  New  Connection() 
Private connectionString As String  
Private m_oParmList As ArrayList = New  ArrayList() ' holds parameters for a stored procedure 
Sub New() 
connectionString = XmlSetting.Read("appsettings", DalRequest.Role) 
Me.Connection.ConSql = New  SqlConnection(connectionString) 
End Sub 
Public Overloads  Overrides Sub Execute() ' handles update and delete commands which return nothing here 
Public Overloads  Overrides Function Execute(ByVal consumer As Integer) As  Integer  
Public Overloads  Overrides Function Execute(ByVal consumer As DataReader) As DataReader 
Public Overloads  Overrides Function Execute(ByVal consumer As DataSet) As  DataSet 
Public Overloads  Overrides Function Execute(ByVal consumer As DataTable) As  DataTable 
Public Overloads  Overrides Function Execute(ByVal consumer As DataRow) As  DataRow 
Public Overloads  Overrides Function Execute(ByVal consumer As Order) As  Order 
Public Sub LogError(ByVal e As SqlException, ByVal Command  As String) 
Public Overloads  Overrides Sub ClearAllParameters() 
Public Overloads  Overrides Sub AddParameter(ByVal sParameterName As String, ByVal  lSqlType As ssenumSqlDataTypes, _ 
  ByVal  iSize As Integer, ByVal sValue As  String, ByVal iDirection As Integer) 
Private Function  ConvertParameterToSqlParameter(ByVal oP As  Parameter) As SqlParameter 
End Class  

Let's look at the simplicity of a few of the overloads. Note that you must declare overloads and overrides when you are subclassing. Note that because of the choice
of my method signature differentiation, I am able to use the input to save lines of code. I return what I input. Ooh I like it when things like this happen. For my error 05/13/2010
Data Access Layer Page 8 of 9

handling I chose to throw and log the problems. Note how stored procedure parameter conversion is handled by a simple logic with a 2 line loop to loop through all
parameters you created. The difference between SPs and Text commands boils down to not much.

Public Overloads  Overrides Function Execute(ByVal consumer As DataSet) As  DataSet 

  Dim oCmd As  SqlCommand = New  SqlCommand() 
  Dim oEnumerator As  IEnumerator = m_oParmList.GetEnumerator() 
    With oCmd 
      .Connection = Me .Connection.ConSql 
      .CommandText = DalRequest.Command 
      .CommandType = DalRequest.CommandType 
    End With 
    If DalRequest.CommandType = CommandType.StoredProcedure Then 
      Do While (oEnumerator.MoveNext()) 
        Dim oP As  Parameter = oEnumerator.Current 
    End If  
    With New SqlDataAdapter() 
      .SelectCommand = oCmd 
    End With 
    Catch  e As SqlException 
      Me.LogError(e, oCmd.CommandText) 
      Throw  e 
    End Try 
    Return consumer 
End Function        

Not much difference here. Remember that order is a custom class defined in consumer.vb library. It could be defined anywhere. To use this for another custom
class, you would have to create another overload of Execute since each class has different properties that need setting. I wouldn't want one hundred overloads of
Execute in sqlProvider. The way I handle this, since every time that only one datarow is returned I want it to set the properties of the associated class and work with
the class not the datarow, is to use LLBLGen to generate data layer classes for each table, and then I convert them to middle-tier classes by taking out all data
provider specific code. Then I return a datarow from my DAL to the class and set the classes properties with it. Then I work with the class in the front end.

Public Overloads  Overrides Function Execute(ByVal consumer As Order) As  Order 

  Dim oCmd As  SqlCommand = New  SqlCommand() 
  Dim oEnumerator As  IEnumerator = m_oParmList.GetEnumerator() 
  Dim dt As  DataTable 
    With oCmd 
      .Connection = Me .Connection.ConSql 
      .CommandText = DalRequest.Command 
      .CommandType = DalRequest.CommandType 
    End With 
    If DalRequest.CommandType = CommandType.StoredProcedure Then 
      Do While (oEnumerator.MoveNext()) 
        Dim oP As  Parameter = oEnumerator.Current 
    End If  
    With New SqlDataAdapter() 
      .SelectCommand = oCmd 
    End With 
    Catch  e As SqlException 
      Me.LogError(e, oCmd.CommandText) 
      Throw  e 
    End Try 
  consumer.OrderDate = dt.Rows(0)("OrderDate") 
  Return consumer 
End Function      

In the past I have found reasons to have a read/write config file like the one below.

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




<external>Data Source=vsnet\dev1;Initial Catalog=Northwind;User ID=ext;Password=earth</external>

<internal>Data Source=vsnet\dev1;Initial Catalog=Northwind;User ID=int;Password=wind</internal> 05/13/2010
Data Access Layer Page 9 of 9

<sueruser>Data Source=vsnet\dev1;Initial Catalog=Northwind;User ID=su;Password=and</sueruser>

<admin>Data Source=vsnet\dev1;Initial Catalog=Northwind;User ID=admin;Password=fire</admin>


The xmlsetting.vb file class which has shared methods, allows me to make simple calls like the ones below to read and write to this xml file. This simple class allows
me same element names since I use one level of context to grab a value. I specify a child in the context of a parent node. Note that xmlsetting.vb requires that the
name of the xml file must match the project name. In this case, DAL.

dim connectionString as  string ="appsettings", "internal") 
xmlsetting.write ("appsetting", "Internal", "safe")             

I am not writing this article because I consider myself a data architecture expert at all, but I feel a great need for development in this area and for more explanation
with examples like the direction and connection cases which I could not find anywhere. Please contact me with ideas or criticisms so that I can continue to evolve
this important area as I know many must be working on similar things.

Below are the best links I know of on this topic:

(please let me know of any that you have found that you feel are better quality)

Microsoft's DAL=Application Block: white paper

Abstracting ADO.NET by Johnny Papa
Developing a Universal Data Access Layer leveraging ADO.NET, C# and Factory Design Pattern
Design Patterns in VB .NET (Chapter 2: Design Patterns in the Data Tier)

Go to the article about modifying the LLBL class generator to work with DAL:

Send mail to Computer Consulting with questions or comments about this web site.
Last modified: January 26, 2006 05/13/2010