65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 N 层架构创建 ASP.NET 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (80投票s)

2012年8月13日

CPOL

6分钟阅读

viewsIcon

455626

downloadIcon

27801

本文介绍如何使用 N 层架构构建 ASP.NET 应用程序。

介绍 

本文介绍如何使用 N 层架构构建 ASP.NET 应用程序。N 层架构的好处在于,所有具有专用功能的模块都可以相互独立。更改一个层不会影响其他层,即使某个层出现故障,也不会出现单点故障。

背景 

典型的 N 层应用程序通常有 4 个层。最底层是数据层,包含表和存储过程、标量函数、表值函数。数据层通常就是数据库引擎本身。在本例中,我们将使用 SqlServer 作为数据层。

在数据层之上,我们有数据访问层 (DAL)。该层负责处理与数据库相关的任务,即仅数据访问。数据访问层被创建为一个单独的解决方案,这样对 DAL 的更改只需要重新编译 DAL,而不需要重新编译整个网站。将此层设为单独解决方案的好处是,如果数据库引擎发生更改,我们只需要更改 DAL,而网站的其他区域则无需更改和重新编译。此外,此解决方案外部的更改也不会要求重新编译 DAL

在 DAL 之上,我们有 业务逻辑层 (BLL)BLL 包含应用程序所需的所有计算和业务规则验证。它也是一个单独的解决方案,原因在于,如果业务规则或计算发生更改,我们只需要重新编译 BLL,应用程序的其他层将不受影响。

最后,在 BLL 之上是我们的表示层。对于 ASP.NET Web Forms 应用程序,表示层包括所有窗体(aspx 页面及其代码隐藏文件)以及 App_Code 文件夹中的类。表示层负责接收用户输入、向用户显示数据以及执行主要的数据输入验证。

注意: 输入数据的过滤和验证通常在表示层(客户端和服务器端)进行。业务规则验证将在 BLL 中进行。

为了可视化上述架构,请看:

注意: 本文中的数据访问层是用经典的 ADO.NET 编写的,因此 DAL 中的代码量有点大。如今,建议使用 ORM,如 Entity Framework,来生成 DALDAL 代码将由 ORM 本身生成。

使用代码

让我们开发一个小巧的 ASP.NET 应用程序,它将使用 N 层架构。我们将为 NorthWind 数据库开发一个小型的员工管理应用程序。(为简化起见,我已删除数据库中的所有其他表以及 Employee 表中的部分列)。该应用程序应该能够对数据库执行基本的 CRUD 操作。

此应用程序的解决方案将包含用于 DALBLL 的单独项目。数据层将是 SqlServer。表示层是一个运行在这些项目之上的 ASP.NET 网站。

数据层

本例中的数据层只包含一个名为 Employee 的表。数据层还包含 Employee 表所有基本操作的存储过程。所以,让我们看看数据层中的表和所有存储过程。


现在,我们将创建一组存储过程来执行 Employee 表上的操作。

--1. Procedure to add a new employee 
CREATE PROCEDURE dbo.AddNewEmployee
	(		
		@LastName	nvarchar(20),
		@FirstName	nvarchar(10),
		@Title		nvarchar(30),	
		@Address	nvarchar(60),
		@City		nvarchar(15),	
		@Region		nvarchar(15),	
		@PostalCode	nvarchar(10),
		@Country	nvarchar(15),
		@Extension	nvarchar(4)
	)
AS
	insert into Employees
	(LastName, FirstName, Title, Address, City, Region, PostalCode, Country, Extension)
	values
	(@LastName, @FirstName, @Title, @Address, @City, @Region, @PostalCode, @Country, @Extension)
	RETURN

--2. Procedure to delete an employee 
CREATE PROCEDURE dbo.DeleteEmployee
	(
	@empId int
	)
	
AS
	delete from Employees where EmployeeID = @empId
	RETURN

--3. Procedure to add get an employee details 
CREATE PROCEDURE dbo.GetEmployeeDetails
	(
	@empId int
	)
	
AS
	Select * from Employees where EmployeeID = @empId
	RETURN

--4. Procedure to get all the employees in the table 
CREATE PROCEDURE dbo.GetEmployeeList	
AS
	Select * from Employees
	
	RETURN

--5. Procedure to update an employee details
CREATE PROCEDURE dbo.UpdateEmployee
	(
		@EmployeeID	int,
		@LastName	nvarchar(20),
		@FirstName	nvarchar(10),
		@Title		nvarchar(30),	
		@Address	nvarchar(60),
		@City		nvarchar(15),	
		@Region		nvarchar(15),	
		@PostalCode	nvarchar(10),
		@Country	nvarchar(15),
		@Extension	nvarchar(4)
	)
