具有流畅的nHibernate和Ninject的多租户。每个租户一个数据库

时间:2012-01-26 16:44:28

标签: asp.net-mvc nhibernate ninject multi-tenant

我正在构建一个多租户Web应用程序,出于安全考虑,我们需要为每个租户提供一个数据库实例。所以我有一个用于身份验证的MainDB和许多用于应用程序数据的ClientDB。

我在Ninject和Fluent nHibernate上使用Asp.net MVC。我已经在应用程序开始时在Ninject模块中使用Ninject和Fluent nHibernate设置了我的SessionFactory / Session / Repositories。我的会话是PerRequestScope,也是存储库。

我的问题是,现在我需要为每个租户实例化一个SessionFactory(SingletonScope)实例,只要其中一个连接到应用程序并为每个webrequest创建一个新会话和必要的存储库。我很困惑如何做到这一点,需要一个具体的例子。

情况就是这样。

应用程序启动:TenantX的用户输入他的登录信息。创建MainDB的SessionFactory并打开与MainDB的会话以对用户进行身份验证。然后应用程序创建auth cookie。

租户访问应用程序:租户名称+ ConnectionString从MainDB中提取,Ninject必须为该租户构建租户特定的SessionFactory(SingletonScope)。 Web请求的其余部分,所有需要存储库的控制器将根据该租户的SessionFactory注入租户特定的会话/存储库。

如何使用Ninject设置动态?当我有多个数据库时,我最初使用的是Named实例,但现在数据库是特定于租户的,我很遗憾......

2 个答案:

答案 0 :(得分:11)

经过进一步研究,我可以给你一个更好的答案。

虽然可以将连接字符串传递给ISession.OpenSession,但更好的方法是创建自定义ConnectionProvider。最简单的方法是从DriverConnectionProvider派生并覆盖ConnectionString属性:

public class TenantConnectionProvider : DriverConnectionProvider
{
    protected override string ConnectionString
    {
        get
        {
            // load the tenant connection string
            return "";
        }
    }

    public override void Configure(IDictionary<string, string> settings)
    {
        ConfigureDriver(settings);
    }
}

使用FluentNHibernate,您可以像这样设置提供程序:

var config = Fluently.Configure()
    .Database(
        MsSqlConfiguration.MsSql2008
            .Provider<TenantConnectionProvider>()
    )

每次打开会话时都会评估ConnectionProvider,以便您连接到应用程序中的特定于租户的数据库。

上述方法的一个问题是SessionFactory是共享的。如果您只使用第一级缓存(因为它与会话相关联),这不是一个真正的问题,但是如果您决定启用二级缓存(与SessionFactory绑定)。

因此,建议的方法是每个租户使用一个SessionFactory(这适用于每个租户的架构和每个租户的数据库策略)。

另一个经常被忽视的问题是,尽管二级缓存与SessionFactory相关联,但在某些情况下,缓存空间本身是共享的(reference)。这可以通过设置提供程序的“regionName”属性来解决。

以下是基于您的要求的每个租户SessionFactory的工作实现。

Tenant类包含为租户设置NHibernate所需的信息:

public class Tenant : IEquatable<Tenant>
{
    public string Name { get; set; }
    public string ConnectionString { get; set; }

    public bool Equals(Tenant other)
    {
        if (other == null)
            return false;

        return other.Name.Equals(Name) && other.ConnectionString.Equals(ConnectionString);
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Tenant);
    }

    public override int GetHashCode()
    {
        return string.Concat(Name, ConnectionString).GetHashCode();
    }
}

由于我们将存储Dictionary<Tenant, ISessionFactory>我们实施IEquatable接口,因此我们可以评估租户密钥。

获取当前租户的过程是这样抽象的:

public interface ITenantAccessor
{
    Tenant GetCurrentTenant();
}

public class DefaultTenantAccessor : ITenantAccessor
{
    public Tenant GetCurrentTenant()
    {
        // your implementation here

        return null;
    }
}

最后管理会话的NHibernateSessionSource

public interface ISessionSource
{
    ISession CreateSession();
}

public class NHibernateSessionSource : ISessionSource
{
    private Dictionary<Tenant, ISessionFactory> sessionFactories = 
        new Dictionary<Tenant, ISessionFactory>();

    private static readonly object factorySyncRoot = new object();

    private string defaultConnectionString = 
        @"Server=(local)\sqlexpress;Database=NHibernateMultiTenancy;integrated security=true;";

    private readonly ISessionFactory defaultSessionFactory;
    private readonly ITenantAccessor tenantAccessor;

    public NHibernateSessionSource(ITenantAccessor tenantAccessor)
    {
        if (tenantAccessor == null)
            throw new ArgumentNullException("tenantAccessor");

        this.tenantAccessor = tenantAccessor;

        lock (factorySyncRoot)
        {
            if (defaultSessionFactory != null) return;

            var configuration = AssembleConfiguration("default", defaultConnectionString);
            defaultSessionFactory = configuration.BuildSessionFactory();
        }
    }

    private Configuration AssembleConfiguration(string name, string connectionString)
    {
        return Fluently.Configure()
            .Database(
                MsSqlConfiguration.MsSql2008.ConnectionString(connectionString)
            )
            .Mappings(cfg =>
            {
                cfg.FluentMappings.AddFromAssemblyOf<NHibernateSessionSource>();
            })
            .Cache(c =>
                c.UseSecondLevelCache()
                .ProviderClass<HashtableCacheProvider>()
                .RegionPrefix(name)
            )
            .ExposeConfiguration(
                c => c.SetProperty(NHibernate.Cfg.Environment.SessionFactoryName, name)
            )
            .BuildConfiguration();
    }

    private ISessionFactory GetSessionFactory(Tenant currentTenant)
    {
        ISessionFactory tenantSessionFactory;

        sessionFactories.TryGetValue(currentTenant, out tenantSessionFactory);

        if (tenantSessionFactory == null)
        {
            var configuration = AssembleConfiguration(currentTenant.Name, currentTenant.ConnectionString);
            tenantSessionFactory = configuration.BuildSessionFactory();

            lock (factorySyncRoot)
            {
                sessionFactories.Add(currentTenant, tenantSessionFactory);
            }
        }

        return tenantSessionFactory;
    }

    public ISession CreateSession()
    {
        var tenant = tenantAccessor.GetCurrentTenant();

        if (tenant == null)
        {
            return defaultSessionFactory.OpenSession();
        }

        return GetSessionFactory(tenant).OpenSession();
    }
}

当我们创建NHibernateSessionSource的实例时,我们为我们的“默认”数据库设置了一个默认的SessionFactory。

调用CreateSession()时,我们会获得ISessionFactory个实例。这将是默认会话工厂(如果当前租户为空)或租户特定会话工厂。查找租户特定会话工厂的任务由GetSessionFactory方法执行。

最后,我们在已获得的OpenSession实例上调用ISessionFactory

请注意,当我们创建会话工厂时,我们设置SessionFactory名称(用于调试/分析目的)和缓存区域前缀(出于上述原因)。

我们的Io​​C工具(在我的例子中是StructureMap)将所有内容连接起来:

    x.For<ISessionSource>().Singleton().Use<NHibernateSessionSource>();
    x.For<ISession>().HttpContextScoped().Use(ctx => 
        ctx.GetInstance<ISessionSource>().CreateSession());
    x.For<ITenantAccessor>().Use<DefaultTenantAccessor>();

这里NHibernateSessionSource的范围是每个请求的单例和ISession。

希望这有帮助。

答案 1 :(得分:0)

如果所有数据库都在同一台机器上,那么类映射的schema属性可能会用于在租户之前设置数据库。