Search code examples
asp.net-mvcasp.net-mvc-4iisorchardcmsorchardcms-1.9

How can I mimic two domains on a single asp.net website? Are RouteConstraints an option?


I'm running a single instance of Orchard CMS on my web server with two custom modules, ModuleFirst and ModuleSecond. For reasons, I want these two to act as separate websites with their own domain and homepage. I can not set up additional websites or use Orchard's built-in Tenants feature.

What I have

The way I went about achieving this is as follows:

  • Added two bindings to my website in IIS: first-domain.com and second-domain.com
  • Implemented a ThemeSelector (which I think acts like an ActionFilter) that switches the theme based on the host of the Url in the incoming Request

    if (host.Contains("second-domain.com"))
    {
        useSecondTheme = true;
    }
    
  • Make sure all routes are unique

This is working reasonably well for the most part. I can navigate to first-domain.com/foo and second-domain.com/bar and it looks like I'm on different websites.

The problem

For the two "homepages" I can't make a unique route because I don't want to add any suffixes. Both projects define a blank route that should lead to their respective Home/Index but I can't figure out how to make this work.

new RouteDescriptor {
    Priority = 90,
    Route = new Route(
        "",
        new RouteValueDictionary { // Defaults
            {"area", "ModuleFirst"},
            {"controller", "Home"},
            {"action", "Index"},
        },
        new RouteValueDictionary(), // Constraints
        new RouteValueDictionary { // Datatokens
            {"area", "ModuleFirst"}
        },
        new MvcRouteHandler())
}

new RouteDescriptor {
    Priority = 100,
    Route = new Route(
        "",
        new RouteValueDictionary { // Defaults
            {"area", "ModuleSecond"},
            {"controller", "Home"},
            {"action", "Index"},
        },
        new RouteValueDictionary(), // Constraints
        new RouteValueDictionary { // Datatokens
            {"area", "ModuleSecond"}
        },
        new MvcRouteHandler())
}

What I tried

I tried to implement an ActionFilter that Redirects to ModuleFirst/Home/Index when a request with host url first-domain.com reaches ModuleSecond/Home/Index but this obviously doesn't work since it just keeps hitting the highest priority route over and over and breaks the website.

I have also tried to implement a custom RouteConstraint on the route with the highest priority to block all incoming request that don't come from its intended domain, assuming that those would then fall back on the lower priority route.

public class SecondConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return httpContext.Request.Url.DnsSafeHost.Contains("second-domain.com");
    }
}

used as follows:

new RouteDescriptor {
    Priority = 100,
    Route = new Route(
        "",
        new RouteValueDictionary { // Defaults
            {"area", "ModuleSecond"},
            {"controller", "Home"},
            {"action", "Index"},
        },
        new RouteValueDictionary { // Constraints
            {"isSecond", new SecondConstraint()}
        }, 
        new RouteValueDictionary { // Datatokens
            {"area", "ModuleSecond"}
        },
        new MvcRouteHandler())
}

I can now navigate to second-domain.com just fine and get the correct page but navigating to first-domain.com times out. I haven't managed to find any examples of RouteConstraints in Orchard though, so maybe I'm doing it wrong.


Solution

  • Though I would recommend anyone who is about to do something like this from scratch to use tenants and OAuth instead if possible, as Bertrand suggests in his comment, I thought of a fairly clean way to accomplish what I wanted.

    Instead of using two separate modules, I just use one.

    In my one module, I implement IRouteProvider to override the default route, which catches both first-domain.com/ and second-domain.com/

    public class Routes : IRouteProvider
    {
        public void GetRoutes(ICollection<RouteDescriptor> routes)
        {
            foreach (var routeDescriptor in GetRoutes())
                routes.Add(routeDescriptor);
        }
    
        public IEnumerable<RouteDescriptor> GetRoutes()
        {
            return new[] {
                new RouteDescriptor {
                    Priority = 100,
                    Route = new Route(
                        "",
                        new RouteValueDictionary {
                            {"area", "MyModule"},
                            {"controller", "Home"},
                            {"action", "Index"},
                        },
                        new RouteValueDictionary(),
                        new RouteValueDictionary {
                            {"area", "MyModule"}
                        },
                        new MvcRouteHandler())
                }
            };
        }
    }
    

    Then I created a custom Part with just a single property bool IsHomepage and attached it to the Page Type

    public class SecondHomepagePart : ContentPart<SecondHomepagePartRecord>
    {
        public bool IsHomepage
        {
            get { return Retrieve(r => r.IsHomepage); }
            set { Store(r => r.IsHomepage, value); }
        }
    }
    

    In the driver I make sure that there is only ever one "second homepage"

    protected override DriverResult Editor(SecondHomepagePart part, IUpdateModel updater, dynamic shapeHelper)
    {
            if (updater.TryUpdateModel(part, Prefix, null, null))
            {
                if (part.IsHomepage)
                {
                    var otherHomepages = _orchardServices.ContentManager.Query<SecondHomepagePart>().Where<SecondHomepagePartRecord>(r => r.IsHomepage && r.Id != part.Id).List();
    
                    foreach (var homepagePart in otherHomepages)
                    {
                        homepagePart.IsHomepage = false;
                    }
                }
            }
    
            return Editor(part, shapeHelper);
    }
    

    Then in my custom HomeController I just check the current domain, fetch the ContentItem with the blank AutoroutePart or the SecondHomepagePart where IsHomepage is set to true depending on the domain, build the Detail Shape of that item and then display that Shape in my custom Index.cshtml view.

    Controller:

    public ActionResult Index()
    {
            var host = HttpContext.Request.Url.DnsSafeHost;
    
            var model = new IndexViewModel();
            IContent homepageItem;
    
            if (host.Contains("first-domain.com"))
            {
                homepageItem = _homeAliasService.GetHomePage();
            }
            else
            {
                homepageItem = _orchardServices.ContentManager.Query<SecondHomepagePart>()
                    .Where<SecondHomepagePartRecord>(r => r.IsHomepage)
                    .List()
                    .FirstOrDefault();
            }
    
            model.Homepage = _orchardServices.ContentManager.BuildDisplay(homepageItem, "Detail");
    
            return View(model);
    }
    

    View:

    @model MyModule.ViewModels.Home.IndexViewModel
    
    @if (Model.Homepage != null)
    {
        @Display(Model.Homepage)
    }
    

    I use the same check in an IConditionProvider and IThemeSelector to have a layer rule for each domain and to set the appropriate theme automatically. Now I essentially have two completely different websites to the outside world but with shared content, widgets, parts, custom settings etc. This is a quick and easy solution for a client who sells the same products from the same stock but under different brandings and under different conditions, along with some exclusive content that's different for both.