Result Pattern in C#

In this post, we will look at a somewhat functional way of programming, in which we mix functional programming with object-oriented programming. We owe this approach, or pattern, to Scott Wlaschin, its original author. You can find more information on his website fsharpforfunandprofit.  

 

The first thing we need to understand before starting is how our code works in a normal use case. For this example, we will simulate the use case of creating a new user in the system.

 

And understand that you do NOT need experience in functional programming to apply this pattern.

 

 

1 - Paths to follow

A - Happy path

As always, we begin our design thinking about what is the correct path to complete the task. In our particular case, it's very simple: we validate the user, save the user, and send a verification code to the email address. 

public string BasicAccountCreation(Account account)
{
    ValidateNewAccount(account);
    SaveUser(account);
    SendCode(account);
    return "Usuario anadido correctamente";
}

happy path

 

B - Non happy path

Of course, users do not always follow "the rules" or may already be registered. In these cases, we need to handle errors to avoid, for example, having the same user duplicated in the database. 

 

non happy path

As we can see, we can have different errors at different points in our application. A common implementation for error handling in object-oriented programming is as follows: 

public string BasicAccountCreation(Account account)
{
    string accountValidated = ValidateNewAccount(account);
    if(!string.IsNullOrWhiteSpace(accountValidated))
    {
        return accountValidated;
    }

    bool isSaved = SaveUser(account);
    if(!isSaved)
    {
        return "Error actualizando la base de datos";
    }

    bool isSent = SendCode(account);
    if(!isSent)
    {
        return "Error Enviando el email";
    }
    return "Usuario anadido correctamente";
}

As you can see, a lot of code, simple to follow in this particular case, but if we had many more validations it could get quite complex. 

 

If we analyze these flows step by step, we see the following result: For the happy path, everything goes well and everything works, so we have a single entry and exit point.

 

Meanwhile, when analyzing failures, we have several breaking points in our code.

full flow

As indicated at the beginning of the post, the main idea of this pattern is to mix the best of functional programming into our object-oriented applications. 

 

To do so, let's analyze the same process, but this time, making a functional analysis or flow 

functional flowThis is the functional representation. We see that the flow is similar but not exactly the same, since, in this scenario, we have not only one response but two at the end of our process. 

 

 

2 - Functional design

To ensure our application returns two results, what we need to do is create an object that contains both elements.

 

But before we start coding like crazy, we need to understand the steps to follow, because it is not just a single error we might have but several.

 

Based on our "non happy path," we have the following path:

non happy path flowAs we can see, there are 3 errors: a validation error, a database error, and a mail server error. 

 

Therefore, the resulting object would be something like the following: an object inside containing "success" and the errors.

type result = 
    Success 
    || ValidationError
    || DatabaseError
    || SMTPServerError

Obviously, this code is not final, as it is focused on this specific use case. We can condense it even further; if we group the errors, either in a list or an error object that contains the list inside, our object will have only two independent properties, regardless of use case.

type result = 
    Success
    || Error

This solution is not 100% correct, since it indicates success but doesn't return any element, only failures. If we want to return the element, what we must do is implement generics, and in this way, we can create Success<T> where T is the type we want to return.

Type result<T> =
    Success of T
    || Error

 

3 - Pattern objective

Once we are clear about the pattern and what it does, as well as its objective, we can define several goals:

  • Combine all errors into a single place.
  • Create smaller functions.
  • Show the data flow.
  • A single function for each use case.

 

4 - How the Result pattern works (Railway Oriented programming) 

We also need to understand how it works. In point two, we have seen that the main objective is to create a wrapper for the type we want to return (Result<T>), but why?

 

This is where the functional programming part comes in. Basically, Result<T> will make the type T change constantly (or not, depending on logic), but to make this clearer, here's the analogy: We have a car that goes into a function and comes out a bike, then that bike goes into another function and comes out a boat, for example. 

rop logic

It's simple: the input type of a function should be the output type of the previous function.

 

4.1 - Comparison

poo vs rop comparison

Compared to a normal check, where we have our validation and if it fails we return, we do not always return, but the program does not continue and instead goes back. In our new pattern, it does "continue", and I put that in quotes because what it really does is skip all the functions that come after our failure.

rop explanation

To see a little more clearly how it works, in each method we receive both input parameters, both the correct and the failed. As long as everything is correct, our function will keep executing the methods in a normal way. But if we throw an error at any time, it will switch to the failure path.

rop functional flow in application

As we see, we have "two" inputs and two possible outputs. Each connection between two methods is made via a binding, which will control whether what we are executing is the happy path or the failure path. 

 

5 - How to apply ROP in C#

The pattern is mainly based on a class, which we will call Result<T>. So the first thing we will do is a struct that contains our Result<T> and, of course, the errors.

 

Another thing to note is that in our code, we might have methods that return void. These methods are not compatible with the new pattern, since, as we saw earlier, each method must return a type. For this specific case, we use the Unit type, which does nothing and simulates void.

