Search code examples
.netvb.netgenerics.net-3.5expression-trees

Creating an Action for unknown number and type of generic parameters


TL;DR

I'm looking for a way to either:

  • Create an Action that takes a ParamArray of Object
  • Use Expression to create an Action that matches the given number/type of generic parameters

Details

I'm writing a function that turns asynchronous calls into blocking calls. My function takes a task to run, a time-out value, and a collection of States (kind of like events) that might occur as a result. We have States defined for multiple numbers of generic parameters IState(of T), IState(of T1, T2), etc...

Function MakeBlocking(task As Action,
                      resultingStates As IEnumberable(of IState),
                      Optional ByVal millisecondsTimeout As Integer = -1) 
                      As Boolean

  Dim are As New AutoResetEvent(False)
  Dim onFinish As New Action(Sub() are.Set())

  For Each state In resultingStates
    state.Subscribe(onFinish)
  Next

  task.Invoke()

  Dim result = are.WaitOne(millisecondsTimeout)

  For Each state In resultingStates
    state.Unsubscribe(onFinish)
  Next

  Return result
End Function

I'd like to be able to accept a collection of IState that have any number and type of parameters. Since IState(Of T) inherits from IState, and IState(Of T1, T2) inherits from IState(Of T), I think IEnumerable(Of IState) will work. The problem is the Action I'm subscribing them to doesn't have matching parameters.

Public Interface IState(Of T1, T2)
  Inherits IState(Of T1)

  Shadows Function Subscribe(ByVal action As Action(Of T1, T2)) As IStateSubscription
  Shadows Function Unsubscribe(ByVal action As Action(Of T1, T2)) As Boolean

End Interface

As you can see, if I have resulting states of IState(Of String, Boolean), IState(Of Integer) and IState(Of String), I'd need a separate Action for each of those. I'd really like to be able to do something like this:

Dim onFinish As New Action(Sub(ParamArray stuff As Object())(Sub() are.Set())

But I can't seem to find the validate syntax to do that (if it is even possible). It seems like the only other option is using Expression.Lambda to dynamically create methods I can subscribe to, but I haven't had much experience with expression trees before. Is there a way I can create an Expression from my OnFinish action?

I should note that the library that deals with States is outside of my control. The underlying code checks that the the parameters match in number and assignability (i.e IsAssignableFrom), so even though I'm passing everything in as IState, it's finding out what the underlying type really is, and then telling me I can't subscribe my Action to that State.


Solution

  • Here's what I came up with...

    With delegates

    It is possible to create a Delegate in VB.Net that takes a ParamArray using a work-around. Not sure if that would have worked though. It is also possible to use Expression to dynamically create a delegate:

    Dim are As New AutoResetEvent(False)
    
    Dim setMethod = are.GetType.GetMethod("Set")
    Dim callSet = Expression.Call(Expression.Constant(are), setMethod)
    
    For Each state In resultingStates
      Dim args = state.GetType.GetGenericArguments()
      Dim params = Enumerable.Range(1, args.Count).Select(
                     Function(x) Expression.Parameter(args(x - 1), "var" & x))
      Dim onFinish = Expression.Lambda(callSet, params.ToArray).Compile()
    
      state.Subscribe(onFinish) 'Doesn't compile since OnFinish isn't an Action
    Next
    

    However, none of these options work, since I need an Action. It looks like I can make an Action from a Delegate, but I need to know the generic types of the Action so I can it, something not available until runtime, in my case... (I believe you can't cast without statically knowing the type).

    My solution

    I ended up breaking the problem apart a bit. Previously, I would have called it like this:

    Public Function GetThing() As Boolean 'Returns success vs timeout
      Return MakeBlocking(Sub() SendAsyncThingRequest(), 
                          {ResultingState1, ResultingState2}, 
                          timeOutVal)
    End Sub
    

    I've instead broken it out, so that the caller has a bit more responsibility:

    Public Function GetThing() As Boolean
      Dim are As New AutoResetEvent(False)
    
      WaitFor(ResultingState1, are)
      WaitFor(ResultingState2, are)
    
      SendAsyncThingRequest()
    
      Return are.WaitOne(timeoutVal)
    End Function
    

    And I have generic methods that "WaitFor" a resulting state, and set the AutoResetEvent when it happens:

    Protected Sub WaitFor(Of T)(ByVal waitUpon As IState(Of T), 
                                ByVal are As AutoResetEvent)
    
      Dim action As New Action(Of T)(
      Sub(x)
        are.Set()
        waitUpon.Unsubscribe(action)
      End Sub)
    
      waitUpon.Subscribe(action)
    End Sub
    

    This ended up being the simplest/cleanest solution for my particular scenario.