Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Layered Application Design Pattern

4.48/5 (23 votes)
25 Sep 2013CPOL5 min read 68.7K   959  
N-tier architecture of Project

Introduction

In critical application development, the most important thing is project architecture or project framework which comprises various modules involved in request processing or business handling.

If the framework is clear and robust, and completely designed with proper planning, then it's very easy to do the code and develop the particular requirement for business.

For any application, if a developer knows the project framework/architecture, then 50% work is done. After that, we need to focus on functionality only and need not worry about the inter connected business component involved in the project.

N-tier Architecture

Image 1

The most important layers in any project are:

  1. Application Layer
  2. Business Layer
  3. Data Layer

But the problem is if we use only these layers, then these layers will communicate with each other directly which may result in tightly-coupled framework. Also, we have to use the same code for logging and error handling in various layers.

Also, if we want to use the common code logic, then we need to write it in various layers.

So, for improving the same, we use the following layers:

  1. Application Layer/Presentation Layer
  2. Business Layer
  3. Data Layer
  4. Framework Layer
  5. Service Proxies Layer
  6. Services Layer

Image 2

Image 3

Each Layer in Detail

1. Data Layer

This layer contains the Database connectivity, i.e., data entities, data connections, etc.

Data access components in this data layer are responsible for exposing the data stored in these databases to the business layer.

Let's implement our Data Layer.

Add ADO.NET Entity Data Model, name it as DataModel and name entities as SampleEntities which will reflect as connection string name in app.config of DataLayer as shown below.

We can select what tables we want as entities in our application.

In the attached code, we are using only two tables, GI_User and Log. We are now done with our Data Layer.

app.config:

XML
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="SampleEntities" 
       connectionString="metadata=res://*/DataModel.csdl|res://*/DataModel.ssdl|res://*/
         DataModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;
         Data Source=(local);Initial Catalog=Sample;User ID=uid;
         Password=pwd;MultipleActiveResultSets=True&quot;" 
       providerName="System.Data.EntityClient" />
  </connectionStrings>
</configuration>  

2. Business Layer

This layer contains the business processing logic.

All the CRUD operations go here for example:

Let's implement our Business Layer. First of all, add DataLayer reference to Business Layer.

In this Layer, we are validating the credentials provided by the end user with the values in our GI_User table.

For performing any operation with database, we use LINQ (Language Integrated Query) and for accessing the table entities, we need to make an object of the connection entities, i.e., SampleEntities as shown in the below code snippet.

We are now done with our Business Layer too.

C#
namespace BusinessLayer
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.Objects;
    using System.Linq;
    using System.Web;
    using DataLayer;

    /// <summary>
    /// Authenticate class.
    /// </summary>
    public class Authenticate
    {
        /// <summary>
        /// AuthenticateUser used for authenticating an user.
        /// </summary>
        /// <param name="userName">User name provided by user.</param>
        /// <param name="password">Password provide by user.</param>
        /// <returns>Authentication status.</returns>
        public int AuthenticateUser(string userName, string password)
        {
            SampleEntities dataContext = new SampleEntities();
            return (from status in dataContext.GI_User
                    where status.UserName.Equals(userName) && status.Password.Equals(password)
                    select status.UserId).FirstOrDefault();
        }
    }
}

3. Framework Layer

Image 4

This layer contains the common configurable items. All the logging mechanism, caching mechanism, etc., goes here. Time to implement our Framework layer which will consist of all logging, mailing, and any other mechanism. For logging purposes, we use log4net (read more on http://www.codeproject.com/Articles/140911/log4net-Tutorial). We declare some enumerations which are required for logging mechanism. For implementing the logging mechanism using log4net, we have to add reference of log4net.dll.

Enums:

C#
namespace Framework.Enums
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel;
    using System.Runtime.Serialization;

    /// <summary>
    /// Framework Enumeration class.
    /// </summary>
    public static class Enums
    {
        /// <summary>
        /// Framework Enumeration ErrorLevel.
        /// </summary>
        [DataContract(Name = "ErrorLevel")]
        public enum ErrorLevel
        {
            /// <summary>
            /// ErrorLevel FATAL. 
            /// </summary>
            [EnumMember]
            FATAL = 0,

            /// <summary>
            /// ErrorLevel ERROR.
            /// </summary>
            [EnumMember]
            ERROR = 1,

            /// <summary>
            /// ErrorLevel WARN.
            /// </summary>
            [EnumMember]
            WARN = 2,

            /// <summary>
            /// ErrorLevel INFO.
            /// </summary>
            [EnumMember]
            INFO = 3,

            /// <summary>
            /// ErrorLevel DEBUG.
            /// </summary>
            [EnumMember]
            DEBUG = 4
        }
    }
}

Logger:

C#
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
namespace Framework.Logging
{
    using System;
    using Framework.Enums;

