Kentico Xperience Design Patterns: Handling Failures - The Result Monad

seangwright

Sean G. Wright

Posted on August 29, 2022

Kentico Xperience Design Patterns: Handling Failures - The Result Monad

In a previous post we looked at the benefits of returning failures instead of throwing exceptions.

We came up with a Result type that could represent an failure or a success of a given operation, either containing a string error message or the value of our operation.

This custom Result type met most of our requirements, but to really unlock its power 🦾 we need to turn it into the Result monad.

Note: This is part 3 of a 3 part series on handling failures.

Part 1 and Part 2

📚 What Will We Learn?

A Refresher on Monads

If you are unsure 😕 of what a monad is, I give a description with some analogies in my previous post Kentico Xperience Design Patterns: Modeling Missing Data - The Maybe Monad.

However, if you don't feel like navigating away and want a quick explanation, think of a monad as a container 🥫, in the same way that Task<T> and List<T> are containers in C# (they are also monads).

You can manipulate these containers in a standard way, independent of what's inside and each type of monad represents a unique concept 🧠. A Task<T> represents a value T that will be available at some point in the future and List<T> represents a collection of 0 or more of some type T.

It's worth noting that monads can contain other monads, because they don't care what kind of data they contain (ex: Task<List<T>> or List<Task<T>>).

Let's consider the Result<TValue, TError> monad 😮.

It is a container that represents an operation that either succeeded or failed (past tense). If it succeeded then it will have a value of type TValue and if it failed it will have an error of type TError.

The specific implementation of the Result monad that we will be using comes from the C# library CSharpFunctionalExtensions 🎉 and we will use the simplified Result<T> where the TError error type is a string and doesn't need to be specified.

Modeling Data Access

Now we can answer the question of when and where do we actually use Result<T>.

I recommend starting somewhere our application is performing an 'operation' which could succeed or fail and we're currently modeling the failure case with a throw new Exception(...); or by ignoring it altogether.

Here's an example that might look similar to some Kentico Xperience code we have written:

public async Task<IActionResult> Index()
{
    BlogPost page = pageDataContextRetriever
        .Retrieve<BlogPost>()
        .Page;

    Author author = await authorService.GetAuthor(page);

    Image bgImage = await blogService.GetDefaultBackgroundImage();

    Image heroImage = await blogService.GetHero(page);

    return new BlogPostViewModel(
        page, 
        author, 
        bgImage, 
        heroImage);
}
Enter fullscreen mode Exit fullscreen mode

In the applications I work on, it's pretty common to grab bits of data from various pages, custom module classes, the media library, ect... to gather all the content needed to render a page 🤓.

However, each of these operations needs to go to the database, an external web service, or the file system to look for something. It might even have some business rules 📃 around how the data is retrieved.

If something goes wrong in the sample code above we can assume that an exception is going to thrown and centralized exception handling will catch it and display an error page.

We end up skipping the rest of the operations, even if they would have succeeded 😞. With centralized exception handling we typically never partially display a page that experienced some failures.

Let's update the code to use Result<T> and see where that gets us:

public async Task<IActionResult> Index()
{
    BlogPost page = pageDataContextRetriever
        .Retrieve<BlogPost>()
        .Page;

    Result<Author> author = await authorService.GetAuthor(page);

    Result<Image> bgImage = await blogService
        .GetDefaultBackgroundImage();

    Result<Image> heroImage = await blogService.GetHero(page);

    return new BlogPostViewModel(...); // 🤔
}
Enter fullscreen mode Exit fullscreen mode

Our potential failures are no longer hidden as exceptions behind a method signature. Instead, we're requiring consumers of the authorService and blogService to deal with both the success and failure cases.

However, we now have a bunch of Result<T> instances that are useless to our BlogPostViewModel 😩, so we'll need to do something to get the data they potentially contain to our Razor views.

Handling Results

