38
loading...
This website collects cookies to deliver better user experience
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; }
}
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...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 of null
and those types" problem, but I don't find it to be the most descriptive technique to represent the data specific to our applications.
null
is kinda unpleasant 😣 to work with.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;
NullReferenceException
😱.public record ImageViewModel(string Path, string AltText)
{
public static ImageViewModel NullImage { get; } =
new ImageViewModel("", "");
public bool IsEmpy => this == NullImage;
public bool IsNotEmpty => this != NullImage;
}
public class HomeViewModel
{
public string Title { get; set; }
// notice no '?' on ImageViewModel
public ImageViewModel Image { get; set; }
}
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
};
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
.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.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).a monad is a design pattern that allows structuring programs generically while automating away boilerplate code needed by the program logic.
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 a Container<Type>
where Container
is our monad.
Bind: a function that takes a Container<Type>
and a function from Type
to Container<OtherType>
and returns a Container<OtherType>
.
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"
Task<T>
is a Monad - its Task.FromResult()
method is the same as Monad.Return()
.IEnumerable<T>
is a Monad, with IEnumerable<T>.SelectMany()
being the same as Monad.Bind()
.IEnumerable<T>
has to make LINQ as awesome as it is 💪🏼!)Maybe
type easy 😎.ImageViewModel
example again:public class HomeViewModel
{
public string Title { get; set; }
public Maybe<ImageViewModel> Image { get; set; }
}
HomeViewModel.Image
property as Maybe<ImageViewModel>
which means it might or might not have a value.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);
};
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..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'.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.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.Maybe
values 😮.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);
ImageViewModel
properties to string
values without taking them out of the Maybe
box.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}'>"));
Maybe
as long as we want, blissfully 😊 ignorant of whether or not there are values to work with.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.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
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.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.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;
Maybe<string> name = // comes from somewhere else
string greeting = name
.Map(n => $"Hello, {n}")
.UnWrap("I don't know your name");
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.SectionViewModel
will have an empty Maybe<ImageViewModel>
👍🏻.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" ...>
}
}
@model SectionViewModel
<p> ... </p>
@if (Model.Image.HasValue)
{
var image = Model.Image.Value;
<img src="@image.Path" alt="@image.AltText"
width="@image.Width" ...>
}
<p> ... </p>
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>
null
check every time we want to access some potentially null
data, which breaks the logic and flow our of code 😑.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 😄.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.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.