DEV Community

loading...
Cover image for Kentico Xperience Design Patterns: Modeling Missing Data with Nullable Reference Types

Kentico Xperience Design Patterns: Modeling Missing Data with Nullable Reference Types

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

Content modeling is part of the foundation of any successful Kentico Xperience application (and is often under-valued โ˜น).

It's up to software developers to implement a content model in Xperience's Page Types and other content management features...

  • Related Pages or Linked Pages?
  • Normalized or de-normalized data?
  • Attachments or the Media Library?
  • Page Type names, Scopes, and Parent/Child hierarchies?
  • Content + design or design agnostic content?

There are many important decisions to make, but once some content has been modeled, developers are always faced with another... how do we structure our code to best align with the content model ๐Ÿคท๐Ÿฝโ€โ™€๏ธ?

What do we do with content models that inevitably include optional or missing data?

๐Ÿ“š What Will We Learn?

  • Modeling code to match Content
  • An Example 'Call To Action' Page Type
  • Using nullable reference types
  • Where do we fall short with nullable reference types?

๐Ÿ— Modeling the Models

We like it when there are well defined rules around how our code works, and when our code works with data, we also prefer that data to be orderly and predictable ๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ”ฌ.

When using Kentico Xperience, we want to create Page Types that most accurately model the content (data) being stored in the application.

The field names should match the words the stakeholders use and we don't want overly complex Page Types, with too many fields, that make content management difficult and violate the SOLID Design principles.

At the same time, we want to be flexible where it counts, making content management easier...

๐Ÿคธ๐Ÿพโ€โ™€๏ธ Flexible Page Types: A Call To Action

Let's imagine we heed to model content that represents a call to action.

Some call to actions in our site might have images, while others might not.

If we are modeling our content with precision, we will up with two different Page Types for our call to action - one with an image field, and one without.

However, this probably isn't a good approach...

Why? ๐Ÿค”

Any given Call To Action might have an image added or removed from it in the future, and we don't want to force the content managers to create/delete a Page just to make this change - that's a bad (content management) user experience.

It's also likely we'll want to display both imaged and image-less call to actions in the same places on the site, without having to query for the data of two different Page Types.

The existence or absence of an Image from a Call to Action is part of the content model, not something we should try to avoid ๐Ÿ˜ฎ.

Instead, we should allow flexibility in the Page Type and handle the existing or missing image in our code ๐Ÿ‘๐Ÿพ.

โ” Displaying Optional Values

