Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Oct 17

Result Pattern in C#

Result Pattern in C#
Автор:
Источник:
Просмотров:
1386

Intro

What is the Result pattern? Basically it is a great way to write error-tolerant code that can be composed.

Do you feel that phrase sounds familiar? If you like F# you are right, I’ve taken it from the Results documentation.

The thing is functional languages have been using this pattern for a long time now. We can see newer languages adopting similar approaches as well, for example: Rust and Go.

Are exceptions not good enough? Exceptions should be exceptional, they shouldn’t be used for control flow. The result pattern is becoming more and more trending due to its power and simplicity. In my opinion is the best alternative that we currently have. I won’t go deeper into the theory since there are many well written articles and videos. Instead of explain it myself I recommend you to review this article on Railway oriented programming from Scott Wlaschin, or this video from him as well. If you don’t want to hear about functional programming mumble jumble then I recommend the following articles directed specifically to C# developers:

Sound good, so how do we use the result pattern in C#? Well we don’t have a consolidated approach and there are many alternatives to choose from:

They are all great implementations of the result pattern, they all have pros and cons and ideally you will try them and pick the one that you like best. As I mentioned before I’m going to cover Results because that’s my preferred one. (Full disclosure: I’m the Author)

What are the advantages of Results over other libraries?

  • It’s simple so you can quickly get started.
  • It’s extensible so you can integrate it with your favorite libraries.
  • It’s composable.
  • It relies on source generators so all the boiler plate code is written for you, at the same time it performs better than libraries that rely on reflection.

Practical example

The following example is based on the article and implementation by Josef Ottosong. I find his example easy to digest and probably you are already familiar with it.

This is the scenario:

  • We have one endpoint that takes a name and returns a hamburger with that name.
  • If we can’t find a hamburger with that name, we need to return 404 Not found
  • If something else goes wrong, we need to return 500 Internal Server Error.

This is our first solution:

public interface IGetHamburgerQuery
{
    Hamburger Execute(string name);
}

public class InMemoryGetHamburgerQuery : IGetHamburgerQuery
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public Hamburger Execute(string name)
    {
        var hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (hamburger == null)
        {
            throw new NotFoundException();
        }

        return hamburger;
    }
}

[HttpGet("{name}")]
public ActionResult<Hamburger> Get(string name)
{
    try
    {
        var hamburger = _getHamburgerQuery.Execute(name);
        return new OkObjectResult(hamburger);
    }
    catch (NotFoundException)
    {
        return new NotFoundResult();
    }
    catch
    {
        return new StatusCodeResult(500);
    }
}

Now we are going to refactor it using the Results library.

The first step is to add the NuGet package to our project and that’s it we are ready to write some code.

Before refactoring existing code let’s create our first Custom Error.
Note that Autogenerated errors have the following requirements, otherwise they won’t be generated or you will get some errors:

  • They need to be partial record types.
  • They need the ErrorResult attribute.
[ErrorResult]
public partial record NotFoundError;

As simple as that we have our first Custom Error. The source generator will generate factory methods, constructors and other methods for us including a generic version NotFoundError<T> for us. I’m going to share more on the generated code later, for now let’s focus on the refactor and use our NotFoundError:

public interface IGetHamburgerResultQuery
{
    // Refactor to return IResult<Hamburger>
    IResult<Hamburger> Execute(string name);
}

// Create our first Domain Error!
[ErrorResult]
public partial record NotFoundError;

public class InMemoryGetHamburgerResultQuery : IGetHamburgerResultQuery
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public IResult<Hamburger> Execute(string name)
    {
        Hamburger? hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (hamburger == null)
        {
            // NotFoundError public constructors are generated for us as well but the factory is recommend,
            // because it returns an IResult<Hamburger> instead of the concrete type.
            return NotFoundError<Hamburger>.Create($"Could not find any hamburger named '{name}'");
        }

        // To return a successful result, we use the static factory method on the Result class.
        return Result.Success(hamburger);
    }
}

[HttpGet("result/{name}")]
public ActionResult<Hamburger> GetResult(string name)
{
    IResult<Hamburger> hamburgerResult = _getHamburgerResultQuery.Execute(name);

    // switch and switch expressions are the recommended approach to handle results.
    // Pattern matching is an excellent alternative as well.
    return hamburgerResult switch
    {
        SuccessResult<Hamburger> success => Ok(success.Data),
        HamburgerNotFoundError<Hamburger> err => NotFound(err.GetDisplayMessage()),
        _ => StatusCode(500)
    };
}

Integration with other libraries

Now let’s say we want to implement an update action. Here are the requirements:

  • The name cannot be empty.
  • We decided to use the well-known FluentValidation library.

First let’s create an error that integrates with the library:

