Dynamic Routes from database for ASP.NET MVC CMS
Asked Answered
H

2

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.

Hierology answered 15/4, 2013 at 23:42 Comment(0)
K
130

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
}
Katlaps answered 16/4, 2013 at 7:13 Comment(12)
Awesome it worked great, just had to add a check if (values[parameterName] != null), but otherwise perfect! Thank You :)Hierology
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.Augusto
@BarbarosAlp as you said "in any case it checks from db", the best i can think of is to optimize the db querying.Katlaps
@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.Chamomile
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 ...Utilitarian
^ not "idea" but "id"Utilitarian
Never mind! I just have to re-query the cms pages table once i get to the action.Utilitarian
@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 (efcache.codeplex.com and msdn.microsoft.com/en-us/magazine/hh394143.aspx)Pupa
@Utilitarian its been a while, but you can use HttpContext.Items dictionary to avoid re querying the cms page in the action.Katlaps
What about loading all permalinks to ApplicationContext on application start function and search in it instead of searching db in each request??Defector
Is it possible to add routes to routetable when application is running?Grindelia
@Katlaps 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-thingCorsage
C
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","",""]
Conni answered 3/7, 2019 at 19:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.