Layered with Free nHibernate and Ninject. One database for each tenant

I am creating a multi-user web application where, for security, we need to have one database instance for each tenant. Thus, I have MainDB for authentication and many ClientDB data for applications.

I am using Asp.net MVC with Ninject and Fluent nHibernate. I already installed my SessionFactory / Session / Repositories using Ninject and Fluent nHibernate in the Ninject module at the beginning of the application. My sessions are PerRequestScope, as are the repositories.

Now I need to create an instance of SessionFactory (SingletonScope) for each of my tenants, when one of them connects to the application and creates a new session and the necessary repositories for each web request. I am puzzled by how to do this, and we need a concrete example.

Here is the situation.

Application Launch . The TenantX user is included in his registration information. A SessionFactory MainDB is created and opens a MainDB session for user authentication. Then the application creates an auth cookie.

The tenant gains access to the application . The name Tenant Name + ConnectionString is retrieved from MainDB, and Ninject must build a specific SessionFactory (SingletonScope) for this tenant. In the rest of the web request, all controllers requiring a repository will receive a special Tenant session / repository based on this SessionFactory tenant.

How to adjust this dynamics using Ninject? I originally used an instance of Named when I had several databases, but now that the databases are specific to tenants, I got lost ...

+7
source share
2 answers

After further research, I can give you a better answer.

Although you can pass the connection string to ISession.OpenSession , it is best to create a custom ConnectionProvider . The easiest way is to extract from DriverConnectionProvider and override the ConnectionString property:

 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); } } 

Using FluentNHibernate, you install the provider as follows:

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

ConnectionProvider is evaluated every time you open a session, allowing you to connect to specific tenant databases in your application.

The problem with the above approach is that SessionFactory is shared. This is not a problem if you use only the first level cache (since this is related to the session), but if you decide to enable the second level cache (bound to SessionFactory).

Therefore, the recommended approach is to have a SessionFactory-for-tenant (this applies to strategies between tenants and a database for each tenant).

Another issue that is often overlooked is that while the second level cache is tied to the SessionFactory, in some cases the cache space is shared ( link ). This can be solved by setting the "regionName" property of the provider.

Below is a working version of SessionFactory-per-tenant based on your requirements.

The Tenant class contains the information needed to install NHibernate for a tenant:

 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(); } } 

Since we will be storing the Dictionary<Tenant, ISessionFactory> , we implement the IEquatable interface IEquatable that we can evaluate the Tenant keys.

The process of obtaining the current tenant is abstracted as follows:

 public interface ITenantAccessor { Tenant GetCurrentTenant(); } public class DefaultTenantAccessor : ITenantAccessor { public Tenant GetCurrentTenant() { // your implementation here return null; } } 

Finally, the NHibernateSessionSource , which manages the sessions:

 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(); } } 

When we create an instance of NHibernateSessionSource , we set the default SessionFactory to our default database.

When CreateSession() is called, we get an instance of ISessionFactory . This will be either the factory default session (if the current tenant is zero) or a specific factory tenant session. The task of locating a specific factory tenant is performed by the GetSessionFactory method.

Finally, we call OpenSession the instance of ISessionFactory that we received.

Note that when creating a factory session, we set the name to SessionFactory (for debugging / profiling purposes) and the cache area prefix (for the reasons mentioned above).

Our IoC tool (in my case, StructureMap) conveys everything:

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

Here NHibernateSessionSource has a Singleton and ISession scope for each request.

Hope this helps.

+11
source

If all the databases are on the same computer, perhaps the schema property of the class mappings can be used to set up the database based on the preliminary tenant.

0
source

All Articles