DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Kentico Xperience Design Patterns: Handling Failures - The Result Monad
Sean G. Wright
Sean G. Wright

Posted on • Updated on

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:

Top comments (0)

๐ŸŒš Browsing with dark mode makes you a better developer by a factor of exactly 40.

It's a scientific fact.