One option for rendering when using Result<T> is to update the BlogPostViewModel to use Result properties and 'unwrap' them in the View:

public record BlogPostViewModel(
  BlogPost Post,
  Result<Author> Author,
  Result<Image> HeroImage,
  Result<Image> BgImage);
Enter fullscreen mode Exit fullscreen mode

In this case, we'd pass our results directly to the BlogPostViewModel constructor in our Index() method:

public async Task<IActionResult> Index()
{
    BlogPost page = // ...
    Result<Author> author = // ...
    Result<Image> bgImage = // ...
    Result<Image> heroImage = // ...

    return new BlogPostViewModel(
        page, author, heroImage, bgImage);
}
Enter fullscreen mode Exit fullscreen mode

Then, in our View we can unwrap conditionally to access the values or errors that were the outcomes of each operation:

@model Sandbox.BlogPostViewModel

@if (Model.HeroImage.TryGetValue(out var img))
{
  <!-- We retrieved content successfully -->
  <div>
    <img src="img.Path" alt="img.AltText" />
  </div>
}
else if (Model.HeroImage.TryGetError(out string err))
{
  <!-- Content retrieval failed - show error to admins -->
  <page-builder-mode exclude="Live">
    <p>Could not retrieve hero image:</p>
    <p>@err</p>
  </page-builder-mode>
}
Enter fullscreen mode Exit fullscreen mode

This is an interesting approach because it allows us to gracefully 💃🏽 display error information for any operations that failed, while continuing to show the correct content for those that succeed.

In this example we were able to treat all the Result<T> as independent because none of the operations were conditional on the others, but that's not always the case 😮.

Let's enhance our BlogPostViewModel to include related posts (by author) and those post's taxonomies:

public async Task<IActionResult> Index()
{
    BlogPost page = // ...
    Result<Author> author = // ...
    Result<Image> bgImage = // ...
    Result<Image> heroImage = // ...

    Result<List<BlogPost>> relatedPosts = await blogService
        .GetRelatedPostsByAuthor(page, author);

    Result<List<Taxonomy>> taxonomies = await taxonomyService
        .GetForPages(relatedPosts);

    return new BlogPostViewModel(...);
}
Enter fullscreen mode Exit fullscreen mode

We're now an in awkward situation where we have to pass Result<T> to our services. Those service methods will have to do some checks to see if the results succeeded or failed and conditionally perform the operations.

The monad is 'infecting' 😷 our code, and not in a good way.

Ideally we'd only call our services if the depending operations (getting the Author and related BlogPosts) succeeded. As with most monads, we'll have a better experience by "lifting" our code up into the Result instead of bringing the Result down into our code 😉.

This is similar to how we work with Task<T>. We don't often pass values of this type as arguments to methods. Instead we await them to get their values and pass those to our methods.

We can also compare this to creating methods that operate on a single value of type T vs updating all of them to accept List<T>.

Fortunately there's an API for that. We want to use Result<T>.Bind() and Result<T>.Map():

public async Task<IActionResult> Index()
{
    BlogPost page = // ...
    Result<Image> bgImage = // ...
    Result<Image> heroImage = // ...

    // result is Result<(Author, List<BlogPost>, List<Taxonomy>)>
    var result = await authorService.GetAuthor(page)
           .Bind((Author author) => 
           {
               return blogService
                   .GetRelatedPostsByAuthor(page, author)
                   .Map((List<BlogPost> posts) => 
                   {
                       return (author, posts);
                   });
           })
           .Bind(((Author, List<BlogPost>) t) => 
           {
               return taxonomyService
                   .GetForPages(t.posts)
                   .Map((List<Taxonomy> taxonomies) => 
                   {
                       return (t.author, t.posts, taxonomies);
                   });
           });

    return new BlogPostViewModel(...);
}
Enter fullscreen mode Exit fullscreen mode

So, what's going on here 😵😵?

Let's break it down piece by piece 🤗.

