I'm looking for recommendations on how to handle the following situation.
I'm creating methods for trying to get at some data, following this pattern:
// Typical pattern
public bool TryBlah(string key, out object value)
{
// ... set value and return boolean
}
I've run into an issue when trying to follow this pattern on they async versions because you cannot use out
on async methods:
// Ideal async pattern (not allowed to use an 'out' parameter, so this fails)
public async Task<bool> TryBlah(string key, out object value)
{
// ... set value, perform some slow io operation, return bool
}
One workaround is to return a tuple containing your data. This works for methods that return a single data type like so:
// Tuple version
public async Task<Tuple<bool, object>> TryBlah(string key)
{
// ... perform some slow io, return new Tuple<bool, object>(...)
}
The issue is when you want to return different data types. Without using async you can create several methods with nearly identical signatures like so:
public bool TryBlah(string key, out byte[] value)
{
// ...
}
public bool TryBlah(string key, out string value)
{
// ...
}
That's great. That's what I'm looking to do. This api is very straightforward and easy to work with (the method names are all the same, only the data that is passed in changes).
Not being able to use out
with async methods messes this up though.
One way to get around this is to return a Tuple
of your data. However now you can't have nearly identical method signatures like the following:
// The suck... the signatures match, but you want to return different values.
// You can't do this:
public async Task<Tuple<bool, byte[]>> TryBlah(string key)
{
// ...
}
public async Task<Tuple<bool, string>> TryBlah(string key)
{
// ...
}
Those methods fail because they have the same signatures. The only way to work around this that comes to mind is to give each method a distinct name, like so:
public async Task<Tuple<bool, byte[]>> TryBlahByteArray(string key)
{
// ...
}
public async Task<Tuple<bool, string>> TryBlahString(string key)
{
// ...
}
My issue is that this now creates what I consider a nasty api where you now have a whole lot of different methods. Yes, it's not that big of an issue, but I feel that there has to be a better way.
Are there other patterns that lend themselves to a nicer api when working with async methods like this? I'm open to any suggestions.
Maybe you could use Action<T>
as the out param substitute
Example:
public async Task<bool> TryBlah(string key, Action<int> value)
{
int something = await DoLongRunningIO();
value(something)
return true;
}
Usage:
int myOutParam = 0;
if (await TryBlah("Something", value => myOutParam = value))
{
// do somthing
}
Here is a circa 2017 update with ValueTuples, your sucky option is not so bad.
public async Task<(bool, byte[])> TryBlahByteArray(string key)
{
// await something
return (true, new byte[1]);
}
public async Task<(bool, string)> TryBlahString(string key)
{
// await something
return (false, "blah");
}
Used as
(bool success, byte[] blahs) = await TryBlahByteArray("key");
And
(bool success, string blah) = await TryBlahString("key");
I don't often want method names that are the same that return different things or a raw object
anyway, so maybe this is less of a concern. Your mileage may vary.
I would not use a Try* method with TPL. Instead use a continuation (Task.ContinueWith) with a the OnlyOnFaulted options.
This way your task completes one way or another and the caller gets to decide how to handle errors, cancellations, etc.
It also gets rid of the Tuple.
As for other design issues, anytime I see someone saying "I want this method to overload based on return type" I smell a bad idea. I'd rather see verbose names (GetString, GetByte, GetByteArray, etc - look at SqlDataReader) or have the API return a very basic type (e.g., byte[] - look at Stream) and let the caller create higher level conversions like StreamReader/TextReader/etc.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With