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






4.81/5 (80投票s)
本文介绍如何使用 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
,来生成 DAL
。DAL
代码将由 ORM 本身生成。
使用代码
让我们开发一个小巧的 ASP.NET
应用程序,它将使用 N 层架构。我们将为 NorthWind
数据库开发一个小型的员工管理应用程序。(为简化起见,我已删除数据库中的所有其他表以及 Employee 表中的部分列)。该应用程序应该能够对数据库执行基本的 CRUD 操作。
此应用程序的解决方案将包含用于 DAL
和 BLL
的单独项目。数据层将是 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 日:第一个版本