Safety and simplicity with LINQ: Working with the result pattern - Part 2 : Andrew Lock

Safety and simplicity with LINQ: Working with the result pattern - Part 2
by: Andrew Lock
blow post content copied from  Andrew Lock | .NET Escapades
click here to view original post


This post follows directly on from the previous post which demonstrated the first stage of replacing exceptions-as-flow-control with the result pattern, but ended with a very ugly final result based on the Switch() method.

In this post we'll start by taking a detour to look at LINQ, before we look at how we can use it with our Result<T>. Finally we'll see how adding a couple of extension methods to our Result<T> type can dramatically improve the ergonomics of using the result pattern.

This series was inspired by a tweet from Jeremy Miller in which he was advocating for not using the result pattern. It's important that you don't blindly apply patterns everywhere just because you can, try to understand where they will actually provide value in your code base.

Background: where we left off

In the last post I showed an example of taking a simple method that was using exceptions for flow control, and refactored it to use the result pattern, where the resulting usage was "safe" (thanks to the Switch() method and hiding the direct Value or Error accessors), but which was verbose and cumbersome to read.

It's our starting point for this post, so I've reproduced the important parts of the Result<T> implementation we ended with below:

public class Result<T>
{
    // We don't expose these publicly
    private readonly T? _value;
    private readonly Exception? _error;
    
    [MemberNotNullWhen(true, nameof(_value))]
    [MemberNotNullWhen(false, nameof(_error))]
    private bool IsSuccess { get; }

    public Result<TReturn> Switch<TReturn>(
        Func<T, TReturn> onSuccess,
        Func<Exception, Exception> onFailure)
    {
        if (IsSuccess)
        {
            return onSuccess(_value);
        }
        else
        {
            var err = onFailure(_error);
            return Result<TReturn>.Fail(err);
        }
    }

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Fail(Exception error) => new(error);
    public static implicit operator Result<T>(T value) => Success(value);
}

By encapsulating the _value, _error and IsSuccess values, and only allowing access via the Switch() method, we ensure that callers can't misuse the Result<T> objects. Unfortunately, that can make for some very ugly code when you need to chain multiple method calls that each return a Result<T>

public class UserProvisioningService(CreateUserService createUserService)
{
    public Result<UserAccount> ProvisionUser(ExternalLoginInfo info)
    {
        Result<Claim[]> claimsResult = GetClaimValues(info);

        return claimsResult.Switch(
            onSuccess: claims =>
            {
                Result<Claim[]> validatedClaimsResult = ValidateClaims(claims);
                return validatedClaimsResult.Switch(
                    onSuccess: validatedClaims =>
                    {
                        Result<Guid> tenantIdResult = GetTenantId(claims);
                        return tenantIdResult.Switch(
                            onSuccess: tenantId =>
                            {
                                Result<ProvisionUserRequest> createRequestResult =
                                    CreateProvisionUserRequest(tenantId, validatedClaims);
                                return createRequestResult.Switch<Result<UserAccount>>(
                                    onSuccess: createRequest =>
                                    {
                                        return createUserService.GetOrCreateAccount(createRequest);
                                    },
                                    onFailure: ex => Result<UserAccount>.Fail(ex));
                            },
                            onFailure: ex => Result<UserAccount>.Fail(ex));

                    },
                    onFailure: ex => Result<UserAccount>.Fail(ex));
            },
            onFailure: ex =>  Result<UserAccount>.Fail(ex));
    }
}

This is clearly some really hard to understand code. If this is what code always looked like with the result pattern it would be very hard to recommend. Luckily there's something we can do to clean this up significantly. But first, we'll take a detour into LINQ.

LINQ: method syntax vs query syntax

When people talk about LINQ, they're typically talking about method syntax, where you're calling methods like Where() and Select() on IEnumerable<T>, to give code like this:

public IEnumerable<int> GetItemCounts(IEnumerable<OrderLine> source) =>
    source
        .Where(line => !line.IsDeleted)
        .Select(line => line.Quantity);

public record OrderLine(int Quantity, bool IsDeleted);