AS
	update Employees
	set	
		LastName = @LastName, 
		FirstName = @FirstName, 
		Title = @Title, 
		Address = @Address, 
		City = @City, 
		Region = @Region, 
		PostalCode = @PostalCode, 
		Country = @Country, 
		Extension = @Extension
	where 
		EmployeeID = @EmployeeID
	RETURN

现在,我们的数据层已经准备就绪。

数据访问层

现在,我们将继续为我们的应用程序创建一个数据访问层。数据访问层将包含 2 种主要类型的类:一组代表表实体的类。以及用于在数据库上执行 CRUD 操作的类。


上面图中的 Employee 类是代表 Employee 表的实体。创建此类的目的是使 DAL 之上的层可以使用此类来执行 Employee 表上的操作,而无需担心与表架构相关的细节。

public class Employee
{
	int employeeID;
	string lastName;    //  should be (20)  chars only
	string firstName;   //	should be (10)	chars only
	string title;       //	should be (30)	chars only
	string address;     //	should be (60)	chars only
	string city;        //	should be (15)	chars only
	string region;      //	should be (15)	chars only
	string postalCode;  //	should be (10)	chars only
	string country;     //	should be (15)	chars only
	string extension;   //	should be (4)	chars only

	public int EmployeeID
	{
		get
		{
			return employeeID;
		}
		set
		{
			employeeID = value;
		}
	}

	public string LastName
	{
		get
		{
			return lastName;
		}
		set
		{
			lastName  = value;
		}
	}

	public string FirstName
	{
		get
		{
			return firstName;
		}
		set
		{
			firstName = value;
		}
	}

	public string Title
	{
		get
		{
			return title;
		}
		set
		{
			title = value;
		}
	}

	public string Address
	{
		get
		{
			return address;
		}
		set
		{
			address = value;
		}
	}

	public string City
	{
		get
		{
			return city;
		}
		set
		{
			city = value;
		}
	}

	public string Region
	{
		get
		{
			return region;
		}
		set
		{
			region = value;
		}
	}

	public string PostalCode
	{
		get
		{
			return postalCode;
		}
		set
		{
			postalCode = value;
		}
	}

	public string Country
	{
		get
		{
			return country;
		}
		set
		{
			country = value;
		}
	}

	public string Extension
	{
		get
		{
			return extension;
		}
		set
		{
			extension = value;
		}
	}
}

EmployeeDBAccess 类公开了用于在 Employee 表上执行 CRUD 操作的方法。

public class EmployeeDBAccess
{
	public bool AddNewEmployee(Employee employee)
	{
		SqlParameter[] parameters = new SqlParameter[]
		{                
			new SqlParameter("@LastName", employee.LastName),
			new SqlParameter("@FirstName", employee.FirstName),
			new SqlParameter("@Title", employee.Title),
			new SqlParameter("@Address", employee.Address),
			new SqlParameter("@City", employee.City),
			new SqlParameter("@Region", employee.Region),
			new SqlParameter("@PostalCode", employee.PostalCode),
			new SqlParameter("@Country", employee.Country),
			new SqlParameter("@Extension", employee.Extension)
		};

		return SqlDBHelper.ExecuteNonQuery("AddNewEmployee", CommandType.StoredProcedure, parameters);
	}

	public bool UpdateEmployee(Employee employee)
	{
		SqlParameter[] parameters = new SqlParameter[]
		{
			new SqlParameter("@EmployeeID", employee.EmployeeID),
			new SqlParameter("@LastName", employee.LastName),
			new SqlParameter("@FirstName", employee.FirstName),
			new SqlParameter("@Title", employee.Title),
			new SqlParameter("@Address", employee.Address),
			new SqlParameter("@City", employee.City),
			new SqlParameter("@Region", employee.Region),
			new SqlParameter("@PostalCode", employee.PostalCode),
			new SqlParameter("@Country", employee.Country),
			new SqlParameter("@Extension", employee.Extension)
		};

		return SqlDBHelper.ExecuteNonQuery("UpdateEmployee", CommandType.StoredProcedure, parameters);
	}

	public bool DeleteEmployee(int empID)
	{
		SqlParameter[] parameters = new SqlParameter[]
		{
			new SqlParameter("@empId", empID)
		};

		return SqlDBHelper.ExecuteNonQuery("DeleteEmployee", CommandType.StoredProcedure, parameters);
	}

