I have the following method:
private async Task<(bool, string)> Relay(
WorkflowTask workflowTask,
MontageUploadConfig montageData,
File sourceFile,
CancellationToken cancellationToken
)
{
try
{
byte[] fileContent = await _httpClient.GetByteArrayAsync(sourceFile.Url, cancellationToken);
await _attachmentController.TryUploadAttachment(montageData.EventId, fileContent, sourceFile.Name);
return (true, null);
}
catch (Exception exception)
{
_logger.LogError(exception, $"File cannot be uploaded: {sourceFile.Name}", workflowTask);
return (false, exception.ToString());
}
}
I'd like to refactor it to use TryAsync
from LanguageExt.Core
(or some other functional Try
type).
I've managed to refactor the above method to:
private TryAsync<bool> Relay(
MontageUploadConfig montageData,
File sourceFile,
CancellationToken cancellationToken
) => new(async () =>
{
byte[] fileContent = await _httpClient.GetByteArrayAsync(sourceFile.Url, cancellationToken);
return await _attachmentController.TryUploadAttachment(montageData.EventId, fileContent, sourceFile.Name);
});
This compiles, but I haven't been able how to consume the result, either doing whatever comes next or logging the exception.
How can I check the result and either do something with either the returned value or any exceptions?
The current way of constructing TryAsync
inside Relay
is not correct. For example suppose you have this:
static async Task<string> MightThrow() {
await Task.Delay(0);
throw new Exception("test");
}
And you construct TryAsync
from it the way you are doing now:
static TryAsync<string> WrongTryAsync() {
return new (async () => {
return await MightThrow();
});
}
Now what would happen if you try to consume this?
var tryAsync = WrongTryAsync();
var result = await tryAsync();
It will throw an exception on await
, this is not what you expect in such case, you expect result in a failed state instead. So while what you return matches TryAsync
signature - that's not enough.
The library has a way to construct TryAsync
in the expected way:
using static LanguageExt.Prelude; // not necessary but convenient
...
static TryAsync<string> CorrectTryAsync() {
return TryAsync(MightThrow);
// use static call if you didn't add using static above
// return LanguageExt.Prelude.TryAsync(MightThrow);
}
Now the code above consuming it will not throw but return expected failed result:
var tryAsync = CorrectTryAsync();
var result = await tryAsync();
// result.IsFaulted == true;
So after you constructed correct TryAsync
- you can either obtain the result from it by executing the delegate (await ...()
) and then work with result, or work with TryAsync
itself, because library provides quite some extension methods for it directly.
Edit: it's not necessary to unwrap the result of TryAsync
right away, you can perform various operations on TryAsync
itself, since it's a monad. Since you have scala background, we can look at this example and do something similar.
using System;
using System.Threading.Tasks;
using static LanguageExt.Prelude;
namespace ConsoleApp4 {
class Program {
static async Task Main(string[] args) {
await Test();
Console.ReadKey();
}
static async Task Test() {
// we have two async methods which return ints, and we want to divide them, but any of them might throw
// we want to do that in a functional fashion
// I use varibles for clarity, you can combine all this into one function call chain.
var dividend = TryAsync(GetDividend);
var divisor = TryAsync(GetDivisor);
// now apply the division function to the (wrapped) values of our TryAsync instances
var result = apply((a, b) => a / (float)b, dividend, divisor);
// decide what to do on success or on failure and execute
await result
.Match(
r => Console.WriteLine($"Success, result is: {r}"),
ex => Console.WriteLine($"Failed to compute, exception was: {ex}")
);
}
static async Task<int> GetDividend() {
await Task.Delay(0);
// or might throw
return 10;
}
static async Task<int> GetDivisor() {
await Task.Delay(0);
return 5;
}
}
}