Using C#, ASP.NET, and ADO.NET To Develop A Multi-Tier App From Start To Finish

This article assumes a familiarity with Visual Studio 2008 and ASP.NET.  In this article I will show you how to create a simple multi-tier (otherwise known as n-tier) application using ASP.NET.  We are going to be using several ASP.NET controls.  What coding there is will be in the Business Logic Layer, and will be done in C#.  There's not too much, and if you only know VB.NET, you should still be able to follow along with no problem.

Multi-tier applications exist for a variety of reasons.  This article won't go into too much depth on when and why to use multi-tier architecture, however a quick overview is in order:

What Is Multi-Tier Architecture, And Why Do We Care?

Multi-tier (or n-tier) architecture refers to the practice of separating an application into layers.  Doing so makes it easier for you and other developers to re-use your code.  Typically, a multi-tier application will be broken into three layers:

  • Data Access Layer (DAL):  This layer contains all the code that interacts with the data store used by the application.  For example, if your data store is a typical RDBMS like Sql Server or Oracle, the data access layer will contain the code that calls the stored procedures in your database to perform reads and writes (or if you don't use stored procedures, the data access layer may contain your SQL code).
  • Business Logic Layer (BLL):  This layer contains all the code that implements your application's business rules.  It makes calls to the DAL, usually on behalf of the Presentation Layer (see below).  The BLL typically contains code that operates on the data received from the DAL prior to passing it back to the Presentation Layer for display to the user.  For example, the BLL may perform calculations based on data retrieved from the DAL to be displayed along with the actual data.
  • Presentation Layer:  This layer typically reads and writes to the BLL.  The Presentation Layer is responsible for taking the data it receives from the BLL, formatting it, and displaying it to the user.  It is also responsible for collecting user-entered data and conveying it to the BLL for further processing.  In the .NET world, the Presentation Layer is typically created with either ASP.NET Web Forms, Windows Forms, or more recently WPF (Windows Presentation Foundation).

Dividing an application's code in this manner allows us to take advantage of distributed deployment. For example, you could deploy your Data Access Layer to a data server, your Business Logic Layer to an application server, and your Presentation Layer to a web server (assuming it was a web-based GUI). Later, should you decide to create a Windows GUI client that consumes the same Business Logic Layer component, you could deploy that client to your users' desktops and reference the Business component on the application server.  In addition, a distributed, multi-tiered application is much more likely to scale for future growth.

How We'll Go About It

We will be creating three Visual Studio projects, all within the same solution for simplicity's sake.  The application we create will read and write to database tables which contain information on car manufacturer's, makes, and models.  Figure 1 shows the structure and relationships of the three tables involved.  You should create these tables for yourself, and when doing so, don't forget to specify Primary Keys for each table as indicated in the diagram:


Figure 1

I've pre-populated each table as shown in Figure 2, Figure 3, and Figure 4:


Figure 2: Manufacturer table


Figure 3: Make table


Figure 4: Model table

You will need to set these tables up in your database in order to play along with us during the creation of the application.  The data doesn't have to match exactly, but the structures of each table does.  Also, when entering data, keep in mind the relationships between the tables as shown in Figure 1 above.

As mentioned earlier, our goal is to create a multi-tier application consisting of a Data Access Layer, a Business Logic Layer, and a Presentation Layer.  There will be absolutely no direct communication between the Presentation Layer and the Data Access Layer.  Furthermore, the Data Access Layer will be totally unaware of the Business Logic Layer, and the Business Logic Layer will be totally unaware of the Presentation Layer.  In this way (as it should always be in multi-tier architecture), each of the layers should be able to support any .NET client. For example, although our Presentation Layer will be web-based, once this application is finished, you will be able to create a new Presentation Layer in Windows Forms (or WPF, for that matter) that can consume the Business Logic Layer we will be building (which in turn will consume the Data Access Layer). 

When we are finished building our application, it will allow us to perform maintenance on the three tables above.  It will also contain a reporting function which will enable a user to display a list of all models in the system, including the Manufacturer Name, Make Name, Model Name, and Model Year.

Let's Do It

OK, so start off by creating a new solution in Visual Studio, and call it "Cars".  (As I mentioned at the top of the article, I'm assuming you know your way around Visual Studio, so I'm not going to take you through creating solutions and projects within Visual Studio.  If you don't know how to do that already, please take advantage of the documentation and videos on Microsoft's web site to familiarize yourself with these procedures.)

Developing the Data Access Layer

Within the Cars solution, create a new Class Library project and call it "CarsDAL".  This project is where we will develop our Data Access Layer.  Once the project is created, delete the Class1.cs file that gets created by default as we won't be needing it.  (I told you there wasn't going to be much coding at the beginning, see?)  Then, create a new folder named App_Code in the project.

