使用Simple Injector管理多个租户数据库连接

时间:2014-12-10 20:35:50

标签: database-connection simple-injector multi-tenant

拥有一个SharePoint"远程网络"将管理多个租户数据库(最终是多个租户数据库连接)的数据的应用程序。实质上,每个操作将处理2个数据库。

第一个是我们的租赁数据库,我们存储特定于每个租户的信息。这可以是SharePoint OAuth客户端ID和机密,以及有关如何连接到租户的特定数据库(第二个数据库)的信息。这意味着在连接到第二个数据库之前,需要连接到第一个数据库。

我相信我知道如何使用Simple Injector进行HTTP请求。我可以在每个Web请求生存期内注册第一个连接类型(无论是使用ADO.NET的IDbConnection包装还是来自实体框架的TenancyDbContext)。

然后我可以注册一个抽象工厂来解析与租户特定数据库的连接。该工厂将依赖于第一个数据库类型以及Simple Injector Container。查询&需要访问租户数据库的命令将依赖于此抽象工厂,并通过将参数传递给工厂方法来使用它来获取与租户数据库的连接。

我的问题主要与如何在可能有或没有非空HttpContext.Current的操作的上下文中处理这个问题有关。安装SharePoint应用程序时,我们有时会运行WCF .svc服务来执行某些操作。当SharePoint调用此方法时,有时HttpContext为空。我需要一个适用于两种情况的解决方案,对于两个数据库连接,这将确保在不再需要连接时处理这些连接。

我有一些使用LifetimeScope的旧示例代码,但我现在看到在nuget上有一个可用于Simple Injector的执行上下文范围包。我想知道是否应该使用它来为这两个数据库连接创建混合范围(有/没有HTTP上下文),如果是这样,它与使用Container.GetCurrentLifetimeScopeContainer.BeginLifetmeScope的生命范围界定有什么不同? / p>

更新

我阅读了执行范围的生活方式,最后得到了以下3路混合:

var hybridDataAccessLifestyle = Lifestyle.CreateHybrid( // create a hybrid lifestyle
    lifestyleSelector: () => HttpContext.Current != null, // when the object is needed by a web request
    trueLifestyle: new WebRequestLifestyle(), // create one instance for all code invoked by the web request
    falseLifestyle: Lifestyle.CreateHybrid( // otherwise, create another hybrid lifestyle
        lifestyleSelector: () => OperationContext.Current != null,  // when the object is needed by a WCF op,
        trueLifestyle: new WcfOperationLifestyle(), // create one instance for all code invoked by the op
        falseLifestyle: new ExecutionContextScopeLifestyle()) // in all other cases, create per execution scope
);

然而,我的问题实际上与如何创建一个依赖关系有关,该依赖关系将在root已经组合之后的某个时间获得它的连接字符串。下面是我提出的一些伪代码,它实现了我对如何实现它的想法:

public class DatabaseConnectionContainerImpl : IDatabaseConnectionContainer, IDisposable
{
    private readonly AllTenantsDbContext _allTenantsDbContext;
    private TenantSpecificDbContext _tenantSpecificDbContext;
    private Uri _tenantUri = null;

    public DatabaseConnectionContainerImpl(AllTenantsDbContext allTenantsDbContext)
    {
        _allTenantsDbContext = allTenantsDbContext;
    }

    public TenantSpecificDbContext GetInstance(Uri tenantUri)
    {
        if (tenantUri == null) throw new ArgumentNullException(“tenantUri”);
        if (_tenantUri != null && _tenantUri.Authority != tenantUri.Authority)
            throw new InvalidOperationException(
                "You can only connect to one tenant database within this scope.");

        if (_tenantSpecificDbContext == null) {
            var tenancy = allTenantsDbContext.Set<Tenancy>()
                .SingleOrDefault(x => x.Authority == tenantUri.Authority);
            if (tenancy == null)
                throw new InvalidOperationException(string.Format(
                    "Tenant with URI Authority {0} does not exist.", tenantUri.Authority));

            _tenantSpecificDbContext = new TenantSpecificDbContext(tenancy.ConnectionString);
           _tenantUri = tenantUri;
        }
        return _tenantSpecificDbContext
    }

    void IDisposable.Dispose()
    {
        if (_tenantSpecificDbContext != null) _tenantSpecificDbContext.Dispose();
    }
}

底线是有一个运行时Uri变量,用于确定连接字符串到TenantSpecificDbContext实例的内容。此Uri变量将传递到所有WCF操作和HTTP Web请求中。由于在编写根之后运行时才知道这个变量,我不认为有任何方法可以将它注入到构造函数中。

任何比上述更好的想法,或者上面的想法是否有问题?

1 个答案:

答案 0 :(得分:2)

由于您希望在同一AppDomain中的两个不同的上下文中运行操作(一个具有Web请求的可用性,何时没有),您需要使用hybrid lifestyle。混合生活方式从一种生活方式自动切换到另一种生活方式。 Simple Injector文档中给出的示例如下:

ScopedLifestyle scopedLifestyle = Lifestyle.CreateHybrid(
    lifestyleSelector: () => container.GetCurrentLifetimeScope() != null,
    trueLifestyle: new LifetimeScopeLifestyle(),
    falseLifestyle: new WebRequestLifestyle());

// The created lifestyle can be reused for many registrations.
container.Register<IUserRepository, SqlUserRepository>(hybridLifestyle);
container.Register<ICustomerRepository, SqlCustomerRepository>(hybridLifestyle);

使用此自定义混合生活方式,实例将在活动生命周期范围内存储,但如果没有活动生命周期范围,我们将回退到每个Web请求的缓存实例。如果没有活动的生命周期范围且没有Web请求,an exception will be thrown

使用Simple Injector,将隐式为您创建Web请求的范围。但就终身范围而言,这是不可能的。这意味着您必须自己明确地开始这样的范围(如图here所示)。这将是be trivial for you,因为您使用命令处理程序。

现在您的问题是lifetime scopeexecution context scope之间的区别。两者之间的区别在于生命周期范围是特定于线程的。它不能在可能从线程跳转到线程的异步操作中流动。它使用了ThreadLocal封面。

但是,如果您使用async / wait并从方法返回Task<T>,则可以使用执行范围。在这种情况下,范围可以放置在不同的线程上,因为它将所有缓存的实例存储在CallContext类中。

在大多数情况下,您将能够在使用生命周期范围的地方使用执行范围,但肯定不是相反。但是,如果您的代码不是异步流动的,那么生命周期范围可以提供更好的性能(尽管可能与执行范围没有显着的性能差异)。