Building an N-Tier Application in .NET
Paul D. Sheriff
Summary: After reviewing the types of n-tier applications, you'll learn how to create a typed dataset that can return data from a Web service and consume a Web service from a Windows application. (17 printed pages)
- Review the types of n-tier applications.
- Review the goals of a good n-tier application.
- Learn how to create a typed dataset that can return data from a Web service.
- Consume a Web service from a Windows application.
- You have developed an n-tier application.
- You are very familiar with classes.
- You have used the OleDbDataAdapter and DataSets.
- You know how to create a Typed DataSet.
- You can create a form in Microsoft® Visual Studio® .NET.
- You are familiar with relational databases and Microsoft ADO.NET.
- You have created a Web service in Microsoft® .NET.
There are many types of n-tier applications that programmers have developed over the years. Since classes were first introduced in Microsoft Visual Basic® 4.0, many programmers have attempted to come up with the definitive method of creating n-tier applications. Although all of them are clever, there has never been a consensus on how every n-tier application should be constructed. As many programmers as there are in the world, there seem to be that many methods of developing n-tier applications. Below is a list of some of the possible ways.
- Create a component with one class that returns a disconnected ADO recordset for any SQL statement sent to it. All updates are performed in the ADO recordset, then passed back to the component for batch updating.
- Create a component that has many classes based around business processes. All data for the business process is passed to this component. The component will update the appropriate tables from the data supplied. Another component would be used to return views of data needed to support the user interface for a business process.
- Create one class per table using ADO embedded into the class. This is a logical n-tier model where both the EXE and the classes (in a DLL) are installed on the client machine.
- Create one class per table using ADO on the client side. This client-side class passes SQL through DCOM to a server-side Data Class running under MTS that returns a disconnected ADO recordset to the client side.
- Create two classes per table, one with properties that can be set from the client side EXE. These properties are then bundled into an XML string and passed across DCOM to a server-side class running under MTS. The XML contains instructions on how to gather or modify the data, and a disconnected ADO recordset is returned back to the client-side class.
- Create one class per table using a DOMDocument object on the client side to process all the data. The client-side class bundles up properties into an XML string that is then sent to a server-side data class. The server side extracts the instructions (the SQL) from the XML and performs the appropriate action on the database server. The server-side class then returns XML to the client-side class to inform the client what happened on the database. In this scenario, there is only one data class for all tables.
- Create one server-side class for all tables. The client side passes SQL to the server-side class and it returns a disconnected ADO recordset. All forms then use these ADO recordsets for processing data.
- Create a client-side EXE that sends a SOAP request to a Web server for XML data. XML is processed on the client side using the DOMDocument object.
As you can see, there are many different methods for creating an n-tier application. They all work, and they each have advantages and disadvantages. The goal of this document is not to dispute any of these, nor to look at the advantages and disadvantages, but to simply present a way to create n-tier data classes within Visual Basic .NET.
In this document, you will learn to create a Typed DataSet using built-in tools in Visual Studio .NET. A Typed DataSet is inherited from the DataSet class in .NET. It also provides you with properties that match up to each column in the base table. You use a standard data adapter object to fill up the DataSet from the data source. The wizard that generates this Typed DataSet reads the schema information from the data source and maps these data types to each of the columns. This is why this is called a Typed DataSet.
A Typed DataSet will help you speed up your development process in a few ways. First, you no longer have to remember column names; you will have a Microsoft® InteliSense® list after creating an object from this DataSet class. This avoids run-time errors as column names can be checked at compile time. Second, you no longer have to see SQL in your front-end client application. All of the SQL is buried in the data adapter. By putting these Typed DataSets into a separate component, you are able to reuse these classes from multiple projects.
Goals of a Good N-Tier Application
N-tier design came about as a result of the failings of the client/server model. There are many goals that an n-tier application design should achieve. Here are some of them.
- If you change the underlying data access methods, the client-side code should not have to change.
- All data access routines should be exposed as objects instead of function calls. As an example, it is much easier to use ADO than the ODBC API calls.
- SQL should be eliminated from the client-side code. The client code should just be concerned with methods and properties.
- Table and column names should be eliminated from the client-side code. Typed datasets can present table and column names as properties, providing an IntelliSense list, as opposed to having to type in a string name. This means at compile time, checks can be made for data types and names of columns.
- The client code should not care where the data comes from. It should just care that it can retrieve and modify the data in some object and the object will take care of the details.
- The coding you need to do on the client side should be simplified. Instead of using many functions, your application should be able to use objects with properties and methods.
- It becomes easier to create and use the classes than the function calls.
- It becomes easier to add functionality to your applications, and change the functionality, without breaking the client-side code.
Disadvantages to N-Tier
Although there are many advantages to a good n-tier application, there are some disadvantages as well.
- You end up creating a lot of classes. This can lead to maintenance issues and could even be a performance issue as it does take time to create a new class at run time.
- N-tier does not work well when you do not know the structure of the tables from which you will be retrieving data. For example, in a Query By Example (QBE) application where the user may put together several columns from several tables, there is no way to generate classes on the fly to accomplish this.
- Creating reports is not something that lends itself to a good n-tier design, as report writers do not use classes to get at data.
In the end, the advantages of a good n-tier design will far outweigh the disadvantages. In the cases where you simply cannot use n-tier, go ahead and use the typical client/server method of development. There is certainly nothing wrong with mixing both of these paradigms in the same application if appropriate.
When you talk about a true distributed n-tier type of application, you are talking about separating the components of the different tiers on different machines as well as in separate components. Figure 1 shows a typical example of an n-tier application with multiple components on each machine.
Figure 1. A distributed n-tier application has three physical tiers with one or more logical tiers on each machine
There are many different ways you could configure an n-tier application. For example, the business rules may go on a separate machine and you might use .NET Remoting to talk from the client application to the business rule tier as shown in Figure 2.
Figure 2. Business rules can be placed on a separate machine to facilitate ease of maintenance
You may also have a data input validation rule component on the client to check simple rules such as required fields and formatting. These are rules that you do not want to make a trip across the network just to check. You may then also add a business rule layer on the same tier as the data layer component to check complicated business rules that compare the data from one table to another.
These are just a few different configurations that you may utilize. Of course, you could come up with something unique that fits your specific situation. Regardless of how you structure the physical implementation of the components, make sure that the logical structure of the program is broken up into components as shown in the above figures.
Creating the User Interface
In the example you see in Figure 1, the client tier consists of a Windows application and a business rule component. The Windows application makes all requests for data, and all updates through the business rule component. This isolates the location of the data from the Windows application. The advantage of doing this is if you change where the data comes from, you do not need to make any changes to the client application, only to the business rule component.
Figure 3. This is a DataGrid that has been bound to the return result from the business rules component
Perform the following steps to build a simple Windows client application that will display employee information in a DataGrid control on a Windows Form.
- Create a new Windows Application project named EmpClient.
- Rename the default form (Form1.vb) file name to frmEmpInfo.vb.
- Set the form's Name property to EmpInfo.
- Set the form's Text property to Employee Information.
- Set the Startup Object in the Project Properties to EmpInfo.
- Drag a DataGrid onto this form. Set the Name property to grdEmps.
- Add a Button control to this form. Set the Name property to btnUpdate. Set the Text property to Update.
At this point, the user interface for your employee form is complete. Now it is time to start building the components so you can retrieve the data to populate this DataGrid.
Creating the Data Tier
The data tier is responsible for connecting to your data source, building a typed data set, and returning that data set from a method within this component.
Follow these steps to build a data tier component.
- In the Solution Explorer window, right-click the solution named EmpClient.
- On the shortcut menu, click Add, and then click New Project.
- Choose the Class Library template. Set the name of this class library to EmpData.
- Delete the class file named Class1.vb from the project.
- To add a new component to the project, on the Project menu, click Add Component. Set the name of the component to clsEmp.vb.
- View the code for this component and change the name of the class from clsEmp to Employees.
- In the design view, click and drag a SqlDataAdapter control from the Data tab of the toolbox onto the design surface of this component.
- Go through the steps of this wizard to connect to your SQL Server, pointing to the Northwind database on that server. Select all rows and columns from the Employee table within this database.
- Rename the SqlConnection object from SqlConnection1 to cnNorthwind. Rename the SqlDataAdapter object from SqlDataAdapter1 to daEmps.
- Click the daEmps object and, on the Data menu, click Generate DataSet. Set the name of the New DataSet to dsEmps.
At this point, you have a component with some data access objects on it. The reason to use a component instead of a regular class is that you need the ability to drag a Connection and DataAdapter object onto a design surface. A component will let you do this; a regular class will not. Of course, you could always just create your own data adapter and connection objects in code, but this way is much easier.
All that is left to do after adding this component is to add a couple of methods to the component. The first method, named GetData, returns a reference to the typed dataset filled with employee data. The second method, named Update, accepts a typed dataset as a parameter and submits the changes in this dataset to the backend data source.
The GetData Method in the Data Component
This method is responsible for declaring an object of the type dsEmps. Remember that this is the name of the typed dataset that you generated. This file is represented in the Solution Explorer window as dsEmps.xsd. This is the schema definition file for the Employees table from the Northwind database. The code behind this xsd file is the generated typed dataset named dsEmps.
Public Function GetData() As dsEmps Dim dsData As dsEmps Try dsData = New dsEmps() daEmps.Fill(dsData) Return dsData Catch Throw End Try End Function
After creating the instance of the dsEmps object, you will use the SqlDataAdapter object to fill the dsEmps object with data from the Employees table. This typed dataset is then returned from the method to be consumed by some other component. You will learn how to consume this dataset in the next section.
The Update Method in the Data Component
The Update method accepts a typed dataset from a calling program, and performs the Update method on the SqlDataAdapter object to send any changes in the dataset to the Employees table. It also returns this same dataset back to the calling program so any updated fields, like TimeStamps or Identity fields, can be merged back into the dataset in the calling program.
Public Function Update(ByVal dsData As dsEmps) As dsEmps Try ' Update Data In Table daEmps.Update(dsData) Catch ' Throw any exceptions back to client Throw End Try Return dsData End Function
Notice the use of the structured exception handling in both of these methods. If any errors are encountered, the exceptions are simply thrown back to the calling component for handling. No errors will be handled in these components.
Creating the Web Service
In Figure 1, you can see that the Data Tier is called from the Web service. You will now create the Web service project that calls the EmpData component that you just created.
- In the Solution Explorer window, click the solution named EmpClient.
- Right-click and on the shortcut menu, click Add, and then click New Project.
- Select the ASP.NET Web service template and set the Name of this project to EmpWS.
- Delete the Service1.asmx file from the project.
- To add a new Web service file, on the Project menu, click Add Web service. Set the name of this new Web service to Employees.asmx.
- Click the EmpWS project and add a reference to the EmpData project.
Now that you have created the Web service named Employees, you can create the two methods that will consume the dataset from the data component. You name these two methods the same name as the methods in the data component for consistency.
The GetData Method in the Web Service
The GetData method in the Web service creates a reference to the EmpData.Employees class. Once you have created this new object, invoke the GetData method on this object and return the dataset from this Web method.
<WebMethod()> Public Function GetData() _ As EmpData.dsEmps Dim dc As EmpData.Employees Try dc = New EmpData.Employees() Return dc.GetData Catch ' Throw any exceptions back to client Throw End Try End Function
Remember from the diagram in Figure 1 that the business rule component retrieves the data from this Web service. The reason you don't just create the typed data set in the Web service project is that you want the flexibility to use the data component directly from a Windows application if you are not going to be doing a true distributed application. So just by eliminating the Web service project, you can change the code in the business rule layer to talk directly to the data component instead of the Web service, and you do not have to change any other code.
The Update Method
The Update method in this Web service project simply passes the dataset that was passed in as an argument directly to the data component's Update method.
<WebMethod()> Public Function Update( _ ByVal dsData As EmpData.dsEmps) As EmpData.dsEmps Dim dc As EmpData.Employees Try dc = New EmpData.Employees() dc.Update(dsData) Catch ' Throw any exceptions back to client Throw End Try Return dsData End Function
Creating the Business Rules Component
Now it is time to bring all of the projects together by hooking up the business rule component to the Windows application, and having the business rule component make the call to the Web service to retrieve the data.
Follow the steps below to create the component that acts as the interface between the Windows application and the data tier.
- In the Solution Explorer window, right-click the EmpClient solution, and on the shortcut menu, click Add, and then click New Project.
- Select the Class Library template and set the Name of this new project to EmpBusRule.
- Rename the Class1.vb file to clsEmployee.vb.
- Change the name of the Public Class from Class1 to Employees.
- To ensure that the solution compiles and to also build the Web service files needed for referencing, on the Build menu, click Build Solution.
- Click the EmpBusRule project and add a Web Reference to the EmpWS.vsdisco file.
Note If you cannot set a reference to the vsdisco file, set a reference to the Employees.asmx file instead.
- Expand the Web References folder in this project and rename the LocalHost item (or whatever the name of your Web Server is) to EmpService.
The GetData Method in the Business Rules Component
The GetData method in the business rule component simply accesses the Web service component to request the data from the data tier. Although this seems to be an indirect approach to getting the data, it allows us to create a truly distributed approach to our n-tier application.
Public Function GetData() As EmpService.dsEmps Dim ws As EmpService.Employees Try ws = New EmpService.Employees() Return ws.GetData Catch Throw End Try End Function
The Update Method in the Business Rules Component
The Update method is responsible for taking the dataset of changes, checking to make sure that the business rules are not violated, and then pushing the data back across the HTTP interface to the Web service component.
Public Function Update( _ ByVal dsData As EmpService.dsEmps) As EmpService.dsEmps Dim ws As EmpService.Employees Try ws = New EmpService.Employees() ' Check business Rules Me.Check(dsData) ' Update Data In Table ws.Update(dsData) Catch ' Throw any exceptions back to client Throw End Try Return dsData End Function
The Check Method in the Business Rules Component
The Check method is where you put in any appropriate business rules that you need to check prior to inserting or updating any rows in the dataset.
Public Sub Check(ByVal dsData As EmpService.dsEmps) Dim strMsg As String Dim row As EmpService.dsEmps.EmployeesRow ' Check business rules For Each row In dsData.Employees.Rows If row.RowState = DataRowState.Added Or _ row.RowState = DataRowState.Modified Then If row.FirstName.Trim() = "" Then strMsg &= "First Name must be filled in" & _ ControlChars.CrLf End If If row.LastName.Trim() = "" Then strMsg &= "Last Name must be filled in" & _ ControlChars.CrLf End If If row.HireDate < row.BirthDate Then strMsg &= "Hire Date must be greater than Birth Date" & _ ControlChars.CrLf End If End If Next If strMsg <> "" Then ' Throw a new ApplicationException ' with our custom error message in it Throw New ApplicationException(strMsg) End If End Sub
Notice that you should check the RowState property to see whether the row has been added or updated. You do not need to check the row if it has been deleted. During the Check method you can see the benefit of using Typed Datasets. Instead of referencing a column name through an index in a Dataset you have an actual property name. This enforces type safety and gives you an IntelliSense list of column names so you do not have to look them up in your database.
Consuming the Data from the Windows Application
Now that you have all of the components hooked up together, it is time to make the Windows application consume data from the business rules component. Follow the steps below to make this work.
- In the Solution Explorer window, click the EmpClient project.
- Add a reference to the EmpBusRule project.
- Bring up the code for the form and add the Private mdsEmps variable, as shown in the code below.
Public Class EmpInfo Inherits System.Windows.Forms.Form Private mdsEmps As EmpBusRule.EmpService.dsEmps
This member variable on this form is a typed dataset that represents the Employees table in the Northwind database. You use this variable to fill the DataGrid control on the employee form.
Loading Data into the DataGrid
Next, you need to create the routine to load the DataGrid. First, you add code to the form's Load event procedure.
- Double-click the form to display the Load event procedure.
- Add the following code:
Private Sub EmpInfo_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load GridLoad() End Sub
- Create the GridLoad procedure immediately below the Load event procedure.
Public Sub GridLoad() Dim br As EmpBusRule.Employees Try br = New EmpBusRule.Employees() ' mdsEmps is a typed dataset on this form mdsEmps = br.GetData grdEmps.DataMember = "Employees" grdEmps.DataSource = mdsEmps Catch exp As Exception MsgBox(exp.Message) End Try End Sub
The GridLoad procedure declares a new business rule object. It then uses the GetData method in the business rule object to assign the new typed dataset mdsEmps. You assign the table name to the DataMember and the dataset object to the DataSource property of the DataGrid control.
You should now be able to run this application, and if you have done everything correctly, the DataGrid should be loaded with employee data.
- Set the EmpClient project as the Startup Project if it is not already.
- Press F5 to run the application.
You should now see employee data in the list box that looks like Figure 2.
Updating Data from the DataGrid
You will now create the procedure under the Update button on the form to take any changes you make to the data in the grid and push them back to the data tier. Of course, you will first push the changes to the DataSet through the business rules component, which then sends the data through the Web service component, and then finally to the data tier component. The data tier component then connects up to the SQL Server and pushes the changes to the server.
Private Sub btnUpdate_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnUpdate.Click Dim br As New EmpBusRule.Employees() Dim dsChanges As New EmpBusRule.EmpService.dsEmps() Dim strMsg As String Try If mdsEmps.HasChanges Then dsChanges.Merge(mdsEmps.GetChanges()) MsgBox("Count = " & _ CStr(dsChanges.Employees.Rows.Count)) ' Update the Data on the Server dsChanges = br.Update(dsChanges) MsgBox("Data Has Been Updated Successfully") ' You can either merge the changes back ' in and accept the changes, or you can ' reload the data from the table. I prefer ' reloading as it gets all the other ' changes by all other users. 'mdsEmps.Merge(dsChanges) 'mdsEmps.AcceptChanges() ' If everything is OK, refresh all data ' from the web service. GridLoad() Else MsgBox("No changes have been made to the data") End If Catch exp As ApplicationException MsgBox(exp.Message) Catch exp As Exception MsgBox(exp.Message) End Try End Sub
The first part of this event procedure checks to see whether any changes have even been made to the DataSet. If they have, you get the changed data rows by using the GetChanges method of the DataSet class. This will return just those rows that were changed in the DataGrid.
Once you have this short list of rows from the dataset you can submit that to the Update method of the business rule component. The business rule component then sends it to the Web service component, which sends it on to the data tier component, and finally updates SQL Server. If any changes are made to the rows in the dataset (such as a TimeStamp field updating, or an identity field updating), these rows are passed back to this procedure. You can then either merge the data back into the main dataset, or you could simply reload the entire dataset directly from the database (going through the components of course).
When attempting to submit the changes to the back end, there could be a business rule that is violated. For example, if someone deletes the first name of an employee, the business rule that checks for the existence of the first name in a row of data will fail. This will be thrown as an ApplicationException object. You should check for this type of exception first, then display the error message returned, as that will be the description of the business rule or rules that are in violation.
The last thing you have to check for is any generic exceptions. For example, the database server might be down, and thus a regular exception might be thrown. Or maybe a concurrency exception is thrown because you updated a row and another user updated a row just before you did.
Although the concepts for designing an n-tier application are pretty much the same as they have always been, the implementation is quite different. You should find that the amount of code you have to write is significantly reduced due to the advancements in Web services and ADO.NET.
In this document, you learned how to put together an n-tier application. Although you are shown just one of the many ways you could accomplish this, the format used here is very simple and easy to create. While the component you build here may be created on one machine, you can easily move the components from one machine to another and scale this solution using .NET Remoting and Web services. Whether you choose a logical or a physical implementation for your applications, you should always strive to develop your application using separate components for each process.
About the Author
Paul D. Sheriff is the owner of PDSA, Inc., a custom software development and consulting company in Southern California. Paul is the MSDN Regional Director for Southern California, is the author of a book on Visual Basic 6 called Paul Sheriff Teaches Visual Basic, and has produced over 72 videos on Visual Basic, SQL Server, .NET and Web Development for Keystone Learning Systems. Paul has co-authored a book entitled ASP.NET Jumpstart. Visit the PDSA, Inc. Web site (www.pdsa.com) for more information.
About Informant Communications Group
Informant Communications Group, Inc. (www.informant.com) is a diversified media company focused on the information technology sector. Specializing in software development publications, conferences, catalog publishing and Web sites, ICG was founded in 1990. With offices in the United States and the United Kingdom, ICG has served as a respected media and marketing content integrator, satisfying the burgeoning appetite of IT professionals for quality technical information.
Copyright © 2002 Informant Communications Group and Microsoft Corporation
Technical editing: PDSA, Inc.