	public Employee GetEmployeeDetails(int empID)
	{
		Employee employee = null;

		SqlParameter[] parameters = new SqlParameter[]
		{
			new SqlParameter("@empId", empID)
		};
		//Lets get the list of all employees in a datataable
		using (DataTable table = SqlDBHelper.ExecuteParamerizedSelectCommand("GetEmployeeDetails", CommandType.StoredProcedure, parameters))
		{
			//check if any record exist or not
			if (table.Rows.Count == 1)
			{
				DataRow row = table.Rows[0];

				//Lets go ahead and create the list of employees
				employee = new Employee();

				//Now lets populate the employee details into the list of employees                                           
				employee.EmployeeID = Convert.ToInt32(row["EmployeeID"]);
				employee.LastName = row["LastName"].ToString();
				employee.FirstName = row["FirstName"].ToString();
				employee.Title = row["Title"].ToString();
				employee.Address = row["Address"].ToString();
				employee.City = row["City"].ToString();
				employee.Region = row["Region"].ToString();
				employee.PostalCode = row["PostalCode"].ToString();
				employee.Country = row["Country"].ToString();
				employee.Extension = row["Extension"].ToString();
			}
		}

		return employee;
	}

	public List<employee> GetEmployeeList()
	{
		List<employee> listEmployees = null;

		//Lets get the list of all employees in a datataable
		using (DataTable table = SqlDBHelper.ExecuteSelectCommand("GetEmployeeList", CommandType.StoredProcedure))
		{
			//check if any record exist or not
			if (table.Rows.Count > 0)
			{
				//Lets go ahead and create the list of employees
				listEmployees = new List<employee>();

				//Now lets populate the employee details into the list of employees
				foreach (DataRow row in table.Rows)
				{
					Employee employee = new Employee();
					employee.EmployeeID = Convert.ToInt32(row["EmployeeID"]);
					employee.LastName = row["LastName"].ToString();
					employee.FirstName = row["FirstName"].ToString();
					employee.Title = row["Title"].ToString();
					employee.Address = row["Address"].ToString();
					employee.City = row["City"].ToString();
					employee.Region = row["Region"].ToString();
					employee.PostalCode = row["PostalCode"].ToString();
					employee.Country = row["Country"].ToString();
					employee.Extension = row["Extension"].ToString();

					listEmployees.Add(employee);
				}
			}
		}

		return listEmployees;
	}        
}
</employee></employee></employee>

SqlDbHelper 类是 ADO.NET 函数的包装类,为 DAL 的其余部分提供了一个更简单的接口。

class SqlDBHelper
{
	const string CONNECTION_STRING = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\NORTHWND.MDF;Integrated Security=True;User Instance=True";

	// This function will be used to execute R(CRUD) operation of parameterless commands
	internal static DataTable ExecuteSelectCommand(string CommandName, CommandType cmdType)
	{
		DataTable table = null;
		using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
		{
			using (SqlCommand cmd = con.CreateCommand())
			{
				cmd.CommandType = cmdType;
				cmd.CommandText = CommandName;

				try
				{
					if (con.State != ConnectionState.Open)
					{
						con.Open();
					}

					using (SqlDataAdapter da = new SqlDataAdapter(cmd))
					{
						table = new DataTable();
						da.Fill(table);
					}
				}
				catch
				{
					throw;
				}                   
			}
		}

		return table;
	}

	// This function will be used to execute R(CRUD) operation of parameterized commands
	internal static DataTable ExecuteParamerizedSelectCommand(string CommandName, CommandType cmdType, SqlParameter[] param)
	{
		DataTable table = new DataTable();

		using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
		{
			using (SqlCommand cmd = con.CreateCommand())
			{
				cmd.CommandType = cmdType;
				cmd.CommandText = CommandName;
				cmd.Parameters.AddRange(param);

				try
				{
					if (con.State != ConnectionState.Open)
					{
						con.Open();
					}

					using (SqlDataAdapter da = new SqlDataAdapter(cmd))
					{
						da.Fill(table);
					}
				}
				catch
				{
					throw;
				}                    
			}
		}

		return table;
	}

	// This function will be used to execute CUD(CRUD) operation of parameterized commands
	internal static bool ExecuteNonQuery(string CommandName, CommandType cmdType, SqlParameter[] pars)
	{
		int result = 0;

		using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
		{
			using (SqlCommand cmd = con.CreateCommand())
			{
				cmd.CommandType = cmdType;
				cmd.CommandText = CommandName;
				cmd.Parameters.AddRange(pars);                    

				try
				{
					if (con.State != ConnectionState.Open)
					{
						con.Open();
					}

					result = cmd.ExecuteNonQuery();
				}
				catch
				{
					throw;
				}                   
			}
		}

		return (result > 0);        
	}        
}