public sealed partial record ValidationErrorResult<T>
{
    // Add a custom constructor to create a ValidationErrorResult<T> from a FluentValidation ValidationResult
    public ValidationErrorResult(ValidationResult validationResult)
    {
        // A valid validationResult means it's a dev error that needs to be fixed immediately
        // for that reason it's ok to throw an exception here.
        if (validationResult.IsValid)
            throw new ValidationErrorResultException();

        Message = typeof(T).Name;

        List<ErrorResultDetail> errorList = new(validationResult.Errors.Count);
        errorList.AddRange(validationResult.Errors.Select(e => new ErrorResultDetail(e.PropertyName, e.ErrorMessage)));
        Errors = errorList;
    }
}

public sealed class ValidationErrorResultException : Exception
{
    public ValidationErrorResultException() : base("ValidationErrorResult cannot be created from a successful validation")
    {
    }
}

Ok great, now we are ready to implement Update functionality:

public class UpdateHamburgerMutation
{
    private static readonly IReadOnlyCollection<Hamburger> Hamburgers = new List<Hamburger>
    {
        new Hamburger("Double Cheese"),
        new Hamburger("Big Mac"),
        new Hamburger("Hamburger")
    };

    public IResult<Hamburger> Execute(string name)
    {
        // First we get the Hamburger, this returns an IResult<IResult>
        // because it could be an error like NotFoundError.
        IResult<Hamburger> hamburgerResult = GetHamburger(name);

        // We use pattern matching for demonstration purposes but you could
        // use switch expressions or statements as well
        if (hamburgerResult is SuccessResult<Hamburger> success)
        {
            return UpdateHamburger(success.Data, name);
        }

        // else we propagate the result, we know it's an error since it was not successful
        return hamburgerResult;
    }

    // We are already familiar with this code, it's the same from the first example
    public IResult<Hamburger> GetHamburger(string name)
    {
        Hamburger? hamburger = Hamburgers.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (hamburger == null)
        {
            // NotFoundError public constructors are generated for us as well but the factory is recommend,
            // because it returns an IResult<T> instead of the concrete type.
            return NotFoundError<Hamburger>.Create($"Could not find any hamburger named '{name}'");
        }

        return Result.Success(hamburger);
    }

    IResult<Hamburger> UpdateHamburger(Hamburger hamburger, string name)
    {
        // Update the hamburger
        hamburger.Name = name;

        // Validate the hamburger
        ValidationResult validationResult = new HamburgerValidator().Validate(hamburger);
        if (!validationResult.IsValid)
        {
            // Create a ValidationErrorResult using the constructor we manually created
            return new ValidationErrorResult<Hamburger>(validationResult);
        }

        // If there are no errors we return a successful result
        return Result.Success(hamburger);
    }
}

Compose with high order functions

Great we have seen that we can extend our errors and integrate them with our favorite libraries, but what about composability?

Results currently has 3 methods that allow you to compose your code:

  • Map. Usage examples can be found in the unit tests
  • FlatMap. Usage examples can be found in the unit tests
  • Action. Usage examples can be found in the unit tests

Map

Map takes a delegate with the signature Func<TIn, TOut>. This function will only be invoked in case the result was successful.

As an example let’s say we don’t want to expose our Hamburger directly, we need to map it to a HamburgetDto first.

// our new DTO
public record HamburgerDto(string Name);

// Refactored update Method (rest of the code omitted for brevity)
public IResult<HamburgerDto> Execute(string name)
{
    IResult<Hamburger> hamburgerResult = GetHamburger(name);

    // This time is more convenient to use a switch expression because we
    // also need to unwrap the error to cast it to the new return type.
    return hamburgerResult switch
    {
        SuccessResult<Hamburger> success => UpdateHamburger(success.Data, name)
            // Map is only invoked if the updated was successful, otherwise the error is propagated.
            // The result of applying map is an IResult<HamburgetDto> which is exactly what we want.
            .Map(updatedHamburger => new HamburgerDto(updatedHamburger.Name)),

        // If there was an error we need to convert our IErrorResult<Hamburger> to an IErrorResult<HamburgerDto>
        // IErrorResult exposes a convenient method Cast<T> that allows you to do this
        IErrorResult<Hamburger> err => err.Cast<HamburgerDto>(),
    };
}

That looks good, can we use map to concat GetHamburger with UpdateHamburger? Yes we can, but we will get an IResult<IResult<Hamburget>> as a result:

IResult<IResult<Hamburger>> updateHamburgerResult = GetHamburger(name)
        .Map(hamburger => UpdateHamburger(hamburger, name));

FlatMap

FlatMap to the rescue. FlatMap “flattens” a Result<Result<T>> into Result<T> which is exactly what we need here.

