In part 1 we have established tenant context. In part 2 we have configured authentication. Now we will be getting tenant data out of our RDBMS. Before we start storing and retrieving data it's important that you figure out what kind of multitenancy data segregation model you are going to go for. This is an important point and it really depends on your business requirements. To help you choose check out this Microsoft article: Multi-tenant SaaS database tenancy patterns.
My sample app is called “Stop The Line”, it is very basic. It holds very little data, and I have no idea if it's going to take off or not. I have decided to go with "Multi-tenant app with a single multi-tenant database" pattern. Put simply, this means that all my tenants are going to share the same database and I am going to use foreign key to discriminate between them. This is one the cheapest and fastest ways to implement tenancy.
When it comes to development what I don't want to do is to specify tenant id in each query and I don't want to manually specify tenant id when I create and update my domain objects. Remember the onion architecture, as far as I am concerned tenancy is an infrastructure concern and it should stay out of my domain. After few minutes of searching I have found this great blog post that meets these requirements: Bolt-on Multi-Tenancy in ASP.NET MVC With Unity and NHibernate: Part II – Commingled Data. Let's incorporate this in to my sample app.
Implementation
In my sample app it's possible to query in multitenancy context and without. For example, if a new tenant is signing up for an account then we would not apply the multitenancy context. This is because TenantContext will not exist until tenant was created in the first place.
Also in this implementation I have tried to keep infrastructure decoupled so that it would not be too difficult to move to another database tenancy pattern.
Base Configuration
As there are going to be two different contexts, non-multitenant and multitenant. I have decided to create NHConfiguration base class.
NHConfiguration.cspublic abstract class NHConfiguration { public abstract global::NHibernate.Cfg.Configuration Config { get; } }
Below is the standard configuration that is going to be used for non-multitenant read/write.
Configuration.cspublic class Configuration : NHConfiguration { public override NHibernate.Cfg.Configuration Config => config; private NHibernate.Cfg.Configuration config; public Configuration() { this.config = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2012 .ConnectionString(c => c.FromConnectionStringWithKey("default")) #if DEBUG .ShowSql() .FormatSql() #endif .AdoNetBatchSize(50) ).Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly()) ) .ExposeConfiguration(c => SchemaMetadataUpdater.QuoteTableAndColumns(c)) .BuildConfiguration(); } }
This configuration will get injected into the Unit of Work. If you are new to this, then you should read Unit Of Work Abstraction For NHibernate or Entity Framework C# Example. I have omitted the class implementation.
public class NHUnitOfWork : IUnitOfWork { //.. private NHConfiguration nhConfiguration; public NHUnitOfWork(NHConfiguration nhConfiguration) { this.nhConfiguration = nhConfiguration; //.. } //.. }
Unfortunately with this solution you will still need to add the private "tenantId" field to your domain classes.
public class Some { //.. private Guid tenantId; }
The good news is that you don't have to do with it anything inside your domain. It will be handled by infrastructure. The bad news is that it still there. I have spent few hours trying to remove it, I have used some reflection and proxy class generation techniques, unfortunately to no avail. If you find an elegant solution please do share it.
Multitenant Writes
This is where standard configuration is expanded and interceptor is set. Interceptor will be used when some object is updated or saved.
ConfigurationMultiTenancy.cspublic class ConfigurationMultiTenancy : Configuration { public ConfigurationMultiTenancy(NHSharedDatabaseMultiTenancyInterceptor interceptor) { this.Config.SetInterceptor(interceptor); } }
Above you will notice that NHSharedDatabaseMultiTenancyInterceptor is injected in to ConfigurationMultiTenancy. Here is the actual interceptor.
NHSharedDatabaseMultiTenancyInterceptor.cspublic class NHSharedDatabaseMultiTenancyInterceptor : EmptyInterceptor { readonly TenantContext tenantContext; public NHSharedDatabaseMultiTenancyInterceptor(TenantContext tenantContext) { this.tenantContext = tenantContext; } public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types) { int index = Array.IndexOf(propertyNames, "tenantId"); if (index == -1) return false; state[index] = this.tenantContext.ID; entity.GetType() .GetField("tenantId", BindingFlags.Instance | BindingFlags.NonPublic) .SetValue(entity, tenantContext.ID); return base.OnSave(entity, id, state, propertyNames, types); } }
Multitenant Reads
Configuring reads is much simpler. You just need to enable a filter and provide the parameter. First you need to define the actual filter.
MultitenantFilter.cspublic class MultitenantFilter : FilterDefinition { public MultitenantFilter() { WithName("MultitenantFilter").AddParameter("TenantId", NHibernateUtil.Guid); } }
Then you need to enable it.
NHUnitOfWorkMultitenancy.cspublic class NHUnitOfWorkMultitenancy : NHUnitOfWork { public NHUnitOfWorkMultitenancy(NHConfiguration nhConfiguration, TenantContext tenantContext) : base(nhConfiguration) { this.Session.EnableFilter("MultitenantFilter").SetParameter("TenantId", tenantContext.ID); } }
Finally you just need to apply it.
public class SomeMap : ClassMap<Some> { public SomeMap() { //.. //This is used for writes Map(Reveal.Member<Some>("tenantId")).Not.Nullable(); //This is used for reads ApplyFilter<MultitenantFilter>("TenantId = :TenantId"); } }
*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment.