Dapper with .NET Core - 注入的 SqlConnection 生命周期/作用域

23
我使用 .NET Core 依赖注入在应用程序启动期间实例化一个 SqlConnection 对象,然后计划将其注入到我的 repository 中。这个 SqlConnection 将由 Dapper 在我的 repository 实现中用于读写数据库。我将使用 Dapper 的 async 调用。问题是:我应该将 SqlConnection 注入为瞬态还是单例?考虑到我想要使用 async,我的想法是使用瞬态,除非 Dapper 在内部实现了一些隔离容器,并且我的单例范围仍将包装在 Dapper 内部使用的任何范围之内。在使用 Dapper 时,是否有关于 SqlConnection 对象生命周期的建议/最佳实践?我可能会忽略哪些注意事项吗?提前感谢您。
3个回答

19

如果您将SQL连接提供为单例,则除非启用MARS(也有其限制),否则无法同时处理多个请求。最佳实践是使用瞬态SQL连接并确保其被正确处理。

在我的应用程序中,我将自定义的IDbConnectionFactory传递给仓储,该工厂用于在using语句中创建连接。在这种情况下,仓库本身可以成为单例,以减少堆上的分配。


我喜欢工厂模式,但是如果一个应用程序有多个数据源(数据库)的情况该怎么处理呢?为每个数据库创建一个子ISomeDbConnectionFactory并传递额外参数给工厂来确定需要创建哪个连接?还是有更优雅的解决方案? - Philip P.
2
我认为同一个仓库将使用单个数据源连接。在这种情况下,我会采用特定于数据源的连接工厂方法。工厂的接口可以相同,只需进行特定于数据库的实现即可。 - Andrii Litvinov
即使启用了MARS,您也不能使用相同的SqlConnection服务于多个并发请求。根据MSDN的说法:“MARS操作不是线程安全的”。 - Steven
不错的观点 @Steven!它支持并行执行。我曾认为可以同时使用同一连接,但实际上仍然是可能的,只是需要异步地启动多个操作。这正是我尝试过的,也是 MARS 支持的。 - Andrii Litvinov
我不确定是否有什么遗漏,你说:“在这种情况下,仓储本身可以是单例以减少堆上的分配”。但是,如果您将瞬态服务注入单例服务中,则该单例服务将使瞬态服务成为单例,对吧?这样做会失去该瞬态服务的生命周期约束。简而言之,当您创建单例服务时,所有依赖项仅为该服务创建一次,然后将该服务固定在内存中。因此,预期的瞬态服务不会像预期的那样重新创建。 - Emran Hussain
2
@EmranHussain,是的,将短暂服务逻辑注入到单例中是正确的。我的回答中并不是要将SQL连接器注入到工厂中,而是将连接字符串注入,并由工厂根据该连接字符串创建SQL连接器。 - Andrii Litvinov

5
很好的问题,已经有两个很好的答案了。一开始我对此感到困惑,后来想出了以下解决方案来解决这个问题,该方案将存储库封装在管理器中。管理器本身负责提取连接字符串并将其注入存储库。
我发现这种方法使得单独测试存储库变得更加简单,比如在模拟控制台应用程序中进行测试,而且我在几个较大规模的项目中遵循这种模式时也非常成功。虽然我承认我不是测试、依赖注入或任何东西的专家!
我留下的主要问题是,DbService是否应该是单例。我的理由是,并没有太多的意义不断地创建和销毁封装在DbService中的各种存储库,而且它们都是无状态的,因此我认为让它们“活着”没有太大问题。虽然这可能是完全无效的逻辑。
引用:
如果您想要一个现成的解决方案,请查看我的Dapper存储库实现GitHub
存储库管理器结构如下:
/*
 * Db Service
 */
public interface IDbService
{
    ISomeRepo SomeRepo { get; }
}

public class DbService : IDbService
{
    readonly string connStr;
    ISomeRepo someRepo;

    public DbService(string connStr)
    {
        this.connStr = connStr;
    }

    public ISomeRepo SomeRepo
    {
        get
        {
            if (someRepo == null)
            {
                someRepo = new SomeRepo(this.connStr);
            }

            return someRepo;
        }
    }
}

一个示例仓库的结构如下:
/*
 * Mock Repo
 */
public interface ISomeRepo
{
    IEnumerable<SomeModel> List();
}

public class SomeRepo : ISomeRepo
{
    readonly string connStr;

    public SomeRepo(string connStr)
    {
        this.connStr = connStr;
    }

    public IEnumerable<SomeModel> List()
    {
        //work to return list of SomeModel 
    }
}

将所有东西连接起来:

/*
 * Startup.cs
 */
public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
    //...rest of services

    services.AddSingleton<IDbService, DbService>();

    //...rest of services
}

最后,使用它:

public SomeController : Controller 
{
    IDbService dbService;

    public SomeController(IDbService dbService)
    {
        this.dbService = dbService;
    }

    public IActionResult Index()
    {
        return View(dbService.SomeRepo.List());
    }
}

你是如何将连接字符串发送到服务的? - Vahid Farahmandian
1
@VahidFarahmandian,你可以通过使用一个函数在注册时设置它。 - Victorio Berra

4

我同意@Andrii Litvinov的观点,包括回答和评论。

在这种情况下,我会采用数据源特定连接工厂的方法。

使用相同的方法,我提到了不同的方法 - UnitOfWork。

请参阅答案中的DalSessionUnitOfWork。这处理连接。
请参阅答案中的BaseDal。这是我对Repository(实际上是BaseRepository)的实现。

  • UnitOfWork作为短暂注入。
  • 可以通过为每个数据源创建单独的DalSession来处理多个数据源。
  • UnitOfWorkBaseDal中被注入。

在使用Dapper时,SqlConnection对象的生命周期是否有任何建议/最佳实践?

大多数开发人员都同意的一件事是,连接应尽可能短暂。我在这里看到两种方法:

  1. 每个操作一个连接。
    当然,这将是连接的最短生命周期。您可以在每个操作的using块中封装连接。只要您不想对操作进行分组,这就是一个好方法。即使您想对操作进行分组,在大多数情况下也可以使用事务。
    问题在于当您想跨多个类/方法对操作进行分组时,无法在此处使用using块。解决方案是以下的UnitOfWork。
  2. 每个工作单元一个连接。
    定义您的工作单元。这将因应用程序而异。在Web应用程序中,“每个请求一个连接”是广泛使用的方法。
    这更有意义,因为通常有(大多数时间)我们想要作为整体执行的一组操作。这在我提供的两个链接中有所解释。
    这种方法的另一个优点是,应用程序(使用DAL的应用程序)对如何使用连接具有更多控制权。在我看来,应用程序比DAL更了解如何使用连接。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接