依赖注入集成
这一页关注 SqlSharp 如何接入应用容器,让
DbContext跟随请求或作用域管理生命周期。
适用场景
- Web 项目或中大型应用中统一管理
DbContext - 想避免在业务代码里到处手动
new AppDbContext() - 需要让数据访问层和其他服务一起纳入容器
核心问题
引入 soulsoft_extensions_sqlsharp 后,可以把 DbContext 注册进依赖注入容器,由容器负责创建和释放。
这样做的直接收益是:
- 生命周期统一
- 配置集中
- 更容易和 Web、服务层、任务调度层协作
快速开始
cangjie
import soulsoft_extensions_sqlsharp.*
import soulsoft_extensions_injection.*
public class WebDbContext <: DbContext & IServiceFactory<WebDbContext> {
public init(options: DbContextOptionsOf<WebDbContext>) { super(options) }
public static func createInstance(sp: IServiceProvider): WebDbContext {
let options = sp.getOrThrow<DbContextOptionsOf<WebDbContext>>()
return WebDbContext(options)
}
public prop users: DbSet<User> {
get() { set<User>() }
}
}
let services = ServiceCollection()
services.addDbContext<WebDbContext> { options =>
options.useMysql("mariadb://localhost:3306?username=root&password=1024&database=mydb", "mariadb")
}核心概念
生命周期
DbContext 在容器里通常注册为 Scoped:
- 一个请求一个实例
- 一个作用域一个实例
这符合绝大多数 Web 开发场景,也能避免多个并发请求共享同一个上下文实例。
使用方式
cangjie
let provider = services.build()
try (scope = provider.createScope()) {
let ctx = scope.services.getOrThrow<WebDbContext>()
let list = ctx.users.toList()
}常见用法
Web 控制器注入
最常见的用法是在控制器或应用服务中通过构造函数注入:
cangjie
public class ProductController <: ControllerBase {
private let _ctx: WebDbContext
public init(ctx: WebDbContext) {
_ctx = ctx
}
public func getList(): ArrayList<Product> {
return _ctx.products.toList()
}
public func create(name: String, price: Int64): Unit {
_ctx.add(Product(name: name, price: price))
_ctx.saveChanges()
}
}每个请求进入时,容器自动创建新的作用域和对应的 WebDbContext 实例。请求结束时作用域释放,DbContext 随之关闭。
多个 DbContext
一个应用可以注册多个 DbContext 类型,指向不同数据库:
cangjie
let services = ServiceCollection()
services.addDbContext<AppDbContext> { options =>
options.useMysql("mariadb://host1:3306?...", "mariadb")
}
services.addDbContext<LogDbContext> { options =>
options.usePostgres("postgresql://host2:5432?...")
}各自的选项是独立的 —— AppDbContext 和 LogDbContext 的 DbContextOptionsOf<T> 分别注册为独立的 Singleton。
选项全局共享
DbContextOptionsOf<T> 注册为 Singleton,同一个 T 的所有作用域共享同一份配置:
cangjie
let services = ServiceCollection()
services.addDbContext<AppDbContext> { options =>
options.useMysql(connectionString, "mariadb")
options.useQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
}
let root = services.build()
try (scope1 = root.createScope()) {
let ctx1 = scope1.services.getOrThrow<AppDbContext>()
let ctx2 = scope1.services.getOrThrow<AppDbContext>()
// ctx1 和 ctx2 是同一个实例(Scoped 内单例)
// ctx1.options 和 ctx2.options 是同一个对象(Singleton)
}
try (scope2 = root.createScope()) {
let ctx3 = scope2.services.getOrThrow<AppDbContext>()
// ctx3 是新的实例,但 options 仍然是同一个
}调度任务中使用独立作用域
cangjie
public class CleanupJob {
private let _services: IServiceProvider
public init(services: IServiceProvider) {
_services = services
}
public func execute(): Unit {
try (scope = _services.createScope()) {
let ctx = scope.services.getOrThrow<AppDbContext>()
// 在独立作用域中操作数据库
let p = DbParameters()
p.add("before", DateTime.now().addDays(-30))
ctx.execute("DELETE FROM temp_logs WHERE created_at < @before", p)
}
}
}与其他模块组合
- 与 DbContext 配置 一起决定运行时行为
- 在 Web 模块里,通常由控制器、应用服务、认证服务共同依赖同一个作用域上下文
- 在实际项目里,也可以让调度任务使用独立作用域创建
DbContext
生产建议
- 不要把
DbContext注册成单例 - 控制器或服务里依赖注入
DbContext即可,不要再手动创建额外实例 - 跨线程、跨请求不要复用同一个
DbContext
常见问题
为什么 DbContext 要注册为 Scoped 而不是 Singleton?
DbContext 内部维护了变更跟踪器(ChangeTracker),它缓存了当前作用域内所有实体的状态快照。如果注册为 Singleton,多个请求的实体变更会互相污染。Scoped 保证每个请求拿到独立的上下文实例。
addDbContext() 和手动 new 有什么区别?
addDbContext() 会注册 DbContextOptionsOf<T> 和 T 本身到容器,并自动管理生命周期。手动 new 需要你自己管理连接字符串、生命周期、以及和其他服务的协作。