    /// <summary>
    /// Logger class.
    /// </summary>
    public static class Logger
    {
        /// <summary>
        /// Logger field of Logger class.
        /// </summary>
        private static readonly log4net.ILog logger = log4net.LogManager.GetLogger(
          System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// Logs info to DB / File based on web.config settings.
        /// </summary>
        /// <param name="ex">Exception object</param>
        /// <param name="customMessage">Custom error message.</param>
        /// <param name="errorLevel">Exception error type.</param>
        public static void Log(Exception ex, string customMessage, Enums.ErrorLevel errorLevel)
        {
            switch (errorLevel)
            {
                case Enums.ErrorLevel.DEBUG:
                    logger.Debug(customMessage, ex);
                    break;
                case Enums.ErrorLevel.ERROR:
                    logger.Error(customMessage, ex);
                    break;
                case Enums.ErrorLevel.FATAL:
                    logger.Fatal(customMessage, ex);
                    break;
                case Enums.ErrorLevel.INFO:
                    logger.Info(customMessage, ex);
                    break;
                case Enums.ErrorLevel.WARN:
                    logger.Warn(customMessage, ex);
                    break;
                default:
                    logger.Error(customMessage, ex);
                    break;
            }
        }
    }
}

app.config:

XML
<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="log4net" 
       type="log4net.Config.Log4NetConfigurationSectionHandler,Log4net"/>
  </configSections>
  <log4net>
    <root>
      <level value="DEBUG"/>
      <appender-ref ref="ADONetAppender"/>
    </root>
    <appender name="ADONetAppender" type="log4net.Appender.ADONetAppender">
      <bufferSize value="1"/>
      <connectionType value="System.Data.SqlClient.SqlConnection, 
         System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
      <connectionString value="server=(local); uid=uid; pwd=pwd; database=Sample"/>
      <commandText value="INSERT INTO Log ([Date],[Thread],[Level],[Logger],
         [Message],[Exception]) VALUES (@log_date, @thread, @log_level, 
         @logger, @message, @exception)"/>
      <parameter>
        <parameterName value="@log_date"/>
        <dbType value="DateTime"/>
        <layout type="log4net.Layout.RawTimeStampLayout"/>
      </parameter>
      <parameter>
        <parameterName value="@thread"/>
        <dbType value="String"/>
        <size value="255"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%thread"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@log_level"/>
        <dbType value="String"/>
        <size value="50"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%level"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@logger"/>
        <dbType value="String"/>
        <size value="255"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%logger"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@message"/>
        <dbType value="String"/>
        <size value="4000"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%message"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@exception"/>
        <dbType value="String"/>
        <size value="2000"/>
        <layout type="log4net.Layout.ExceptionLayout"/>
      </parameter>
    </appender>
  </log4net>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
</configuration>

4. Services Layer

Image 5

This layer is the bridge between Application Layer and Business Layer.

Application Layer communicates with Business Layer through this layer.

If we want, we can also use this layer as a bridge between Business Layer and Data Layer.

Basically, this layer is a WCF layer.

In this layer, we can add third party services also.

Add one abstract class and make it BaseService which consist all common code and all other service classes should inherit the BaseService.

The Bridge between Application Layer and Business Layer is Services Layer, so let's jump into Services Layer.

Add the reference of Business Layer.

Add New item as WCF Service and name it as AuthenticationService which will result in AuthenticationService.svc and IAuthenticationService.cs.

Declare AuthenticateUser(string userName,string password) in IAuthenticationService.cs interface and implement that interface method in AuthenticationService.svc clss which will access the business layer method.

BaseService.cs:

C#
namespace Services
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Framework.Logging;    

    /// <summary>
    /// BaseService Class.
    /// </summary>
    public abstract class BaseService
    {
        /// <summary>
        /// Used for logging the web service errors.
        /// </summary>
        /// <param name="ex">Exception generated.</param>
        /// <param name="customMessage">Custom message.</param>
        /// <param name="errorLevel">Error level.</param>
        /// <param name="errCode">Error code.</param>
        /// <returns>Fault exception.</returns>
        public System.ServiceModel.FaultException LogWcfError(Exception ex, 
          string customMessage, Framework.Enums.Enums.ErrorLevel errorLevel, string errCode)
        {
            Logger.Log(ex, customMessage, errorLevel);
            return new System.ServiceModel.FaultException(
              new System.ServiceModel.FaultReason(customMessage), 
              new System.ServiceModel.FaultCode(errCode));
        }
    }
}

web.config:

