loading...

Kentico 12: Design Patterns Part 13 - Generating Page URLs

seangwright profile image Sean G. Wright Updated on ใƒป9 min read

Chain links

Photo by Markus Spiske on Unsplash

The web applications we build using Kentico CMS generate pages that are full of content (otherwise, why would we be using a CMS ๐Ÿคทโ€โ™€๏ธ?!).

That content can be text, markup, images, or links (to name a few possibilities).

Any links to our own site (internal links) need to be treated with care...

Due to the flexibility of the CMS platform, and the MVC framework, it's possible to create URLs that either don't navigate to any controllers or actions in your application, or don't represent any real content in the CMS ๐Ÿ˜ฎ.

Let's look at the various ways MVC helps us generate URLs, and my current method for generating URLs to content in the CMS tree.

Hopefully our URLs can be as flexible as our code and content management ๐Ÿ‘!


Generating URLs with MVC APIs

So much of the MVC framework is based around routes, controllers, and actions - they are the foundation of how we identify and separate one request for content from another.

Fortunately, MVC exposes various methods of using these parts of the framework to consistently, and (in some cases) with strong typing support, generate URLs.

The simplest way to generate URLs in MVC is to use any of the helper methods in the System.Web.Mvc.UrlHelper class.

Imagine we have a controller as follows:

public class HomeController : Controller
{
    public ActionResult Home(int id)
    {
        // ...

        return View();
    }
}

To generate a url for the above action and controller we can call UrlHelper.Action() as follows:

string url = urlHelper.Action("Index", "Home", new { id = 5 });

We can also do this in our Razor files using the following call:

<a href="@(Url.Action("Index", "Home", new { id = 5 }))"></a>

If we know the name of our route (which would have been specified when calling System.Web.Mvc.RouteCollection.MapRoute()) and the parameters that the route requires we can use UrlHelper.RouteUrl() as follows:

// Assume "Index" and "Home" are the default
//   action and controller values for this route

string url = urlHelper.RouteUrl(
    routeName: "Default Route", 
    routeValues: new { id = 5 });

While these are the official APIs for generating URLs in MVC, they are lacking in my opinion ๐Ÿ˜•.

They take strings and anonymous objects as parameters, which are very easy to typo ๐Ÿ˜’!

Not only that, but these parameters are values that are supposed to exactly match the names of C# classes, methods, and parameters.

What happens when rename something ๐Ÿค”?

Our URL generation breaks - but only at runtime! Time to go check all those links in the application ๐Ÿ˜ž!


T4MVC

There are alternatives to this standard approach of URL generation, like T4MVC.

T4MVC generates static classes with names of all the controllers and actions we might want to route to.

We would use T4MVC as follows (taken from their docs):

@Html.ActionLink("Dinner Details", MVC.Dinners.Details(Model.DinnerID))

My problem with this approach is it requires a re-build of the project to generate the templated classes, which means our code could be type-safe one minute, and then only after a compile show a bunch of errors.

This isn't what most developers expect from working with C#/.NET in Visual Studio.

That said, I think for extremely large MVC driven applications, it can help manage the increased complexity of URL generation.


Using MVC Futures

If we install the Microsoft.AspNet.Mvc.Futures NuGet package, we get access to some type safe URL generation from the Microsoft.Web.Mvc.LinkExtensions class.

Microsoft.AspNet.Mvc.Futures is a library of 'prototypes of features' being considered for 'future versions of MVC'.

Obviously, since ASP.NET Core has usurped classic MVC development, the Futures code will never make it into MVC 5, but you can still use it in your projects by installing the package ๐Ÿ‘.

Below we can see how these extensions use Expressions to generate an HTML anchor using the Controller classes in our application:

@Html.ActionLink<HomeController>(c => c.Index(5), "Home")

Unfortunately, this API doesn't give us a raw URL. If we wanted something like this on the UrlHelper class we'd need use the method Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression<T>() in our own code.

What's doubly unfortunate is that these APIs were not built for action methods that return Task<T> ๐Ÿ˜‘.

If you use it with Task returning actions, you'll get the following warning:

The current method calls an async method that returns a Task
or a Task<TResult> and doesn't apply the await operator to the result.
The call to the async method starts an asynchronous task.
However, because no await operator is applied, the program continues
without waiting for the task to complete...

This doesn't prevent the code from working, because we aren't actually executing the action.

But if you do choose to use this method, I hope you aren't writing a bunch of async controller actions ๐Ÿ˜ข, because those warnings are gonna get annoying.


I have seen older comments from people concerned about the performance impact of generating URLs with Expressions instead of the built-in methods I detailed at the beginning.

There is definitely going to be an impact on performance from using the Expression approach - and it's also definitely not going to matter when comparing that to things like database access time ๐Ÿ˜„.

With any of these things, we should:

  • Choose an approach that helps us manage the complexity of our apps
  • Measure our performance
  • Set goals of what level of performance is acceptable
  • Make small changes
  • Measure again to see if our goals were met, and repeat

... We should be using caching anyway, right ๐Ÿค“?


Simpler Type-Safe URLs with Func<> and nameof()

I created my own extension on UrlHelper that gives some type safety without going down the rabbit ๐Ÿ‡ hole of tearing apart Expressions:

public static string Action<T>(
    this UrlHelper helper, 
    Func<T, string> nameOfAction, 
    object routeValues = default) where T : Controller
{
    string controllerTypeName = typeof(T).Name;

    string controllerName = controllerTypeName
        .Substring(0, controllerTypeName.Length - 10);

    return helper.Action(nameOfAction(null), controllerName, routeValues);
}

This can be called like so (either in Razor or C#):

string url = urlHelper.Action<HomeController>(
    nameOfAction: c => nameof(c.Index), 
    routeValues: new { id = 5 });

The benefit here is that if we rename HomeController.Index to OtherController.Blah, we will get a compile error if our rename didn't refactor the above code automatically โšก.

At the same time, it doesn't matter if HomeController.Index() returns an ActionResult or Task<ActionResult> ๐Ÿ™‚.

This method doesn't handle renaming parameters or ensuring their type-safety - Ya can't win 'em all!

I use it mostly for parameterless Index() action methods, so the lack of parameter type-safety isn't as much of an issue.


NodeAliasPath Link

I previously wrote about using NodeAliasPath as a routing mechanism:

For any pages that are directly represented by specific nodes in the CMS content tree, this routing approach is my preferred one.

You might be wondering how we can use the above MVC utility methods to generate URLs for this kinds of routes...

Well, we can't ๐Ÿ˜ฃ.

We instead need to reverse the process used for routing in my previous post.

To quickly summarize, the Kentico MVC application receives a request and checks to see if the path of the request matches a NodeAliasPath of a given TreeNode.

If there is a match, it sends the request to the controller action that handles requests for the Page Type associated with the ClassName of that TreeNode.

Otherwise, the request is handled by MVC's standard routing approaches (convention based or attribute routing).


LinkablePage

I've come up with a pattern I'm calling 'Linkable Pages' wherein we make links in our application code to specific nodes in the CMS content tree that can be routed to.

I'm calling these routable nodes 'Pages', but there's only a subset of nodes I actually want to explicitly create links to (ex: Home, Contact Us, Products Landing Page)

We then rely on content synchronization to ensure the connection between the CMS content nodes and the 'Linkable Pages', in application code, remain consistent no matter what environment our application runs in ๐Ÿค”.

Value Object

I use a Value Object pattern to ensure only an immutable and discrete set of pages can be linked to ๐Ÿค“.

Let's look at example code for the LinkablePage class:

public class LinkablePage
{
    static LinkablePage()
    {
        Home = new LinkablePage(
            new Guid("65bc6106-74c5-456d-bc5b-71ae0d89c92a"));

        ContactUs = new LinkablePage(
            new Guid("2a955262-1fd2-44d6-b0f2-9329b42eefa3"));

        Products = new LinkablePage(
            new Guid("61a5f388-1c19-4645-a628-4a0e4b70fbac"));
    }

    public static LinkablePage Home { get; protected set; }
    public static LinkablePage ContactUs { get; protected set; }
    public static LinkablePage Products { get; protected set; }

    public Guid NodeGuid { get; }

    protected LinkablePage(Guid nodeGuid) => NodeGuid = nodeGuid;
}

The class only has 1 constructor, which is protected, meaning instances can only be created within the class itself (or a child class).

The instances are static properties on the class, which means they are well defined and easy to access ๐Ÿ‘.

The class is also a non-primitive type (ex: int, string, Guid) so it can be the parameter of methods, which gives more domain meaning to those methods ๐Ÿ™‚.

The class instances and the entire set of possible instance values is immutable, which makes validating the correct set of possible values easier ๐Ÿ˜Ž.

I'm leaving out some of the equality comparison functionality of Value Objects here since I'm not comparing LinkablePage instances, but if you want to read more on this topic check out Value Objects explained.

Html Extension

To render out links in Razor views I use an HtmlHelper extension method:

public static void RenderPageLink(
    this HtmlHelper htmlHelper, 
    LinkablePage page, 
    string linkText, 
    string elementClasses = "")
{
    var request = new PageLinkRequest(page, linkText, elementClasses);

    htmlHelper.RenderAction(
        nameof(NavigationController.PageLink),
        "Navigation", 
        new { request });
}

Child Action

The Razor child action rendered by this call is as follows:

[ChildActionOnly]
public ActionResult PageLink(PageLinkRequest request)
{
    // The following call is wrapped in a caching layer to prevent
    //   repeat trips to the database to generate the same links

    string nodeAliasPath = queryDispatcher
        .Dispatch(new NodeAliasPathByNodeGuidQuery(request.Page));

    string linkUrl = string.IsNullOrWhiteSpace(nodeAliasPath)
        ? ""
        : nodeAliasPath.ToLower();

    var viewModel = new PageLinkViewModel(
        label: request.Text, 
        linkUrl: linkUrl, 
        classes: request.Classes);

    return PartialView(viewModel);
}

Razor View

And the Razor view is as simple as the few lines below:

@{ 
    if (string.IsNullOrWhiteSpace(Model.LinkUrl)
      || string.IsNullOrWhiteSpace(Model.Label))
    {
        return;
    }
}

<a href="@Model.LinkUrl" class="@Model.Classes">@Model.Label</a>

Generating the Link

So, to pull all of this together we use the HtmlHelper extension in another Razor view as follows:

<li>
  @{ Html.RenderPageLink(
         LinkablePage.Home, 
         "Home", 
         "btn btn-primary"); 
  }
</li>
<li>
  @{ Html.RenderPageLink(
         LinkablePage.ContactUs, 
         "Contact Us", 
         "btn btn-primary"); 
  }
</li>

The call to queryDispatcher.Dispatch(new NodeAliasPathByNodeGuidQuery(nodeGuid)); is wrapped in caching, with all the correct dependency keys and the SQL query itself is minimal and selects only the NodeAliasPath column.

If you are looking for ideas on how to apply consistent caching across all your database queries, checkout the following post ๐Ÿ‘:

If you are interested in a simple, readable way to create Kentico cache dependency keys, try this post ๐Ÿ‘:

Generating Page URLs in Code

We can also use the above approach to generate URLs in our C# code. These can be used in redirect responses from controllers or in emails sent from the MVC application.

Below is an example of generating a redirect url in a LogoutController:

public ActionResult Index()
{
    authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);

    string nodeAliasPath = queryDispatcher
        .Dispatch(new NodeAliasPathByNodeGuidQuery(LinkablePage.Home));

    string homePageUrl = string.IsNullOrWhiteSpace(nodeAliasPath)
        ? "/"
        : nodeAliasPath.ToLower();

    return Redirect(homePageUrl);
}