public sealed class Unit
{
    public static readonly Unit Value = new Unit();
    private Unit() { }
}

public struct Result<T>
{
    public readonly T Value;

    public static implicit operator Result<T>(T value) => new Result<T>(value);

    public readonly ImmutableArray<string> Errors;
    public bool Success => Errors.Length == 0;
    
    public Result(T value)
    {
        Value = value;
        Errors = ImmutableArray<string>.Empty;
    }
    
    public Result(ImmutableArray<string> errors)
    {
        if (errors.Length == 0)
        {
            throw new InvalidOperationException("debes indicar almenso un error");
        }

        Value = default(T);
        Errors = errors;
    }
}

If we look closely at the code, we see Result<T> contains T with the type we pass, and the errors.

Note: the errors are just a simple string for simplicity, but we could have an "Error" type with codes, etc.

 

All handling in this pattern is done via extension methods. In this way, we can define when it's `Success` or `Failure`.

public static class Result
{
    public static readonly Unit Unit = Unit.Value;
    
    public static Result<T> Success<T>(this T value) => new Result<T>(value);
    
    public static Result<T> Failure<T>(ImmutableArray<string> errors) => new Result<T>(errors);

    public static Result<T> Failure<T>(string error) => new Result<T>(ImmutableArray.Create(error));

    public static Result<Unit> Success() => new Result<Unit>(Unit);

    public static Result<Unit> Failure(ImmutableArray<string> errors) => new Result<Unit>(errors);

    public static Result<Unit> Failure(IEnumerable<string> errors) => new Result<Unit>(ImmutableArray.Create(errors.ToArray()));

    public static Result<Unit> Failure(string error) => new Result<Unit>(ImmutableArray.Create(error));
}

The code is straightforward: we simply call success or failure when we want, and it creates the Result<T> type for us.

 

5.1 - Building the environment for the ROP pattern

Now let's look at how to create the method that will decide whether program execution should continue on the happy path or skip the methods and go straight to the end. 

 

In this post, we'll see the creation of the .Bind() method. For this we use extension methods together with delegates, since our Bind method will receive a delegate, i.e., a method, as a parameter. 

public static class Result_Bind
{
    public static Result<U> Bind<T, U>(this Result<T> r, Func<T, Result<U>> method)
    {
        try
        {
            return r.Success
                ? method(r.Value)
                : Result.Failure<U>(r.Errors);
        }
        catch (Exception e)
        {
            ExceptionDispatchInfo.Capture(e).Throw();
            throw;
        }
    }
}

We can see from the code that it is a simple if statement in a ternary operator. If our r (result) is Success (meaning its error list is empty), it will execute the method passed as a parameter, otherwise, it creates an object with the errors.

 

5.2 - Expanding the environment for the ROP pattern

For this example, I did not want to overcomplicate the methods; currently, it works synchronously. If we wanted it to work asynchronously, we should add Task<T> both to the return value and to input parameters, and of course, wait for the result by doing await.

Or, for example, if we wanted to create a .Then() method, which ignores the delegate's result (a delegate Action) and returns the input value. 

Or a .Map() where we map one value to another.

public static async Task<Result<U>> Bind<T, U>(this Task<Result<T>> result, Func<T, Task<Result<U>>> method)
{
    try
    {
        var r = await result;
        return r.Success
            ? await method(r.Value)
            : Result.Failure<U>(r.Errors);
    }
    catch (Exception e)
    {
        ExceptionDispatchInfo.Capture(e).Throw();
        throw;
    }
}

public static Result<T> Then<T>(this Result<T> r, Action<T> action)
{
    try
    {
        if (r.Success)
        {
            action(r.Value);
        }

        return r;
    }
    catch (Exception e)
    {
        ExceptionDispatchInfo.Capture(e).Throw();
        throw;
    }
}

public static Result<U> Map<T, U>(this Result<T> r, Func<T, U> mapper)
{
    try
    {
        return r.Success
            ? Result.Success(mapper(r.Value))
            : Result.Failure<U>(r.Errors);
    }
    catch (Exception e)
    {
        ExceptionDispatchInfo.Capture(e).Throw();
        throw;
    }
}

 

6 - Use case

The code is available on github.

Note: the code is synchronous for simplicity. 

 

6.1 - Creating a user using OOP

For this example, I have created a use case or service where we simulate adding a user to our database. First, we will implement the service using object-oriented programming:

public interface IAddUserPOOServiceDependencies
{
    bool AddUser(UserAccount userAccount);
    bool EnviarCorreo(UserAccount userAccount);
}


/// <summary>
/// Add the user using an object-oriented programming structure.
/// </summary>
public class AddUserPOOService
{
    private readonly IAddUserPOOServiceDependencies _dependencies;
    
    public AddUserPOOService(IAddUserPOOServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }

