37

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.

JesseBuesking
  • 6,496
  • 4
  • 44
  • 89

5 Answers5

40

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
}
sa_ddam213
  • 42,848
  • 7
  • 101
  • 110
7

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.

KCD
  • 9,873
  • 5
  • 66
  • 75
  • 13
    In C# 8, this can be shortened even further: with `if (await TryBlahString("foo") is (true, var result))`, the variable `result` will be a `string`, and the code path will only be entered with the first value in the tuple is `true`. – Sören Kuklau Sep 01 '20 at 09:34
  • 1
    @SörenKuklau I have always used the callback pattern for async Try, but I really like what you did here. Thank you for sharing it. – Daniel Feb 03 '22 at 20:17
2

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.

Robert Horvick
  • 3,966
  • 21
  • 18
1

Sounds like a problem for generics.

public async Task<Tuple<bool, TResult>> TryBlah<TResult>(string key)
{
    var resultType = typeof(TResult);
    // ... perform some slow io, return new Tuple<bool, TResult>(...)
}
Adam Maras
  • 26,269
  • 6
  • 65
  • 91
  • I thought about suggesting this, but given he seems concerned with a "nasty API" I'm not sure there's much advantage over the distinct named methods that the OP originally suggested - in fact you have to type 2 extra characters: < and > :) I suppose there might be advantages from the point of view of authoring these methods though... – mutex Aug 08 '13 at 03:50
  • @mutex Well, aside from it solving the initial problem of being able to (essentially) overload on return type, it might also be a better solution to the more general problem of having to handle the same operation across several different data types. – Adam Maras Aug 08 '13 at 04:32
  • The big problem with this is that there is no indication which types are allowed. Ideally, generics should be used a method can work with with any type, not just half a dozen specific types. – svick Aug 08 '13 at 08:15
  • @svick you're right, but do consider that this method provides a boolean result, which would allow it to gracefully fail (instead of throwing) if it is called with an unexpected type. – Adam Maras Aug 08 '13 at 16:22
0

It looks like you are trying to make an API that takes in a request and then retrieves some data and then processes/converts that data in a particular way and returns it back to the caller. What if you implemented a manager that would handle the different processing methods.

I propose a solution of creating a request and response class that will be passed to the manager, and the manager then returns a result after the processing is complete.

public class Request
{
    public Type ReturnType;
    public string Key { get; set; }
    public Request(string Key, Type returnType)
    {
        this.Key = Key;
        this.ReturnType = returnType;
    }
}

public class Response
{
    public object value;
    public Type returnType;
}

//Singleton processor to get data out of cache
public class CacheProcessor
{
    private static CacheProcessor instance;

    public static CacheProcessor Process
    {
        get
        {
            if (instance == null)
                instance = new CacheProcessor();
            return instance;
        }
    }

    private Dictionary<Type, Func<Request, object>> Processors = new Dictionary<Type, Func<Request, object>>();

    private CacheProcessor()
    {
        CreateAvailableProcessors(); 
    }

    //All available processors available here
    //You could change type to string or some other type 
    //to extend if you need something like "CrazyZipUtility" as a processor
    private void CreateAvailableProcessors()
    {
        Processors.Add(typeof(string), ProcessString);
        Processors.Add(typeof(byte[]), ProcessByteArry);   
    }

    //Fake method, this should encapsulate all crazy 
    //cache code to retrieve stuff out
    private static string CacheGetKey(string p)
    {
        return "1dimd09823f02mf23f23f0";  //Bullshit data
    }

    //The goood old tryBlah... So Sexy
    public Response TryBlah(Request request)
    {
        if (Processors.ContainsKey(request.ReturnType))
        {
            object processedObject = Processors[request.ReturnType].Invoke(request);
            return new Response()
            {
                returnType = request.ReturnType,
                value = processedObject
            };
        }
        return null;
    }

    //Maybe put these in their own class along with the dictionary
    //So you can maintain them in their own file
    private static object ProcessString(Request request)
    {
        var value = CacheGetKey(request.Key);
        //Do some shit
        return value;
    }

    private static object ProcessByteArry(Request request)
    {
        var value = CacheGetKey(request.Key);
        ASCIIEncoding encoding = new ASCIIEncoding();
        Byte[] bytes = encoding.GetBytes(value);
        return bytes;
    }
}

The big thing is the Dictionary (or a HashSet) holds your available processors. Then based on type, the correct processor is invoked and results are returned.

The code would be invoked as follows.

var makeByteRequest = new Request("SomeValue", typeof(byte[]));
Response btyeResponse = CacheProcessor.Process.TryBlah(makeByteRequest);
cgatian
  • 22,047
  • 9
  • 56
  • 76