authorService.GetAuthor(), blogService.GetRelatedPostsByAuthor() and taxonomyService.GetForPages() return Result<T>, so we can chain off them with Result<T>'s extension methods.

The Map() and Bind() extension methods will only execute their delegate parameter if the Result is in a successful state - that is, if the previous operation didn't fail 👍🏾.

This means we only get related blog posts if we were able to get the current post's author. And, we only get the taxonomies if we were able to get related blog posts.

If any part of this dependency chain fails, all later operations are skipped and the failed Result is returned from the final extension method 🙌🏼.

Map and Bind

To help make the above code a bit more transparent, let's review the functionality of Map() and Bind():

Map() is like LINQ's Select method, which converts the contents of the Result<T> from one value to another (it could be to the same or different types).

We often use Map() when we want to transform the data returned from a method call to something else - like converting a DTO to a view model. We don't know if the method successfully completed its operation 😏, we assume it did and declare how to transform the result. Our transformation is skipped if the data retrieval failed.

Bind() is like LINQ's SelectMany, which flattens out a nested Result (ex IEnumerable<IEnumerable<T>> or Result<Result<T>>).

We'll typically use Bind() when we have dependent operations.

When one service returns a Result and we only want to call another service if the first one succeeded we will write something like:

Result<OtherData> result = service1
    .GetData()
    .Bind(data => service2.GetOtherData())
Enter fullscreen mode Exit fullscreen mode

You'll notice 🧐 we have a common pattern of Bind() followed by a nested Map(). Bind() is calling the dependent operation and Map() lets us continue to gather up the data from each operation into a new C# tuple.

We can skip the braces, type annotations, and return keywords and use expressions to keep our code terse and declarative - reading like a recipe 👩🏻‍🍳:

public async Task<IActionResult> Index()
{
    BlogPost page = // ...
    Result<Image> bgImage = // ...
    Result<Image> heroImage = // ...

    // result is Result<(Author, List<BlogPost>, List<Taxonomy>)
    var result = await authorService.GetAuthor(page)
           .Bind(author => blogService
               .GetRelatedPostsByAuthor(page, author)
               .Map(posts => (author, posts)))
           .Bind(set => taxonomyService
               .GetForPages(set.posts)
               .Map(taxonomies => (set.author, set.posts, taxonomies)));

    return new BlogPostViewModel(...);
}
Enter fullscreen mode Exit fullscreen mode

If this feels too unfamiliar, and we want a place to add breakpoints for debuggability, we can extract each delegate to a method and pass the method group, which can be even more readable 😁:

public async Task<IActionResult> Index()
{
    BlogPost page = // ...
    Result<Image> bgImage = // ...
    Result<Image> heroImage = // ...

    // Here is our recipe of operations
    var result = await Result.Success(page)
           .Bind(GetAuthor)
           .Bind(GetRelatedPosts)
           .Bind(GetTaxonomies);

    return new BlogPostViewModel(...);
}

private Task<Result<(BlogPost, Author)>> GetAuthor(BlogPost page)
{
    return authorService.GetAuthor(page)
        .Map(author => (page, author));
}

private Task<Result<(List<BlogPost>, Author)>> GetRelatedPosts(
    (BlogPost page, Author author) t)
{
    return blogService.GetRelatedPostsByAuthor(
            t.page, t.author)
        .Map(posts => (posts, t.author));
}

private Task<Result<(Author, List<BlogPost>, List<Taxonomy>)>> GetTaxonomies(
    (List<BlogPost> posts, Author author) t)
{
    return taxonomyService
        .GetForPages(t.posts)
        .Map(taxonomies => (t.author, t.posts, taxonomies));
}
Enter fullscreen mode Exit fullscreen mode

Handling Failures

If our entire pipeline of operations are dependent and we should skip trying to render content if we can't access everything we need, we can use the Match() extension and provide delegates that define what should happen for both success and failure scenarios:

