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

Extend API to facilitate multiple error handling #112

Open
bidolah opened this issue May 28, 2024 · 3 comments
Open

Extend API to facilitate multiple error handling #112

bidolah opened this issue May 28, 2024 · 3 comments

Comments

@bidolah
Copy link

bidolah commented May 28, 2024

What dou you think about the following propose?

Before:

 public static ErrorOr<SomeResult> Create(string value1, string value2)
 {
     ErrorOr<Value1> value1OrError = Value1.Create(value1);
     ErrorOr<Value2> value2OrError = Value2.Create(value2);
     var errors = new List<Error>();
     errors.AddRange(value1OrError.Errors);
     errors.AddRange(value2OrError.Errors);

    if(errors.Count > 0) {
         return errors;
    }

    return  new SomeResult(value1OrError.Value, value2OrError.Value);
}

After (Option 1 - Separate class)

 public static ErrorOr<SomeResult> Create(string value1, string value2)
 {
     ErrorOr<Value1> value1OrError = Value1.Create(value1);
     ErrorOr<Value2> value2OrError = Value2.Create(value2);

     return ErrorCollector
        .Collect(value1OrError , value2OrError )
        .Then(x => new SomeResult(value1OrError.Value, value2OrError.Value));
 }

After (Option 2 - ErrorOrFactory)

 public static ErrorOr<SomeResult> Create(string value1, string value2)
 {
     ErrorOr<Value1> value1OrError = Value1.Create(value1);
     ErrorOr<Value2> value2OrError = Value2.Create(value2);

     return ErrorOrFactory
        .CollectErrors(value1OrError , value2OrError )
        .Then(() => new SomeResult(value1OrError.Value, value2OrError.Value));
 }

Implementation propose:

 public static ErrorOr<Result> CollectErrors<Result>(params IErrorOr[] errorsToCombine)
 {
     if (errorsToCombine.Any(x => x.IsError))
     {
         return errorsToCombine.SelectMany(x => x.Errors);
     }

     return Result.Success;
 }

Of course, improvements are required to handle cases like null values.

@yeongjonglim
Copy link

I am also looking at a similar feature like this. On top of that, I am also trying to have Then mixing to aggregate multiple results from previous steps. Maybe the said proposal can also cover the following scenario at the same time, where I frequently will create multiple ErrorOr values that have nothing to do with each other, but will be used in a common function at the end.

var userQuery = await sender.Send(new UserQuery(request.UserExternalId), cancellationToken); --> This method returns ErrorOr value

if (userQuery.IsError)
{
    return userQuery.Errors;
}

return await sender.Send(new TransactionSingleQuery(request.UserExternalId, request.TransactionGroupId), cancellationToken) --> This method returns ErrorOr value
    .ThenAsync(async x =>
    {
        var update = x.TransactionGroup.Update(
            request.TransactionTime,
            request.Description,
            request.Amount,
            userQuery.Value.User, --> This is the part that I hope to be able to chain together in Then mixing from userQuery
            timeProvider.GetUtcNow(),
            from,
            to);

        await repository.UpdateTransactionGroup(update);

        return new TransactionUpdateResult(update.Id);
    })
    .Else(x => x);

@ahmtsen
Copy link
Contributor

ahmtsen commented Jun 5, 2024

I am also looking at a similar feature like this. On top of that, I am also trying to have Then mixing to aggregate multiple results from previous steps. Maybe the said proposal can also cover the following scenario at the same time, where I frequently will create multiple ErrorOr values that have nothing to do with each other, but will be used in a common function at the end.

var userQuery = await sender.Send(new UserQuery(request.UserExternalId), cancellationToken); --> This method returns ErrorOr value

if (userQuery.IsError)
{
    return userQuery.Errors;
}

return await sender.Send(new TransactionSingleQuery(request.UserExternalId, request.TransactionGroupId), cancellationToken) --> This method returns ErrorOr value
    .ThenAsync(async x =>
    {
        var update = x.TransactionGroup.Update(
            request.TransactionTime,
            request.Description,
            request.Amount,
            userQuery.Value.User, --> This is the part that I hope to be able to chain together in Then mixing from userQuery
            timeProvider.GetUtcNow(),
            from,
            to);

        await repository.UpdateTransactionGroup(update);

        return new TransactionUpdateResult(update.Id);
    })
    .Else(x => x);

