Search code examples
c#asp.net-mvcasp.net-mvc-4asp.net-mvc-routing

MVC4 Custom Routing - Validating Personalized Urls


I have a website in MVC4 that I am developing that requires some custom routing. It is a simple website with a few pages. For example:

/index
/plan
/investing
... etc.. a few others

Through an admin panel the site administrator can create "branded" sites, that basically mirror the above content, but swap out a few things like branded company name, logo etc. Once created, the URLs would look like

/{personalizedurl}/index
/{personalizedurl}/plan
/{personalizedurl}/investing

... etc... (exact same pages as the non branded pages.

I am validating the personalized urls with an action filter attribute on the controller method and returning a 404 if not found in the database.

Here is an example of one of my actions:

[ValidatePersonalizedUrl]
[ActionName("plan")]
public ActionResult Plan(string url)
{
    return View("Plan", GetSite(url));
}

Easy-peasy so far and works pretty well with the following routes:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Admin",
        url: "Admin/{action}/{id}",
        defaults: new { controller = "Admin", action = "Index", id = UrlParameter.Optional }
    );

    routes.MapRoute(
        name: "Default",
        url: "{action}",
        defaults: new { controller = "Default", action = "Index" }
    );

    routes.MapRoute(
        "Branded", // Route name
        "{url}/{action}", // URL with parameters
        new { controller = "Default", action = "Index" } // Parameter defaults
    );

/*
    routes.MapRoute(
        "BrandedHome", // Route name
         "{url}/", // URL with parameters
         new { controller = "Default", action = "Index" } // Parameter defaults
    );
*/
}

The problem I currently have is with the bottom commented out route. I'd like to be able to go to /{personalizedurl}/ and have it find the correct action (Index action in default controller). right now with the bottom line commented out, I get a 404 because it thinks its an action and its not found. When I un-comment it, the index pages, work however the individual actions do not /plan for example because it thinks its a pUrl and can't find it in the database.

Anyway, sorry for the long question. Any help or suggestions on how to set this up would be greatly appreciated.

James


Solution

  • The problem is that MVC will use the first matching url and since the second route is:

    routes.MapRoute(
      name: "Default",
      url: "{action}",
      defaults: new { controller = "Default", action = "Index" }
    );
    

    and that matches your /{personalizedurl}/ it will route to Default/{action}.

    What you want gets a bit tricky! I assume the personalizing is to be dynamic, not some static list of branded companies and you wouldn't want to recompile and deploy every time you add/remove a new one.

    I think you will need to handle this in the controller, it won't work well in routing; unless it is a static list of personalized companies. You will need the ability to check if the first part is one of your actions and to check if it is a valid company, I will give you an example with simple string arrays. I believe you will be building the array by query some sort of data store for your personalized companies. I also have created a quick view model called PersonalizedViewModel that takes a string for the name.

    Your routing will be simplified:

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
      routes.MapRoute(
        name: "Admin",
        url: "Admin/{action}/{id}",
        defaults: new { controller = "Admin", action = "Index", id = UrlParameter.Optional }
      );
    
      routes.MapRoute(
        name: "Default",
        url: "{url}/{action}",
        defaults: new { controller = "Default", action = "Index", url = UrlParameter.Optional }
      );
    }
    

    Here is the view model my example uses:

    public class PersonalizedViewModel
    {
      public string Name { get; private set; }
      public PersonalizedViewModel(string name)
      {
        Name = name;
      }
    }
    

    And the Default controller:

    public class DefaultController : Controller
    {
      private static readonly IEnumerable<string> personalizedSites = new[] { "companyA", "companyB" };
      private static readonly IEnumerable<string> actions = new[] { "index", "plan", "investing", "etc" };
    
      public ActionResult Index(string url)
      {
        string view;
        PersonalizedViewModel viewModel;
        if (string.IsNullOrWhiteSpace(url) || actions.Any(a => a.Equals(url, StringComparison.CurrentCultureIgnoreCase)))
        {
          view = url;
          viewModel = new PersonalizedViewModel("Default");
        }
        else if (personalizedSites.Any(s => s.Equals(url, StringComparison.CurrentCultureIgnoreCase)))
        {
          view = "index";
          viewModel = new PersonalizedViewModel(url);
        }
        else
        {
          return View("Error404");
        }
    
        return View(view, viewModel);
      }
    
      public ActionResult Plan(string url)
      {
        PersonalizedViewModel viewModel;
        if (string.IsNullOrWhiteSpace(url))
        {
          viewModel = new PersonalizedViewModel("Default");
        }
        else if (personalizedSites.Any(s => s.Equals(url, StringComparison.CurrentCultureIgnoreCase)))
        {
          viewModel = new PersonalizedViewModel(url);
        }
        else
        {
          return View("Error404");
        }
    
        return View(viewModel);
      }
    }