在 ASP.NET MVC 中显示详细记录(不使用 Entity Framework)

ASP.NET MVC 框架提供了一个强大的模式来构建可维护的 Web 应用程序。Entity Framework (EF) 作为微软官方的对象关系映射器(ORM),极大地简化了数据访问层的工作。然而,在某些场景下,开发者可能会选择不使用 EF,例如:

  • 需要对数据库查询进行极致的性能优化和精细控制。
  • 在遗留系统中,已经存在成熟稳定的 ADO.NET 数据访问代码。
  • 团队对纯 SQL 的熟悉程度远超 ORM。
  • 项目要求避免 ORM 带来的额外开销或抽象泄漏。

本博客将详细讲解如何在不依赖 Entity Framework 的情况下,实现 MVC 架构中一个经典场景:显示某条记录的详细信息。我们将从零开始,构建一个完整的数据流,涵盖模型、数据访问层、控制器和视图。

目录#

  1. 架构概述
  2. 第一步:创建模型
  3. 第二步:建立数据访问层
  4. 第三步:创建控制器
  5. 第四步:构建视图
  6. 常见实践与最佳实践
  7. 总结
  8. 参考

架构概述#

在开始编码之前,我们先理解一下整体的数据流。我们的目标是:当用户访问 /Product/Details/5 时,页面显示 ID 为 5 的产品的详细信息。

流程如下:

  1. 路由:ASP.NET MVC 路由系统将请求映射到 ProductControllerDetails 动作方法,并将 id=5 作为参数传入。
  2. 控制器ProductController 接收请求,调用数据访问层(如 ProductRepository)的方法,传入 ID 5。
  3. 数据访问层ProductRepository 使用 ADO.NET(如 SqlConnectionSqlCommand)执行 SQL 查询,从数据库中获取数据。
  4. 模型映射:将数据库返回的结果(通常是 SqlDataReaderDataTable)映射到强类型的 Product 模型对象。
  5. 返回视图:控制器将填充好的 Product 对象传递给 Details 视图。
  6. 视图渲染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 语句是关键,它能确保 SqlConnectionSqlDataReader 在使用完毕后被立即关闭和销毁,避免资源泄漏。
  • 参数化查询:使用 @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 类型进行格式化)。

常见实践与最佳实践#

  1. 使用依赖注入:将 IProductRepository 注册到 DI 容器中,并在控制器的构造函数中注入它。这解耦了控制器和具体的数据访问实现。
  2. 使用 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 });
        }
    }
  3. 集中化异常处理:在 Global.asax 中或使用过滤器(Filter)实现全局异常处理,记录日志并向用户显示友好的错误页面,而不是暴露底层数据库错误。
  4. 日志记录:在数据访问层记录重要的操作(如错误信息)和慢查询,以便于调试和监控。
  5. 异步编程:对于 I/O 密集型的数据库操作,使用异步方法(async/await)可以提高应用程序的吞吐量。ADO.NET 提供了 ExecuteReaderAsync 等异步方法。

总结#

通过本博客的步骤,我们成功地在不使用 Entity Framework 的情况下,构建了一个完整的 ASP.NET MVC 应用来显示记录的详细信息。我们实现了清晰的架构分层(Model - DAL - Controller - View),使用了 Repository 模式进行数据抽象,并严格遵守了使用参数化查询等安全最佳实践。

虽然代码量比使用 EF 更多,但这种方法提供了对数据库交互的完全控制,在需要极致性能或处理复杂遗留系统时是非常有价值的技能。对于大多数新项目,EF Core 仍然是推荐的首选,但了解其底层原理(ADO.NET)无疑会让你成为一个更全面的开发者。

参考#

  1. Microsoft Docs: ASP.NET MVC Overview
  2. Microsoft Docs: ADO.NET Documentation
  3. Dapper: a simple object mapper for .NET
  4. Repository Pattern in ASP.NET MVC