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 {
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() {
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.