So, let's assume we've created a Page Type, CallToAction, that has a set of optional fields for our image (I'm leaving out a lot of the auto-generated Page Type code for brevity):

public class CallToAction : TreeNode
{
    /// A checkbox โœ… in the form
    [DatabaseField]
    public bool HasImage
    {
        get => ValidationHelper.GetBoolean(
            GetValue("HasImage"), false);
        set => SetValue("HasImage", value);
    }

    /// A URL Selector form control
    [DatabaseField]
    public string ImagePath
    {
        get => ValidationHelper.GetString(
            GetValue("ImagePath"), "");
        set => SetValue("ImagePath", value);
    }

    /// A Text field form control
    [DatabaseField]
    public string ImageAltText
    {
        get => ValidationHelper.GetString(
            GetValue("ImageAltText"), "");
        set => SetValue("ImageAltText", value);
    }

    // Other Call To Action fields ...
}
Enter fullscreen mode Exit fullscreen mode

Now, let's write our C# to retrieve this content and present it in a Razor View:

I am putting data access code in an MVC Controller for brevity. In practice these operations should be behind an abstraction ... not in the presentation layer of our application ๐Ÿ‘Ž.

public class HomeController : Controller
{
    private readonly IPageRetriever pageRetriever;
    private readonly IPageDataContextRetriever contextRetriever;

    // ... Constructor

    public ActionResult Index()
    {
        var home = contextRetriever.Retrieve().Page;

        var cta = retriever.Retrieve<CallToAction>(
            q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
            .FirstOrDefault();

        var viewModel = // What do we do here?

        return View(viewModel);
    }
}
Enter fullscreen mode Exit fullscreen mode

Retrieving our content is easy with Xperience's IPageRetriever service, but what about creating our View Model that we'll use to send our Call to Action content to the Razor View? How should this class be defined

Let's try modeling it ๐Ÿ™‚:

public class HomeViewModel
{
    public string Title { get; set; }
    public bool HasImage { get; set; }
    public string ImagePath { get; set; }
    public string ImageAltText { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

I have a few concerns ๐Ÿ˜’ about this model:

  • HasImage needs to be checked before trying to render the image, but nothing about the model expresses or enforces this
  • ImagePath could have a value while HasImage is false if the content manager unchecked the "Has Image?" checkbox without removing the "Image Path" value
  • HasImage is about whether or not some content should be displayed, where ImagePath and ImageAltText are the actual pieces of content

So, let's try again to resolve these issues:

public class HomeViewModel
{
    public string Title { get; set; }
    public bool HasImage { get; set; }
    public ImageViewModel Image { get; set; }
}

public class ImageViewModel
{
    public string Path { get; set; }
    public string AltText { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now we are grouping the content in a new ImageViewModel class and the HomeViewModel has an instance of it.

However, here we're writing our code with C# 8.0 (or newer), so let's use nullable reference types and get rid of the HasImage property:

Here's a full step by step guide to enabling nullable reference types in your application.

While we're at it, we should set all warnings from nullable reference types as errors, otherwise it's easy to ignore the benefit they bring ๐Ÿ˜‰.

public class HomeViewModel
{
    public string Title { get; set; }
    public ImageViewModel? Image { get; set; }
}

public class ImageViewModel
{
    public string Path { get; set; }
    public string AltText { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now, if CallToAction.HasImage is false, the HomeViewModel.Image value will be null:

var viewModel = new HomeViewModel
{
    Title = home.Fields.Title,
    Image = cta.HasImage
        ? new ImageViewModel
          { 
              Path = cta.ImagePath,
              AltText = cta.AltText
          }
        : null;
}
Enter fullscreen mode Exit fullscreen mode

In our Razor view we can add a check for a null value and render the image conditionally:

Note: I use the C# 9.0 not operator for pattern matching here. The C# 8.0 alternative is Model.Image is object.

@model HomeViewModel

<h1>@Model.Title</h1>

@if (Model.Image is not null)
{
    <img src="@(Model.Image.Path)" alt="@(Model.Image.AltText)" />
}
Enter fullscreen mode Exit fullscreen mode

This appears to express the intent of the functionality and the content being modeled ๐Ÿ’ช๐Ÿฝ.

๐ŸฆŠ What Does the Docs Say?

The Kentico Xperience documentation discusses using nullable properties for non-required fields.

This is a neat idea, but has a few too many caveats at the moment:

  • Generated Page Type classes don't include nullable properties
  • Re-generating the Page Type class will remove customized nullable properties
  • Putting nullable types on a separate partial class doesn't enforce their use (the non-nullable ones are still accessible)
  • Only nullable value types are supported if the Page Type class is shared between ASP.NET Core and the Web Forms Content Management application (.NET 4.8 doesn't support nullable reference types)

At the same time, I think nullable types in generated code is definitely something to keep an eye on ๐Ÿ˜ as Xperience moves to run fully on ASP.NET Core in the next version of the platform, Odyssey.

๐Ÿ›ฃ A Fork in the Road: Missing Data

We could say we've solved this problem and head home for dinner ๐Ÿฅ™!

But I think we should consider another common variation of this issue...

We decided to use nullable reference types to represent an optional value (or set of values), but what about data that should exist but is missing (unintentionally)?

If we look back at our HomeController example, we can see that we found our CallToAction content by querying for the node with a NodeGUID that matched the value of our Home Page's CTAPage field:

var home = contextRetriever.Retrieve().Page;

var cta = retriever.Retrieve<CallToAction>(
    q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
    .FirstOrDefault();
Enter fullscreen mode Exit fullscreen mode

This means there's a field in the Home Page Type that holds a reference to the CallToAction Page in the content tree. But what happens if that Page is deleted ๐Ÿคทโ€โ™‚๏ธ

Currently, our application would throw an exception because we try to access the HasImage property on a cta variable that has the value of null ๐Ÿคฆ๐Ÿฝ.

The C# LINQ method .FirstOrDefault() knows it's returning a reference type and the "default" value of a reference type is null, so it's marked as possibly returning a null value (CallToAction?).

What Is Null? Baby, Don't Hurt Me!

Jokes aside, we are now in a situation where null now represents two different ideas.

Let's look at our HomeViewModel again:

public class HomeViewModel
{
    public string Title { get; set; }
    public ImageViewModel? Image { get; set; }
}

public class ImageViewModel
{
    public string Path { get; set; }
    public string AltText { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The nullable public ImageViewModel? Image { get; set; } represents an optional value - it's ok if it's not there, and we should account for that.

However, with the .NET LINQ .FirstOrDefault() method, a null value doesn't have any meaning except what we decide it should have... and in that case, it's definitely not an optional value.

When we can't find the CallToAction Page with our query, we might want to treat that as an error and log it to the Xperience event log. We might also want to display a message to the Content Manager when the View is rendered in the Page tab ๐Ÿค”.


Here are some important questions that should be answerable if we want to design robust and maintainable applications:

  • In our code, where is the value null found and what does it mean?
  • If I'm a new developer coming into this code base and I see a method returning a nullable value, do null results imply something I should expect? (Should I log/display an error or not?)
  • Is null a normal value that represents the content model (intentionally missing) or is it the result of bad data (unintentionally missing)?

Unfortunately, when null represents two different things at the same time, we will always have a hard time answering these questions or we'll arrive at inconsistent answers over time and across the development team ๐Ÿ˜ซ.

๐Ÿคจ Conclusion

We've seen how the process of content modeling impacts the way we write our code, and this is a good thing!

Accounting for optional or missing data is the responsibility of software developers, especially when working with content in a platform like Kentico Xperience.

The C# nullable reference type language feature is a great way to expose the hidden use of null in our code, and I recommend we all enable in all of our ASP.NET Core projects. However, since it is a low level language feature, it doesn't have much meaning on its own, besides 'there is no value here'.

In my next post we'll look at how combining nullable reference types with the null object pattern can get us closer to modeling content accurately in our code.

As always, thanks for reading ๐Ÿ™!

References


Photo by Jordan Madrid on Unsplash

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.

Or my Kentico Xperience blog series, like:

Discussion (0)

pic
Editor guide