Layering ASP.NET Application Code

http://www.aspnetpro.com/NewsletterArticle/2007/02/asp200702dw_l/asp200702dw_l.asp


The Stratifier

Layering ASP.NET Application Code

 

 

It seems like more and more is being asked of developers these days, and more and more we are expected to be coding superheroes. Although development platforms are becoming increasingly productive, more applications are connecting to legacy or external systems and data sources, resulting in additional complexities. This increase in complexity results in more failure points and more spaghetti code. Added complexity requires that code be divided properly so it can more easily be re-used, maintained, and managed.

 

In this article I’ll focus on the fundamentals of creating n-tier/n-layer ASP.NET applications, as well as discuss the pros and cons of separating your code into distinct tiers or layers. Throughout the article you’ll see how to create applications that scale well, promote code re-use, and ease maintenance headaches, so you, too, can become a coding superhero.

 

N-tier versus N-layer

N-tier is a phrase most seasoned developers have heard, but it has multiple meanings depending on to whom you talk. In this article I’ll define n-tier as, “A software architecture methodology that defines distinct physical or process boundaries between application modules.” N-tier architectures typically break down tiers into presentation, business, and data tiers, although other tiers and patterns may certainly be used. Figure 1 shows an example of a standard n-tier architecture.

 

 Figure 1: N-tier architectures divide code into physical tiers to promote code-reuse.

 

N-tier architectures have several advantages, including better code re-use, easier maintenance, enhanced scalability, and fault isolation. However, there are disadvantages as well, such as additional failure points in the application and added complexity as code makes calls between multiple machines on the network.

 

While the term n-tier is certainly relevant for many applications, I prefer to minimize failure points as much as possible and keep things simple. As a result, I subscribe to the n-layer approach, where code is divided into distinct layers (presentation, business, and data for example), but is not separated by physical or process boundaries unless absolutely necessary. Doing this keeps things simple and preserves the principles of code-reuse, easier maintenance, and so on. Figure 2 shows an example of an n-layer architecture. Notice that the presentation, business, and data classes are all located on the Web server, but are logically separated.

 

 Figure 2: N-layer architectures organize code into logical modules to promote code-reuse and minimize failure points.

 

There are two main ways to create n-layer applications using Visual Studio .NET 2005 and ASP.NET 2.0 (see Figure 3). The first involves the App_Code folder available in ASP.NET 2.0. The code-behind files in the Web site contain the presentation logic; the business and data classes are placed in App_Code. Although this is the simplest solution, it doesn’t make the business and data classes as re-useable as they could be.

 

 Figure 3: Visual Studio .NET 2005 provides two main ways for logically separating code. Separate projects can be created for each layer, or layers can be added into the App_Code folder.

 

In cases where business and data classes will be re-used across several applications, separate VS.NET 2005 class library projects can be created to separate the business and data classes into unique dlls. Doing this allows the classes to be given a strong-name (sn.exe) and installed in the GAC on the Web server (gacutil.exe). The downloadable code for this article demonstrates using both techniques, although the remainder of the article will focus on using the App_Code folder.

 

So why would you want to take the time to separate your application logic into separate layers? After all, putting it all directly into your code-behind file works fine, right? First, by creating separate code layers, code can more easily be re-used across one or more applications. If you put all your logic directly into code-behind pages, then only that page can use it without resorting to hacks. Second, by layering, code maintenance becomes much easier. If there’s a bug in the business logic, you know where to start looking. If you’ve ever waded through someone else’s spaghetti code, you know what I’m talking about. Finally, by layering code you can minimize the amount of code in the actual Web pages by using controls, such as the new ObjectDataSource control, to bind layers together.

 

Steps to Create an N-layer Application

