Dealing with Multitenancy, Hosts file and IIS

As I was setting up a demo project on my local computer for dealing with a Multi-Tenancy application, I came across with some problems (like many other times). The issue this time had to do with accessing the same web application using different host names, each one employed for handling a different tenant (client).

So, here are the first steps to configure Hosts file in Windows, Visual Studio Web Applications and IIS and to begin to work properly with this kind of multi-tenant functionality.

HOST FILE

To begin with, it is necessary to update the "host" file located at "C:\Windows\System32\drivers\etc". This file contains the mappings of IP addresses to host names. So, you have to add the host names you want to use to access your Local or Express IIS Server. As you can see below, I have added three different host names, "tenant1", "tenant2" and "tenant3", mapped to 127.0.0.1 IP address, which is assigned by default to local machine.

There are also three additional lines, mapped to "::1" address. This is for dealing with IPv6 IP addresses. Previous lines manage IPv4 IP addresses. You can obtain more information at here.

Host File

VISUAL STUDIO WEB APPLICATION PROJECT

I have set up my Visual Studio Web Application Project to make use of Local IIS, using "http://localhost/MultitenancyDemo" as a project URL . The application will be accessed from  "http://tenant1/MultitenancyDemo",  "http://tenant2/MultitenancyDemo" and  "http://tenant3/MultitenancyDemo" URLs, passing the responsibility of serving different content to the same web application (based on host name).

Visual Studio Web Application Properties

Once you have filled up the "Project Url" field, you should "Create a Virtual Directory" in the Local IIS.

LOCAL IIS (INTERNET INFORMATION SERVER)

Just after creating the new virtual directory, this will appear under the Default site (in most cases) or the mapped site based on "Project Url" you entered before. Without any other type of specification the virtual directory will be added to the website using "port=80" and  "protocol=http" in its binding properties.

Here is the Default site along with the new just created application:

IIS Admin Window

And below is the basic configuration from the virtual directory:

Web Application Config in IIS

At last, here is the basic configuration for the binding on "Default" site.

Binding for Web Application in IIS

Eventually, you will be able to gain access to the same web application using different host names. The application will manage each different tenant to show different information, styles, images, etc:

  • http://tenant1/MultitenancyDemo

  • http://tenant2/MultitenancyDemo

  • http://tenant3/MultitenancyDemo

I expect to write a little more about this topic, but it will be later....The main rule to accomplish is to extract the "host" name from the url and employ it to select, filter and secure any kind of returned information. For now, here it is the structure of my solution:

Solution Tree

Briefly, I'm using a typical MVC Web application project with some little new features.

  • I've incorporated a ViewModels folder to include a new layer of abstraction between de business model and the user interface (UI). It's not really necessary but it gives you more flexibility to deal with changes of functionality.
  • "Content" folder is divided into different separated folders to contain "Texts", "Images", "Styles", etc. independently. It will be very useful in case you have to translate and localize the application to other languages.
  • "Helpers" folder will contain classes to be used as "helpers" all over the application, as its own name implies.
  • "DataAccess" will contain the employed classes to access data from database. As I indicate at "References" section, I've based this solution on that source code (Simple Multitenancy with ASP.Net MVC 4). Then, a LinqToSql class "DataClasses.dbml" is used to interact with a SQL Server Database (this database only holds a very simple "Tenant" table with an "Id" as primary key and simple nvarchar fields to save basic information) and a "LinqTenantDataProvider" class implementing "ITenantDataProvider" to interact with LinqToSql classes.‚Äč

Below are some key pieces of my code:

Global.asax file

public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }

        internal protected void Application_BeginRequest(object sender, EventArgs e)
        {
            // Get the client host name from the incoming request.
            GlobalHelper.Host = this.Request.Headers["Host"].ToLower();
        }
    }

As it is said at www.w3.org, the Host request-header field specifies the Internet host and port number of the resource being requested. The Host field value must represent the naming authority of the origin server or gateway given by the original URL. This allows the origin server or gateway to differentiate between internally-ambiguous URLs, such as the root "/" URL of a server for multiple host names on a single IP address.

In other words, Host header lets you manage requests over differents domains (or subdomains) on the same IP Address.The Host header will be used by the web-server to bind incoming requests to the correct host or website. Then, you have the possibility of using your website to read that header manually and provide different behavior according to different domains (or subdomains) addressed. This is exactly what I'm doing in the code above whitin Application_BeginRequest handler.

 

- ITenantDataProvider interface:

namespace MultitenancyDemo.DataAccess
{
    public interface ITenantDataProvider
    {
        Tenant GetTenant(string host);
    }
}

 

LinqTenantDataProvider class:

 public class LinqTenantDataProvider : ITenantDataProvider
    {
        public LinqTenantDataProvider()
        {
        }

        public Tenant GetTenant(string host)
        {
            // Check host name.
            if (string.IsNullOrEmpty(host))
            {
                // Throws an exception if host is not present
                throw new ArgumentNullException("host");
            }

            // Removes port number if necessary
            int portNumberIndex = host.LastIndexOf(':');
            if (portNumberIndex > 0)
            {
                host = host.Substring(0, portNumberIndex);
            }

            // Get an instance of LINQ to SQL Data Context Class
            DataClassesDataContext database = new DataClassesDataContext();

            // Matches the host name with the content of host field name from stored information
            Tenant result = (from tenant in database.Tenants
                             where tenant.Host == host
                             select tenant).SingleOrDefault();

            // Return the selected tenant or null if not encountered
            return result;
        }
    }

 

Home Controller:

public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            // Retrieve the client-specific data from the data provider.
            ITenantDataProvider provider = new LinqTenantDataProvider();
            Tenant tenant = provider.GetTenant(GlobalHelper.Host);

            // Create model
            TenantBaseModel model = new TenantBaseModel();

            if (tenant != null)
            {
                // Data from different tenants
                model.Name = tenant.Name;
                model.Title = tenant.Title;
                model.Theme = tenant.Theme;
                model.About = tenant.About;
            }
            else
            {
                // Data from default site
                model.Name = RGlobal.DefaultSiteName;
                model.Title = RGlobal.DefaultSiteTitle;
                model.Theme = string.Empty;
                model.About = string.Empty;
            }

            // Return the Index view with the ViewModel data.
            return View(new TenantBaseViewModel(model));
        }
    }

ISSUES

All of the above commented works fine by using Local IIS, but I haven't be able to make work properly using IIS Express. So, I'd really appreciate some kind of help for me....

REFERENCES

http://www.dennisonpro.info | Simple Multitenancy with ASP.Net MVC 4

Add comment