You can achive this behavior by forwarding the result using Tuples.

Created an example for you

using ErrorOr;

ErrorOr<TransactionGroup> Handle(ISender sender, int externalUserId, int transactionGroupId) =>
    new UserQuery(externalUserId)
        .ToErrorOr()
        .Then(userQuery => sender.Send(userQuery))
        .Then(user => (user, new TransactionSingleQuery(externalUserId, transactionGroupId)))
        .Then(tuple => (tuple.Item1, sender.Send(tuple.Item2)))
        .Then(
            (tuple) =>
            {
                User user = tuple.Item1;
                TransactionGroup transactionGroup = tuple.Item2;

                // do work with these

                return transactionGroup;
            }
        );

record User(int Id, string Name);

record UserQuery(int ExternalUserId);

record TransactionSingleQuery(int ExternalUserId, int TransactionGroupId);

record TransactionGroup(int Id, DateTime TransactionDateTime);

interface ISender
{
    User Send(UserQuery query);

    TransactionGroup Send(TransactionSingleQuery query);
}

@ahmtsen
Copy link
Contributor

ahmtsen commented Jun 5, 2024

What dou you think about the following propose?

Before:

 public static ErrorOr<SomeResult> Create(string value1, string value2)
 {
     ErrorOr<Value1> value1OrError = Value1.Create(value1);
     ErrorOr<Value2> value2OrError = Value2.Create(value2);
     var errors = new List<Error>();
     errors.AddRange(value1OrError.Errors);
     errors.AddRange(value2OrError.Errors);

    if(errors.Count > 0) {
         return errors;
    }

    return  new SomeResult(value1OrError.Value, value2OrError.Value);
}

After (Option 1 - Separate class)

 public static ErrorOr<SomeResult> Create(string value1, string value2)
 {
     ErrorOr<Value1> value1OrError = Value1.Create(value1);
     ErrorOr<Value2> value2OrError = Value2.Create(value2);

     return ErrorCollector
        .Collect(value1OrError , value2OrError )
        .Then(x => new SomeResult(value1OrError.Value, value2OrError.Value));
 }

After (Option 2 - ErrorOrFactory)

 public static ErrorOr<SomeResult> Create(string value1, string value2)
 {
     ErrorOr<Value1> value1OrError = Value1.Create(value1);
     ErrorOr<Value2> value2OrError = Value2.Create(value2);

     return ErrorOrFactory
        .CollectErrors(value1OrError , value2OrError )
        .Then(() => new SomeResult(value1OrError.Value, value2OrError.Value));
 }

Implementation propose:

 public static ErrorOr<Result> CollectErrors<Result>(params IErrorOr[] errorsToCombine)
 {
     if (errorsToCombine.Any(x => x.IsError))
     {
         return errorsToCombine.SelectMany(x => x.Errors);
     }

     return Result.Success;
 }

Of course, improvements are required to handle cases like null values.

To achieve the similar behavior. I created a custom extension like this.

public static class Errors
{
  public static List<Error> Combine(params IErrorOr[] errorOrs) =>
      errorOrs
          .Where(x => x.IsError)
          .Where(x => x.Errors is not null)
          .SelectMany(x => x.Errors!)
          .ToList();
}

Maybe a static method on Error or ErrorOrFactory will do the trick.

You can find an example usage of the extension method I created below.

ErrorOr<Success> HandleUpdateUser()
{
    User user = GetUser();

    if (
        Errors.Combine(user.SetFirstName(string.Empty), user.SetLastName(string.Empty))
        is var errors
            and { Count: not 0 }
    )
    {
        return errors;
    }

    return Result.Success;
}


class User
{
    public string FirstName { get; private set; }

    public string LastName { get; private set; }

    public ErrorOr<Updated> SetFirstName(string firstName)
    {
        if (string.IsNullOrWhiteSpace(firstName))
        {
            return Error.Validation(description: "First name is required");
        }

        return Result.Updated;
    }

    public ErrorOr<Updated> SetLastName(string lastName)
    {
        if (string.IsNullOrWhiteSpace(lastName))
        {
            return Error.Validation(description: "Last name is required");
        }
        return Result.Updated;
    }
}

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