N-Tier Architecture Best Practices, Part 1: 2-Tier Architecture with just a Data

Microsoft .NET

Before I start off on my lengthy post, this article is what sparked my interest. With that linked for you, this will be split into a 5 part blog series each part covering a specific type of n-tier architecture. If there is not a link to the article it has not been published yet.

N-Tier Architecture Best Practices, Part 1: 2-Tier Architecture with just a Data Tierthis article
N-Tier Architecture Best Practices, Part 2: 3-Tier Architecture with interfaces and a Data Tier
N-Tier Architecture Best Practices, Part 3: DLinq / Linq to SQL
N-Tier Architecture Best Practices, Part 4: Entity Framework
N-Tier Architecture Best Practices, Part 5: Unity Framework

Download the Source Code for this Article
N-Tier Architecture (2-Tier)

In most projects you will have to communicate with other objects or a data tier (database engine, file system, or other data source), and passing your objects around can make for some complicated scenarios. One common rut that many developers find themselves in is circular-dependencies.

What is N-Tier Architecture?
N-Tier Archiecture is a term that refers to the number of assemblies, modules, or services that make up a system that allows its different parts to communicate with one another. An example would be a simple system that has some business objects and a database. You may have a module that handles the communication with the data tier, and also stores your business objects, and then maybe a second module that actually handles the processing of the objects such as input and change. This would be a 2-Tier architecture. A 3-Tier architecture would be something like having a module that stores interfaces that defines your business objects, another module that actually implements the business objects, and then a third module that again handles the input and change. N-Tier architecture can get vastly complex, especially in software applications like video games. Imagine a game engine where you have modules that must communicate with one another for handling rendering, player locations, particle effects, game data, network information. You can easily get into a complex system.

What I will cover

  • 2-Tier with just a data tier
  • 3-Tier with common interfaces, and a data tier
  • Briefly explain DLinq (Linq to Sql)
  • Briefly explain Entity Framework (Linq to Entities)
  • Briefly explain Unity Framework (Data Injection Framework)

Technologies we will be using for this article

  • C# 4.0
  • .NET 3.5
  • SQL Server CE

2-Tier architecture with a Data Tier Project Structure (image to the left)
We are going to create a solution with two projects. The first, 'NTier.Data' will act as the Data Tier. Thie purpose of this is to act as the dependency for all other aspects of the software. It will store the database objects, as well as the class that handles communication between the objects and the database. The second will be 'NTier.Presentation', which acts as the executable that is actually used by the client as the UI, and in our case is a very simple Console Application.

The database
The database is just a SqlCe database with an Employees table. Our employee object will match the database schema perfectly in terms of its properties (ie. FirstName, LastName, Email, etc).

The code
There are only three class files, here is the code of all three.

//-----------------------------------------------------------------------------
// <copyright file="Employee.cs" company="DCOM Productions">
//     Copyright (c) DCOM Productions.  All rights reserved.
// </copyright>
//-----------------------------------------------------------------------------

namespace NTier.Data.Objects {
    using System;

    /// <summary>
    /// Represents a Employee in the data tier
    /// </summary>
    public class Employee {
        /// <summary>
        /// Gets the Employee's ID
        /// </summary>
        public int ID {
            get;
            internal set;
        }