But the original design of LINQ includes another syntax, query syntax which was intended to read more like SQL. Query syntax is generally interchangeable with the method syntax, so we could rewrite the above method to be functionally the same using the alternative syntax:

public IEnumerable<int> GetItemCounts(IEnumerable<OrderLine> source) =>
    from line in source
    where !line.IsDeleted
    select line.Quantity;

This code does the same thing as the previous example, so which you choose is mostly a matter of taste, and people generally gravitate to the method syntax in my experience.

But there's something neat about the query syntax; it uses "duck typing" to decide which types can be used with it. Technically all you need to provide is a single method with the right format: SelectMany()

SelectMany(): the monadic bind of C#

Depending on your proficiency with LINQ, you may or may not have run into SelectMany(). Either way, for most people SelectMany() is probably "just" the method you call to "flatten" a collection of elements. For example, if you have a collection of Order, and you want to enumerate through all the OrderLines across all Order objects, you might have something like this:

public IEnumerable<OrderLine> GetAllLines(IEnumerable<Order> orders)
    => orders.SelectMany(order => order.Lines);

If you were to write this all out as foreach statements, it would look something like this:

public IEnumerable<OrderLine> GetAllLines(IEnumerable<Order> orders)
{
    foreach(var order in orders)
    {
        foreach (var line in order.Lines)
        {
            yield return line;
        }
    }
}

So SelectMany() might seem like a niche method in your LINQ arsenal. However, in practice, SelectMany() is probably the most important LINQ method of all as it implements the "Bind" operation, one of the most important operations in functional programming.

No, I'm not going to talk about monads in this post. I'm not even going to talk about functional programming much. If either of those spark your interest, I strongly suggest looking at F#: https://fsharpforfunandprofit.com/ is a great place to start.

There are lots of different overloads of SelectMany(), but the one we care about has two arguments, a "collection selector" and a "result selector". The following shows what the method looks like for the IEnumerable<T> extension method version, along with a basic implementation showing how it might be implemented:

public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, IEnumerable<TCollection>> collectionSelector, 
    Func<TSource, TCollection, TResult> resultSelector)
{
    // We loop through every element in the original source collection
    foreach (TSource ele in source)
    {
        // Apply the collectionSelector function to each element,
        // to get the collection value
        IEnumerable<TCollection> collection = collectionSelector(ele);
        foreach (TCollection collectionEle in collection)
        {
            // For each element in the selected collection, execute
            // the resultSelector function, and return the result
            TResult result = resultSelector(collectionEle);
            yield return result;
        }
    }
}

The example above is written in terms of IEnumerable<T>, but the SelectMany() pattern actually works for any type that implements a method like the above pattern. So let's see what that looks like for Result<T>.

Adding LINQ support to Result<T>

In this section we're going to implement SelectMany() for Result<T>. To simplify some things, we're also going to implement the equivalent of Select() too. Let's start with that.

Implementing Result<T>.Select()

So first of all we should think what would it even mean to implement Select().

If you have an IEnumerable<T>, then Select() maps from one type, T to another type TResult, based on a Func<T, TResult> selector function. We can achieve something similar for Result<T>:

  • If the Result<T> is a success, run the selector function on the T, and return a new Result<TResult>.
  • If the Result<T> is a failure, return a Result<TResult> using the same original Exception.

We can easily implement that functionality using the Switch() method we defined earlier:

// Creating these as extension methods to keep the Result<T> type small
public static class ResultExtensions
{
    public static Result<TResult> Select<TFrom, TResult>(
        this Result<TFrom> source, // The target for the extension
        Func<TFrom, TResult> selector) // The mapping/selector method
    {
        return source.Switch(
            onSuccess: r => selector(r), // success -> run the selector and implicitly convert to Result<TResult>
            onFailure: e => Result<TResult>.Fail(e)); // error -> return a failed Result<TResult>
    }
}

So Select() is a simple mapping of the "inner" TFrom type to a TResult, converting a Result<TFrom> into a Result<TResult>. Now lets move on to SelectMany().

Implementing Result<T>.SelectMany()

