FluentResults icon indicating copy to clipboard operation
FluentResults copied to clipboard

Usage in Railway Oriented Programming

Open yarus opened this issue 4 years ago • 3 comments

Hi all, thanks for the great package!

I have a question about using Result<T> class in ROP manner to implement parallel validations.

Check this code as example:

public static Result<Email> Create(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        return Result.Fail<Email>("Email is empty");

    email = email.Trim();

    var validationResult = Result.Merge(
        Result.FailIf(email.Length > 200, "Email is bigger then 200 symbols"),
        Result.FailIf(!Regex.IsMatch(email, @"^(.+)@(.+)$"), $"'{email}' is not a valid email address")
    );

    if (validationResult.IsFailed)
        return validationResult;

    return Result.Ok(new Email(email));
}

This is an implementation of a factory method for Email ValueObject which is a common pattern for DDD.

Result.Merge allows me to do some checks in parallel and return an array of errors instead of failing on the first error. This is quite useful since we can send all found errors to the client. The downside of Result<T> usage here is the code become very verbose and we have to check if Result fail on every level which is using this type.

On of suggested ways to reduce verbosity while using Result type is to follow Railway Oriented Programming (ROP) approach. By writing a number of Generic extension method for Result<T> class I was able to refactor this code to the following:

return email.ToResult("Email is empty")
    .OnSuccess(value => value.Trim())
    .Ensure(value => value.Length <= 200, "Email is bigger then 200 symbols")
    .Ensure(value => Regex.IsMatch(value, @"^(.+)@(.+)$"), $"'{email}' is not a valid email address")
    .Map(value => new Email(value));

This variant is less verbose. Though I have a problem here. The way how Ensure method is implemented rely on the fact that previous method in the chain succeeded. This mean we'll be able to catch only the first error in the chain. What I want to achieve is to keep parallel validation here after we checked that Email is not null or empty which can be achieved by .ToResult() call.

public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, string errorMessage)
{
    if (result.IsFailed)
        return result;

    if (!predicate(result.Value))
        return Result.Fail<T>(errorMessage);

    return result;
}

Can someone help me with some Validate extension method (or maybe some other which going to be more common for functional programming paradigm) which could take a params list of predicates with error messages, run the predicate and accumulate errors in case of failures. I want to achieve something like that:

public static Result<Email> Create(string email)
{
  return email.ToResult("Email is empty")
      .OnSuccess(value => value.Trim())
      .Validate(
        value => value.Length <= 200, "Email is bigger then 200 symbols"),
        value => Regex.IsMatch(value, @"^(.+)@(.+)$"), $"'{email}' is not a valid email address")
      )
      .Map(value => new Email(value));
}

Thanks in advance!

yarus avatar Jan 29 '22 20:01 yarus

Hi,

I know this railway oriented style of programming in combination with the result pattern because I used it in a project some years ago. I'am not a big fan of that style. Its readable in simple scenarios but is hard to read in complex scenarios. Your scenario is not that complex and you hit already the border. But be aware - I didn't used it much.

You can inspire yourself from the github project https://github.com/vkhorikov/CSharpFunctionalExtensions. This project has many extension methods to support this railway style. Maybe you find appropriate methods for your scenario.

What we did in our current project is to write a small validation engine with works with the result pattern. You can pass a list of validation rules (simple delegates) and the input value and then a result is returned with the error messages. Then you collect all error messages together. For us it is good enough.

I hope it helps.

altmann avatar Feb 02 '22 18:02 altmann

@altmann for the answer but I am still not sure how to better do this. I looked into CSharpFunctionalExceptions project but can't seems to find what I really need.

yarus avatar Feb 11 '22 08:02 yarus