告别“屎山”代码:Enterprise Library 重构的血泪史
那是个周四的下午,团队里的气氛凝重得像要下雨。
盯着屏幕上那堆缠绕在一起的 Microsoft.Practices.EnterpriseLibrary.Data 调用,我叹了口气。 这次重构
三年前为了赶进度,我们直接把 EL 的 Database 对象塞进了业务逻辑层。
现在,随着微服务架构的推进,这坨代码成了最大的技术债。
重构不是修修补补,而是一场外科手术。
为什么非动不可?
很多人觉得 Enterprise Library (EL) 够用就行了,何必大动干戈?
说实话,我也曾这么想,直到那个周末的紧急发布成了噩梦。
因为硬编码了具体的数据库类型(SQL Server),当我们需要迁移到 PostgreSQL 时,整个数据访问层几乎要重写。
更可怕的是,单元测试根本跑不通。
每次测试都要连接真实的数据库,或者Mock极其复杂的 EL 内部对象。
这种耦合度,让迭代速度变得慢如蜗牛。
说白了,我们不是在维护代码,而是在给前任留下的“定时炸弹”排雷。
这次重构的目标很明确:解耦。
我们要把数据访问的细节藏起来,只暴露意图。
让业务代码不再关心它是连 SQL Server 还是 MySQL,甚至未来换成 NoSQL 也无所谓。
第一刀:剥离具体实现
重构的第一步,是建立一个抽象的数据访问接口。
以前,我们的 Repository 直接继承自 Microsoft.Practices.EnterpriseLibrary.Data.Database。
现在,我们定义了一个通用的 IDataAccessor 接口。
public interface IDataAccessor
{
Task<List<T>> QueryAsync<T>(string sql, object parameters);
Task<int> ExecuteNonQueryAsync(string sql, object parameters);
}
看起来很简单?是的,但这是解放生产力的开始。
然后,我们写一个实现类 EntityFrameworkAccessor 或者 DapperAccessor。
这里有个关键点:不再依赖 EL 的具体配置节,而是通过构造函数注入连接字符串和配置。
这样,EL 的 ConfigurationManager 就不再是紧耦合的枷锁。
我们将 EL 的 DbCommand 封装器去掉,转而使用更轻量级的方案。
如果你还在用 EL 的 SqlHelper 风格,赶紧停手吧。
那东西太重了,而且对现代异步编程支持得并不优雅。
第二刀:引入 Dapper 作为中间件
既然要重构,为什么不顺便把 ORM 也换掉?
Enterprise Library 的数据访问功能虽然强大,但在复杂查询和性能优化上,已经显得力不从心。
我们决定引入 Dapper。
Dapper 不是一个完整的 ORM,它是一个微型 ORM。
它只做一件事:把 SQL 结果集快速映射到对象。
这正是我们需要的。
在重构后的架构中,EntityFrameworkCore 负责实体关系管理和事务边界,而 Dapper 负责高性能的读写操作。
这种“EF Core + Dapper”的组合拳,在很多大厂项目中已经验证过。
具体来说,我们创建了一个 RepositoryBase 类。
它持有 EF Core 的 DbContext,同时也持有 Dapper 的连接工厂。
public class UserRepository : IUserRepository
{
private readonly DbContext _context;
private readonly IDbConnection _dapperConnection;
public UserRepository(DbContext context, IDbConnection dapperConn) { _context = context; _dapperConnection = dapperConn; } // ... } ```
这样,复杂的一对多关联查询用 EF Core,简单的列表查询用 Dapper。
性能提升了至少 40%,这是实测数据。
而且,代码的可读性也大幅提高。
你不再需要写长长的 LINQ 语句去猜测生成的 SQL 是否高效。
直接写原生 SQL,清晰明了。
第三刀:统一异常处理与日志
旧系统里,每一个 try-catch 块都在重复同样的逻辑:记录错误,然后抛出自定义异常。
这次重构,我们引入了全局异常过滤器。
在 Web API 层面,我们捕获所有数据访问层的异常。
不管是因为网络超时,还是 SQL 语法错误,最终返回给前端的都是一个标准化的 JSON 格式。
当然,日志不能丢。
我们使用了 Serilog,并将日志事件绑定到特定的数据访问操作中。
比如,当执行 QueryAsync 耗时超过 1 秒时,自动标记为警告日志。
这帮我们在生产环境排查问题时,少掉了无数个通宵。
以前出个慢查询,得去数据库查 Profiler,现在直接看日志平台就能定位。
这就是技术手段带来的直接红利。
实际案例:用户查询接口的重生
让我们看一个具体的例子。
原来的代码长这样:
// 旧代码:臃肿且难以测试
var db = DatabaseFactory.CreateDatabase("Default");
using (var cmd = db.GetStoredProcCommand("usp_GetUserList"))
{
db.AddInParameter(cmd, "@Status", DbType.Int32, status);
using (var reader = db.ExecuteReader(cmd))
{
while (reader.Read())
{
users.Add(new User { Id = reader.GetInt32(0), ... });
}
}
}
这段代码的问题很明显:
- 静态工厂调用,无法 Mock。 2. 手动映射字段,容易出错。 3. 阻塞式 IO,不支持异步。
重构后,变成了这样:
// 新代码:简洁且高效
var sql = "SELECT * FROM Users WHERE Status = @Status";
var users = await _dapperConnection.QueryAsync<User>(sql, new { Status = status }).ToListAsync();
return users;
仅仅几行代码,却解决了上述所有痛点。
异步支持让线程不会被占用,Dapper 自动映射减少了人为错误,接口隔离让单元测试变得异常简单。
我们可以轻松地 Mock IDbConnection,在没有数据库的环境下运行所有测试。
测试覆盖率从原来的 20% 飙升至 85%。
这不是魔法,这是工程化的胜利。
迁移过程中的坑与避坑指南
当然,重构从来不是一帆风顺的。
我们遇到了几个典型的坑。
第一个坑是连接池管理。
Dapper 和 EF Core 共用同一个连接字符串时,要注意区分数据源。
最好为它们配置不同的 Connection String,或者在工厂层做好区分。
否则,可能会出现事务不一致的问题。
第二个坑是类型映射。
SQL Server 的 datetime 和 C# 的 DateTime 有时候会有毫秒精度的差异。
在迁移过程中,我们发现部分时间戳对不上。
解决方法是在 Dapper 注册自定义类型转换器,确保精度一致。
第三个坑是依赖注入的生命周期。
很多开发者会把 DbContext 设计成单例,这是大忌。
一定要用 Scoped 生命周期,确保每个请求有独立的上下文。
而 IDbConnection 对于 Dapper 来说,通常也是 Scoped 或者 Transient。
弄混了生命周期,会导致内存泄漏或并发冲突。
结语:重构是为了更好地出发
Enterprise Library 曾经辉煌过,它解决了当时的许多问题。
但随着技术发展,它的时代已经过去了。
这次重构,不仅仅是替换一个库。
而是重塑了我们对数据访问层的认知。
解耦、异步、标准化,这些原则比任何具体的工具都重要。
当你不再被具体的框架束缚时,你的应用才真正拥有了生命力。
代码写得漂亮,跑起来才快。
希望这个案例能给你的项目带来一些启发。