OracleConnection 不可注册为 Singleton,必须按请求新建并及时释放;连接字符串应通过 IConfiguration 安全注入,事务需在 Scoped 服务内共享同一连接。

注册 OracleConnection 时不能直接注册为 Singleton
Oracle 官方驱动(Oracle.ManagedDataAccess)的 OracleConnection 不是线程安全的,且内部持有非托管资源。如果在 Program.cs 中错误地注册为 Singleton,会导致连接被多个请求复用、状态混乱、抛出 ORA-01000: maximum open cursors exceeded 或 ObjectDisposedException。
正确做法是:只注册 OracleConnectionStringBuilder 或连接字符串本身,而把 OracleConnection 的创建推迟到作用域内(即每次 HTTP 请求或业务逻辑块中按需新建)。
- ✅ 推荐注册连接字符串:
builder.Services.AddSingleton<oracleconnectionstringbuilder>(sp => new OracleConnectionStringBuilder("User Id=..."))</oracleconnectionstringbuilder> - ❌ 禁止注册连接实例:
builder.Services.AddSingleton<oracleconnection>(sp => new OracleConnection(...))</oracleconnection> - ⚠️ 若必须封装连接工厂,应注册为
Scoped并确保每次调用CreateConnection()都返回新实例
配合 Dapper 使用时,连接应由调用方控制生命周期
Dapper 本身不管理连接生命周期,它只负责执行 SQL 和映射结果。因此,即使你用 DI 注入了 IRepository,其内部的 OracleConnection 也必须在方法体内显式创建、使用、释放——不能缓存或跨方法复用。
典型错误写法是把 OracleConnection 声明为类字段并复用;正确模式是:
public class UserRepository : IUserRepository
{
private readonly OracleConnectionStringBuilder _csb;
public UserRepository(OracleConnectionStringBuilder csb) => _csb = csb;
<pre class="brush:php;toolbar:false;">public async Task<User> GetById(int id)
{
// 每次调用都新建连接,用完即关
using var conn = new OracleConnection(_csb.ConnectionString);
await conn.OpenAsync();
return await conn.QueryFirstOrDefaultAsync<User>("SELECT * FROM USERS WHERE ID = :id", new { id });
}}
- 连接对象必须包裹在
using块或await using中 - 不要在构造函数里打开连接,也不要把它存为字段
- 若使用
OracleCommand手动执行,同样要确保Dispose()被调用
需要事务时,必须在同一个 Scoped 服务内共享连接
当你有多个仓储方法需要参与同一事务(例如“扣减库存 + 记录日志”),它们必须共用同一个 OracleConnection 实例,否则 Oracle 无法识别跨连接事务。
此时不能靠每个仓储自己 new 连接,而应引入一个 OracleConnectionAccessor 或类似上下文持有者,并注册为 Scoped:
builder.Services.AddScoped<OracleConnectionAccessor>();
然后在仓储中注入它,首次访问时创建连接,后续复用:
public class OrderRepository
{
private readonly OracleConnectionAccessor _accessor;
public OrderRepository(OracleConnectionAccessor accessor) => _accessor = accessor;
<pre class="brush:php;toolbar:false;">public async Task PlaceOrder(Order order)
{
var conn = await _accessor.GetConnectionAsync(); // 同一请求内始终返回同一个连接
using var tx = await conn.BeginTransactionAsync();
try
{
await conn.ExecuteAsync("INSERT INTO ORDERS ...", order, tx);
await conn.ExecuteAsync("INSERT INTO LOGS ...", new { msg = "order placed" }, tx);
await tx.CommitAsync();
}
catch
{
await tx.RollbackAsync();
throw;
}
}}
- 该模式依赖
Scoped生命周期保证单请求内唯一性 - 务必在
OracleConnectionAccessor中实现懒加载 + 异步初始化 - 避免在不同 Scoped 服务之间传递连接对象,容易引发生命周期错乱
连接字符串敏感信息必须从配置中心读取,而非硬编码
Oracle 连接字符串含用户名、密码、主机等敏感字段,绝不可写死在代码或 Program.cs 里。应通过 IConfiguration 绑定配置节,并启用密钥管理(如 Azure Key Vault、AWS Secrets Manager 或本地 UserSecrets)。
示例配置结构(appsettings.json):
"OracleSettings": {
"ConnectionString": "User Id={UserId};Password={Password};Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={Host})(PORT={Port}))(CONNECT_DATA=(SERVICE_NAME={ServiceName})));"
}
绑定后注入:
builder.Services.Configure<OracleSettings>(builder.Configuration.GetSection("OracleSettings"));
builder.Services.AddSingleton<OracleConnectionStringBuilder>(sp =>
{
var settings = sp.GetRequiredService<IOptions<OracleSettings>>().Value;
return new OracleConnectionStringBuilder(settings.ConnectionString)
.Replace("{UserId}", builder.Configuration["Oracle:UserId"])
.Replace("{Password}", builder.Configuration["Oracle:Password"]);
});
- 替换逻辑应放在运行时,而非编译期
- 密码字段严禁出现在日志、异常堆栈或调试输出中
- 若使用 TNSNAMES.ORA,路径需确保部署环境可访问且权限受限
连接生命周期和配置解耦是 Oracle 场景下最容易被跳过的两环。很多团队卡在“连接偶尔超时”或“ORA-03113”上,根源往往不是网络或数据库,而是 DI 注册方式与 Oracle 驱动行为不匹配。


















