In two previous posts (Modeling Missing Data with Nullable Reference Types and Modeling Missing Data with The Null Object Pattern) we compared approaches for representing missing data in our code and considered the implications that modeling had for how we could handle the missing data.
As we saw through some example implementations, both options have pros and cons ๐ค.
Let's quickly review how those approaches work and where they fall short. Then we will dive into my favorite approach for modeling missing data in Kentico Xperience applications - the Maybe monad ๐คฉ!
๐ What Will We Learn?
- Why nullable reference types and the Null Object Pattern don't quite work
- What is a Monad?
- The
Maybe
Monad! - How we can use
Maybe
with Kentico Xperience
๐ A Refresher - The Problems with Our Options
โ Nullable Reference Types
While I do think we should enable nullable reference types in our Kentico Xperience applications ๐๐พ, trying to model our data with this language feature alone can lead to confusion ๐ต.
Let's look at the example HomeViewModel
below:
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; }
}
We can see that HomeViewModel.Image
is nullable, so the C# compiler (and our IDE) can alert us to places in our code where we don't first check to see if its value is null
before accessing its Path
or AltText
properties...
If we are unfamiliar with this application, we might look at the code above and ask "But, why is it nullable?" ๐คท๐ฝโโ๏ธ
The LINQ method IEnumerable<T>.FirstOrDefault()
can also return null
, but does a null
value returned here, have the same meaning as a null
value on one of our View Model's properties? LINQ was not designed with our data model in mind, but the HomeViewModel
class is specific to our content model - it feels like these two uses of null
should have very different meanings.
I find that
null
is great for fixing the "All reference types are a union ofnull
and those types" problem, but I don't find it to be the most descriptive technique to represent the data specific to our applications.
As we start to annotate our code with nullable reference types, we'll also discover that null
is kinda unpleasant ๐ฃ to work with.
If we try to work with a reference type variable or property that is nullable, we have to constantly add checks in our code to tell the C# compiler that within a block of code, we know the value isn't null
:
HomeViewModel vm = ...
// vm.Image might be null
if (vm.Image is null)
{
// vm.Image is definitely null
return;
}
// vm.Image is definitely not null
string altText = vm.Image;
These checks add complexity to our apps for the sake of protecting ourselves from the dreaded NullReferenceException
๐ฑ.
It would be great if we could work with our 'empty' values in the same way as our 'not empty' ones and not have to have all these guards! It would also be nice if we could represent their 'emptiness' in a way that felt closer to our data model - not using a low level language feature.
๐งฉ Null Object Pattern
The Null Object Pattern lets us treat an 'empty' value of a type as a 'special case' of its type.
From the previous post in this series, we came up with the following example:
public record ImageViewModel(string Path, string AltText)
{
public static ImageViewModel NullImage { get; } =
new ImageViewModel("", "");
public bool IsEmpy => this == NullImage;
public bool IsNotEmpty => this != NullImage;
}
We've move the representation of 'empty' or 'missing data' into the type itself, which means all of our APIs, properties, and variables can avoid adding the null reference type annotation when using this type:
public class HomeViewModel
{
public string Title { get; set; }
// notice no '?' on ImageViewModel
public ImageViewModel Image { get; set; }
}
Instead we'll use NullImage
property:
var home = contextRetriever.Retrieve().Page;
var cta = retriever.Retrieve<CallToAction>(
q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
.FirstOrDefault();
var viewModel = new HomeViewModel
{
Title = home.Fields.Title,
Image = cta.HasImage
? new ImageViewModel(cta.ImagePath, cta.AltText)
: ImageViewModel.NullImage // takes the place of null
};
Now, we don't have to guard against viewModel.Image
being null
to interact with it ๐๐ป, and if we want to know if it is our 'Null Object' (empty) we can check the value of viewModel.Image.IsEmpty
.
Despite these benefits, we've unfortunately swung in the complete opposite direction of null reference types and brought the 'empty' value logic into our type, making it more complex.
Even worse, we need to duplicate this logic for every type that represents data that might be missing in our application.
๐งฉ What is a Monad?
We would really like something outside of our ImageViewModel
class that lets us represent missing data like null reference types, but in an unambiguous way. This approach also should allow us to work with those 'empty' data scenarios without doing gymnastics ๐คธ๐ฟโโ๏ธ to check if the data is there or not.
The answer to our requirements is the Maybe
monad, a container for our data that lets us operate on it as though it exists (no conditionals) while expressing 'emptiness' (without putting it into our model).
So what is a Monad ๐จ?
A Monad is a concept from functional programming that sounds complex, and can take a little effort to reason about if you've never seen it before, but don't get scared, we'll take it slow ๐ค.
I really like this description of a Monad:
a monad is a design pattern that allows structuring programs generically while automating away boilerplate code needed by the program logic.
This sounds nice, right?
- Developers love design patterns ๐คฉ!
- C# developers know the benefits of using generics ๐ง
- And we probably feel like half of our working lives involves automating away boilerplate ๐ค
Let's get a little more formal with this minimalist definition:
A Monad is a container (
Container<Type>
) of something that defines two functions:Return: a function that takes a value of type
Type
and gives us aContainer<Type>
whereContainer
is our monad.Bind: a function that takes a
Container<Type>
and a function fromType
toContainer<OtherType>
and returns aContainer<OtherType>
.
So, a Monad is a 'container' type, which, in C#, means it is generic on some type T
and it has 2 methods, Bind
and Return
:
public class Monad<T>
{
public T Value { get; }
public Monad<T>(T value)
{
this.Value = value;
}
public static Monad<T> Return<T>(T value)
{
return new Monad<T>(value);
}
public static Monad<R> Bind<T, R>(
Monad<T> source,
Func<T, Monad<R> operation)
{
return operation(source.Value);
}
}
Return<T>
takes a normal T
value and puts it in our Monad container type. It's like a constructor:
Monad<int> fiveMonad = Monad<int>.Return(5);
Console.Write(fiveMonad.Value); // 5
Bind<T, R>
takes 2 parameters, a Monad<T>
and a function that accepts a T
value and returns a Monad<R>
(R
and T
can be the same type). It's a way of unwrapping an existing Monad to convert it to a Monad of a different type:
public Monad<string> ConvertToString(int number)
{
return Monad<string>.Return(number.ToString());
}
Monad<string> fiveStringMonad = Monad<int>.Bind(
fiveMonad, ConvertToString);
Console.Write(numberAsString.Value); // "5"
The simplicity of Monads is what makes them so useful ๐. Once you start to work with them, you'll start to see them everywhere - both in existing code and all the ways you can use them in your applications.
๐ฆธโโ๏ธ Friendly Neighborhood C# Monads
If we look ๐ at some C# code we are used to writing, we can recognize 2 of the types as Monads!
Our favorite C# representation of asynchronous work, Task<T>
is a Monad - its Task.FromResult()
method is the same as Monad.Return()
.
Also, the always present enumerable type, IEnumerable<T>
is a Monad, with IEnumerable<T>.SelectMany()
being the same as Monad.Bind()
.
Specific monads can have a lot more features, and methods/functions to make them more useful (think of all the extension methods that IEnumerable<T>
has to make LINQ as awesome as it is ๐ช๐ผ!)
๐ The Maybe Monad!
Now, let's get to the Maybe Monad and see how it helps model missing data! We can think of the Maybe Monad as a magic box ๐ง๐พโโ๏ธ...
This magic box might or might not contain something.
It accepts commands from us and can change its contents into whatever we want. If there is something in the box, it changes its contents to what we command. If the box is empty, it ignores the command and nothing happens.
We could also command the box to fill itself, with something specific, if it happens to be empty. If it's not empty, it will ignore the command.
The most interesting aspect of the box that it does all this without us needing to open it up to check if there is actually anything inside.
However, once we are finally done changing or populating the contents of the box, we can open it up and look inside...
The benefit of keeping the box closed is that we can give it unlimited instructions without any checks on its contents (unlike accessing data with null reference types).
Also, if we instead wanted to change items from one thing to another, like an apple ๐ into a rocket ๐, without the box, that apple would need to be magical (normal apples can't change into rockets ๐ฟ). With this box, the items don't need to have special qualities (unlike the Null Object Pattern classes we create).
With this understanding of what Monads are and how the Maybe Monad gives us the power of the magic box we described above, let's look at how we'd use it with our Kentico Xperience sample code.
๐ฉโ๐ป Using Maybe with Kentico Xperience
Maybe + ImageViewModel
My favorite implementation of the Maybe Monad in C# comes from the CSharpFunctionalExtensions library. It's well designed and includes lots of extension methods to make working with the Maybe
type easy ๐.
Let's look at the ImageViewModel
example again:
public class HomeViewModel
{
public string Title { get; set; }
public Maybe<ImageViewModel> Image { get; set; }
}
We now type the HomeViewModel.Image
property as Maybe<ImageViewModel>
which means it might or might not have a value.
Moving to the example Controller action method, we create a new Maybe<ImageViewModel>
based on the existence of the CallToAction
page:
var home = contextRetriever.Retrieve().Page;
Maybe<CallToAction> cta = retriever.Retrieve<CallToAction>(
q => q.WhereEquals("NodeGUID", home.Fields.CTAPage))
.TryFirst();
HomeViewModel homeModel = new()
{
Title = home.Fields.Title,
Image = cta.Bind(c => c.HasImage
? new ImageViewModel(c.ImagePath, c.AltText)
: Maybe<ImageViewModel>.None);
};
You might also notice the TryFirst()
call ๐คจ. It's and extension method in CSharpFunctionalExtensions and a nice way of integrating Maybe
into Kentico Xperience's APIs to help avoid the null
checks we might have if we instead used .FirstOrDefault()
. It tries to get the first item out of a collection - if there is one, it populates a Maybe<T>
with that value, if the collection is empty, it creates a Maybe<T>
that is empty.
The .Bind()
call on Maybe<CallToAction> cta
is saying 'if we have a Call To Action and that Call To Action has an Image, create a Maybe<ImageViewModel>
with some values, otherwise create an empty one'.
When creating the HomeViewModel.Image
, we can see there's an implicit conversion from T
to Maybe<T>
, so we don't need to create a new Maybe<ImageViewModel>
... C# does it for us.
When the CallToAction
doesn't have an image, we assign Maybe<ImageViewModel>.None
, which is our representation of 'missing' data. It's a Maybe<ImageViewModel>
that is empty.
So far this doesn't look too different from our null reference type implementation, but the real value ๐ฐ comes with the way we work with our Maybe
values ๐ฎ.
Let's say we wanted to separate out our ImageViewModel.Path
and ImageViewModel.AltText
into separate variables. With Maybe
we don't have to do any checks to see if HomeViewModel.Image
is null
:
HomeViewModel homeModel = // ...
Maybe<string> path = homeModel.Image.Map(i => i.AltText);
Maybe<string> altText = homeModel.Image.Map(i => i.Path);
We magically changed our ImageViewModel
properties to string
values without taking them out of the Maybe
box.
If we wanted to create an HTML image element by combining the path
and altText
variables, how could we do that while keeping them in their Maybe
boxes?
Maybe<string> htmlImage = path
.Bind(p => altText
.Map(a => $"<img src='{p}' alt='{a}'>"));
No conditional, no guards, no checks. We can stay in the happy world of Maybe
as long as we want, blissfully ๐ ignorant of whether or not there are values to work with.
The Maybe<T>
container always exists, and exposes many methods to do transformations on the data inside (like .Map()
and .Bind()
). If there's no data, the transformations (magic box commands) never happen - but we always end up with another Maybe<T>
, ready to perform more transformations.
If we ever want to get the value out of the Maybe
and supply a fallback value if its empty, we can use the UnWrap()
method:
string imagePath = image
.Map(i => i.Path)
.UnWrap("/placeholder.jpg");
UnWrap()
is a lot like Kentico Xperience's ValidationHelper
type, with calls like ValidationHelper.GetString(someVariable, "ourDefaultValue");
.
Maybe Some Best Practices
We can mix and match these extension methods however we want, creating a complex pipeline of operations for our data.
We only open the Maybe
box when we need to turn it into a traditional C# value - in Kentico Xperience applications this will often be in a Razor View where we have to convert/render the value to HTML or JSON.
Because we don't need to know about the existence of our value until we are ready to render, we should attempt to keep the value in the Maybe Monad for as long as possible. Similar to Task<T>
, it's common for a Maybe<T>
to bubble up and down the layers of our application code, since we defer unwrapping until the last possible moment.
The CSharpFunctionalExtensions library does support the pattern below (and it's ok to start with when exploring how Maybe works), but I advise against it โ when seriously integrating Maybe in an application:
Maybe<string> name = // comes from somewhere else
string greeting = "";
if (name.HasValue)
{
greeting = $"Hello, {name.Value}";
}
else
{
greeting = "I don't know your name";
}
return greeting;
The Maybe Monad is meant to reduce the number of conditional checks we need to make, however getting the value out of the container can sometimes be a little un-ergonomic. We will have the best developer experiences with them when we go all-in, using expressions instead of statements, and thinking about declarative data transformations instead of procedural data manipulations.
Try this instead:
Maybe<string> name = // comes from somewhere else
string greeting = name
.Map(n => $"Hello, {n}")
.UnWrap("I don't know your name");
Here's some real, production Kentico Xperience Page Builder Section code where I use Maybe
and some of its extensions to both represent missing data and avoid conditional statements by staying in the Maybe Monad box as long as possible:
TreeNode page = vm.Page;
SectionProperties props = vm.Properties;
Maybe<ImageViewModel> imageModel = vm.Page
.HeroImage() // could be empty
.Bind(attachment =>
{
var url = retriever.Retrieve(attachment);
if (url is null)
{
return Maybe<ImageContent>.None;
}
return new ImageContent(
attachment.AttachmentGUID,
url.RelativePath,
vm.Page.PageTitle().Unwrap(""),
a.AttachmentImageWidth,
a.AttachmentImageHeight);
})
.Map(content => new ImageViewModel(content, props.SizeConstraint));
return View(new SectionViewModel(imageModel));
Page.HeroImage()
returns Maybe<DocumentAttachment>
. My data access and transformation code never needs to check for missing data - it's a set of instructions I give to the magic ๐ง๐ฝโโ๏ธ Maybe Monad box.
If there is no Hero Image, the code to fetch the Attachment data will never be executed and at the end of my method, my SectionViewModel
will have an empty Maybe<ImageViewModel>
๐๐ป.
Maybe Some Rendering
Even in our Razor View, we can continue to be ignorant about the status of our Maybe<ImageViewModel>
by using the extension method Maybe<T>.Execute()
which is only 'executed' when the Maybe<T>
has a value:
@model SectionViewModel
<p> ... </p>
<!-- Maybe<T>.Execute() the Image method defined below -->
@{ Model.Image.Execute(Image); }
<p> ... </p>
<!-- We create a helper method to render the image HTML -->
@{
void Image(ImageViewModel image)
{
<img src="@image.Path" alt="@image.AltText"
width="@image.Width" ...>
}
}
The first approach is great if we want to embrace ๐ค the Monad!
Here's another way we can render the Maybe value:
@model SectionViewModel
<p> ... </p>
@if (Model.Image.TryGetValue(out var image))
{
<img src="@image.Path" alt="@image.AltText"
width="@image.Width" ...>
}
<p> ... </p>
This one is better if we want to keep our conditional HTML inline with the rest of our markup and have traditional looking Razor code.
Another technique is to use the Partial Tag Helper and pass the Maybe<T>
type to it, handling the conditional rendering and unwrapping logic outside of our primary View:
@model SectionViewModel
<p> ... </p>
<partial name="_Image" model="Model.Image" />
<p> ... </p>
All three are perfectly valid choices and each one has pros and cons, so pick the one that fits your use-case!
๐คฏ Conclusion!!
We finally maybe'd... I mean made it ๐ !
Reviewing our previous two attempts at modeling missing data (null reference types, and the Null Object Pattern), we saw how they can be helpful but both either fail to model accurately or are painful to work with.
Null reference types impose a null
check every time we want to access some potentially null
data, which breaks the logic and flow our of code ๐.
The Null Object Pattern alleviates us from having to make those checks, but requires enhancing, potentially many, models with properties that express their 'is this model's data missing?' nature. This puts a burden on developers and clutters up our model types ๐.
Monads might seem like a scary ๐ป functional programming concept at first, but they're actually pretty simple!
The Maybe
Monad combines both techniques from our previous approaches into a single container type that lets us operate on our data without having to know whether or not it's missing ๐.
By putting all the state that keeps track of whether or not a value exists, in the Maybe<T>
, we are able to keep our model types simple. Also, by being able to transform and access data independent of whether or not its there, we code that is clearer and reads more like a series of instructions without numerous checks.
The Maybe
type is most effective in our apps when we leverage it to the fullest, letting it flow through our data access, business logic, and presentation code. Once we get to the Razor View we have several options available for rendering that data.
Of the three options I've covered in these posts, which do you use to model missing data in your Kentico Xperience applications?
Do you have any other approaches you recommend?
Let us know in the comments below ๐.
...
As always, thanks for reading ๐!
References
- Modeling Missing Data with Nullable Reference Types (part 1)
- Modeling Missing Data with The Null Object Pattern (part 2)
- The Maybe Monad in C#
- Enabling null reference types in C# 8
- What does the phrase monadic bind mean?
- Tasks, Monads, and LINQ
- The Task Monad in C#
- Map and bind - A hidden functional concept in C#
- CSharpFunctionalExtensions Maybe and Result monad library
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:
Top comments (0)