TL;DR
I'm looking for a way to either:
Action
that takes a ParamArray
of Object
Expression
to create an Action that matches the given number/type of generic parametersDetails
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
.
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.