70

Basically I have a CMS backend I built using ASP.NET MVC and now I'm moving on to the frontend site and need to be able to load pages from my CMS database, based on the route entered.

So if the user enters example.com/students/information, MVC would look in the pages table to see if a page exists that has a permalink that matches students/information, if so it would redirect to the page controller and then load the page data from the database and return it to the view for display.

So far I have tried to have a catch all route, but it only works for two URL segments, so /students/information, but not /students/information/fall. I can't find anything online on how to accomplish this, so I though I would ask here, before I find and open source ASP.NET MVC CMS and dissect the code.

Here is the route configuration I have so far, but I feel there is a better way to do this.

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

        // Default route to handle core pages
        routes.MapRoute(null,"{controller}/{action}/{id}",
                        new { action = "Index", id = UrlParameter.Optional },
                        new { controller = "Index" }
        );

        // CMS route to handle routing to the PageController to check the database for the route.

        var db = new MvcCMS.Models.MvcCMSContext();
        //var page = db.CMSPages.Where(p => p.Permalink == )
        routes.MapRoute(
            null,
            "{*.}",
            new { controller = "Page", action = "Index" }
        );
    }

If anybody can point me in the right direction on how I would go about loading CMS pages from the database, with up to three URL segments, and still be able to load core pages, that have a controller and action predefined.

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Carl Weis
  • 6,794
  • 15
  • 63
  • 86

2 Answers2

131

You can use a constraint to decide whether to override the default routing logic.

public class CmsUrlConstraint : IRouteConstraint
{
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        var db = new MvcCMS.Models.MvcCMSContext();
        if (values[parameterName] != null)
        {
            var permalink = values[parameterName].ToString();
            return db.CMSPages.Any(p => p.Permalink == permalink);
        }
        return false;
    }
}

use it in route definition like,

routes.MapRoute(
    name: "CmsRoute",
    url: "{*permalink}",
    defaults: new {controller = "Page", action = "Index"},
    constraints: new { permalink = new CmsUrlConstraint() }
);

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

Now if you have an 'Index' action in 'Page' Controller like,

public ActionResult Index(string permalink)
{
    //load the content from db with permalink
    //show the content with view
}
  1. all urls will be caught by the first route and be verified by the constraint.
  2. if the permalink exists in db the url will be handled by Index action in Page controller.
  3. if not the constraint will fail and the url will fallback to default route(i dont know if you have any other controllers in the project and how you will decide your 404 logic).

EDIT

To avoid re querying the cms page in the Index action in Page controller, one can use the HttpContext.Items dictionary, like

in the constraint

var db = new MvcCMS.Models.MvcCMSContext();
if (values[parameterName] != null)
{
    var permalink = values[parameterName].ToString();
    var page =  db.CMSPages.Where(p => p.Permalink == permalink).FirstOrDefault();
    if(page != null)
    {
        HttpContext.Items["cmspage"] = page;
        return true;
    }
    return false;
}
return false;

then in the action,

public ActionResult Index(string permalink)
{
    var page = HttpContext.Items["cmspage"] as CMSPage;
    //show the content with view
}
starball
  • 20,030
  • 7
  • 43
  • 238
shakib
  • 5,449
  • 2
  • 30
  • 39
  • 3
    Awesome it worked great, just had to add a check if (values[parameterName] != null), but otherwise perfect! Thank You :) – Carl Weis Apr 16 '13 at 17:40
  • Hello @shakib, what about the performance if there are 50K items. As far as i know there is a route table caching or something like that but in any case it checks from db. – Barbaros Alp Jan 14 '14 at 13:49
  • @BarbarosAlp as you said "in any case it checks from db", the best i can think of is to optimize the db querying. – shakib Jan 14 '14 at 19:07
  • @BarbarosAlp You could also create some kind of flat file that would be loaded in the memory of the app that would represent some sort of "cache" of your database route table. Then you'll just need to implement some sort of "create cache" and "flush cache" action. – RPDeshaies Nov 13 '14 at 14:51
  • Does anyone know how this would be altered if you required an idea from the cms table to be passed to the route? For example, if the permalink were associated with an id and that id needed to be passed to the controller action? I know this is a real old post here ... – jallen May 15 '15 at 23:50
  • ^ not "idea" but "id" – jallen May 16 '15 at 00:12
  • Never mind! I just have to re-query the cms pages table once i get to the action. – jallen May 16 '15 at 00:16
  • @BarbarosAlp That is a quite valid concern, I'd suggest using second level cache to speed that process up, keep in mind second level cache requires EF 6.1 (https://efcache.codeplex.com/ and https://msdn.microsoft.com/en-us/magazine/hh394143.aspx) – c0y0teX May 18 '15 at 19:06
  • 1
    @jallen its been a while, but you can use `HttpContext.Items` dictionary to avoid re querying the cms page in the action. – shakib Sep 05 '15 at 16:15
  • What about loading all permalinks to ApplicationContext on application start function and search in it instead of searching db in each request?? – mesut Nov 25 '15 at 12:45
  • Is it possible to add routes to routetable when application is running? – I Love Stackoverflow Apr 13 '17 at 10:09
  • @shakib When I use this - my permalink is null - what if I only want to look up the Url see if it is in my table or similar and if so route it that way.Kind of Like a URL redirect without redirecting .. So they want mystuff/some-thing and the real link is mystuff/real-thing – Ken Mar 09 '19 at 15:27
0

I use simpler approach that doesn't require any custom router handling. Simply create a single/global Controller that handles a few optional parameters, then process those parameters as you like:

//Route all traffic through this controller with the base URL being the domain
[Route("")]
[ApiController]
public class ValuesController : ControllerBase
{
    //GET api/values
    [HttpGet("{a1?}/{a2?}/{a3?}/{a4?}/{a5?}")]
    public ActionResult<IEnumerable<string>> Get(string a1 = "", string a2 = "", string a3 = "", string a4 = "", string a5 = "")
    {
        //Custom logic processing each of the route values
        return new string[] { a1, a2, a3, a4, a5 };
    }
}

Sample output at example.com/test1/test2/test3

["test1","test2","test3","",""]
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Kon Rad
  • 174
  • 2
  • 10