A Data Access Layer
page 1 of 1
Published: 13 Oct 2003
Unedited - Community Contributed
Abstract
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 ado.net 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.
by Terry Voss
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 9629/ 14

DAL with code generation

One Data Access Layer: (vb.net, 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 ado.net 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.
 

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


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.


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
    dgOrders.DataBind()
  End Sub

  Private Sub deletetop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles deletetop.Click
    Dim customer As New customer()
    customer.DeleteTopOrder(txtCustomerID.Text)
  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.ClearAllParameters()
    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.ClearAllParameters()
    dataProvider.AddParameter("@customerID", ssenumSqlDataTypes.ssSDT_String, 8, id, 1)
    dataProvider.Execute()
  End Function

  Public Function AddCustomer(ByVal customerID As String, ByVal consumer As Integer) As Integer
    dataProvider.ClearAllParameters()
    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
    dataProvider.Connection.ConSql.Close()
  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 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 system.data namespace. I'd rather not have a system.data 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
  Sql
  Oracle
  Oledb
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.

Public MustInherit Class AbstractProvider
  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 

 Public Class Parameter

 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.
 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
  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)
  m_oParmList.Add(oParam)
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 consumer.
Imports System.IO
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.VisualBasic.ControlChars

Public Enum ssenumSqlDataTypes ssSDT_Bit
ssSDT_DateTime
ssSDT_Decimal
ssSDT_Integer
ssSDT_Money
ssSDT_String
End Enum  Public Class Parameter

 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 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()
  Try
    Me.Connection.ConSql.Open()
    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
        oCmd.Parameters.Add(ConvertParameterToSqlParameter(oP))
      Loop
    End If
    With New SqlDataAdapter()
      .SelectCommand = oCmd
      .Fill(consumer)
    End With
    Catch e As SqlException
      Me.LogError(e, oCmd.CommandText)
      Throw e
    Finally
      Me.Connection.ConSql.Close()
    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
  Try
    Me.Connection.ConSql.Open()
    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
        oCmd.Parameters.Add(ConvertParameterToSqlParameter(oP))
      Loop
    End If
    With New SqlDataAdapter()
      .SelectCommand = oCmd
      .Fill(dt)
    End With
    Catch e As SqlException
      Me.LogError(e, oCmd.CommandText)
      Throw e
    Finally
      Me.Connection.ConSql.Close()
    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.
 











c:\inetpub\wwwroot\dal\



c:\inetpub\wwwroot\dal\



Data Source=vsnet\dev1;Initial Catalog=Northwind;User ID=ext;Password=earth



Data Source=vsnet\dev1;Initial Catalog=Northwind;User ID=int;Password=wind



Data Source=vsnet\dev1;Initial Catalog=Northwind;User ID=su;Password=and



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







 

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 = xmlsetting.read("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)
Send mail to Computer Consulting with questions or comments about this web site.
Last modified: October 10, 2003



 





User Comments

No comments posted yet.

Product Spotlight
Product Spotlight 





Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-28 2:52:42 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search