In my previous post, Multi-tenancy part 1: Strategy, I talked about some of the high level decisions we have to make when building a single software product for multiple users with diverse requirements. Today I'm going to look at implementing basic multi-tenancy with Suteki Shop. I'm going to assume that my customers have identical functional requirements, but will obviously need to have separate styles and databases. Some other simple configurable items will also be different, such as the name of their company and their contact email address.
But first I want to suggest something that's become quite clear as I've started to think more about implementing multi-tenancy. I call it:
Hadlow's first law of multi-tenancy: A multi-tenanted application should not look like a multi-tenanted application.
What do I mean by this? The idea is that you should not have to change your existing single-tenant application in any way in order to have it serve multiple clients. If you build your application using SOLID principles, if you architect it as a collection of components using Dependency Injection and an IoC container, then you should be able to compose the components at runtime based on some kind of user context without changing the components themselves.
I am going to get Suteki Shop to serve two different tenants without changing a single existing line of of (component) code.
We are going to be serving two clients. The first one is our existing client, the mod-tastic Jump the Gun. I've invented the new client zanywear.com. I just made the name up, it's actually a registered domain name but it's not being used. We're going to serve our clients from the same application instance, so we create a new web site and point it to an instance of Suteki Shop. Now we configure two host headers (AKA Site Bindings) for the web application:
test.jumpthegun.co.uk
zanywear.com
For testing purposes (and because I don't own zanywear.com :) I've added the two domains to my C:\WINDOWS\system32\drivers\etc\hosts file so that it looks like this:
# Copyright (c) 1993-1999 Microsoft Corp.## This is a sample HOSTS file used by Microsoft TCP/IP for Windows.## This file contains the mappings of IP addresses to host names. Each# entry should be kept on an individual line. The IP address should# be placed in the first column followed by the corresponding host name.# The IP address and the host name should be separated by at least one# space.## Additionally, comments (such as these) may be inserted on individual# lines or following the machine name denoted by a '#' symbol.## For example:## 102.54.94.97 rhino.acme.com # source server# 38.25.63.10 x.acme.com # x client host127.0.0.1 localhost127.0.0.1 test.jumpthegun.co.uk127.0.0.1 zanywear.com
Now when I browse to test.jumpthegun.co.uk or zanywear.com I see the Jump the Gun website.
The task now is to choose a different database, style-sheet and some basic configuration settings when the HTTP request's host name is zanywear.com. Conventiently Suteki Shop has two services that define these items. The first is IConnectionStringProvider which provides (you guessed it) the database connection string:
namespace Suteki.Common.Repositories
{public interface IConnectionStringProvider{string ConnectionString { get; }}}
And the other is the somewhat badly named IBaseControllerService that supplies some repositories and config values to be consumed by the master view:
using Suteki.Common.Repositories;
namespace Suteki.Shop.Services
{public interface IBaseControllerService{IRepository<Category> CategoryRepository { get; }
IRepository<Content> ContentRepository { get; }
string GoogleTrackingCode { get; set; }string ShopName { get; set; }string EmailAddress { get; set; }string SiteUrl { get; }string MetaDescription { get; set; }string Copyright { get; set; }string PhoneNumber { get; set; }string SiteCss { get; set; }}}
Note that this allows us to to set the name of the style-sheet and some basic information about the shop.
In order to choose which component is used to satisfy a service at runtime we use an IHandlerSelector. This interface was recently introduced to the Windsor IoC container by Oren Eini (AKA Ayende Rahien) specifically to satisfy the requirements of multi-tenanted applications. You need to compile the trunk if you want to use it. It's not in the release candidate. It looks like this:
using System;
namespace Castle.MicroKernel
{/// <summary>
/// Implementors of this interface allow to extend the way the container perform
/// component resolution based on some application specific business logic.
/// </summary>
/// <remarks>
/// This is the sibling interface to <seealso cref="ISubDependencyResolver"/>.
/// This is dealing strictly with root components, while the <seealso cref="ISubDependencyResolver"/> is dealing with
/// dependent components.
/// </remarks>
public interface IHandlerSelector{/// <summary>
/// Whatever the selector has an opinion about resolving a component with the
/// specified service and key.
/// </summary>
/// <param name="key">The service key - can be null</param>
/// <param name="service">The service interface that we want to resolve</param>
bool HasOpinionAbout(string key, Type service);/// <summary>
/// Select the appropriate handler from the list of defined handlers.
/// The returned handler should be a member from the <paramref name="handlers"/> array.
/// </summary>
/// <param name="key">The service key - can be null</param>
/// <param name="service">The service interface that we want to resolve</param>
/// <param name="handlers">The defined handlers</param>
/// <returns>The selected handler, or null</returns>
IHandler SelectHandler(string key, Type service, IHandler[] handlers);
}}
The comments are self explanatory. I've implemented the interface as a HostBasedComponentSelector that can choose components based on the HTTP request's SERVER_NAME value:
using System;
using System.Linq;
using System.Web;
using Castle.MicroKernel;
using Suteki.Common.Extensions;
namespace Suteki.Common.Windsor
{public class HostBasedComponentSelector : IHandlerSelector{private readonly Type[] selectableTypes;public HostBasedComponentSelector(params Type[] selectableTypes){this.selectableTypes = selectableTypes;
}public bool HasOpinionAbout(string key, Type service){foreach (var type in selectableTypes){if(service == type) return true;}return false;}public IHandler SelectHandler(string key, Type service, IHandler[] handlers){var id = string.Format("{0}:{1}", service.Name, GetHostname());var selectedHandler = handlers.Where(h => h.ComponentModel.Name == id).FirstOrDefault() ??GetDefaultHandler(service, handlers);return selectedHandler;
}private IHandler GetDefaultHandler(Type service, IHandler[] handlers)
{if (handlers.Length == 0)
{throw new ApplicationException("No components registered for service {0}".With(service.Name));}return handlers[0];
}protected string GetHostname(){return HttpContext.Current.Request.ServerVariables["SERVER_NAME"];}}}
It works like this: It expects an array of types to be supplied as constructor arguments. These are the service types that we want to choose based on the host name. The HasOpinionAbout method simply checks the supplied serivce type against the array of types and returns true if there are any matches. If we have an opinion about a service type the container will ask the IHandlerSelector to supply a handler by calling the SelectHandler method. We create an id by concatenating the service name with the host name and then return the component that's configured with that id. So the configuration for Jump the Gun's IConnectionStringProvider will look like this:
<componentid="IConnectionStringProvider:test.jumpthegun.co.uk"service="Suteki.Common.Repositories.IConnectionStringProvider, Suteki.Common"type="Suteki.Common.Repositories.ConnectionStringProvider, Suteki.Common"lifestyle="transient"><parameters><ConnectionString>Data Source=.\SQLEXPRESS;Initial Catalog=JumpTheGun;Integrated Security=True</ConnectionString></parameters></component>
Note the id is <name of service>:<host name>.
The configuration for Zanywear looks like this:
<componentid="IConnectionStringProvider:zanywear.com"service="Suteki.Common.Repositories.IConnectionStringProvider, Suteki.Common"type="Suteki.Common.Repositories.ConnectionStringProvider, Suteki.Common"lifestyle="transient"><parameters><ConnectionString>Data Source=.\SQLEXPRESS;Initial Catalog=Zanywear;Integrated Security=True</ConnectionString></parameters></component>
Note that you can have multiple configurations for the same service/component in Windsor so long as ids are different.
When the host name is test.jumpthegun.co.uk the HostBasedComponentSelector will create a new instance of ConnectionStringProvider with a connection string that points to the JumpTheGun database. When the host name is zanywear.com it will create a new instance of ConnectionStringProvider with a connection string that points to the Zanywear database. We configure our IBaseControllerService in a similar way.
The only thing left to do is register our IHandlerSelector with the container. When I said I didn't have to change a single line of code I was telling a fib, we do have to change the windsor initialization to include this:
protected virtual void InitializeWindsor(){if (container == null){// create a new Windsor Container
container = new WindsorContainer(new XmlInterpreter("Configuration\\Windsor.config"));// register handler selectors
RegisterHandlerSelectors(container);// register WCF integration
RegisterWCFIntegration(container);// automatically register controllers
container.Register(AllTypes.Of<Controller>().FromAssembly(Assembly.GetExecutingAssembly()).Configure(c => c.LifeStyle.Transient.Named(c.Implementation.Name.ToLower())));// set the controller factory to the Windsor controller factory (in MVC Contrib)
System.Web.Mvc.ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(container));
}}/// <summary>
/// Get any configured IHandlerSelectors and register them.
/// </summary>
/// <param name="windsorContainer"></param>
protected virtual void RegisterHandlerSelectors(IWindsorContainer windsorContainer){var handlerSelectors = windsorContainer.ResolveAll<IHandlerSelector>();foreach (var handlerSelector in handlerSelectors){windsorContainer.Kernel.AddHandlerSelector(handlerSelector);}}
The handler selector setup occurs in the RegisterHandlerSelectors method. We simply ask the container to resolve any configured IHandlerSelectors and add them in. The configuration for our HostBasedComponentSelector looks like this:
<componentid="urlbased.handlerselector"service="Castle.MicroKernel.IHandlerSelector, Castle.MicroKernel"type="Suteki.Common.Windsor.HostBasedComponentSelector, Suteki.Common"lifestyle="transient"><paramters><selectableTypes><array><item>Suteki.Shop.Services.IBaseControllerService, Suteki.Shop</item><item>Suteki.Common.Repositories.IConnectionStringProvider, Suteki.Common</item></array></selectableTypes></paramters></component>
Note that we are configuring the list of services that we want to be selected by the HostBasedHandlerSelector by using the array parameter configuration syntax.
And that's it. We now have a single instance of Suteki Shop serving two different clients: Jump the Gun and Zanywear.
Today I've demonstrated the simplest case of multi-tenanting. It hardly qualifies as such because our two tenants both have identical requirements. The core message here is that we didn't need to change a single line of code in any of our existing components. You can still install Suteki Shop and run it as a single-tenant application by default.
In the next installment I want to show how we can provide different implementations of components using this approach. Later I'll deal with the more complex problem of variable domain models. Watch this space!
9 comments:
Congratulations. Excellent work.
This is a great article. Thanks Mike.
It really serves to show why a container can be so useful when developing any non-trivial application. Sometimes, I find it hard to articulate to my juniors why the container is so neccessary - from now on I will just point developers to this article!
This article has got my mind ticking over at a rapid rate... think of the possibilities!!
Luca, thanks!
Berko,
Yes, the possibilities are huge. I'd be very interested in hearing any ideas you might you have.
Hi Mike. Are you any closer to writing the other parts of this series? Variable domain models, etc.?
Cheers, Johnny
Johnny,
Sorry, it's gone on to the back burner for the time being :(
I tried to use your code and i got an error.
Because of myself being blind a user from stackoverflow.com pointed me out that there is a typo in your article:
in the urlbased.handlerselector component there is a "paramters" section, but should be "parameters".
Hope it helps someone in the future...
BTW ... great article!!
Hi Mike,
do you still have the sample app handy? I mean we can understand that you do not have the time to blog all the details, but maybe post the code? ;)
Thanks.
Thanks Mike. I am working on exactly this sort of issue right now and your post saved me a lot of time. I really like the way you handled HasAnOpinionAbout.
Can you provide a sample project or a sample code to download
thank you
Post a Comment