Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage in Railway Oriented Programming #108

Open
yarus opened this issue Jan 29, 2022 · 3 comments
Open

Usage in Railway Oriented Programming #108

yarus opened this issue Jan 29, 2022 · 3 comments

Comments

@yarus
Copy link

yarus commented Jan 29, 2022

Hi all, thanks for the great package!

I have a question about using Result 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 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 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!

@altmann
Copy link
Owner

altmann commented Feb 2, 2022

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.

@yarus
Copy link
Author

yarus commented Feb 11, 2022

@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.

@nkz-soft
Copy link

Hi, @altmann

Here I tried to adapt some features of CSharpFunctionalExtensions to use Result from this library. Do you think we could include a separate extension module in this project to support railway style development?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants