Kentico Xperience Design Patterns: Protecting Hard Coded Identifiers

seangwright

Sean G. Wright

Posted on June 13, 2022

Kentico Xperience Design Patterns: Protecting Hard Coded Identifiers

When we need access to specific values or data in our applications, we might inject them through configuration (especially if they are different for each environment), or store them in a database with an identifier and access them by this identifier in our code.

The first approach is often thought of as being architecturally sound 😎 because the application doesn't need to re-built (or even re-deployed) if the data changes. We only need to update the configuration, and maybe restart the app.

The second is considered bad 😒, because an accidental or incorrect change to the database means an application needs rebuilt and redeployed.

But, what if hard coding an identifier wasn't so risky and the identifier wasn't environment specific 😮? That sounds great, but we'll need a way of guaranteeing these things!

📚 What Will We Learn?

  • When would we hard code identifiers in Xperience?
  • How do we ensure they are stable between environments?
  • How do we protect them from problematic modifications?

💎 Hard Coding Identifiers

Typically, when using a DXP, we will let content managers and marketers customize content for a site 😉, but sometimes it's more convenient to code content directly into an HTML template.

An example of this could be product pages on a site with thousands of products, each with a link to the Contact Us page (/contact-us) at the bottom of the layout. This scenario isn't appropriate for the Page Builder and there's not much benefit to adding a custom Page Type field to a page in the content tree for the "Contact Us" link.

What's the best way to add these links to our templates 🤷‍♀️?

Generating Page Links

Hard-coding links in templates is a scenario where we want to reference an identifier in code, because we don't want to maintain a hard-coded URL over time.

Yes, Xperience's former URLs feature helps to ensure that moving pages around in the Content Tree or changing a URL slug will never break a URL 😅. However, there's several benefits to not hard-coding URLs:

  • SEO: crawlers won't need to follow 301 redirects of moved pages
  • UX: visitors won't need to follow 301 redirects and will have faster page loads
  • Developers: it will be easier to maintain a site when links aren't hard-coded
    • An innocent looking /special-product link could redirect to something completely unexpected 😑!
  • Content Management: Multi-lingual sites will have a different URL per culture, which means a hard-coded path won't work 😞.

Ok, we've decided we do want to sometimes hard-code links, but we don't want to hard-code URLs. So, instead, we'll use an identifier of the page we are linking to.

In Xperience we have a couple of options to pick from:

  • NodeID
  • DocumentID
  • DocumentGUID
  • NodeGUID

Which should we choose 🧐?

🏛 Keeping Identifiers Stable Between Environments

If we want our URLs to be correctly generated on a multi-lingual site, we'll want to use one of the Node values, since the Document ones reference a single specific culture for a page - it wouldn't be a very friendly site if we always generated URLs to the Spanish site for all the other cultures 😁.

Xperience's SQL Server database will auto-increment page integer IDs, which means creating a page in one environment could associate it with a NodeID that is being used by another page in a different environment.

If we have "Local", "Test", and "Production" environments, we don't want our code to fail to generate URLs 😬 (or generate incorrect URLs) because the identifier we reference isn't correct for that environment.

When using content staging Xperience guarantees pages synced from one environment to another will keep their same GUID values (NodeGUID and DocumentGUID) while their ID values can change.

So, given the option of NodeID and NodeGUID, we are going to select NodeGUID because it's stable between environments 🙌🏾!

🏗 Generating URLs

Now that we've decided on our stable page identifier, how are we going to generate URLs for those pages?

We could use Page Type Guid fields combined with the Page Selector Form Control, and then retrieve the URL in code:

Guid nodeGUID = productPage.Fields.CTALinkPageNodeGUID;

TreeNode? linkedPage = pageRetrieve
    .Retrieve<TreeNode>(
        query => query
            .WhereEquals(nameof(TreeNode.NodeGUID), nodeGUID),
        cache => cache.Key($"CTA|{nodeGUID}"))
    .FirstOrDefault();

if (linkedPage is null)
{
    return null;
}

PageUrl linkedPageUrl = pageUrlRetriever.Retrieve(linkedPage);

return ProductViewModel(productPage, linkedPageUrl);
Enter fullscreen mode Exit fullscreen mode

This isn't too complicated, but the whole idea here was to generate the URLs without content management customization, so a Page Type field doesn't align with our goal 🙁.

Page Link Tag Helper

Instead, I'm recommending we use an ASP.NET Core Tag Helper that will enhance <a> elements in our Views:

<a href="" xp-page-link="..."></a>
Enter fullscreen mode Exit fullscreen mode

This Tag Helper will use one of our identifiers to perform a very similar series of data retrieval steps to what we outlined above, and then populate the href and link text of our <a>.

But what do we pass to the xp-page-link attribute? A hardcoded Guid, while technically correct, is going to be very difficult to understand when reading the .cshtml file:

  • Which page does it represent 😵?
  • Did we accidentally typo it somewhere 😖?
  • What if we need to delete and then recreate the page we want to link to and have to replace the Guid everywhere ☠?

LinkablePage

Instead, let's create a class with friendly names to encapsulate our identifiers:

public class LinkablePage
{
    public static LinkablePage Home { get; } = 
        new LinkablePage(new Guid("..."));

    public static LinkablePage ContactUs { get; } = 
        new LinkablePage(new Guid("..."));

    public static LinkablePage PrivacyPolicy { get; } = 
        new LinkablePage(new Guid("..."));

    public Guid NodeGUID { get; }

    protected LinkablePage(Guid nodeGUID) => NodeGUID = nodeGUID;

    public static All LinkablePage[] { get; } = new[]
    {
        Home,
        ContactUs,
        PrivacyPolicy
    };
}
Enter fullscreen mode Exit fullscreen mode

This LinkablePage class contains the full set of any pages we might want to generate URLs for, stores the NodeGUID values of each of those pages, and makes them immutable. The constructor is also protected, so no new objects can be created elsewhere in the application.

This last point is important because these values need to be known at development time, not runtime.

Here's how we'd use one with our Tag Helper:

<a href="" xp-page-link="LinkablePage.PrivacyPolicy"></a>
Enter fullscreen mode Exit fullscreen mode

Because these identifiers are static properties, we could get fancy with our Razor and add a @using static at the top of the Razor file to access the LinkablePage instances directly:

@using static Sandbox.Data.LinkablePage

<a href="" xp-page-link="PrivacyPolicy"></a>
Enter fullscreen mode Exit fullscreen mode

These are both readable, and easy to author 👏🏼.

Assuming the Tag Helper caches the URLs of links it generates for each of the different pages, it's performant as well 💪🏽!

The implementation for this tag helper can be found in the NuGet package for Xperience Page Link Tag Helpers. Try it out!

👮🏽‍♀️ Protecting Identifiers

Now that we've leveraged this convenient and maintainable approach for generating links to pages in the content tree, how do we guarantee that these don't suddenly stop working because pages get deleted?

Kentico Xperience has the perfect tool for us to leverage to guarantee the consistency of our data while the application is running - Global Events.

We can register an event handler for the DocumentEvents.Delete.Before event and then cancel the event if the Page being deleted matches a LinkablePage:

public class LinkablePageProtectionModule : Module
{
    public LinkablePageProtectionModule 
        : base(nameof(LinkablePageProtectionModule));

    protected override void OnInit()
    {
        base.OnInit();

        DocumentEvents.Delete.Before += Delete_Before;
    }

    private void Delete_Before(object sender, DocumentEventArgs e)
    {
        if (LinkablePage.All.Any(
            p => p.NodeGUID == e.Node.NodeGUID))
        {
            e.Cancel();

            var log = Service.Resolve<IEventLogService>();

            string message = $"Cannot delete Linkable Page [{e.Node.NodeAliasPath}], as it might be in use. Please first remove the Linkable Page in the application code and re-deploy the application.";

            log.LogError(
                nameof(LinkablePageEventHandler),
                "DELETE_PAGE",
                message);

            return;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This class's Delete_Before method will be called each time a page is deleted and it will check to make sure that page doesn't have a NodeGUID that matches one of the values we've hard coded in the application. If it does match, the deletion is canceled and we log a helpful message explaining why 😉.

The final step is to register our custom module in our application (preferably somewhere in our CMSApp project, like a DependencyRegistrations.cs file):

[assembly: RegisterModule(typeof(LinkablePageProtectionModule))]
Enter fullscreen mode Exit fullscreen mode

This pattern for protecting hard coded identifiers can be used for anything in Xperience that supports content staging:

  • E-Commerce data (payment methods, shipping options)
  • Pages
  • Custom Module class records
  • Users/roles
  • Categories

All data our application code directly depends on through hard-coded identifiers is probably worth protecting with this approach 🤓.

🏁 Conclusion

Usually we want to leave content management up to marketers and content managers (that's the whole point of a DXP!) 👍🏿.

But sometimes it's far more convenient to manage some of the content ourselves - especially for links to pages that don't need to change frequently (or ever).

However, we still want to make sure that these links are correct and don't break between environments or while the site is running.

By storing a page's NodeGUID value in the application code, and using both content staging and a custom module to prevent accidental page deletions, we can achieve all of our goals 🤗.

We've created a readable, maintainable, scalable, performant, environment-consistent, and robust way of generating links to pages in our application 🥳.

As always, thanks for reading 🙏!

References


We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

#kentico

#xperience

Or my Kentico Xperience blog series, like:

💖 💪 🙅 🚩
seangwright
Sean G. Wright

Posted on June 13, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related