        /// <summary>
        /// Gets or sets the Employee's email address
        /// </summary>
        public string Email {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Employee's first name
        /// </summary>
        public string FirstName {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Employee's last name
        /// </summary>
        public string LastName {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Employee's payrate
        /// </summary>
        public float Payrate {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the Employee's title
        /// </summary>
        public string Title {
            get;
            set;
        }

        /// <summary>
        /// Overrides base.ToString()
        /// </summary>
        public override string ToString() {
            return string.Format("{0} {1}, {2}", FirstName, LastName, Title);
        }
    }
}
//-----------------------------------------------------------------------------
// <copyright file="DataTier.cs" company="DCOM Productions">
//     Copyright (c) DCOM Productions.  All rights reserved.
// </copyright>
//-----------------------------------------------------------------------------

namespace NTier.Data {
    using System;
    using System.Collections.Generic;
    using System.Data.SqlServerCe;
    using NTier.Data.Objects;

    /// <summary>
    /// Interactive class between business logic and the data tier
    /// </summary>
    public static class DataTier {

        private static string m_ConnectionString = @"Data Source=.\Northwind.sdf";
        /// <summary>
        /// Gets the connection string for SQL Server CE
        /// </summary>
        public static string ConnectionString {
            get {
                return m_ConnectionString;
            }
        }

        /// <summary>
        /// Adds the specified employee to the data tier
        /// </summary>
        public static bool AddEmployee(NTier.Data.Objects.Employee employee) {
            using (SqlCeConnection connection = new SqlCeConnection(ConnectionString))
            using (SqlCeCommand command = new SqlCeCommand(Properties.Resources.InsertCommandText, connection)) {
                command.Parameters.AddWithValue("@Email", employee.Email);
                command.Parameters.AddWithValue("@FirstName", employee.FirstName);
                command.Parameters.AddWithValue("@LastName", employee.LastName);
                command.Parameters.AddWithValue("@Payrate", employee.Payrate);
                command.Parameters.AddWithValue("@Title", employee.Title);
                try {
                    connection.Open();
                    return command.ExecuteNonQuery() == 1;
                }
                catch (System.Data.SqlServerCe.SqlCeException) {
                    return false;
                }
            }
        }

        /// <summary>
        /// Returns a collection of all the employee's in the data tier
        /// </summary>
        public static IEnumerable<NTier.Data.Objects.Employee> GetEmployees() {
            using (SqlCeConnection connection = new SqlCeConnection(ConnectionString))
            using (SqlCeCommand command = new SqlCeCommand(Properties.Resources.SelectCommandText, connection)) {
                try {
                    connection.Open();
                }
                catch (System.Data.SqlServerCe.SqlCeException) {
                    yield break;
                }
                using (SqlCeDataReader reader = command.ExecuteReader()) {
                    while (reader.Read()) {
                        Employee employee = new Employee();
                        employee.ID = (int)reader["ID"];
                        employee.Email = (string)reader["Email"];
                        employee.FirstName = (string)reader["FirstName"];
                        employee.LastName = (string)reader["LastName"];
                        employee.Payrate = (float)(double)reader["Payrate"];
                        employee.Title = (string)reader["Title"];
                        yield return employee;
                    }
                }
            }
        }

        /// <summary>
        /// Removes the specified employee from the data tier
        /// </summary>
        public static bool RemoveEmployee(NTier.Data.Objects.Employee employee) {
            using (SqlCeConnection connection = new SqlCeConnection(ConnectionString))
            using (SqlCeCommand command = new SqlCeCommand(Properties.Resources.DeleteCommandText, connection)) {
                command.Parameters.AddWithValue("@ID", employee.ID);
                try {
                    connection.Open();
                    return command.ExecuteNonQuery() == 1;
                }
                catch (System.Data.SqlServerCe.SqlCeException) {
                    return false;
                }
            }
        }

        /// <summary>
        /// Updates the specified employee's information in the data tier
        /// </summary>
        public static bool UpdateEmployee(NTier.Data.Objects.Employee employee) {
            using (SqlCeConnection connection = new SqlCeConnection(ConnectionString))
            using (SqlCeCommand command = new SqlCeCommand(Properties.Resources.UpdateCommandText, connection)) {
                command.Parameters.AddWithValue("@Email", employee.Email);
                command.Parameters.AddWithValue("@FirstName", employee.FirstName);
                command.Parameters.AddWithValue("@LastName", employee.LastName);
                command.Parameters.AddWithValue("@Payrate", employee.Payrate);
                command.Parameters.AddWithValue("@Title", employee.Title);
                command.Parameters.AddWithValue("@ID", employee.ID);
                try {
                    connection.Open();
                    return command.ExecuteNonQuery() == 1;
                }
                catch (System.Data.SqlServerCe.SqlCeException) {
                    return false;
                }
            }
        }
    }
}
//-----------------------------------------------------------------------------
// <copyright file="Program.cs" company="DCOM Productions">
//     Copyright (c) DCOM Productions.  All rights reserved.
// </copyright>
//-----------------------------------------------------------------------------

namespace NTier.Presentation {
    using System;
    using NTier.Data.Objects;
    using NTier.Data;
    using System.Collections.Generic;

    internal static class Program {
        /// <summary>
        /// Entry Point
        /// </summary>
        public static void Main() {
            Employee employee = new Employee();
            employee.Email = "danderson@dcomproductions.com";
            employee.FirstName = "David";
            employee.LastName = "Anderson";
            employee.Payrate = 35F;
            employee.Title = "President";
            DataTier.AddEmployee(employee);
            foreach (Employee item in DataTier.GetEmployees()) {
                Console.WriteLine(item.ToString());
                Console.ReadKey(true);
            }
            Console.Write("Press any key to exit...");
            Console.ReadKey(true);
        }
    }
}

Something to note is lines 33, 54, 81, 98 of Employee.cs. I saved the query texts in the project's resource file to keep the code tidy.

Now you can write Program.cs to act however you want, and play with the code. In my example all I am doing is adding an employee to the table with my name, then enumerating the employees and listing them to the console window. Since each time you run my example it adds an employee with the same information and never removes it, you will have x(f) employees where f is the number of times the application has been launched, in the database with the same information.

What advantage does 2-Tier architecture serve?
Alright, in this 2-tier architecture, the advantage is its simple. That's all there is to it. For each data tier object (ie. something that would need to be committed into a data source), you can simply create the object with its properties, and write the methods in your data tier class to do the work. The simplicity of being able to just pass your object is what makes it desireable. Also note that you do not have any complex dependencies between assemblies, thus you never have to worry about a circular dependency, and its extremely easy to note where everything is at in your project.

In large projects, your Objects folder would get rather large with data tier objects. Which is fine. The disadvantage of a 2-tier architecture is generally as your application becomes exceedingly complex, the simple structure of the project no longer becomes viable because its just not that flexible. Further down the road if you wanted to refactor your objects out of the data tier assembly into a 3-tier architecture, your data tier breaks because you can no longer pass the reference of the object. Thus bringing us into the next part of the blog series, 3-tier architecture.

When to use 2-Tier Architecture
When your requirements are simple and you have a minimal number of assemblies that must reference your business objects. More so, it is actually easier to explain when not to use 2-tier architecture. I've compiled a short list of the most common scenarios you should not use 2-tier architecture for.

  • An assembly needs to reference your business objects, but not have access to the data tier
  • You want to extend your business object with additional functionality, but not expose those new features to the data tier

Those are actually the only two reasons that come to mind immediately. If you have some other reasons you can think of that are good, throw a comment down and I will add it to the list. At any rate, the point is that 2-tier is easy to implement, and pretty easy to manage with little effort. The huge downside is once it becomes large, it will be hard to refactor later on. The next blog in the series will be on 3-tier architecture which I will hope to start writing by Monday.

Leave a Comment