Andre Pena
Andre Pena

Reputation: 59416

How to set up a multitenancy application using ASP.NET MVC?

A multitenancy application is an app that is shared by multiple organizations (medical practices, law offices..) and each organization, in turn, has it's own users. They all log on a centralized environment.

To be identified within the application, the organization must be expressed in the URL. There are two major URL forms for that. Subdomains and folders:

At first I tried the second because this solution does not involve dealing with DNSs. But then the problem: Everytime the developer needs to express an url (@Html.Action or @Url.Action) it has to explicitly pass the [tenancy_name]. This adds an unwanted overhead to the development. A possible workaround would be to implement custom versions of these HTML helpers that automatically take into account the tenancy name. I'm considering this option but looking for something more straitghtforward. I also realized ASP.NET MVC automatically passes route values for outgoing URLs but only when the controller and action are the same as the current. It would be nice if route values were always passed.

To implement the first option, the subdomain one, I think, I would need some third party DNS manager. I heard of DynDNS and took a look at it but I thought it unclear how they work just looking at their site. Would I need to trigger a web-service to tell them to create another subdomain everytime a new tenancy is created? Do they support wildcards in the DNS? Do they work on Windows Azure or shared hostings?

I'm here looking for directions. Which way should I go?

Upvotes: 3

Views: 921

Answers (2)

bushed
bushed

Reputation: 1060

Following made View resolution trivial in our app:

How to use: For views that you need to overload for a particular tenant - treat them same way as custom display modes: Following will work:

Index.cshtml
Index.cust2.mobile.cshtml

or

Partials/CustomerAgreement.cust1.cshtml
Partials/CustomerAgreement.cust2.cshtml

as far as I remember display/editor templates also work same way

Known issues: 1. You have to create Layouts for all combinations of primary+secondary (for whatever MVC-reason) 2. Regardless of what resharper is saying about its support of display modes - it does not support "." as part of the display mode name (here's an issue to track progress http://youtrack.jetbrains.com/issue/RSRP-422413)

//put in application start --------

DisplayModeProvider.Instance.Modes.Clear();
foreach (var displayMode in GetDisplayModes())
{
    DisplayModeProvider.Instance.Modes.Add(displayMode);
}

private IEnumerable<IDisplayMode> GetDisplayModes()
{
    return new CompoundDisplayModeBuilder()
        .AddPrimaryFilter(_ => dependencyResolver.GetService(typeof(IResolveCustomerFromUrl)).GetName(),
            "cust1",
            "cust2")
        .AddSecondaryFilter(ctx => ctx.Request.Browser.IsMobileDevice, "mobile")
        .BuildDisplayModes();
}

//end of application start part


//and the mode builder implementation:
public class CompoundDisplayModeBuilder
{
    private readonly IList<DefaultDisplayMode> _primaryDisplayModes = new List<DefaultDisplayMode>();
    private readonly IList<DefaultDisplayMode> _secondaryDisplayModes = new List<DefaultDisplayMode>();

    //NOTE: this is just a helper method to make it easier to specify multiple tenants in 1 line in global asax
    //You can as well remove it and add all tenants one by one, especially if resolution delegates are different
    public CompoundDisplayModeBuilder AddPrimaryFilter(Func<HttpContextBase, string> contextEval, params string[] valuesAsSuffixes)
    {
        foreach (var suffix in valuesAsSuffixes)
        {
            var val = suffix;
            AddPrimaryFilter(ctx => string.Equals(contextEval(ctx), val, StringComparison.InvariantCultureIgnoreCase), val);
        }

        return this;
    }

    public CompoundDisplayModeBuilder AddPrimaryFilter(Func<HttpContextBase, bool> contextCondition, string suffix)
    {
        _primaryDisplayModes.Add(new DefaultDisplayMode(suffix) { ContextCondition = contextCondition });
        return this;
    }

    public CompoundDisplayModeBuilder AddSecondaryFilter(Func<HttpContextBase, bool> contextCondition, string suffix)
    {
        _secondaryDisplayModes.Add(new DefaultDisplayMode(suffix) { ContextCondition = contextCondition });
        return this;
    }

    public IEnumerable<IDisplayMode> BuildDisplayModes()
    {
        foreach (var primaryMode in _primaryDisplayModes)
        {
            var primaryCondition = primaryMode.ContextCondition;
            foreach (var secondaryMode in _secondaryDisplayModes)
            {
                var secondaryCondition = secondaryMode.ContextCondition;
                yield return new DefaultDisplayMode(primaryMode.DisplayModeId + "." + secondaryMode.DisplayModeId){
                    ContextCondition = ctx => primaryCondition(ctx) && secondaryCondition(ctx)
                };
            }
        }

        foreach (var primaryFilter in _primaryDisplayModes)
        {
            yield return primaryFilter;
        }

        foreach (var secondaryFilter in _secondaryDisplayModes)
        {
            yield return secondaryFilter;
        }

        yield return new DefaultDisplayMode();
    }
}

Upvotes: 0

Marcelo De Zen
Marcelo De Zen

Reputation: 9507

look this project on codeplex, the "baseRoute" maybe can help you.

http://mvccoderouting.codeplex.com/

Regards.

Upvotes: 1

Related Questions