If you're not familiar with DotNetNuke, be sure you visit http://www.dotnetnuke.com to learn more about this fast growing open source portal that you can use and develop against absolutely free.
In the
previous
article we discussed how to get your module project set up, we also began development
on the user interface pieces of our module. In this article we're going into what
makes the DNN architecture stand out from most .NET applications available, specifically
the data provider model. The provider model is a dominant theme in the upcoming
Visual Studio 2005 release, and is what makes DNN flexible enough to have any database
as the backend. Here we will cover the Business Logic Layer, and Data
Abstraction Layer of the DNN architecture, in part III of this series we will
finish it up with an overview of the Database Provider class.
BLL, and DAL
I'm sure the previous acronyms are ones you've seen tossed around if you're an
avid user of the ASP.NET forums. What do these stand for?
- Business Logic Layer (BLL) - Contains all the logic that is specific to
your business. For example, if you debit one account and then credit another
account, this would occur in the BLL.
- Data Abstraction Layer - This is the layer within your application that
connects to the data provider. It contains overridable methods that your provider
will over ride for it's specific vendor database. Your application's BLL will
interface with the DAL via a custom business objects helper class (CBO).
The custom business object helper class is basically a class that populates a
collection of objects or objects that you created within your module. Usually these
objects are directly related to stored procedures or queries from your database.
Reflection versus Abstraction
There are definitely plenty of changes here in DNN 2.x, in DNN 1.x we obtained
data from our database using reflection. The major problem with the previous architecture
of DNN was the lack of a clear definition between our different layers, i.e. data
tier, business logic, and user tiers. Now with the provider model we have a clear
separation of these tiers.
Another big improvement in DNN 2.x architecture is performance. Thanks to abstraction
and caching techniques there has been a huge performance increase as exhibited in
the following metrics:
DotNetNuke 2.0 ( DAL enhancement )
Total number of requests: 93,254
Total number of connections: 93,253
Average requests per second: 310.85
Average time to first byte (msecs): 2.37
Average time to last byte (msecs): 2.46
Average time to last byte per iteration (msecs): 29.58
Number of unique requests made in test: 12
Number of unique response codes: 1
DotNetNuke 1.0.10 ( SqlCommandGenerator )
Total number of requests: 42,350
Total number of connections: 42,350
Average requests per second: 141.17
Average time to first byte (msecs): 6.02
Average time to last byte (msecs): 6.15
Average time to last byte per iteration (msecs): 116.94
Number of unique requests made in test: 17
Number of unique response codes: 2
As you can see response time has been improved phenomenally!
Business Logic Layer
Now let's get into the different classes you will need to create for interacting
with a database. In part I of this series we examined the user interface pieces
of the survey module that comes bundled with the DNN distribution, now lets look
at the BLL piece. In the survey example the BLL class is the SurveyDB.vb, in your
modules you would follow the same naming convention, i.e.: ModuleNameDB. Within this
file is at least two classes for your module, a ModuleInfo class and a ModuleController,
for this example SurveyInfo, and SurveyController. Here's a description of these
two classes:
- ModuleInfo: Sets and gets public properties for our objects being generated
by our controller class.
- ModuleController: Used for obtaining objects,, basically uses the custom
business objects helper class to populate data from our data provider into a
collection of objects.
Let's look at the SurveyInfo class and see what's going on in there:
Imports System
Imports System.Data
Imports DotNetNuke
Namespace YourCompanyName.Survey
Public Class SurveyInfo
' local property declarations
Private _SurveyId As Integer
::
' initialization
Public Sub New()
End Sub
' public properties
Public Property SurveyId() As Integer
Get
Return _SurveyId
End Get
Set(ByVal Value As Integer)
_SurveyId = Value
End Set
End Property
::
End Class
Basically a simple class that is exposing prosperities for our controller class.
Now let's look at the controller class:
Public Class SurveyController
Public Function GetSurveys(ByVal ModuleId As Integer) As ArrayList
Return CBO.FillCollection(DataProvider.Instance().GetSurveys(ModuleId), _
GetType(SurveyInfo))
End Function
Public Function GetSurvey(ByVal SurveyID As Integer, _
ByVal ModuleId As Integer) As SurveyInfo
Return CType(CBO.FillObject(DataProvider.Instance().GetSurvey(SurveyID, ModuleId), _
GetType(SurveyInfo)), SurveyInfo)
End Function
Public Sub DeleteSurvey(ByVal SurveyID As Integer)
DataProvider.Instance().DeleteSurvey(SurveyID)
End Sub
Public Function AddSurvey(ByVal objSurvey As SurveyInfo) As Integer
Return CType(DataProvider.Instance().AddSurvey(objSurvey.ModuleId, objSurvey.Question, _
objSurvey.ViewOrder, objSurvey.OptionType, objSurvey.CreatedByUser), Integer)
End Function
Public Sub UpdateSurvey(ByVal objSurvey As SurveyInfo)
DataProvider.Instance().UpdateSurvey(objSurvey.SurveyId, objSurvey.Question, _
objSurvey.ViewOrder, objSurvey.OptionType, objSurvey.CreatedByUser)
End Sub
End Class
.So what's going on the controller class? If you look at the code above you will
see several methods, GetSurveys, GetSurvey, DeleteSurvey, AddSurvey, and UpdateSurvey.
In most cases these routines directly correspond to a stored procedure contained
within your database. For example, there is a GetSurveys stored procedure in our
database that accepts ModuleID as a parameter, and returns a record. Now in the
controller class you are doing something similar here, in the GetSurveys method
you accept the ModuleID as input (which will be passed to the provider class for
database execution). Then the return will be of type SurveyInfo. Remember in SurveyInfo
we defined properties for our class, these properties normally correspond to fields
contained within our database. We now create an collection object for our module
to work with instead of a record set or data reader. This combined with the data provider
classes is what provides abstraction from our vendor's database.
Looking more into this class you see that the GetSurveys method first calls a
CBO method, which is passed an instance of our data abstraction class. The CBO or
custom business object class is what takes the data from our database via the data
abstraction class, and then populates a collection of objects as the return to our
BLL. So the main concept here is we don't directly work with data from database
calls, our module deals with a collection of objects that contain properties that
we defined in our SurveyInfo class.
Another thing to note is when you are doing update, add, and delete operations
there is no need to wrap your call within the CBO method, because in most cases
there is no return coming from the database.
Database Abstraction
Now that we covered the BLL, let's cover abstraction a bit more, we know that
methods in our BLL correspond with stored procedures (in the case of SQL), but how
are these calls actually abstracted out? This abstraction is provided by our DataProvider.vb
class in the Survey example. You can see from the code below, this class contains
overridable methods that our actual SQLDataProvider class will provide. By abstracting
out these methods in our DataProvider.vb abstraction class, all we need to do is
add a new assembly for the database we want to use as a backend for DNN, no need
to recompile or change our module in any way. This abstraction class becomes part
of your actual module project.
Imports System
Imports System.Web.Caching
Imports System.Reflection
Namespace YourCompanyName.Survey
Public MustInherit Class DataProvider
' provider constants - eliminates need for Reflection later
Private Const [ProviderType] As String = "data" ' maps to in web.config
Private Const [NameSpace] As String = "YourCompanyName.Survey" ' project namespace
Private Const [AssemblyName] As String = "YourCompanyName.Survey" ' project assemblyname
Public Shared Shadows Function Instance() As DataProvider
Dim strCacheKey As String = [NameSpace] & "." & [ProviderType] & "provider"
' Use the cache because the reflection used later is expensive
Dim objConstructor As ConstructorInfo = _
CType(DotNetNuke.DataCache.GetCache(strCacheKey), ConstructorInfo)
If objConstructor Is Nothing Then
' Get the provider configuration based on the type
Dim objProviderConfiguration As DotNetNuke.ProviderConfiguration = _
DotNetNuke.ProviderConfiguration.GetProviderConfiguration([ProviderType])
' The assembly should be in \bin or GAC,
' so we simply need to get an instance of the type
Try
' Override the typename if a ProviderName is specified
' ( this allows the application to load a different
' DataProvider assembly for custom modules )
Dim strTypeName As String = [NameSpace] & "." & _
objProviderConfiguration.DefaultProvider & _
", " & [AssemblyName] & "." & objProviderConfiguration.DefaultProvider
' Use reflection to store the constructor of the class that implements DataProvider
Dim t As Type = Type.GetType(strTypeName, True)
objConstructor = t.GetConstructor(System.Type.EmptyTypes)
' Insert the type into the cache
DotNetNuke.DataCache.SetCache(strCacheKey, objConstructor)
Catch e As Exception
' Could not load the provider - this is likely due to binary compatibility issues
End Try
End If
Return CType(objConstructor.Invoke(Nothing), DataProvider)
End Function
' all core methods defined below
Public MustOverride Function GetSurveys(ByVal ModuleId As Integer) As _
IDataReader
Public MustOverride Function GetSurvey(ByVal SurveyID As Integer, _
ByVal ModuleId As Integer) As IDataReader
Public MustOverride Function AddSurvey(ByVal ModuleId As Integer, ByVal Question As _
String, ByVal ViewOrder As Integer, ByVal OptionType As String, _
ByVal UserName As String) As Integer
Public MustOverride Sub UpdateSurvey(ByVal SurveyId As Integer, ByVal Question As _
String, ByVal ViewOrder As Integer, ByVal OptionType As String, ByVal UserName As String)
Public MustOverride Sub DeleteSurvey(ByVal SurveyID As Integer)
Public MustOverride Function GetSurveyOptions(ByVal SurveyId As Integer) As _
IDataReader
Public MustOverride Function AddSurveyOption(ByVal SurveyId As Integer, _
ByVal OptionName As String, ByVal ViewOrder As Integer) As Integer
Public MustOverride Sub UpdateSurveyOption(ByVal SurveyOptionId As Integer, _
ByVal OptionName As String, ByVal ViewOrder As Integer)
Public MustOverride Sub DeleteSurveyOption(ByVal SurveyOptionID As Integer)
Public MustOverride Sub AddSurveyResult(ByVal SurveyOptionId As Integer)
End Class
End Namespace
So let's go over what the code actually does in the abstraction class.
- Asbstracts calls to the database using overridable methods.
- Contains an instance() method which determines which data provider to use
for our module (more about this later).
- Contains all data access methods for your modules which are then forwarded
to the data provider for direct interaction with a specific vendor database.
Look at the instance method of this class, you can see it checks to see which
database provider we're going to use for our module, it will then load the correct
provider and our provider will provide the direct database interaction for our module.
The way the provider is specified in DotNetNuke is by modifying the Web.config file
under the DotNetNuke key. For a default install of DNN, there are two providers
available, Access, and SQL Server, by default Access is specified, but that can
be changed by pointing the defaultProvider to SQLDataProvider. Let's look at the
Web.config with this SQL modification.
<dotnetnuke>
<data defaultProvider="SqlDataProvider" >
<providers>
<clear/>
<add name = "SqlDataProvider"
type = "DotNetNuke.Data.SqlDataProvider, DotNetNuke.SqlDataProvider"
connectionString = "Server=localhost;Database=Demo;uid=sa;pwd=;"
providerPath = "~\Providers\DataProviders\SqlDataProvider\"
objectQualifier = ""
databaseOwner = "dbo"
/>
<add name = "AccessDataProvider"
type = "DotNetNuke.Data.AccessDataProvider, DotNetNuke.AccessDataProvider"
connectionString = "PROVIDER=Microsoft.Jet.OLEDB.4.0;"
providerPath = "~\Providers\DataProviders\AccessDataProvider\"
objectQualifier = "DotNetNuke"
databaseFilename = "DotNetNuke.mdb.resources"
/>
</providers>
</data>
</dotnetnuke>
In part three of this series we will wrap up development by covering the
SQLDataProvider. In the final part of this series we will explain how you can
create a private assembly installation package for distribution.
By: Patrick Santry, Microsoft MVP (ASP/ASP.NET), developer of this site, author of books on Web technologies, and member of the DotNetNuke core development team. If you're interested in the services provided by Patrick, visit his company Website at Santry.com.