注意: 如果我们使用任何 ORM(对象关系映射器),则无需编写 DAL。ORM 将生成所有 DAL 代码。Entity Framework 是一个可用的最佳 ORM 之一。此 DAL 可以简单地替换为包含 Entity Framework 生成的实体和上下文的类库。

业务逻辑层

业务逻辑层将引用 DAL,主要执行业务规则验证和特定于业务逻辑的计算。在本例中,我将编写一个简单的 BLL,它将管理 DAL 和表示层之间的 I/O。在实际应用程序中,BLL 将包含更多的逻辑和代码。

public class EmployeeHandler
{
	// Handle to the Employee DBAccess class
	EmployeeDBAccess employeeDb = null;

	public EmployeeHandler()
	{
		employeeDb = new EmployeeDBAccess();
	}

	// This fuction does not contain any business logic, it simply returns the 
	// list of employees, we can put some logic here if needed
	public List<employee> GetEmployeeList()
	{
		return employeeDb.GetEmployeeList();
	}

	// This fuction does not contain any business logic, it simply returns the 
	// list of employees, we can put some logic here if needed
	public bool UpdateEmployee(Employee employee)
	{
		return employeeDb.UpdateEmployee(employee);
	}

	// This fuction does not contain any business logic, it simply returns the 
	// list of employees, we can put some logic here if needed
	public Employee GetEmployeeDetails(int empID)
	{
		return employeeDb.GetEmployeeDetails(empID);
	}

	// This fuction does not contain any business logic, it simply returns the 
	// list of employees, we can put some logic here if needed
	public bool DeleteEmployee(int empID)
	{
		return employeeDb.DeleteEmployee(empID);
	}

	// This fuction does not contain any business logic, it simply returns the 
	// list of employees, we can put some logic here if needed
	public bool AddNewEmployee(Employee employee)
	{
		return employeeDb.AddNewEmployee(employee);
	}
}

表示层

表示层现在只包含一组页面和代码隐藏文件,它将使用 BLL 和 Employee 类来执行所有操作。添加操作可以作为如何使用 BLL 执行操作的一个例子。

Employee emp = new Employee();

emp.LastName = txtLName.Text;
emp.FirstName = txtFName.Text;
emp.Address = txtAddress.Text;
emp.City = txtCity.Text;
emp.Country = txtCountry.Text;
emp.Region = txtRegion.Text;
emp.PostalCode = txtCode.Text;
emp.Extension = txtExtension.Text;
emp.Title = txtTitle.Text;

EmployeeHandler empHandler = new EmployeeHandler();

if (empHandler.AddNewEmployee(emp) == true)
{
	//Successfully added a new employee in the database
	Response.Redirect("Default.aspx");
}

注意: 所有 CRUD 操作都已实现。有关所有详细信息,请参阅示例代码。运行应用程序时,我们可以看到所有 EDIT/UPDATE、DELETE 和 ADD 操作都在运行。

看点

我创建了这个小型应用程序来演示使用 N 层架构进行应用程序开发。该演示应用程序旨在展示三层架构背后的基本思想。从完成的角度来看,此示例仍缺少许多内容。表示层中的客户端验证和服务器端验证,BLL 中的业务规则验证和计算是其中一些缺失的内容。

由于本文的重点是讨论如何将 N 层架构实际应用到代码中,我认为这篇文章可能提供了一些有用的信息。我希望它具有启发性。

[更新] 注意: 在本文中,我在表示层中重用了 Employee 模型。此模型定义在数据访问层中。因此,表示层必须引用数据访问层。这在实际场景中并非理想(如许多以下评论中所指出的)。理想的解决方案是为 Employee 创建两个不同的模型。当前定义在数据访问层中的模型可以称为数据模型,业务逻辑层可以创建一个称为领域模型的 Employee 模型。然后,业务逻辑层将包含将数据模型映射到领域模型以及反向映射的代码。此映射可以手动完成,也可以使用 AutoMapper 等工具来执行。通过此更改,表示层无需引用数据访问层,但可以引用业务逻辑层并从中使用的 Employee 领域模型。

在本文中,N 层架构特指以数据为中心的 N 层,而不是以域为中心的。如果我们希望以域为中心的 N 层架构来设计应用程序,那么我们需要遵循一种不同的方式来组织我们的层。但这也许是一个值得单独讨论的主题,但我想在本文中指出以域为中心的 N 层架构的可能性。

历史  

  • 2013 年 11 月 12 日:添加了解释性文本以说明设计缺陷。
  • 2012 年 8 月 13 日:第一个版本 

© . All rights reserved.