Categories
Hard Skills

ASP.NET MVC 5 Custom “Static” Razor Pages

This post is one of the ASP.NET MVC 5 Custom Routes series.

The problem

You can usually identify this problem when you are creating a lot of custom routes for each one of your empty controller actions. If you follow this antipattern you will end up with an endless RouteConfig.cs , that will point to too much unneeded controllers and empty actions.

Unfortunately, I saw this a few times. There are different ways how you can solve this problem but I hope you will like the way I handled this problem.

Example Project

You can find the example project for this post here:

https://github.com/botondev/ASP.NET-MVC5-CustomRoute

Solution

The beauty of ASP.NET is its extendibility. In the case of routing it means you can write your own constraints and rules to let the framework know, you want something unique. When you create a new ASP.NET MVC project it automatically creates a “Default” route for you that you can find in “~/App_Start/RouteConfig.cs”.

I assume you already know the basics of ASP.NET MVC, if not please leave a comment below and I can help you with the details or you can visit the ASP.NET MVC page.

Custom Route

The first thing you have to do is to create a new route rule just before the “Default” route that looks like this.


routes.MapRoute(
  name: "StaticPages",
  url: "content/{staticPageName}",
  defaults: new { action = "Index", controller = "StaticPages" },
  constraints: new { isStaticPlace = new StaticPagesConstraint() }
);

Custom Route Constraint (StaticPagesConstraint)

Here you can see we defined a new constraint for static pages. In the example solution, you can find it in the route of “CustomRoute” project.


namespace CustomRoute
{
  public class StaticPagesConstraint : IRouteConstraint
  {
    public bool Match
    (
      HttpContextBase httpContext,
      Route route,
      string parameterName,
      RouteValueDictionary values,
      RouteDirection routeDirection
    )
    {
      return RouteHelper.IsDestinationViewExists(values, "staticPageName", "~/Views/StaticPages");
    }
  }
}

In order to be able to define a new constraint, you have to inherit from the System.Web.Routing.IRouteConstraint interface. You can read more about this interface here.

RouteHelper

In this implementation of IRouteConstraint we called RouteHelper.IsDestinationViewExists(values, “staticPageName”, “~/Views/StaticPages”);

Here it is its implementation


static class RouteHelper
{
  /// <summary>
  /// Case insensitive lookup for a view in a specific folder.
  /// </summary>
  /// <param name="values">IRouteConstraint's RouteValueDictionary</param>
  /// <param name="valueKey">Case insensitive name of the .cshtml view passed from the route</param>
  /// <param name="path">Container folder path like: "~/Views/StaticPages"</param>
  /// <returns></returns>
  public static bool IsDestinationViewExists(RouteValueDictionary values, string valueKey, string path)
  {
    //we have to make sure that we have a view name specified to look for
    if (values[valueKey] == null) return false;
 
    //get all .cshtml file in the specified path
    List<string> destinationViewList = Directory.GetFiles(
      System.Web.Hosting.HostingEnvironment.MapPath(path), "*.cshtml").ToList();
 
    foreach (string destinationPath in destinationViewList)
    {
      FileInfo destinationInfo = new FileInfo(destinationPath);
      //remove the extension ".cshtml" from the view name
      string destination = destinationInfo.Name.Replace(destinationInfo.Extension, "");
 
      //case insensitive match
      //if the file name matches the requested valueKey/view it returns true
      if (destination.ToLower() == values[valueKey].ToString().ToLower())
      return true;
    }
 
    //if we couldn't find a matching view
    return false;
  }
}

In short it returns true if it finds the requested view specified in MapRoute.

When you call it from StaticPagesConstraint you have to pass the RouteValueDictionary that contains all the key value pairs defined in our route definition. In that case it will find the “action” as “Index”, the “controller” as “StaticPages” and “staticPageName” as whatever were requested from the browser.

In a case of “/content/AboutUs” the staticPageName will be “AboutUs”.

You also have to pass the valueKey that will contain the requested view. It is the same string that you specified in the MapRoute’s url between the curly braces. In our case it is “staticPageName”.

The third parameter called “path” must be the path to the controller’s view folder, where the controller can find it. If you specified a different folder from that, you have to make sure in the controller to tell it where it can find the view. I think it is much easier to use the framework’s convention in that situation.

The Single Controller with infinite views

The next step is to create the controller that is simple as this:


public class StaticPagesController : Controller
{
  public ActionResult Index(string staticPageName)
  {
    //it must pass the name of the view
    return View(staticPageName);
  }
}

The key feature of this controller is that it requests whatever page you defined in the staticPageName variable. You just have to make sure to have a view with the same name in the controller’s view folder with “.cshtml” extension. Remember that the current implementation of “RouteHelper” is case insensitive.

Considerations

You do not have to worry about if it will find a view or not because our route constraint will make sure the requested view does exist. If it doesn’t then the route handler won’t call our controller, instead, it will continue to search for a matching MapRoute definition. If the Default MapRoute does not fit as a last resort then it will send a 404 response to the browser with a custom 404 page if we specified one.

By Botond Bertalan

I love programming and architecting code that solves real business problems and gives value for the end-user.

One reply on “ASP.NET MVC 5 Custom “Static” Razor Pages”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.