// Refactored update Method (rest of the code omitted for brevity)
public IResult<HamburgerDto> Execute(string name)
{
    // We use FlatMap and Map to chain successful results and transform them.
    // If there is an error the error is passed along the chain as you would expect.
    return GetHamburger(name)
        // FlatMap flattens Result<Result<Hamburger>> to Result<Hamburger>
        .FlatMap(hamburger => UpdateHamburger(hamburger, name))
        // Map transforms the internal value of type Hamburger to HamburgerDto
        .Map(updatedHamburger => new HamburgerDto(updatedHamburger.Name));
}

And like that we ended up with a very declarative and succinct Update method.

Action

Action can be used when you want to process the result but the return type of your function is void or Task. For example when you work in a background task and it makes no sense to return something.

public async Task ExecuteAsync(string name)
{
    Task<IResult<Hamburger>> updateResult = PerformInternalOperation();

    await updateResult.Action(
        onSuccess: async hamburger =>
        {
            // do something with the hamburger
            await Task.CompletedTask;
        },

        // onError is optional
        onError: err => Console.WriteLine(err.GetDisplayMessage()));
}

Closing thoughts

I hope you find the result pattern useful; it’s becoming more and more trending due to its power and simplicity. If you like the library please feel free to open an issue or a PR for new features or improvements. If you find that the examples in the repo and this post are not sufficient let me know in the comments and I’ll add more documentation.

Happy coding and as the go probers say don’t just check errors, handle them gracefully.

Annexes

Are you curious about the generated code? You can explore all the generated code in your favorite IDE.

In rider you need to navigate to your project -> Dependencies -> [Framework version] -> Source Generators -> Results.ErrorResultGenerator

Rider Source Generators Explorer
Rider Source Generators Explorer

In Visual Studio go to your project -> Dependencies -> Analyzers -> Results

Visual Studio Source Generator Explorer
Visual Studio Source Generator Explorer

Finally here is the resulting code that is generated:

public partial record NotFoundError : IErrorResult
{
    /// <summary>
    /// Public constructor that takes two parameters the message and the errors.
    /// This constructor it's provided for convenience but the equivalent factory method is recommend since it returns
    /// and IResult instead of the concrete type.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    /// <param name="errors">A list of error details that are composed of a Code and a Detail.</param>
    public NotFoundError(string message, IEnumerable<ErrorResultDetail> errors)
    {
        Message = message ?? throw new ArgumentNullException(nameof(message));
        Errors = errors?.ToList() ?? throw new ArgumentNullException(nameof(errors));
    }

    /// <summary>
    /// Public constructor that only takes a message. Error details are initialized to an empty array.
    /// This constructor it's provided for convenience but the equivalent factory method is recommend since it returns
    /// and IResult instead of the concrete type.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    public NotFoundError(string message) : this(message, Array.Empty<ErrorResultDetail>())
    {
    }

    /// <summary>
    /// This is the recommend way to create an instance of this class.
    /// It takes two parameters the message and the errors.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    /// <param name="errors">A list of error details that are composed of a Code and a Detail.</param>
    /// <returns>An instance of the class abstracted as an IResult.</returns>
    public static IResult Create(string message, IEnumerable<ErrorResultDetail> errors)
    {
        return new NotFoundError(message, errors);
    }

    /// <summary>
    /// This is the recommend way to create an instance of this class.
    /// It only takes a message. Error details are initialized to an empty array.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    /// <returns>An instance of the class abstracted as an IResult.</returns>
    public static IResult Create(string message)
    {
        return new NotFoundError(message);
    }

    public string Message { get; }

    public IReadOnlyCollection<ErrorResultDetail> Errors { get; }

    /// <summary>
    /// Useful method to get a string representation of the error.
    /// It concatenates the Message with the Error list.
    /// </summary>
    /// <returns>A string representation of the error.</returns>
    public string GetDisplayMessage()
    {
        StringBuilder sb = new(Message);
        foreach (ErrorResultDetail detail in Errors)
        {
            sb = sb.Append(Environment.NewLine);
            sb = sb.Append(detail.Code).Append(value: ": ").Append(detail.Details);
        }

        return sb.ToString();
    }

    /// <summary>
    /// This method is used to cast the result to a different type.
    /// You can find examples on how it's used in the extension methods for Map, FlatMap and Action.
    /// https://github.com/wtorricos/Results/blob/main/src/Results/ResultExtensions.cs
    ///
    /// For example:
    ///     IResult<string> GetResult(int value)
    ///     {
    ///         IResult<int> intResult = Result.Success(value);
    ///
    ///         return intResult switch
    ///         {
    ///              SuccessResult<int> success => Result.Success(success.Data.ToString()),
    ///
    ///              // In the case of an error we need to cast it to comply with the method signature.
    ///              IErrorResult error => error.Cast<string>(),
    ///         }
    ///     }
    /// </summary>
    /// <typeparam name="TOut">The type to cast to.</typeparam>
    /// <returns>The same error but with a different type parameter.</returns>
    public IResult<TOut> Cast<TOut>()
    {
        return NotFoundError<TOut>.Create(Message, Errors);
    }
}

