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
.
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
CodePudding user response:
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();
CodePudding user response:
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();