Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async Try(blah) pattern [duplicate]

Tags:

c#

async-await

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.

like image 931
JesseBuesking Avatar asked Aug 08 '13 03:08

JesseBuesking


Video Answer


3 Answers

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
}
like image 195
sa_ddam213 Avatar answered Oct 13 '22 09:10

sa_ddam213


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.

like image 21
KCD Avatar answered Oct 13 '22 10:10

KCD


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.

like image 2
Robert Horvick Avatar answered Oct 13 '22 08:10

Robert Horvick