XML
<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,Log4net"/>
  </configSections>
  <log4net>
    <root>
      <level value="DEBUG"/>
      <appender-ref ref="ADONetAppender"/>
    </root>
    <appender name="ADONetAppender" type="log4net.Appender.ADONetAppender">
      <bufferSize value="1"/>
      <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, 
         Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
      <connectionString value="server=(local); uid=uid; pwd=pwd; database=Sample"/>
      <commandText value="INSERT INTO Log ([Date],[Thread],[Level],[Logger],
        [Message],[Exception]) VALUES 
             (@log_date, @thread, @log_level, @logger, @message, @exception)"/>
      <parameter>
        <parameterName value="@log_date"/>
        <dbType value="DateTime"/>
        <layout type="log4net.Layout.RawTimeStampLayout"/>
      </parameter>
      <parameter>
        <parameterName value="@thread"/>
        <dbType value="String"/>
        <size value="255"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%thread"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@log_level"/>
        <dbType value="String"/>
        <size value="50"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%level"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@logger"/>
        <dbType value="String"/>
        <size value="255"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%logger"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@message"/>
        <dbType value="String"/>
        <size value="4000"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%message"/>
        </layout>
      </parameter>
      <parameter>
        <parameterName value="@exception"/>
        <dbType value="String"/>
        <size value="2000"/>
        <layout type="log4net.Layout.ExceptionLayout"/>
      </parameter>
    </appender>
  </log4net>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- To avoid disclosing metadata information, set the value below 
            to false and remove the metadata endpoint above before deployment -->
          <serviceMetadata httpGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, 
            set the value below to true.  Set to false before deployment 
            to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
  <connectionStrings>
    <add name="SampleEntities" 
      connectionString="metadata=res://*/DataModel.csdl|res://*/DataModel.ssdl|
         res://*/DataModel.msl;provider=System.Data.SqlClient;provider connection string=
         &quot;Data Source=(local);Initial Catalog=Sample;User ID=uid;
         Password=pwd;MultipleActiveResultSets=True&quot;" 
      providerName="System.Data.EntityClient" />
  </connectionStrings>
</configuration>

5. Service Proxies Layer

Let's implement our Service Proxies Layer whose advantages we already discussed.

Add reference of Services Layer.

Add service reference and click on Discover, it will show the AuthenticationService so click on OK and it will add the service reference of AuthenticationService in Service Proxies layer.

It will automatically add all the required information like binding, endpoint, etc.

Basically, this contains the services from Services layer.

If we don't use this layer, then suppose anyone made some changes in any service and he forgot to check-in that updated service response, then all other users get an error.

Now if we use this layer, then if anyone made any changes and check-in the same then if particular user wants that change, he/she simply needs to update the proxies and build the solution.

For achieving this, simply add one class library and name it as ServiceProxies.

app.config:

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_IAuthenticateService" />
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:9321/Classes/AuthenticateService.svc"
                binding="basicHttpBinding" 
                bindingConfiguration="BasicHttpBinding_IAuthenticateService"
                contract="AuthenticateService.IAuthenticateService" 
                name="BasicHttpBinding_IAuthenticateService" />
        </client>
    </system.serviceModel>
</configuration>

6. Application Layer

Image 6

This layer contains the presentation logic.

The application consists of a series of forms (pages) with which the user interacts. Each form contains a number of fields that display output from lower layers and collect user input.

Add one class and make it BasePage which will inherit from System.Web.UI.Page and all other pages should inherit the BasePage.

The advantage of having the BasePage is that we don't need to write some configurable items in all the pages like setting of culture, CSRF checking, etc.

Now all the layers are implemented except our Application Layer.

Add references of Framework and Service Proxies layer.

Copy paste the <system.serviceModel> portion from Service Proxies layer's app.config to Application Layer's Web.config to access the implemented service.

Now just access the AuthenticationService's method by using the client object of the same which will in turn gives call to Business Layer's functionality.

We use logging mechanism in Application Layer only as of now for exception logging but if we want to log any informational messages, then we can also do that.

The main logic of doing an exception logging in application layer is that ultimately, we call business logic through services and if any one fails, then it will get caught in application layer's method from which the call has been made.

Also, for logging any WCF errors/exception, we used the same logging mechanism in Services Layer.

C#
namespace ApplicationLayer.BasePage
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Globalization;
    using Framework.Logging;
    using System.Web.UI.WebControls;
    using System.Web.UI;
    using System.IO;

    /// <summary>
    /// BasePage class.
    /// </summary>
    public class BasePage : System.Web.UI.Page
    {
        /// <summary>
        /// OnInit predefined event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            System.Threading.Thread.CurrentThread.CurrentCulture = 
                         new CultureInfo("en-US", true);
        }

        /// <summary>
        /// OnPreLoad predefined event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected override void OnPreLoad(System.EventArgs e)
        {
            base.OnPreLoad(e);
        }

        /// <summary>
        /// OnLoad predefined event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected override void OnLoad(EventArgs e)
        {

        }        

        /// <summary>
        /// LogError used for logging various types of information.
        /// </summary>
        /// <param name="ex">Exception occured.</param>
        /// <param name="customException">Custom exception text.</param>
        /// <param name="level">Error level.</param>
        public void LogError(Exception ex, string customException, 
                    Framework.Enums.Enums.ErrorLevel level)
        {
            Logger.Log(ex, customException, level);
        }
    }
}

References

History

  • 25th September, 2013: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)