How do I return a non-Error type for Left with LanguageExt EitherAsync?

Viewed 80

I have a method that looks for a loyalty card in the database, and returns it if it was found and is valid, or an enum value if not. This allows the calling code to switch on the enum value. I want to return an Either<TransactionRequestStates, Card>, as the method will be bound along with several similar methods that fetch and verify the incoming data. As database calls should be made async, I'll need to return EitherAsync as opposed to just Either.

To clarify, I have several methods, each of which performs a particular step. If successful, each method returns a Right containing some data that is used by later methods. If not, it returns a Left with an enum value indicating what went wrong. I want to compose them as follows (simplified for clarity)...

public static async Task<TransactionRequestStates> Process(
        IHttpContextAccessor httpContextAccessor,
        AppDbContext context, UserManager<User> userMgr,
        string cardSerialNumber, string uri, int amount, string hash) {
  Either<TransactionRequestStates, TransactionRequestStates> result = await (
    from card in GetCard(context, cardSerialNumber, amount)
    from deviceSerialNumber in GetDeviceSerialNumberFromToken(httpContextAccessor, context, userMgr)
    from device in GetDevice(httpContextAccessor, context, deviceSerialNumber)
    from rental in GetOrganisationRental(context, device)
    from vcResponse in GetVcResponse(rental, card, amount, hash, uri)
    from saveResponse in SaveResponse()
    select TransactionRequestStates.Success
  );
  return result.Match(_ => TransactionRequestStates.Success, msg => msg);
}

This question covers the GetCard method, as I assume that if I can get that working, the others will all follow a similar pattern.

I can do the following...

  private static EitherAsync<TransactionRequestStates, Card> GetCard(AppDbContext context, string cardSerialNumber) {
    Card? card = context.Cards.SingleOrDefault(c => c.SerialNumber == cardSerialNumber);
    if (card == null) {
      return TransactionRequestStates.CardNotFound.AsTask();
    }
    if (card.Locked) {
      return TransactionRequestStates.CardLocked.AsTask();
    }
    // Quite a few more checks are done in the real code. If we pass them all, return the card...
    return card.AsTask();
  }

However, this is all sync code. I want the database call to be async. Following the answer I got from Guru Stron in a previous question, I know I can do something like this...

static EitherAsync<string, int> Square2(int n) =>
    TryAsync(async () =>
        {
            await Task.Delay(300);
            throw new Exception();
            return n * n;
        })
        .ToEither(error => error.Message);

However, I don't want to return a string in case of exception, I want to return an enum value, as shown above.

I can't work out how to do this. If I try returning the enum value directly...

  private static EitherAsync<TransactionRequestStates, Card> GetCard2(AppDbContext context, string cardSerialNumber) =>
    TryAsync(async () => {
      Card? card = await context.Cards.SingleOrDefaultAsync(c => c.SerialNumber == cardSerialNumber);
      if (card == null) {
        return TransactionRequestStates.CardNotFound.AsTask();
      }
      if (card.Locked) {
        return TransactionRequestStates.CardLocked.AsTask();
      }
      return card;
    });

...then I get a compiler error "The type arguments for method 'Prelude.TryAsync(Func<Task>)' cannot be inferred from the usage. Try specifying the type arguments explicitly". My experience with errors of this sort imply that my code is wrong, as if it were correct, the compiler should be able to infer the types.

I tried returning Left<TransactionRequestStates, Card>(TransactionRequestStates.CardNotFound) (with and without calling AsTask() at the end) instead, but that didn't change the error. I also tried adding the call to ToEither() at the end, but that also didn't help.

Anyone able to explain what I need to do to be able to return an EitherAsync<TransactionRequestStates, Card>? Thanks

2 Answers

Why make things so difficult?

A stack of monads like EitherAsync mainly exists because it, itself, is a monad, and so defines a SelectMany method that affords query syntax (syntactic sugar). This is mostly useful if you need to compose multiple EitherAsync values with each other. That's not the case here.

It'd be much easier to define methods as returning Task<Either<L, R>>, because then you can rely on C# native async and await functionality. You can always use ToAsync and ToEither to convert back and forth if you (internally in a method implementation) need the EitherAsync functionality.

If you do this, you can use fairly idiomatic C# to implement the desired functionality:

public static async Task<Either<TransactionRequestStates, Card>> GetCard(
    AppDbContext context,
    string cardSerialNumber)
{
    Card? card = await context.Cards.SingleOrDefaultAsync(
        c => c.SerialNumber == cardSerialNumber);
    if (card == null)
        return Left<TransactionRequestStates, Card>(
            TransactionRequestStates.CardNotFound);
    if (card.Locked)
        return Left<TransactionRequestStates, Card>(
            TransactionRequestStates.CardLocked);

    return Right<TransactionRequestStates, Card>(card);
}

If you want to convert it to EitherAsync you can easily do that:

public static EitherAsync<TransactionRequestStates, Card> GetCard2(
    AppDbContext context, string cardSerialNumber) =>
    GetCard(context, cardSerialNumber).ToAsync();

Try using OptionalAsync:

static EitherAsync<TransactionRequestStates, Card> GetCard2(AppDbContext context, string cardSerialNumber) => 
    OptionalAsync(context.Cards.SingleOrDefaultAsync(c => c.SerialNumber == cardSerialNumber))
        .ToEither(TransactionRequestStates.CardNotFound);

Note that this will swallow any exception and will return TransactionRequestStates.CardNotFound;

UPD

To handle multiple cases you can try using Map and Flatten:

static EitherAsync<TransactionRequestStates, Card> GetCard2(AppDbContext context, string cardSerialNumber) =>
    TryAsync(context.Cards.SingleOrDefaultAsync(c => c.SerialNumber == cardSerialNumber))
        .Map(card => (card switch
            {
                null => Left<TransactionRequestStates, Card>(TransactionRequestStates.CardNotFound),
                // {} when ...  => Left<TransactionRequestStates, Card>(TransactionRequestStates.CardNotFound),
                { } => Right<TransactionRequestStates, Card>(card)
            })
            .ToAsync())
        .ToEither(_ => TransactionRequestStates.Failed)
        .Flatten();
Related