Right click on the App_Code folder, add a new DataSet, and name it "CarsDataSet". Once the DataSet is added, a Table Adapter Design Window will open.  Right click anywhere within the TableAdapter Design Window, the click on Add, then TableAdapter from the popup menu.  This will launch the TableAdapter Configuration Wizard.  We will now create our first TableAdapter, the Manufacturer TableAdapter, by taking the following steps:

  • On the first page of the TableAdapter Configuration Wizard, choose your data connection, which should point to the "Cars" database schema you created in preparation earlier, then click Next.
  • The next page asks you to choose a Command Type.  For the purposes of this article we will choose the option that says, "Create New Stored Procedures".  Then click Next. 
  • On the next screen, click the Query Builder button, which will load the Query Builder window.  The Add Table dialog box is displayed.  Select Manufacturer, then click the Add button, and then close the Add Table dialog box.  In the Manufacturer table now displayed in the top frame of the Query Builder window, check each of the fields.  Leave the "*(All Columns)" checkbox unchecked.  Then click OK in the Query Builder window, and click Next to advance the TableAdapter Configuration Wizard.
  • In the next screen you are asked to name the four stored procedures that will be created for you for the Manufacturer TableAdapter.  (That's right, you don't need to create them yourself, Visual Studio does it for you.)  Change the default names so that the four stored procedures are named Manufacturer_s, Manufacturer_i, Manufacturer_u, and Manufacturer_d, for Select, Insert, Update, and Delete respectively.  Click Next.
  • The next screen is the Choose Methods screen.  We're going to uncheck the "Fill A DataTable" checkbox, and leave "Return A DataTable" and "GenerateDBDirectMethods" checkboxes checked.  Also, in the text field under the "Return A DataTable" checkbox, change the method name to GetManufacturerData.  Click Next.
  • The Wizard Results screen will list all of the procedures and methods that were generated for you.  Look these over, then click Finish.
  • When the TableAdapter Configuration Wizard closes, you should see the Manufacturer TableAdapter represented in the TableAdapter Design Window.  It should contain two sections; the Manufacturer section which lists the fields, and the ManufacturerTableAdapter section which lists the queries available in the TableAdapter.  If you recall, we only created one query for this TableAdapter, called GetManufacturerData(), which returns all of the rows in the Manufacturer table.  It's a good idea to create one more query which we will call GetManufacturerDataById(), so that we can load a particular Manufacturer record if necessary.  This query is going to accept the MfrId as a parameter.  We'll create it as follows:
  • Right click on the bottom half of the Manufacturer TableAdapter, anywhere in the ManufacturerTableAdapter section.  Choose Add Query from the popup menu, and you will be presented with the TableAdapter Query ConfigurationWizard.  Choose "Create New Stored Procedure", then click Next.
  • On the Query Type screen choose the first option, "SELECT which returns rows", then click Next.
  • In the Generate Stored Procedure screen, you can use the Query Builder, or you can manually type in your query.  It should end up looking like this:

    SELECT MfrId, MfrName, Created_Date FROM Manufacturer WHERE (MfrId = @MfrId)

  • Then click Next.  On the Create Stored Procedure screen, name the stored procedure "Manufacturer_By_Id_s", then click Next.
  • On the Choose Methods screen, uncheck the "Fill A DataTable" checkbox and leave the "Return A DataTable" checkbox checked.  Change the name of the method to "GetManufacturerDataById", then click Next.
  • The Wizard Results screen will list all of the procedures and methods that were generated for you.  Look these over, then click Finish.
  • You should now see the Manufacturer TableAdapter represented in the TableAdapter Design Window with the new method included.

Now, repeat each of those steps for the Make table, and then again for the Model table.  Be sure to create "ById" queries and methods for each table, passing the appropriate field (MakeId and ModelId) and naming each query correctly depending on the table you're dealing with. When you're done, there is just one more step and we'll be done with the Data Access Layer.

