DEV Community

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

Posted on

5 2

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

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.

#kentico

#xperience

Or my Kentico Xperience blog series, like:

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more