You’ll probably want to jump right into coding after business requirements have been gathered for an application. After all, where’s the fun in planning? Although coding is fun, before creating an n-layer application you’ll want to take the time to plan what business rules your application has, how data will be stored and organized, how data will be passed between layers, etc., before going too far. Taking the time to architect and “spec out” the application up front can save you a lot of time down the road. By architecting the application first you’ll know how each layer (presentation, business, and data) will be organized, and what they will contain. Although application architecture is beyond the scope of this article, you’ll find many great examples at MSDN’s Patterns and Practices Web site (http://msdn.microsoft.com/practices).

 

Once an application’s architecture is finished, I like to take a top-down approach to building the code. These are the steps I normally follow:

  • Create the database (tables, log tables, views, triggers, and stored procedures).
  • Create the model layer (data entity classes that will be filled with data and passed between layers; more on this later).
  • Create the data layer (data access classes).
  • Create the business layer (business rules and data processing classes).
  • Create the presentation layer (Web pages and code-behind pages that consume and gather data).

 

Once the database is created, the steps that follow can certainly be divided among different people if you’re working with a team. Let’s take a more detailed look at the steps involved in creating the model, data, business, and presentation layers.

 

The Model Layer

The model layer contains data entity classes that have fields and properties defined in them, but typically have no methods. These classes are normally filled with data in the data layer and then passed to the business and presentation layers. Think of them as the “glue” between all the layers. Figure 4 shows a portion of a model layer class named Customer. Although this class can be created by hand, I used the xsd.exe command-line utility to generate the class automatically. This was done by adding an empty XML Schema file in the Web site and then dragging the Customers table (from the Northwind database) onto the schema design surface from the Server Explorer. I then ran the following syntax using the Visual Studio Command Prompt to generate the class and place it in a namespace of Model.

 

xsd /classes /namespace:Model Customer.xsd

 

namespace Model {

   public partial class Customer {

 

       private string customerIDField;

       private string companyNameField;

       private string contactNameField;

       private string contactTitleField;

       private string addressField;

       private string cityField;

       private string regionField;

       private string postalCodeField;

       private string countryField;

       private string phoneField;

       private string faxField;

 

       public string CustomerID {

           get {

               return this.customerIDField;

           }

           set {

               this.customerIDField = value;

           }

       }

 

       public string CompanyName {

           get {

               return this.companyNameField;

           }

           set {

               this.companyNameField = value;

           }

       }

 

       //Additional properties would follow

 

   }

}

Figure 4: The Model.Customer class is used to hold data that is passed between other layers.

 

When using the App_Code folder to create an n-layer application, I like to create a folder for each layer. The Customer class shown in Figure 4 is placed in the Model folder (again, refer to Figure 3). Alternatively, you may prefer to use strongly-typed DataSets to pass data between layers rather than creating custom classes. I normally place my strongly-typed DataSets in the model layer. However, because .NET version 2 offers TableAdapters that can access the database and act like the data layer, you may feel more comfortable placing them in the data layer. Although I won’t discuss strongly-typed DataSets in this article, the downloadable code contains examples of using them within n-layer applications.

 

The Data Layer

The data layer has a very simple (yet important) job; perform select, insert, update, and delete operations against the database. It can also be used to perform those types of operations against a Web service. When performing select operations, it is responsible for creating a model layer object, filling it with data, and passing it to the business layer. When performing insert, update, or delete operations, it will be passed a model layer object to use as appropriate for the operations. Listing One shows an example of a data layer class named DAL that lives in the Data namespace. The class selects a customer’s data from the Northwind database, instantiates a Model.Customer class, fills it with data, and returns it to the caller. The data layer is responsible for performing database operations and passing Model objects back to the business layer. It is not responsible for performing any business rules.

 

Looking through the code in Listing One you can see that the GetCustomer method accepts a customer ID parameter, which it uses to query the database. This method uses the new .NET version 2.0 DbProviderFactory class to create a generic DbConnection object that can be used against SQL Server, Oracle, and many other databases. The records are streamed from the database using the DbDataReader class, which is then passed to a method named BuildCustomer that is responsible for creating the Model.Customer object instance, filling it with data, and returning it to GetCustomer. GetCustomer then forwards it to the calling business layer code.

 

The Business Layer

The business layer acts as the middleman between the presentation and data layers. It is responsible for processing data and applying business rules. Figure 5 shows an example of a business layer class named BAL (business access layer) that performs a few simple business rules before a customer record is updated by the data layer. In the “real world” you’ll more than likely give your business layer classes more descriptive names because multiple classes may exist in an application.

 

namespace Biz

{

 public class BAL

 {

   public static bool UpdateCustomer(Customer cust)

   {

       Customer newCust = ApplyBusinessRules(cust);

       return Data.DAL.UpdateCustomer(newCust);

   }

 

   private static Customer ApplyBusinessRules(Customer cust)

   {

       //Business rule says that all sales titles must

       //be stored as "Sales Staff" in database

       if (cust.ContactTitle.ToLower().Contains("sales"))

       {

           cust.ContactTitle = "Sales Staff";

       }

 

       if (String.IsNullOrEmpty(cust.Region))

       {

           cust.Region = "No Region";

       }

 

       cust.Phone = FixPhone(cust.Phone);

       cust.Fax = FixPhone(cust.Fax);

       return cust;

   }

 

 

   private static string FixPhone(string phone)

   {

       phone = phone.Replace("(", String.Empty);

       phone = phone.Replace(")", String.Empty);

       phone = phone.Replace("-", String.Empty);

       phone = phone.Replace(" ", String.Empty);

       return phone;

   }

 }

}

Figure 5: The business layer is responsible for performing business logic in the application. This example shows how rules are applied to customer data to change values before a customer record is updated.

 

While the business rules shown in Figure 5 could certainly be performed in the presentation layer, by placing them into a separate layer (and namespace) they can be re-used by multiple pages or applications in cases where the business assembly is placed in the Global Assembly Cache.

 

There will be cases where no business rules need to be applied to perform a particular operation. Should you create a business layer method for the operation or have the presentation layer talk directly to the data layer classes? In this situation I always create a business layer method as it allows for adding business rules in the future. Having a business layer also isolates the presentation layer from the data layer. If the database back-end is switched out, the presentation layer won’t know the difference because it only communicates directly with the business layer and never talks with the data layer directly.

 

Figure 6 shows a simple method named GetCustomer that calls the Data.DAL.GetCustomer method shown in Listing One. It doesn’t perform any business rules functionality and simply acts as a pass-through in this case.

 

namespace Biz

{

 public class BAL

 {

 

   public static Model.Customer GetCustomer(string custID)

   {

     return Data.DAL.GetCustomer(custID);

   }

 }

}

Figure 6: Business layer methods may act as pass-through objects in cases where no business rules need to be applied to an operation.

 

The Presentation Layer

The presentation layer is where many applications written in the not so distant past place the majority of their code. Using the n-layer approach, combined with code-behind files, spaghetti code can be greatly minimized - resulting in easier to maintain and less bug infested applications. It can be challenging to know which code should go in the presentation layer and which code should go in the business layer. My general rule is that if the code directly manipulates a control, it should go in the presentation layer. If the code applies a rule to data, it should go in the business layer. There always are, of course, exceptions to the rule.

 

While you could certainly write VB.NET or C# code to tie the presentation layer to the business layer, the new ObjectDataSource control minimizes the amount of code you need to write. Figure 7 shows code from an ASP.NET Web Form that uses the ObjectDataSource control to call a business layer object and bind data to a DetailsView control.

 

 DataSourceID="odsCustomer" AutoGenerateRows="False"

 DataKeyNames="CustomerID">

  

   

   

   

   

     HorizontalAlign="Center" />

   

   

     ForeColor="White" />

   

   

       

          HeaderText="CustomerID" SortExpression="CustomerID" />

       

          HeaderText="ContactName" SortExpression="ContactName" />

       

       

   

 

 DataObjectTypeName="Model.Customer"

 SelectMethod="GetCustomer" TypeName="Biz.BAL"

 UpdateMethod="UpdateCustomer"

 OnUpdated="odsCustomer_Updated">

   

       

           PropertyName="SelectedValue" Type="String" />

   

Figure 7: The ObjectDataSource control can be used to call business layer objects (or any objects for that matter) with a minimal amount of code.

 

This code uses the ObjectDataSource control to call the Biz.BAL class’ GetCustomer method and return a Model.Customer object. It grabs the customer ID value to use in the query from a GridView control by using the asp:ControlParameter control. Several different parameter controls are available - such as session, querystring, and form - for retrieving data to pass to business layer methods.

 

The DataView control is associated with the ObjectDataSource control by using the DataSourceID property. Once the ObjectDataSource control retrieves the Model.Customer object it is automatically bound to the DetailsView control without writing any VB.NET or C# code. Figure 8 shows the output generated by the n-layer application available with this article’s accompanying downloadable code.

 

 
Figure 8: Output generated by the n-layer application.

 

Conclusion

In this article you’ve seen how code can be divided into layers to promote better code re-use and maintenance. There are many different ways to architect and organize code in ASP.NET applications; it’s recommended you study the various code patterns available before deciding what is best for your project. Regardless of which pattern you choose, by organizing code into layers you’ll certainly reap many benefits down the road.

 

The source code accompanying this article is available for download.

 

Dan Wahlin (Microsoft Most Valuable Professional for ASP.NET and XML Web services) is a .NET development instructor at Interface Technical Training (http://www.interfacett.com). Dan founded the XML for ASP.NET Developers Web site (http://www.XMLforASP.NET), which focuses on using XML, ADO.NET, and Web services in Microsoft’s .NET platform. He’s also on the INETA Speaker’s Bureau and speaks at several conferences. Dan co-authored Professional Windows DNA (Wrox), ASP.NET: Tips, Tutorials, and Code (SAMS), ASP.NET 1.1 Insider Solutions (SAMS), and ASP.NET 2.0 MVP Hacks (Wrox), and authored XML for ASP.NET Developers (SAMS). When he’s not writing code, articles, or books, Dan enjoys writing and recording music and playing golf and basketball with his wife and kids. He recently wrote and recorded a new song with Spike Xavier called “No More DLL Hell”, which can be downloaded fromhttp://www.interfacett.com/dllhell.

 

Coding Conventions

Based on some positive feedback and questions about this article, I thought I’d go into a little more detail about some practices that can be implemented when deciding on names for namespaces, classes, fields, methods, and events. Thanks to Robert Dannelly for sending some references and for suggesting that I discuss best practices that developers should follow with regard to naming and casing. Whether you work on a team with several developers or are your own one man or woman team, starting a project with coding guidelines in mind can ease maintenance down the road and make application programming interfaces (APIs) more consistent.

 

Microsoft published an excellent document that should serve as the starting point for any developer or company looking to establish a set of guidelines (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconcapitalizationstyles.asp). While flexibility and common sense need to be applied to any company document that provides coding guidelines and standards, the following table (provided in Microsoft’s document) serves as a good starting point:

 

Identifier

Case

Example

Class

Pascal

AppDomain

Enum type

Pascal

ErrorLevel

Enum values

Pascal

FatalError

Event

Pascal

ValueChange

Exception class

Pascal

WebException

Read-only Static field

Pascal

RedValue

Interface

Pascal

IDisposable

Method

Pascal

ToString

Namespace

Pascal

System.Drawing

Parameter

Camel

typeName

Property

Pascal

BackColor

Protected instance field

Camel

redValue

Note: Rarely used. A property is preferable to using a protected instance field.

Public instance field

Pascal

RedValue

Note: Rarely used. A property is preferable to using a public instance field.

 

I’m a big believer in the guidelines listed in the previous table, and like to use Pascal or Camel casing as indicated. In addition to casing guidelines, you may also want to enhance the guidelines even more by establishing naming guidelines. For example, if you work in the finance department for AcmeCorp and are writing data access classes for an application named “Payroll”, you might decide on the following namespace to hold your related data classes:

 

namespace AcmeCorp.Finance.Payroll.Data

{

}

 

Classes that perform business rules functionality may go into the following namespace:

 

namespace AcmeCorp.Finance.Payroll.Biz

{

}

 

Coming up with a naming convention for namespaces, classes, events, properties, and methods can help establish a consistent framework for teams that makes debugging other people’s code a more pleasant experience overall (not that debugging someone else’s code is ever pleasant). In addition to the guidelines you’ve seen to this point, there are a few others I like to follow.

 

First, I like to prefix all private fields with an underscore character. For example, to define a first name field I do the following:

 

string _FirstName;

 

Adding the underscore allows me to instantly know when an object is a private field and it matches up nicely with related property statements:

 

public string FirstName

{

    get { return _FirstName; }

    set { _FirstName = value; }

}

 

Other common naming practices that you’ll see with the .NET Framework are delegate names. When defining a delegate you’ll want to add the word “Handler” on the end of it. So, if I have an event named StageCompleted, I’ll normally name my delegate StageCompletedHandler, and if I created a custom EventArgs class, I’ll name it StageCompletedEventArgs:

 

public delegate void StageCompletedHandler(object sender,

 StageCompletedEventArgs e);

 

There are a lot of other naming conventions that could be covered, but I’ll leave that as an exercise for the reader since it really depends on your company’s guidelines and, in some cases, personal preference. After all, should you name a textbox control txtFirstName, firstNameTextBox, or something else? I certainly have my preference (hint: shorter is better), but it’s something you’ll need to debate with your team or manager.

 

If your company doesn’t have any guidelines I recommend putting together a simple document that outlines the standards. Publish it to a place where all team members have access so everyone knows the expectations. Establishing core coding guidelines will make your applications more consistent and easier to work with in the future.

 

Begin Listing One — a data layer class named DAL

namespace Data

{

 public class DAL

 {

   public static Customer GetCustomer(string custID)

   {

     DbConnection conn = null;

     DbDataReader reader = null;

     try

     {

       conn = GetConnection();

       DbCommand cmd = conn.CreateCommand();

       cmd.CommandText = "ap_GetCustomer";

       cmd.CommandType = CommandType.StoredProcedure;

 

       DbParameter param = cmd.CreateParameter();

       param.ParameterName = "@CustomerID";

       param.Value = custID;

       cmd.Parameters.Add(param);

 

       conn.Open();

       reader = cmd.ExecuteReader();

       Customer[] custs = BuildCustomer(reader);

       if (custs != null && custs.Length > 0)

       {

           return custs[0];

       }

     }

     catch (Exception exp)

     {

       HttpContext.Current.Trace.Warn("Error",

         "Error in GetCustomer: " + exp.Message);

     }

     finally

     {

      if (conn != null) conn.Close();

      if (reader != null) reader.Close();

     }

     return null;

   }

 

   //Re-useable routine that maps a reader to the

   //Model.Customer class properties

   public static Customer[] BuildCustomer(

    DbDataReader reader)

   {

     List custs = new List();

     if (reader != null && reader.HasRows)

     {

       while (reader.Read())

       {

 

         Model.Customer cust = new Model.Customer();

         cust.Address = reader["Address"].ToString();

         cust.City = reader["City"].ToString();

         cust.CompanyName =

          reader["CompanyName"].ToString();

         cust.ContactName =

          reader["ContactName"].ToString();

         cust.ContactTitle =

          reader["ContactTitle"].ToString();

         cust.Country = reader["Country"].ToString();

         cust.CustomerID = reader["CustomerID"].ToString();

         cust.Fax = reader["Fax"].ToString();

         cust.Phone = reader["Phone"].ToString();

         cust.PostalCode = reader["PostalCode"].ToString();

         cust.Region = reader["Region"].ToString();

         custs.Add(cust);

       }

     }

     return custs.ToArray();

   }

 

   //Create generic database connection object using new

   //DbProviderFactories class

   public static DbConnection GetConnection()

   {

     ConnectionStringSettings connStr =

       ConfigurationManager.ConnectionStrings[

        "NorthwindConnStr"];

     DbProviderFactory f =

       DbProviderFactories.GetFactory(

        connStr.ProviderName);

     DbConnection conn = f.CreateConnection();

     conn.ConnectionString = connStr.ConnectionString;

     return conn;

   }

 }

}

End Listing One

search this blog (most likely not here)