public partial record NotFoundError<T> : IErrorResult<T>
{
    /// <summary>
    /// Public constructor that takes two parameters the message and the errors.
    /// This constructor it's provided for convenience but the equivalent factory method is recommend since it returns
    /// and IResult instead of the concrete type.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    /// <param name="errors">A list of error details that are composed of a Code and a Detail.</param>
    public NotFoundError(string message, IEnumerable<ErrorResultDetail> errors)
    {
        Message = message ?? throw new ArgumentNullException(nameof(message));
        Errors = errors?.ToList() ?? throw new ArgumentNullException(nameof(errors));
    }

    /// <summary>
    /// Public constructor that only takes a message. Error details are initialized to an empty array.
    /// This constructor it's provided for convenience but the equivalent factory method is recommend since it returns
    /// and IResult instead of the concrete type.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    public NotFoundError(string message) : this(message, Array.Empty<ErrorResultDetail>())
    {
    }

    /// <summary>
    /// This is the recommend way to create an instance of this class.
    /// It takes two parameters the message and the errors.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    /// <param name="errors">A list of error details that are composed of a Code and a Detail.</param>
    /// <returns>An instance of the class abstracted as an IResult.</returns>
    public static IResult<T> Create(string message, IEnumerable<ErrorResultDetail> errors)
    {
        return new NotFoundError<T>(message, errors);
    }

    /// <summary>
    /// This is the recommend way to create an instance of this class.
    /// It only takes a message. Error details are initialized to an empty array.
    /// </summary>
    /// <param name="message">A generic string that describes the problem.</param>
    /// <returns>An instance of the class abstracted as an IResult.</returns>
    public static IResult<T> Create(string message)
    {
        return new NotFoundError<T>(message);
    }

    public string Message { get; }

    public IReadOnlyCollection<ErrorResultDetail> Errors { get; }

    /// <summary>
    /// Useful method to get a string representation of the error.
    /// It concatenates the Message with the Error list.
    /// </summary>
    /// <returns>A string representation of the error.</returns>
    public string GetDisplayMessage()
    {
        StringBuilder sb = new(Message);
        foreach (ErrorResultDetail detail in Errors)
        {
            sb = sb.Append(Environment.NewLine);
            sb = sb.Append(detail.Code).Append(value: ": ").Append(detail.Details);
        }

        return sb.ToString();
    }

    /// <summary>
    /// This method is used to cast the result to a different type.
    /// You can find examples on how it's used in the extension methods for Map, FlatMap and Action.
    /// https://github.com/wtorricos/Results/blob/main/src/Results/ResultExtensions.cs
    ///
    /// For example:
    ///     IResult<string> GetResult(int value)
    ///     {
    ///         IResult<int> intResult = Result.Success(value);
    ///
    ///         return intResult switch
    ///         {
    ///              SuccessResult<int> success => Result.Success(success.Data.ToString()),
    ///
    ///              // In the case of an error we need to cast it to comply with the method signature.
    ///              IErrorResult error => error.Cast<string>(),
    ///         }
    ///     }
    /// </summary>
    /// <typeparam name="TOut">The type to cast to.</typeparam>
    /// <returns>The same error but with a different type parameter.</returns>
    public IResult<TOut> Cast<TOut>()
    {
        return NotFoundError<TOut>.Create(Message, Errors);
    }

    /// <summary>
    /// Method that allows us to go from IResult<T> to IResult.
    /// </summary>
    /// <returns>An IResult.</returns>
    public IResult ToResult()
    {
        return NotFoundError.Create(Message, Errors);
    }
}
Похожее
Nov 27, 2023
Author: Juldhais Hengkyawan
Use the Bogus library to generate and insert 1 million dummy product data into the SQL Server database We need to create 1 million dummy product data into the SQL Server database, which can be used for development or performance...
Nov 30, 2023
Author: Dev·edium
QUIC (Quick UDP Internet Connections) is a new transport protocol for the Internet that runs on top of User Datagram Protocol (UDP) QUIC (Quick UDP Internet Connections) is a new transport protocol for the Internet that runs on top of...
Dec 25, 2022
Author: Sannan Malik
This article will look at the differences between static and readonly C# fields. Basically, a static readonly field can never be replaced by an instance of a reference type. In contrast, a const constant object cannot be replaced by a...
Aug 11, 2021
Author: Mel Grubb
Code Generation Code generation is a great way to apply patterns consistently across a solution or to create multiple similar classes based on some outside input file, or even other classes in the same solution. The tooling has changed over...
Написать сообщение
Тип
Почта
Имя
*Сообщение