public async Task<IActionResult> Index()
{
    BlogPost page = // ...

    return await authorService.GetAuthor(page)
           .Bind(author => blogService
               .GetRelatedPostsByAuthor(page, author)
               .Map(posts => (author, posts)))
           .Bind(t => taxonomyService
               .GetForPages(t.posts)
               .Map(taxonomies => new BlogPostViewModel(t.author, t.posts, taxonomies)))
           .Match(
               viewModel => View(viewModel),
               error => View("Error", error));
}
Enter fullscreen mode Exit fullscreen mode

Now we're really seeing the power of composing Result<T> 💪🏽. Each of our services can fail, but that doesn't complicate our logic that composes the data returned by each service.

Without Result<T> we could use exceptions to signal errors - hidden from method signatures and used as secret control flow 😝.

Or, we have a bunch of conditional statements 😝, checking to see what 'state' we are in (success/failure), often modeled with Nullable Reference Types.

With Result<T> we let the monad maintain the internal success/failure state and write our code as a series of steps that clearly defines what we need to proceed.

If we have operations that aren't dependent, we can gather up all the various Result<T> values, put them in our view model and handle the conditional rendering in the view 🤘🏼.

My general recommendation is to separate independent sets of operations into View Components 🤔. We treat each View Component as a boundary for errors or failures instead of populating a view model with a bunch of Result<T> values, potentially making our views overly complex.

We can create a ViewComponent extension method to help us easily convert a failed Result to an error View:

public static class ViewComponentResultExtensions
{
    public static Task<IViewComponentResult> View<T>(
        this Task<Result<T>> result, 
        ViewComponent vc)
    {
        return result.Match(
            value => vc.View(value),
            error => vc.View("ComponentFailure", error));
    }
}
Enter fullscreen mode Exit fullscreen mode

We can place the failure View in a shared location ~/Views/Shared/ComponentFailure.cshtml and have it show an error message in the Page Builder and Preview modes, but hide everything on the Live site 🙂:

@model string

<page-builder-mode exclude="Live">
    <p>There was a problem loading this component.</p>
    <p>@Model</p>
</page-builder-mode>
Enter fullscreen mode Exit fullscreen mode

If we're following the PTVC pattern, we can use this in a View Component as follows:

public class BlogViewComponent : ViewComponent
{
    public Task<IViewComponentResult> InvokeAsync(
        BlogPost post) =>
        authorService.GetAuthor(page)
           .Bind(author => blogService
               .GetRelatedPostsByAuthor(page, author)
               .Map(posts => (author, posts)))
           .Bind(t => taxonomyService
               .GetForPages(t.posts)
               .Map(taxonomies => new BlogPostViewModel(t.author, t.posts, taxonomies)))
           .View(this);
    }
}
Enter fullscreen mode Exit fullscreen mode

We now have the added benefit of an expression bodied member as a method implementation 😲!

Conclusion

Moving from C# exceptions to the Result monad can take some getting used to. It's also a change that should be discussed with out team members and implemented where appropriate (Exceptions still have their place! 🧐).

If we do decide it's an option worth exploring, what do we gain?

  • Honest methods that don't hide the possibility of failures from their signatures.
  • A consistent set of patterns and tools for combining Results.
  • Local, targeted handling of failures that prevents them from failing an entire page (unlike centralized exception handling).
  • A recipe of operations that reads like a set of instructions on how to gather up the data we need to proceed through our app.
  • No repeated boilerplate if/else statements to handle various failures.

If you think your Kentico Xperience site might benefit from returning failures and using the Result monad, checkout the CSharpFunctionalExtensions library and my library-in-progress XperienceCommunity.CQRS. It codifies these patterns for data retrieval and integrates cross-cutting concerns like logging and caching.

I'd love to hear your thoughts 👋!

...

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:

💖 💪 🙅 🚩
seangwright
Sean G. Wright

Posted on August 29, 2022

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

Sign up to receive the latest update from our blog.

Related