OK, now we need to create just one more TableAdapter in the DAL.  As before, right click on any empty area within the TableAdapter Design Window, and click Add, and then TableAdapter in the popup menu.  This will load the TableAdapter Configuration Wizard one more time:

  • Choose your Cars connection string, then click Next.
  • Choose Create New Stored Procedures, then click Next.
  • In the Query Builder window, either click the Query Builder button or manually type your SQL query.  Either way, it should look pretty much like this when you're done:

    SELECT Manufacturer.MfrName, Make.MakeName, Model.ModelName, Model.ModelYear FROM Model INNER JOIN Make ON Model.MakeId = Make.MakeId INNER JOIN Manufacturer ON Make.MfrId = Manufacturer.MfrId

  • Then click Next.  In the Create Stored Procedures screen, change the name of the Select query to "ModelReport_s".  The rest of the queries won't matter since you can't insert, update, or delete when joining tables as we did with this query.  Visual Studio is smart enough to recognize this and doesn't actually create these queries regardless of what you name them.  (Why it wasn't smart enough to just disable them is beyond me.)  Anyway, click Next when that's done.
  • Again you will see the Choose Methods screen.  As before, uncheck the "Fill A DataTable" checkbox and leave the "Return A DataTable" checkbox checked.  Change the method name to "GetModelReportData".  Click Next.
  • The Wizard Results screen will list all of the procedures and methods that were generated for you.  Look these over, then click Finish.

The Data Access Layer is now complete.  Next, we'll take a look at the Business Logic Layer.

Business Logic Layer

Create a new Class Library project in the Cars solution, and call it CarsBLL.  Next, add a reference to the CarsDLL project.  Then rename the Class1.cs file created by default to CarsBLL.cs, add the [System.ComponentModel.DataObject] attribute to the CarsBLL class, and add four fields, one for each adapter created in the DAL. The CarsBLL should look like this at this point:

The [System.ComponentModel.DataObject] attribute, along with the others we will be adding to the methods we create next in this class, will make the BLL and its methods visible to the controls we will be using later in the Presentation Layer.

Next, we'll add a property for each adapter, methods to Add, Update, Delete, and Get data for the Manufacturer, Make, and Model tables, and a Get method for the Report adapter.  Note that each of these methods will make calls into the DAL, which as we saw earlier, makes calls to the database.  Once these properties and methods are created, your CarsBLL class should look something like this:

Note the attributes added to each method: [System.ComponentModel.DataObjectMethodAttribute (System.ComponentModel.DataObjectMethodType.Select, true)]. Again, this ensures that they are visible to the ObjectDataSource control which will be used in the Presentation Layer.

