views:

666

answers:

3

When writing async method implementations using the BeginInvoke/EndInvoke pattern the code might look something like the following (and to save you guessing this is an async wrapper around a cache):

IAsyncResult BeginPut(string key, object value)
{
    Action<string, object> put = this.cache.Put;
    return put.BeginInvoke(key, value, null, null);
}

void EndPut(IAsyncResult asyncResult)
{
    var put = (Action<string, object>)((AsyncResult)asyncResult).AsyncDelegate;
    put.EndInvoke(asyncResult);
}

This works perfectly well because it's known what the type of delegate is, so it can be cast. However it starts to get messy when you have two Put methods, because although the method returns void you seemingly have to cast it to a strongly typed delegate to end the invocation, e.g.

IAsyncResult BeginPut(string key, object value)
{
    Action<string, object> put = this.cache.Put;
    return put.BeginInvoke(key, value, null, null);
}

IAsyncResult BeginPut(string region, string key, object value)
{
    Action<string, string, object> put = this.cache.Put;
    return put.BeginInvoke(region, key, value, null, null);
}

void EndPut(IAsyncResult asyncResult)
{
    var put = ((AsyncResult)asyncResult).AsyncDelegate;

    var put1 = put as Action<string, object>;
    if (put1 != null) 
    {
        put1.EndInvoke(asyncResult);
        return;
    }

    var put2 = put as Action<string, string, object>;
    if (put2 != null) 
    {
        put2.EndInvoke(asyncResult);
        return;
    }

    throw new ArgumentException("Invalid async result", "asyncResult");
}

I'm hoping there is a cleaner way to do this, because the only thing I care about the delegate is the return type (in this case void) and not the arguments that were supplied to it. But I've racked my brains and asked others in the office, and nobody can think of the answer.

I know one solution is to write a custom IAsyncResult, but that's such a difficult task with the potential threading issues around things like lazy instantiation of the WaitHandle that I'd rather have this slightly hacky looking code than go down that route.

Any ideas on how to end the invocation without a cascading set of is checks?

A: 

Why not avoid the problem by just going back to the more general overload:

IAsyncResult BeginPut(string key, object value) {
   return this.BeginPut(null, key, value);
}

IAsyncResult BeginPut(string region, string key, object value) {
   Action<string, string, object> put = this.Put;
   return put.BeginInvoke(region, key, value, null, null);
}

void EndPut(IAsyncResult asyncResult) {
   var put = (Action<string, string, object>)((AsyncResult)asyncResult).AsyncDelegate;
   put.EndInvoke(asyncResult);
}
Mark Brackett
Unfortunately the Put(string,object) method does not pass through to the Put(string,string,object) method internally, and it is illegal to pass null as the first argument to the latter [this does seem reasonable as they have somewhat different semantics in spite of the name similarity].
Greg Beech
A: 

Edit: Please see my other answer. It can be cleaner.

I don't think there is a way to make this cleaner. You essentially made this inevitable by using the same EndPut method to end more than one type of async call. You could just as well be following the normal pattern by passing EndPut and the put delegate as the last two parameters to BeginInvoke() (callback and asyncstate), and you would still have to test what type the delegate is in order call EndInvoke().

Casting them to Delegate doesn't help at all.

I like Mark Brackett's idea. I think it comes down to these options:

  • Join them completely by having one overload call another. One delegate, one callback.
  • Separate them completely by having two callbacks for calling EndInvoke().

Aside from those, the only thing is to make your code just a little cleaner and use a switch or lookup dictionary using asyncResult.AsyncState.GetType(), passing the put delegate as that state object.

Joel B Fant
Unfortunately as commented on Mark's answer, null is illegal for the first argument to Put(string,string,object) so overloads calling each other isn't possible. Separating the callbacks isn't possible either because EndPut needs to be callable by the client and it must be called this by convention.
Greg Beech
So it sounds like there's not much that can be done :( What I'd really like is to be able to cast it to just Action because I don't care about the argument types, but it doesn't seem this type of thing is possible in C# as Action<T> is not an Action.
Greg Beech
That's alright, I figured it out.
Joel B Fant
+4  A: 

I was wrong, there is a cleaner way.

You create Action( IAsyncResult ) delegates for the specific EndInvoke() method in the same context where you already know the specific type of the delegate, passing it as the AsyncState. I'm passing EndPut() as the callback for convenience.

IAsyncResult BeginPut( string key, object value ) {
    Action<string, object> put = this.Put;
    return put.BeginInvoke( key, value, EndPut,
        new Action<IAsyncResult>( put.EndInvoke ) );
}

IAsyncResult BeginPut( string region, string key, object value ) {
    Action<string, string, object> put = this.Put;
    return put.BeginInvoke( region, key, value, EndPut,
        new Action<IAsyncResult>( put.EndInvoke ) );
}

And then you finish it off.

void EndPut( IAsyncResult asyncResult ) {
    var del = asyncResult.AsyncState as Action<IAsyncResult>;
    del( asyncResult );
}
Joel B Fant
Nice :) I knew there had to be some way of doing it! Thanks!
Greg Beech
It can also be used for EndInvoke() calls that have out arguments, but requires one more level of abstraction in the form of a closure that hides EndInvoke(out myObj, asyncResult) behind EndInvoke(asyncResult).
Joel B Fant
I only have methods like Put which return void, or Get which returns a single object, so it looks like I can cover them all with "delegate void EndInvokeAction(IAsyncResult asyncResult)" and "delegate T EndInvokeFunc<T>(IAsyncResult asyncResult)". These would be useful outside the cache actually!
Greg Beech
Or thinking about it more, this solution can just use Action<IAsyncResult> and Func<IAsyncResult, T> without having to define any new delegates. Now I'm really happy!
Greg Beech
Ahhh, good point. I'm going to edit for that.
Joel B Fant