Skip to content

依赖注入集成

这一页关注 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?...")
}

各自的选项是独立的 —— AppDbContextLogDbContextDbContextOptionsOf<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 需要你自己管理连接字符串、生命周期、以及和其他服务的协作。