Now, you may have noticed that the BLL Delete methods all accept one parameter, which represents the primary key for the corresponding table.  We're going to have to go back to the DAL and adjust the Delete methods there and in the Delete stored procedures that were generated by Visual Studio.  For some reason, Visual Studio's TableAdapter Configuration Wizard generates these methods and procedures so that it's necessary to pass a variable for every field into them.  That's just ridiculous.  Visual Studio should be able to recognize when a table has a primary key, and it should generate the Delete methods and procedures accordingly.  Well, it doesn't, so we're going to do that.  (We could leave it the way it is and adjust our BLL to deal with this stupidity, but I have a low tolerance for stupidity.  Besides, it's almost as easy to do it correctly, and certainly worth the extra effort.)

So, let's go back to our CarsDAL project and take a look at the CarsDataSet.Designer.cs file that was generated by the TableAdapter Configuration Wizard.  Within that file there should be three different methods called InitAdapter(), one for each table in our schema.  Within each of these methods you can see how each command object is created.  You want to look for where the parameters are being added to the command object.  Look for code that looks something like this:

The above code adds four parameters to the DeleteCommand object of the Manufacturer TableAdapter.  We only need the first two; the first parameter represents the return value of the stored procedure, and the second one represents the primary key value of the record to be deleted.  That's all we really need, so delete the third and fourth parameters.  Once you've done that, repeat the process for both the Make TableAdapter and the Model TableAdapter.

Now we need to adjust the three stored procedures that perform the Deletes in the respective tables.  You will need to adjust the parameter list within each stored proc so that it only expects the first "input" parameter as mentioned above.  In addition, you will need to adjust the WHERE clause in each of these stored procs as well, so that they only include the primary key field.  Here's what the Manufacturer_d stored procedure should look like when you're done:

You will need to do this for both the Make_d and Model_d stored procedures as well.

We have now completed both the Data Access Layer and the Business Logic Layer.

Presentation Layer

The Presentation Layer is the final piece to our puzzle.  Keep in mind that although I'll be using ASP.NET in order to create a web GUI for the purposes of this article, you could just as easily create a Windows Forms or a WPF-based presentation layer for your app, and utilize the exact same Business Logic Layer and Data Access Layer components we created previously.  After all, that is the point of multi-tier architecture.

Within our Cars solution, create a new ASP.NET Web Application project, and name it CarsWeb.  VS will create a Default.aspx page within the project which we will look at soon.  You can leave it alone for now.

Before we start coding, we want to add a reference to the CarsBLL project.  Just right-click on the CarsWeb project, choose Add Reference from the menu, then choose the CarsBLL project from the Project tab. After that's done, rebuild the solution.  This will ensure that CarsBLL is visible from within CarsWeb.

Our presentation layer is going to consist of a menu of options which will be contained in Default.aspx.  It will also contain a Maintenance page for each of our tables where a user can view, edit, or delete records.  In addition, there will be a Reports page which will list each model, along with the names of its Manufacturer and Make.  VS already created our Default.aspx page, so just add four new pages, naming them mfrmaint.aspx, makemaint.aspx, modelmaint.aspx, and report.aspx.  You should have five aspx files in total, including Default.aspx.  (Our GUI is not going to be pretty or fancy, but it will be functional.  You can add pretty and fancy at your leisure.)

We'll start by adding an ObjectDataSource to the Manufacturer maintenance page:

  • Open up the mfrmaint.aspx page in design mode.
  • Add an ObjectDataSource control to the page by dragging it in from the Data section of the VS toolbox. It will be named ObjectDataSource1 by default.
  • Right-click on the ObjectDataSource control and choose Configure Data Source from the context menu.
  • In the configuration window, choose CarsBLL.CarsBLL from the business objects list, then click Next.
  • The next screen contains four tabs; SELECT, UPDATE, INSERT, and DELETE. In each of these tabs, ensure that the appropriate method is selected. The chosen methods should be GetManufacturerData(), UpdateManufacturer(), AddManufacturer(), and DeleteManufacturer() respectively.
  • Click Finish
  • Switch to Source view, and change the OldValuesParameterFormatString attribute from "original_{0}" to just "{0}" (due to a bug in the ObjectDataSource control)

And now, a GridView control:

  • Open up the mfrmaint.aspx page in design mode again
  • Add a GridView control to the page by dragging it in from the Data section of the Toolbox
  • In the Properties window of the GridView control, assign ObjectDataSource1 to the DataSourceID property
  • In the GridView Tasks sheet, check the following options:  Enable Paging, Enable Sorting, Enable Editing, and Enable Deleting
  • Set the ReadOnly property to True for the MfrId and Created_Date fields
  • Set the DataKeyNames attribute of the GridView control to "MfrId", so that the control knows which is the Primary Key field

Now, repeat the above steps in both makemaint.aspx and modelmaint.aspx for Make Maintenance and Model Maintenance.  When that's all done, meet me at the next paragraph.

Ok, now that the table maintenance pages are done, we'll do the Report page.  The report will consist simply of a call to the GetAllModels() method of our BLL.  It's just like the table maintenance pages we just completed, only easier.  Open up report.aspx in Design mode and add an ObjectDataSource just like we did on the table maintenance pages.  Right-click on it and choose Configure Data Source from the menu.  In the configuration window, choose CarsBLL.CarsBLL from the business objects list, then click Next.  On the next screen we'll only be concerned with the SELECT tab this time.  In the SELECT tab, choose the GetAllModels() method and click Finish.  Next, add a GridView control, and assign ObjectDataSource1 as its DataSourceID property.  Then go to the GridView control's GridView Tasks sheet, check Enable Paging and Enable Sorting.

Lastly, open up Default.aspx and create a simple menu with links to each of the other four pages.  Something like this:

And that's it!  Build your solution, and you should have pages that look as follows:


Default.aspx


mfrmaint.aspx


makemaint.aspx


modelmaint.aspx


report.aspx

You can see that I wasn't lying when I said the GUI wouldn't be pretty, but hopefully I've shed some light on the subject of building a multi-tier application in C#.

Dave Verschleiser
Murray Hill Technologies


DateComment
3/2/2010 9:17:23 PM rajnishgreat article, i want to use it on my current article. rajnish http://ignou-student.blogspot.com
3/3/2010 9:59:06 AM ChrisIs there example downloadable code for this?
3/3/2010 10:07:44 AM KshitijThnx
3/3/2010 10:57:30 AM AdriNice article, I hope that in the next series of articles you are explaining custom collections with n-tier instead of using dataset. Thanks
3/3/2010 10:58:02 AM JieGreat overview! Has all the best practice summurized. Anybody who follows this won't get wrong to have a decent architecture!
3/3/2010 11:10:46 AM Peterdave, great article. I have a question about making an instance of the table adapter in the bll. What are your thoughts on making that property static? Ive seen it done different ways but wasnt real sure which is most efficient
3/3/2010 1:49:29 PM SamuelThis is a good starting point, but I would point the readers to more advanced techniques that are considered best design practices. For example, use interface oriented programming to isolate the implementation from the contracts. This will make your application more testable and allow inversion of control containers to inject different concrete implementations dynamically. I personally don't like to use ADO.NET Data adapters, so I would rewrite the DAL using more transparent frameworks like the Enterprise Library, NHibernate, Linq for SQL, or my favorite CoNatural.Data. The BLL could also be exposed as a web service using WCF, etc, etc. Although I'm trying to make a point that your design is not following many best practices, the n-tier layered approach is a great starting point for anyone trying to build a system from scratch. Keep the good work!

Hi Samuel. Thanks for your comments. You are of course correct in saying that there are many different ways to create each layer, and to have them interface with each other. The main point I'm trying to make is that the layers should be separated. The methods I've chosen to demonstrate in this article are not necessarily the best methods. For an enterprise level production application, I would certainly not use a data adapter for example, but in my opinion the methods used in the article are the easiest for someone who has not done this in the past to grasp. They are my intended audience. - Dave
3/3/2010 5:30:13 PM Jess VermontExcellent article. Covers the essentials in a clear, concise manner. This should be ground-level knowledge for every ASP.Net developer.
3/3/2010 8:04:20 PM Hyacinth BroadchestMany thanks for this. With you so far. I would be interested if there are any books that you can recommend on good n-tier OO design. My current problem, that Adri above makes reference of, is, say I have a custom collection, say a List of Widgets. I wind up with a routine that walks the list and pulls all of the fields out of the Company.Domain.DAL.Widget and builds a Company.Domain.BLL.Widget. This seems like a lot of extra cycles because the namespaces can't share abstract datatypes? I think I need to read more so's I know where I am suppsed to be heading and why...
3/4/2010 2:02:38 AM ThomasAnother, very good, article with custom collections is: N-Layered Web Applications with ASP.NET 3.5
3/5/2010 4:15:43 AM AlA nice starting point for someone new to Multi-Tier Architecture and wanting to get into good habits when working on smaller projects.
3/5/2010 7:10:23 AM VinodThis is cool!
3/8/2010 3:06:53 AM AimaNice and simple!
3/8/2010 3:11:02 AM JagFollowing on from what Samuel said, I am also interested in a more indepth look at this. This is a great starting point, but I have rarely seen anyone do more than this. Any change you can do followup articles on the next steps - especially on creating layers that can be tested automatically.
3/8/2010 6:08:24 AM Biraja Mishrathis is a very good article for the beginners.please post an article on CRUD operations in ado.net.
3/8/2010 10:00:20 PM andyExcellent.. Good Explanation... Thanks..
3/16/2010 10:38:25 AM mhbaltiVery professional and very good for beginners........thnx
3/24/2010 8:18:36 AM Jaber M. JaberThanks Dave for your nice & well-explained article. Really useful, especially for .net beginners. However, I tried to run your example using visual studio 2005, but unfortunately I was unable to build the solution. The compiler was unable to find the ""System.Linq"" namespace when I tried to build the solution. I've already downloaded and installed ""LINQ Preview (May 2006).msi"" on my Vista Ultimate 64-bit, SP1 machine, but still have the same error. Any help would be greatly appreciated! Thanks!

-- Actually, the System.Linq library is unnecessary for this project. I have Visual Studio 2008 configured to include it by default, but for this particular project it's safe to remove. Hope that helps. Thanks, Dave


Enter your comments here:


Enter your name:   
 
 


===============================================================================================================