在 ASP.NET MVC 中显示详细记录(不使用 Entity Framework)
ASP.NET MVC 框架提供了一个强大的模式来构建可维护的 Web 应用程序。Entity Framework (EF) 作为微软官方的对象关系映射器(ORM),极大地简化了数据访问层的工作。然而,在某些场景下,开发者可能会选择不使用 EF,例如:
- 需要对数据库查询进行极致的性能优化和精细控制。
- 在遗留系统中,已经存在成熟稳定的 ADO.NET 数据访问代码。
- 团队对纯 SQL 的熟悉程度远超 ORM。
- 项目要求避免 ORM 带来的额外开销或抽象泄漏。
本博客将详细讲解如何在不依赖 Entity Framework 的情况下,实现 MVC 架构中一个经典场景:显示某条记录的详细信息。我们将从零开始,构建一个完整的数据流,涵盖模型、数据访问层、控制器和视图。
目录#
架构概述#
在开始编码之前,我们先理解一下整体的数据流。我们的目标是:当用户访问 /Product/Details/5 时,页面显示 ID 为 5 的产品的详细信息。
流程如下:
- 路由:ASP.NET MVC 路由系统将请求映射到
ProductController的Details动作方法,并将id=5作为参数传入。 - 控制器:
ProductController接收请求,调用数据访问层(如ProductRepository)的方法,传入 ID 5。 - 数据访问层:
ProductRepository使用 ADO.NET(如SqlConnection和SqlCommand)执行 SQL 查询,从数据库中获取数据。 - 模型映射:将数据库返回的结果(通常是
SqlDataReader或DataTable)映射到强类型的Product模型对象。 - 返回视图:控制器将填充好的
Product对象传递给Details视图。 - 视图渲染:
Details视图(Razor)接收Product模型,并将其属性以 HTML 形式呈现给用户。
下图清晰地展示了这一过程:
sequenceDiagram
participant U as 用户 (浏览器)
participant R as 路由
participant C as ProductController
participant D as 数据访问层 (Repository)
participant DB as 数据库
participant V as Details 视图
U->>R: 请求 /Product/Details/5
R->>C: 调用 Details(5)
C->>D: 调用 GetProductById(5)
D->>DB: 执行 SQL SELECT查询
DB->>D: 返回数据行
D->>C: 返回 Product 对象
C->>V: 传递 Product 模型
V->>U: 渲染 HTML 详情页接下来,我们一步步实现这个流程。
第一步:创建模型#
模型是表示我们业务数据的核心类。它应该是简单的 POCO(Plain Old CLR Object)类,不包含任何数据访问逻辑。
我们创建一个 Product 类来表示产品。
Models/Product.cs
namespace WithoutEF.MvcDemo.Models
{
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public DateTime ReleaseDate { get; set; }
public bool IsActive { get; set; }
}
}最佳实践:
- 将模型放在
Models文件夹中。 - 使用有意义的属性名,并与数据库表中的列名对应(映射工作将在数据访问层完成)。
- 包含数据注解(如
[Required],[StringLength])可以进行模型验证,这在与表单一起使用时非常有用,但简单的显示场景不是必须的。
第二步:建立数据访问层#
这是与 Entity Framework 方案最主要的区别所在。我们将使用原始的 ADO.NET 来连接和查询数据库。
2.1 核心 Helper 类#
为了遵守 DRY(Don't Repeat Yourself)原则,我们创建一个 helper 类来管理数据库连接字符串和处理一些常见的 ADO.NET 样板代码。
DAL/DatabaseHelper.cs
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
namespace WithoutEF.MvcDemo.DAL
{
public static class DatabaseHelper
{
// 从 Web.config 中获取连接字符串
private static readonly string ConnectionString = ConfigurationManager.ConnectionStrings["YourConnectionStringName"].ConnectionString;
// 创建并打开一个数据库连接
public static SqlConnection GetConnection()
{
var connection = new SqlConnection(ConnectionString);
connection.Open();
return connection;
}
// 一个通用的方法,用于执行返回单个值的查询
public static object ExecuteScalar(string query, params SqlParameter[] parameters)
{
using (var connection = GetConnection())
using (var command = new SqlCommand(query, connection))
{
command.Parameters.AddRange(parameters);
return command.ExecuteScalar();
}
}
// 注意:更复杂的方法如 GetDataReader 或 GetDataTable 可以在需要时添加,
// 但 Repository 应该负责控制连接的生命周期。
}
}请确保在 Web.config 文件中配置了名为 "YourConnectionStringName" 的连接字符串。
2.2 实现 Repository 模式#
Repository 模式抽象了数据访问逻辑,使控制器不依赖于特定的数据访问技术(ADO.NET, EF 等)。这提高了代码的可测试性和可维护性。
我们为 Product 模型创建一个 Repository 接口和实现。
DAL/IProductRepository.cs(接口)
using WithoutEF.MvcDemo.Models;
using System.Collections.Generic;
namespace WithoutEF.MvcDemo.DAL
{
public interface IProductRepository
{
Product GetProductById(int productId);
IEnumerable<Product> GetAllProducts();
// 可以在此添加 Create, Update, Delete 等方法
}
}DAL/ProductRepository.cs(实现)
using WithoutEF.MvcDemo.Models;
using System.Collections.Generic;
using System.Data.SqlClient;
namespace WithoutEF.MvcDemo.DAL
{
public class ProductRepository : IProductRepository
{
public Product GetProductById(int productId)
{
Product product = null;
// 使用 using 语句确保连接和命令对象被正确释放
using (var connection = DatabaseHelper.GetConnection())
{
string query = @"
SELECT ProductId, ProductName, Description, Price, ReleaseDate, IsActive
FROM Products
WHERE ProductId = @ProductId";
var command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@ProductId", productId);
using (var reader = command.ExecuteReader())
{
// 如果查询到记录
if (reader.Read())
{
product = MapReaderToProduct(reader);
}
}
}
return product; // 如果没找到,返回 null
}
public IEnumerable<Product> GetAllProducts()
{
var products = new List<Product>();
using (var connection = DatabaseHelper.GetConnection())
{
string query = "SELECT ProductId, ProductName, Description, Price, ReleaseDate, IsActive FROM Products";
var command = new SqlCommand(query, connection);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(MapReaderToProduct(reader));
}
}
}
return products;
}
// 辅助方法:将 SqlDataReader 的一行数据映射到 Product 对象
private Product MapReaderToProduct(SqlDataReader reader)
{
// 重要:处理 DBNull.Value
return new Product
{
ProductId = (int)reader["ProductId"],
ProductName = reader["ProductName"] as string ?? string.Empty,
Description = reader["Description"] as string ?? string.Empty, // 如果数据库允许 NULL,这样处理更安全
Price = (decimal)reader["Price"],
ReleaseDate = (DateTime)reader["ReleaseDate"],
IsActive = (bool)reader["IsActive"]
};
}
}
}关键点说明:
- 连接管理:
using语句是关键,它能确保SqlConnection和SqlDataReader在使用完毕后被立即关闭和销毁,避免资源泄漏。 - 参数化查询:使用
@ProductId这样的参数,而不是拼接字符串,这是防止 SQL 注入攻击的最基本且必要的安全措施。 - 空值处理:在
MapReaderToProduct方法中,使用as string ?? string.Empty来安全地处理数据库中的NULL值。对于值类型(如int,DateTime),如果数据库允许NULL,则需要使用reader["ColumnName"] as int?或reader.IsDBNull(reader.GetOrdinal("ColumnName"))进行检查。 - 映射:手动将
DataReader映射到模型对象虽然有些繁琐,但提供了最大的灵活性。
第三步:创建控制器#
控制器是模型和视图之间的协调者。
Controllers/ProductController.cs
using System.Web.Mvc;
using WithoutEF.MvcDemo.Models;
using WithoutEF.MvcDemo.DAL;
namespace WithoutEF.MvcDemo.Controllers
{
public class ProductController : Controller
{
// 依赖注入是更好的方式,这里为了简单直接 new
private readonly IProductRepository _productRepository = new ProductRepository();
// GET: Product/Details/5
public ActionResult Details(int id)
{
// 1. 通过 Repository 获取产品
Product product = _productRepository.GetProductById(id);
// 2. 处理未找到产品的情况
if (product == null)
{
// 返回 404 Not Found 错误页面
return HttpNotFound();
}
// 3. 将产品模型传递给视图
return View(product);
}
}
}最佳实践:
- 依赖注入:在上面的代码中,我们在控制器内部直接实例化了
ProductRepository。更好的做法是使用依赖注入容器(如 ASP.NET Core 内置的 IoC 或 Autofac、Ninject 等),通过构造函数注入IProductRepository。这使得控制器更容易测试。 - 错误处理:一定要处理像
id对应的记录不存在这样的情况。返回HttpNotFound()是标准的做法。
第四步:构建视图#
视图负责将模型呈现为 HTML。
Views/Product/Details.cshtml
@model WithoutEF.MvcDemo.Models.Product
@{
ViewBag.Title = "产品详情";
}
<h2>产品详情</h2>
<div>
<hr />
<dl class="dl-horizontal"> <!-- 使用 Bootstrap 的样式 -->
<dt>@Html.DisplayNameFor(model => model.ProductId)</dt>
<dd>@Html.DisplayFor(model => model.ProductId)</dd>
<dt>@Html.DisplayNameFor(model => model.ProductName)</dt>
<dd>@Html.DisplayFor(model => model.ProductName)</dd>
<dt>@Html.DisplayNameFor(model => model.Description)</dt>
<dd>@Html.DisplayFor(model => model.Description)</dd>
<dt>@Html.DisplayNameFor(model => model.Price)</dt>
<dd>@Html.DisplayFor(model => model.Price)</dd>
<dt>@Html.DisplayNameFor(model => model.ReleaseDate)</dt>
<dd>@Html.DisplayFor(model => model.ReleaseDate)</dd>
<dt>@Html.DisplayNameFor(model => model.IsActive)</dt>
<dd>@Html.DisplayFor(model => model.IsActive)</dd>
</dl>
</div>
<p>
@Html.ActionLink("返回产品列表", "Index") <!-- 假设你有一个 Index 动作 -->
</p>说明:
@model指令指定了视图期望的强类型模型。Html.DisplayNameFor()用于显示模型的属性名称(可能会使用[Display]数据注解)。Html.DisplayFor()是用于显示模型属性值的辅助方法。它会根据数据类型选择合适的显示模板(例如,对DateTime类型进行格式化)。
常见实践与最佳实践#
- 使用依赖注入:将
IProductRepository注册到 DI 容器中,并在控制器的构造函数中注入它。这解耦了控制器和具体的数据访问实现。 - 使用 ORM 风格的微型工具:如果你觉得纯 ADO.NET 映射太麻烦,但又不想用完整的 EF,可以考虑使用 Dapper。Dapper 是一个轻量级的 ORM,它扩展了
IDbConnection接口,可以轻松地将查询结果映射到对象,性能极高。// 使用 Dapper 重写 Repository 中的方法 public Product GetProductById(int productId) { using (var connection = DatabaseHelper.GetConnection()) { string query = "SELECT * FROM Products WHERE ProductId = @ProductId"; return connection.QuerySingleOrDefault<Product>(query, new { ProductId = productId }); } } - 集中化异常处理:在
Global.asax中或使用过滤器(Filter)实现全局异常处理,记录日志并向用户显示友好的错误页面,而不是暴露底层数据库错误。 - 日志记录:在数据访问层记录重要的操作(如错误信息)和慢查询,以便于调试和监控。
- 异步编程:对于 I/O 密集型的数据库操作,使用异步方法(
async/await)可以提高应用程序的吞吐量。ADO.NET 提供了ExecuteReaderAsync等异步方法。
总结#
通过本博客的步骤,我们成功地在不使用 Entity Framework 的情况下,构建了一个完整的 ASP.NET MVC 应用来显示记录的详细信息。我们实现了清晰的架构分层(Model - DAL - Controller - View),使用了 Repository 模式进行数据抽象,并严格遵守了使用参数化查询等安全最佳实践。
虽然代码量比使用 EF 更多,但这种方法提供了对数据库交互的完全控制,在需要极致性能或处理复杂遗留系统时是非常有价值的技能。对于大多数新项目,EF Core 仍然是推荐的首选,但了解其底层原理(ADO.NET)无疑会让你成为一个更全面的开发者。