The SelectMany() method I showed earlier is a bit trickier to think about. The functionality we need is:

  • The "collection selector" is the function that runs against the T of a Result<T>, and should return a Result<TMiddle>. Note that this is different to the previous Select() case:
    • Select() uses a selector like this: Func<T, TMiddle>
    • SelectMany() uses a selector like this: Func<T, Result<TMiddle>>
  • In addition, the overload of SelectMany() that we need to implement takes a select-like "result selector" that maps from a T and TMiddle pair to a TResult, to get the final value.

Putting all that together, our SelectMany implementation for Result<T> might look something like this:

// The same extension class, Select() not shown for brevity
public static class ResultExtensions
{
    public static Result<TResult> SelectMany<TSource, TMiddle, TResult>(
        this Result<TSource> source, // The target for the extension
        Func<TSource, Result<TMiddle>> collectionSelector, // How to map to the Result<TMiddle> type
        Func<TSource, TMiddle, TResult> resultSelector) // How to map a TMiddle to a TResult
    {
        return source.Switch(
            onSuccess: r => // success -> run the selectors
            {
                // First run the "collection selector"
                Result<TMiddle> result = collectionSelector(r);

                // If result is a success, we run the "result selector" to
                // get the final TResult. If it is not a success, then
                // Select() just passes the error through as a failed Result<TResult>
                return result.Select(v => resultSelector(r, v));
            },
            onFailure: e => Result<TResult>.Fail(e)); // error -> return a failed Result<TResult>
    } 
}

If you're struggling to follow all those many generic types, don't worry, it is confusing 😅 The good news is that you only have to implement this once (or rather, preferably, the library you're using implements this for you).

The really good news is that this is the last piece we need to get back to improving our nasty Result<T> based ProvisionUser() method!

Creating readable Result<T> code with LINQ

For maximum effect, lets recap what our gnarly, but safe, Result<T>-based version of ProvisionUser() looks like (this is the same code I showed at the start of this post):

public class UserProvisioningService(CreateUserService createUserService)
{
    public Result<UserAccount> ProvisionUser(ExternalLoginInfo info)
    {
        Result<Claim[]> claimsResult = GetClaimValues(info);

        return claimsResult.Switch(
            onSuccess: claims =>
            {
                Result<Claim[]> validatedClaimsResult = ValidateClaims(claims);
                return validatedClaimsResult.Switch(
                    onSuccess: validatedClaims =>
                    {
                        Result<Guid> tenantIdResult = GetTenantId(claims);
                        return tenantIdResult.Switch(
                            onSuccess: tenantId =>
                            {
                                Result<ProvisionUserRequest> createRequestResult =
                                    CreateProvisionUserRequest(tenantId, validatedClaims);
                                return createRequestResult.Switch<Result<UserAccount>>(
                                    onSuccess: createRequest =>
                                    {
                                        return createUserService.GetOrCreateAccount(createRequest);
                                    },
                                    onFailure: ex => Result<UserAccount>.Fail(ex));
                            },
                            onFailure: ex => Result<UserAccount>.Fail(ex));

                    },
                    onFailure: ex => Result<UserAccount>.Fail(ex));
            },
            onFailure: ex =>  Result<UserAccount>.Fail(ex));
    }
}

This is nearly 30 lines of (very hard to read) code. 🙈

But by adding the SelectMany() extension method, we've unlocked a superpower. We can remove all the duplication and verbosity in the above implementation, use the LINQ query expression syntax, and implement the ProvisionUser() method in a fraction of the amount of code.

public class UserProvisioningService(CreateUserService createUserService)
{
    public Result<UserAccount> ProvisionUser(ExternalLoginInfo info)
    {
        return
            from claims in GetClaimValues(info)
            from validatedClaims in ValidateClaims(claims)
            from tenantId in GetTenantId(validatedClaims)
            from createRequest in CreateProvisionUserRequest(tenantId, validatedClaims)
            from userAccount in createUserService.GetOrCreateAccount2(createRequest)
            select userAccount;
    }
}

I think it's really hard to argue with the elegance, simplicity, and readability of this code. It almost reads like the procedural code we wrote way back at the start of the previous post, before we had error handling. But we actually do have error handling; any failure Result<T> returned by GetClaimValues(), ValidateClaims() etc will automatically short-circuit the chain of calls and return the error.

This style of error handling is sometimes described as "Railway Oriented Programming", a term coined by Scott Wlaschin. You can read more and find links to his various talks on the topic here.

Before I close this post, I want to finish with a few warnings about taking this to the extreme.

Caveats to the result pattern and railway oriented programming

Through this series, I've been using a really quite simple scenario, showing how you might add flow control to it "incorrectly" before refactoring to use Result<T> with LINQ. But it's easy to take things too far and use Result<T> in ways that you probably shouldn't, so this section describes a few caveats to be aware of (many of which are covered in Scott's post).