    public string AddUser(UserAccount userAccount)
    {
        var validacionUsuario = ValidateUser(userAccount);
        if (!string.IsNullOrWhiteSpace(validacionUsuario))
        {
            return validacionUsuario;

        }

        var addUserDB = AddUserToDatabase(userAccount);
        if (!string.IsNullOrWhiteSpace(addUserDB))
        {
            return addUserDB;

        }
        var sendEmail = SendEmail(userAccount);
        if (!string.IsNullOrWhiteSpace(sendEmail))
        {
            return sendEmail;
        }

        return "Usuario añadido correctamente";
    }

    private string ValidateUser(UserAccount userAccount)
    {
        if (string.IsNullOrWhiteSpace(userAccount.FirstName))
            return "El nombre propio no puede estar vacio";
        if (string.IsNullOrWhiteSpace(userAccount.LastName))
            return "El apellido propio no puede estar vacio";
        if (string.IsNullOrWhiteSpace(userAccount.UserName))
            return "El nombre de usuario no debe estar vacio";

        return "";
    }

    private string AddUserToDatabase(UserAccount userAccount)
    {
        if (!_dependencies.AddUser(userAccount))
        {
            return "Error añadiendo el usuario en la base de datos";
        }

        return "";
    }
    private string SendEmail(UserAccount userAccount)
    {
        if (!_dependencies.EnviarCorreo(userAccount))
        {
            return "Error enviando el correo al email del usuario";
        }

        return "";
    }

}

As we see, we have several problems. First, by returning a single type (in this case a string), we are limiting the value we can return. We want to check multiple validations, but we can only return one at a time.

 

We could fix this problem by returning a List<string> instead of a single string, but in that case, if everything works correctly, we're creating a list when we only need a single value. 

 

Apart from this issue, the code is more or less difficult to read. In this use case, we only need to check a couple of elements, yet the class remains very long and hard to follow. But the code works, with no major problems, the main AddUser method is not aesthetically pleasing but gets the job done. 

 

6.2 - Creating a user using ROP

Now let's perform the same process, but using the "Railway oriented programming" pattern we've discussed in this post. 

 

Before continuing, remember that in this use case we want to use our Result<T> type. I've changed the dependency interface a bit to also implement our new pattern.

 

This would be our implementation:

public interface IAdduserROPServiceDependencies
{
    Result<bool> AddUser(UserAccount userAccount);
    Result<bool> EnviarCorreo(string email);
}

public class AdduserROPService
{
    private readonly IAdduserROPServiceDependencies _dependencies;

    public AdduserROPService(IAdduserROPServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }

    public Result<UserAccount> AddUser(UserAccount userAccount)
    {
        return ValidateUser(userAccount)
            .Bind(AddUserToDatabase)
            .Bind(SendEmail)
            .Map(_ => userAccount);
    }

    private Result<UserAccount> ValidateUser(UserAccount userAccount)
    {
        List<string> errores = new List<string>();

        if (string.IsNullOrWhiteSpace(userAccount.FirstName))
            errores.Add("El nombre propio no puede estar vacio");
        if (string.IsNullOrWhiteSpace(userAccount.LastName))
            errores.Add("El apellido propio no puede estar vacio");
        if (string.IsNullOrWhiteSpace(userAccount.UserName))
            errores.Add("El nombre de usuario no debe estar vacio");

        return errores.Any() 
            ? Result.Failure<UserAccount>(errores.ToImmutableArray()) 
            : userAccount;
    }

    private Result<string> AddUserToDatabase(UserAccount userAccount)
    {
        return _dependencies.AddUser(userAccount)
            .Map(_ => userAccount.Email);
    }

    private Result<bool> SendEmail(string email)
    {
        return _dependencies.EnviarCorreo(email);
    }
}

Before explaining the code in more detail, I want to point out how much the amount of code has been reduced. Having less code usually means having cleaner and easier-to-understand code.

 

The main method, AddUser, is much smaller and easier to understand. As we see, we use our .Bind method to call each method we want to invoke, and at the end of the sequence, we call .Map, which will act if everything is correct, returning the user account.

 

The ValidateUser method is the one that changed the most, since being the first to be executed, this is where we create our Result<T>, which we make a success if all is well, or a failure if there is any error.

 

The AddUserToDatabase and SendEmail methods still perform the same job, but now SendEmail only receives a string email. This is intentional, to show that the output of the previous method is the input to the next method. 

 

In these methods we do not need to check for errors because the dependency (interface) returns Result<T>. So if there's any error, it will be captured when we do .Map() or .Bind() in the main function.

 

 

Conclusion

In this post, we have seen how to combine functional programming and object-oriented programming in a way that greatly helps us with code cleanliness, especially regarding error handling.

 

I personally recommend using this pattern because programming becomes much easier. In fact, this website is entirely written with this pattern. Using it has allowed me to have much cleaner code, and when I need to change something, it takes me very little time to read and fully understand what the code does.

 

The code from this example is on GitHub. I invite you to take it and try it out in some of your projects. Over the next days/weeks, I will be expanding the functionality, such as allowing all methods to be asynchronous. In any case, if you have any improvement ideas, feel free to make a Pull Request.

 

If you enjoyed the content, please share it (menu on the left).

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café