Now, I can see how you might think this scenario is a little contrived ๐Ÿ˜‹, but I believe the readability of LinkablePage.Home is better and provides more business meaning than "/" by itself.

In my code I've hidden this querying logic behind an IUrlBuilder.BuildNodeAliasPath(LinkablePage page); method which keeps the above methods even leaner and easier to test ๐Ÿ˜Ž.

Summary

We took a look at all of the out-of-the-box URL generation options that MVC provides.

Unfortunately, they lack a major feature most C# developers rely on - type safety ๐Ÿ‘Ž. These work, but don't scale with larger applications.

Projects like T4MVC and Mvc.Futures help fill the gaps somewhat, each taking different approachs (templating and Expressions) to create type-safe URLs.

They also come with their own caveats - but for your team and project the trade-offs might be worth it.

I showed how we can get part of the way to type-safe URL generation using the nameof() operator and Func<T, string>.

It's simple, which, in my opinion, is the best part ๐Ÿ˜‰!

Finally, we took a look at how we can generate URLs for pages based on NodeAliasPath routes using the LinkablePage value object class.

This approach requires us to rely on content synchronization between environments, and also some caching infrastructure to ensure we don't overload our database with queries.

However, it let's us treat content items in the CMS tree as specific domain objects in our code and use the path to those items in the tree as the generated URLs.

If those NodeAliasPath values change, all of our links will update ๐Ÿค—.

If those nodes are removed from the CMS, and we remove the specific LinkablePage instance (ex: LinkablePage.ContactUs), our code won't compile until we change all references to it ๐Ÿ˜….

I'm interested to know what techniques you and your team have used to create URLs consistently and reliably in your Kentico 12 MVC code bases.

Let me know in the comments below!

Thanks for reading ๐Ÿ™.


If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

Or my Kentico blog series:

Posted on by:

seangwright profile

Sean G. Wright

@seangwright

dev lead @WiredViews, founding partner @craftbrewingbiz. @Kentico Xperience MVP. love to learn / teach web dev & software engineering, collecting vinyl records, mowing my lawn, craft ๐Ÿบ

Discussion

pic
Editor guide