Don't try to wrap all exceptions in a Result<T>

When I introduced Result<T> it was as a way to replace the exceptions that were being used for flow control. But it's important to understand that it was only those flow-control exceptions I was trying to replace.

Trying to catch every exception and return a Result<T> can end up adding a lot of complexity to your application. Result<T> is great for handling flow control; exceptions should typically be for exceptional situations.

The Result<T> shown in these posts is not what you want

In all the examples I've shown so far, I've used Exception as the error type of the Result<T>, but the reality is that might not what you want to do in practice. Exception in this post was a stand in for "description of the error", but it's a pretty indirect way of doing that.

Another approach might be to define your result type like this:

public class Result<T, TError>
{
    private readonly T? _value;
    private readonly TError? _error;
}

You could then also define a UserProvisioningError

public enum UserProvisioningError
{
    InvalidClaims,
    MissingTennantId,
    UnknownTenantId,
    // ...
}

These could provide fine-grained reasons about why the method failed, and the method signature becomes something like this:

public Result<UserAccount, UserProvisioningError> ProvisionUser(ExternalLoginInfo info)

This is another case where F# really handles this more nicely, as you can attach arbitrary additional data to each of the error cases if useful. You can do that with some libraries in C# (such as OneOf for example) but it's more clunky than having support native to the language.

If you're doing domain modelling then defining all these error cases can be very useful. But it's also easy to go too far with this approach.

Consider whether you need Result<T> at all

Having argued throughout this series that we can use Result<T> without making our code horribly gnarly and verbose, I'm now going to ask you to think about whether this is actually better.

Initially I was comparing to the case where we're throwing exceptions for flow control, and expecting the caller to handle all the different errors we might throw. I think the Result<T> approach shown here is clearly preferable.

But that doesn't mean you need (or want) to use Result<T> everywhere. If you're just wrapping your method calls in try-catch statements and converting that to Result<T> then are you actually adding any value by using Result<T>? You've essentially just re-implemented exception handling!

Internalising this is really difficult - when I first started exploring F# and functional programming I tried to make it so that exceptions would never escape a method, which meant Result<T> everywhere… and frankly it kinda sucked 😅

On the other hand, if you are using exceptions for handling common situations (which aren't just due to programming-errors), then the result pattern can be very useful for codifying paths through your code.

In the next post of this series I show some examples of how to handle some variations on this pattern, such as when the method's you're calling use async and await, or you want to manipulate Result<T> objects without "unwrapping" them.

Summary

In this post I discussed LINQ, focusing on the Select() and SelectMany() methods, and how they can be used to enable LINQ's query syntax. Next I showed how you could implement these methods for a Result<T> type. Finally I refactored the gnarly result pattern code that used Switch() to use LINQ query syntax, and showed how this dramatically improves readability. Finally I discussed some reasons that you should consider not using the result pattern. In the next post, I show how you can take this LINQ support further, to support more situations.


October 15, 2024 at 02:30PM
Click here for more details...

=============================
The original post is available in Andrew Lock | .NET Escapades by Andrew Lock
this post has been published as it is through automation. Automation script brings all the top bloggers post under a single umbrella.
The purpose of this blog, Follow the top Salesforce bloggers and collect all blogs in a single place through automation.
============================

Salesforce