Expression Lambda to order fields with null value - vb.net

I use a dynamic method to order a list of object.
For Each Sort In MesDonnees.MesOptions
If Sort.Sens < 2 Then
Dim sortPropertyName As String = Sort.ColName
If (TypeClass.GetProperties().Any(Function(prop) prop.Name = sortPropertyName AndAlso prop.CanRead)) Then
'Information sur la propriété recherchée
Dim pinfo As PropertyInfo = TypeClass.GetProperty(sortPropertyName)
Dim Expr As Expression = Expression.Property(paramExpr, pinfo)
Dim Tostr As Expression = Expression.Call(Expr, "ToString", Nothing)
Dim orderByFunc As Func(Of MaClass, Object) = Expression.Lambda(Of Func(Of MaClass, Object))(Tostr, paramExpr).Compile()
Dim sortFunc As Func(Of IEnumerable(Of MaClass), IOrderedEnumerable(Of MaClass)) = Nothing
If (Not CBool(Sort.Sens)) Then
sortFunc = (Function(source) source.OrderBy(orderByFunc))
Else
sortFunc = (Function(source) source.OrderByDescending(orderByFunc))
End If
query = sortFunc(query).ToList()
End If
End If
Next
Sort.ColName is the name of my field I want to filter.
When I have no null value, it's perfect but when I have a null value, I get an exception in the line query = sortFunc(query).ToList() :
Object reference not set to an instance of an object
I see different code with Expression.Call and ISNullOrEmpty but I don't know how use it correctly in my case. I want null or Empty are in first like a "".
Thanks for your help and explanation

You can change your toStr expression to include a Coalesce operation where the right expression is a constant. Note that the left expression needs to be reference type (or a nullable value type) so you also need to ensure that the property type is not a value type.
Here's an example that should work:
Dim toStr As Expression = Expression.Call(
If(pinfo.PropertyType.IsValueType, expr, Expression.Coalesce(expr, Expression.Constant(String.Empty))),
"ToString",
Nothing
)

Related

Pass an arbitrary set of paired parameters, where one of the pair items needs to be an arbitrary datatype

I have a function to which I want to pass an arbitrary number of paired parameters (i.e. a String variable and a second arbitrary type (could be a String, Integer etc.) - hence am declaring the second half of the pair as an Object. There could be one or more pairs of this nature.
The most obvious structure I could think of for this was therefore a Tuple(Of String, Object)
Here is the function :
Private Function TableLookup(
table As DataTable,
ByVal columnNamesAndKeys As List(Of Tuple(Of String, Object)),
resultColumnName As String) As Object
Dim filterExpression As String = ""
For i = 0 To columnNamesAndKeys.Count
Dim lookupColumn As String = columnNamesAndKeys(i).Item1
Dim lookupKey As Object = columnNamesAndKeys(i).Item2
Dim keyType = lookupKey.GetType()
If keyType IsNot table.Columns(lookupColumn).DataType Then Return Nothing
If keyType Is GetType(String) Then
filterExpression += IIf(Len(filterExpression) > 0, " AND ", "") + $"{lookupColumn} = '{lookupKey}'"
ElseIf keyType Is GetType(Date) Then
filterExpression += IIf(Len(filterExpression) > 0, " AND ", "") + $"{lookupColumn} = #{lookupKey:M/dd/yyyy h:mm:ss tt}#"
Else
filterExpression += IIf(Len(filterExpression) > 0, " AND ", "") + $"{lookupColumn} = {lookupKey}"
End If
Next
Dim row = table.Select(filterExpression).FirstOrDefault()
Return If(row Is Nothing, Nothing, row(resultColumnName))
End Function
Called thus (for a single pair) :
Dim someKey As Integer
Dim someValue = TableLookup(
dtbSomeTable,
New List(Of Tuple(Of String, Object))
From {("SomeKey", DirectCast(someKey, Object)).ToTuple},
"SomeOtherColumn")
And thus (for multiple pairs) :
Dim someKey As Integer
Dim someOtherKey As String
Dim someValue = TableLookup(
dtbSomeTable,
New List(Of Tuple(Of String, Object))
From {("SomeKey", DirectCast(someKey, Object)).ToTuple,
("SomeOtherKey", DirectCast(someOtherKey, Object)).ToTuple},
"SomeOtherColumn")
So - this works - but it feels awfully clunky calling it each time, having to create an ad-hoc list of Tuples, then declare each Tuple and DirectCast each key as an Object to obey the strongly-typed requirement.
The whole point of the function was to provide an easy one-liner throughout the code to quickly look up columns with potentially multiple, arbitrary, criteria but all of these manipulations within the call makes it less intelligible to anyone unfortunate enough to ever have to maintain this...
Is there a smarter / cleaner way to pass an arbitrary set of paired parameters, where one of the pair items needs to be an arbitrary Type?
You can achieve this by using a Parameter Array (ParamArray ) in conjunction with value tuples. This allows you to call the method easily with any number of parameters and no explicit list or array instantiation and neither a New keyword for the values nor any casts. Note that we must swap columnNamesAndKeys and resultColumnName, since the ParamArray used to pass columnNamesAndKeys must be the last parameter of the method.
Usage example:
Dim result = TableLookup(
dtbSomeTable,
"ResultColumn",
("id", 12), ("name", "Joe"), ("date", #2022/10/28#), ("someKey", someValue))
The adapted function:
Private Function TableLookup(
table As DataTable,
resultColumnName As String,
ParamArray columnNamesAndKeys() As (key As String, value As Object)
) As Object
Dim filterExpression As String = ""
For i = 0 To columnNamesAndKeys.Length - 1
Dim lookupColumn As String = columnNamesAndKeys(i).key
Dim lookupKey As Object = columnNamesAndKeys(i).value
Dim keyType = lookupKey.GetType()
If keyType IsNot table.Columns(lookupColumn).DataType Then Return Nothing
If filterExpression.Length > 0 Then
filterExpression += " AND "
End If
If keyType Is GetType(String) Then
filterExpression += $"{lookupColumn} = '{DirectCast(lookupKey, String).Replace("'", "''")}'"
ElseIf keyType Is GetType(Date) Then
filterExpression += $"{lookupColumn} = #{lookupKey:yyyy/MM/dd h:mm:ss tt}#"
Else
filterExpression += $"{lookupColumn} = {lookupKey}"
End If
Next
Dim row = table.Select(filterExpression).FirstOrDefault()
Return If(row Is Nothing, Nothing, row(resultColumnName))
End Function
I also did some refactorings.
Since I used a named tuple, you can access the fields by name instead of just Item1 or `Item2'.
Conditionally adding the " AND " part can be done once before the lengthy If Then Else ...
I replace any single quotes within a string value by two single quotes. E.g., a string like "John's Pub" becomes 'John''s Pub' in SQL notation.
For i = 0 To columnNamesAndKeys.Count is wrong. The index goes from 0 to Count - 1. And since we have an array now, we must use Length.

ArgumentException calling Expression.IfThenElse

I'm trying to build this LINQ query:
Result = Result.Where(Function(Row) If(IsDBNull(Row(7)), False, Convert.ToInt32(Row(7)) > 10))
Result is a IEnumerable(Of Object()).
I manage to build the expression with this code, but at the last line, I get an error message.
The code I have is this:
Dim whereMethod = GetType(Queryable).GetMethods(BindingFlags.Public Or BindingFlags.Static).First(Function(m) m.Name = "Where").MakeGenericMethod(GetType(Object()))
Dim convertMethod As MethodInfo = Nothing
Dim rowParameter = Expression.Parameter(GetType(Object()), "Row")
Dim isdbnullMethod As MethodInfo = GetType(System.Convert).GetMethod("IsDBNull", New Type() {GetType(Object)})
Dim expr As Expression = Nothing
Dim tempexpr As Expressions.LambdaExpression = Nothing
convertMethod = GetType(System.Convert).GetMethod("ToInt32", New Type() {GetType(Object)})
tempexpr = Expression.Lambda(Expression.IfThenElse(
Expression.Call(isdbnullMethod,
Expression.ArrayAccess(rowParameter, Expression.Constant(7))),
Expression.Constant(False),
Expression.GreaterThan(
Expression.Call(
convertMethod,
Expression.ArrayAccess(rowParameter, Expression.Constant(7))),
Expression.Constant(10))),
rowParameter)
Then I call:
expr = Expression.Call(whereMethod, Result.AsQueryable.Expression, Expression.Lambda(tempexpr.Body, rowParameter))
And at this line I get this error:
What can be the problem? Without the IfThenElse it works. Also this:
Result = Result.Where(Function(Row) Convert.ToInt32(Row(7)) > 10)
EDIT
Is this because the If operator is an "Action" method and doesn't returns a value?
Btw. the Expression.IfThenElse uses the IIf function. How could I use the If function?
EDIT II
I think, I found it: Expression.Condition. It uses IIf too, but with this, I don't get an exception.
Your Edit II is correct: Expression.IfThenElse returns void, making the whole expression an Action. Expression.Condition returns whatever the type is in the ifTrue parameter, making your expression Expression(Of Func(Of Boolean)), which is what you want.
As an aside, I don't believe it's really calling the IIf function. That's simply a debug view of what is going on. I don't think it's really calling either of those VB.NET-only methods

Dynamically Sort Datetime with reflection and expression lambda

I built a VB.NET code to sort several type like string,number ... Now I try to Had date.
If (TypeClass.GetProperties().Any(Function(prop) prop.Name = sortPropertyName AndAlso prop.CanRead)) Then
'Information sur la propriété recherchée
Dim pinfo As PropertyInfo = TypeClass.GetProperty(sortPropertyName)
Dim Typ = pinfo.PropertyType.Name
Dim toStr As Expression
Dim Expr As Expression = Expression.Property(paramExpr, pinfo)
toStr = Expression.Call(If(pinfo.PropertyType.IsValueType, Expr, Expression.Coalesce(Expr, Expression.Constant(String.Empty))), "ToString", Nothing)
Dim orderByFunc As Func(Of MaClass, Object) = Expression.Lambda(Of Func(Of MaClass, Object))(toStr, paramExpr).Compile()
Dim sortFunc As Func(Of IEnumerable(Of MaClass), IOrderedEnumerable(Of MaClass)) = Nothing
If (Not CBool(Sort.Sens)) Then
sortFunc = (Function(source) source.OrderBy(orderByFunc))
Else
sortFunc = (Function(source) source.OrderByDescending(orderByFunc))
End If
query = sortFunc(query).ToList()
End If
The problem is when I sort it's not sort Date but a string like
31/12/2005; 31/11/2011; 31/10/2007 ...
When I Sort it's better to find
31/11/2011; 31/10/2007; 31/12/2005
Then I try this modify
If Typ.Contains("DateTime") Then 'Add For DateTime here
toStr = Expression.Call(If(pinfo.PropertyType.IsValueType, Expr, Expression.Coalesce(Expr, Expression.Constant(Date.MinValue))), "ToString", Nothing)
Else
toStr = Expression.Call(If(pinfo.PropertyType.IsValueType, Expr, Expression.Coalesce(Expr, Expression.Constant(String.Empty))), "ToString", Nothing)
End If
but i don't know how replace 'ToString'
I try
toStr = Expression.Call(If(pinfo.PropertyType.IsValueType, Expr, Expression.Coalesce(Expr, Expression.Constant(Date.MinValue))), "ToString(""yyyy MM dd"")", Nothing)
But I was following error
ex = {"Aucune méthode 'ToString("yyyy MM dd")' n'existe sur le type 'System.Nullable`1[System.DateTime]'."}
Translate by Google
"No method 'ToString (" yyyy dd MM ")' exists on the type 'System.Nullable`1 [System.DateTime]'.
I try too Ticks, Date or Year,,Value.Ticks, GetValueOrDefault.Year.ToString but same error
Perhaps there is a better way
Thnks for your help
.Contains("DateTime") will match both Nullable<DateTime> and DateTime types, the error your seeing is because you're trying to call obj.Value.ToString("yyyy MM dd") what you've written isn't a ToString overload on the nullable object (which just calls it's contained ToString method) it's an overload on the contained DateTime object
There is a boolean on the reflection objects that will tell you if your looking at the actual type or a Generic object (like Nullable<>)
This is also why you can't find the Ticks property, as it only exists on the child DateTime object. In your normal code Nullable<> objects are implicitly cast to their contained type (automatically navigating to the Value object)
looking at msdn, you pass in arguments as the 4th parameter in C# as opposed to setting them in the string, VB is likely identical
I don't think you can navigate to the Value object using the string parameter by calling "Value.Ticks" as firstly "Ticks" isn't a method it's a property and secondly .Net won't be able to translate the string into a methodInfo object from the Nullable<T> type - because it doesn't exist.
You should navigate to the "Value" object or cast to the underlying object type as part of the Expr expression by detecting if it's a generic Nullable<T> type

expression.call value cannot be null

I'm trying to code this LINQ query with expression trees:
Result = Result.Where(Function(Row) Convert.ToInt32(Row(2)) <= 10)
Result is declared as Dim Result As IEnumerable(Of Object()).
I have this code so far:
Dim queryiabledata As IQueryable(Of Object()) = Result.AsQueryable
Dim pe As ParameterExpression = Expression.Parameter(GetType(String), "Row(2)")
Dim left As expression = Expression.Call(pe, GetType(String).GetMethod("Convert.ToInt32", System.Type.EmptyTypes))
Dim right As Expression = Expression.Constant(10)
Dim e1 As Expression = Expression.LessThanOrEqual(left, right)
Dim predicatebody As Expression = e1
Dim wherecallexpression As MethodCallExpression = Expression.Call(
GetType(Queryable), "Where", New Type() {queryiabledata.ElementType}, queryiabledata.Expression,
Expression.Lambda(Of Func(Of Object(), Boolean))(predicatebody, New ParameterExpression() {pe}))
Result = queryiabledata.Provider.CreateQuery(Of Object())(wherecallexpression)
But if I run the query, I get an ArgumentNullException (Value cannot be null. Parameter name: method) at Expression.Call.
I tried to change "Convert.ToInt32" to "Value", but I got the same error.
How can I fix it?
Are the another code lines right to get the desired result?
I'm more used to C#, though I do VB.NET occasionally. Reflection in VB.NET is downright ugly. Getting the Where method is a bit of a hack. Here's the code:
shortForm and longForm should be identical.
Dim result As IEnumerable(Of Object()) = New List(Of Object())()
Dim queryiabledata As IQueryable(Of Object()) = result.AsQueryable()
Dim shortForm As Expression = queryiabledata.Where(Function(Row) Convert.ToInt32(Row(2)) <= 10).Expression
Dim whereMethod = GetType(Queryable).GetMethods(BindingFlags.Public Or BindingFlags.Static).
First(Function(m) m.Name = "Where").
MakeGenericMethod(GetType(Object()))
Dim convertMethod = GetType(System.Convert).GetMethod("ToInt32", New Type() {GetType(Object)})
Dim rowParameter = Expression.Parameter(GetType(Object()), "Row")
Dim longform As Expression =
Expression.Call(
whereMethod,
queryiabledata.Expression,
Expression.Lambda(
Expression.LessThanOrEqual(
Expression.Call(
convertMethod,
Expression.ArrayAccess(
rowParameter,
Expression.Constant(2)
)
),
Expression.Constant(10)
),
rowParameter
)
)
result = queryiabledata.Provider.CreateQuery(longform)
This line seems suspect to me:
GetType(String).GetMethod("Convert.ToInt32", System.Type.EmptyTypes)
GetType(String) returns the runtime type String. You then try to get a method called "Convert.ToInt32" which doesn't exist on the string type. I suspect that is returning null which is the source of your exception.
Perhaps you need to use something like this:
GetType(Convert).GetMethod("ToInt32", new Type() {GetType(Object)})
Since there are multiple overloads of the ToInt32 method of the Convert class, you need to specify which of the overloads you want by providing an array of Type as the second parameter. In other words, you a saying "give me the overload that takes an Object type as it's parameter".

SQL Data Reader - How to handle Null column values elegantly

I'm using an SQLDataReader to retrieve values from a database which may be null. I've worked out how to handle Null string values but can't get the same trick to work with integers or booleans:
Using cmd As DbCommand = store.GetStoredProcCommand("RetrievePOCO")
store.AddInParameter(cmd, "ID", DbType.Int32, ID)
Using reader As IDataReader = store.ExecuteReader(cmd)
If reader.Read() = True Then
Dim newPOCO As New POCO()
With newPOCO
'If the source column is null TryCast will return nothing without throwing an error
.StatusXML = TryCast(reader.GetString(reader.GetOrdinal("StatusXML")), String)
'How can a null integer or boolean be set elegantly?
.AppType = TryCast(reader.GetInt32(reader.GetOrdinal("AppType")), System.Nullable(Of Integer))
.Archived = TryCast(reader.GetBoolean(reader.GetOrdinal("Archived")), Boolean)
So how can a null integer or boolean be set elegantly? I've seen suggestions in C# but they don't translate correctly to VB giving a 'TryCast operand must be reference type, but integer? is a value type' compiler errors.
I use the following function in this scenario:
Public Shared Function NoNull(ByVal checkValue As Object, ByVal returnIfNull As Object) As Object
If checkValue Is DBNull.Value Then
Return returnIfNull
Else
Return checkValue
End If
End Function
Your code would look something like this:
With newPOCO
.StatusXML = NoNull(reader("StatusXML"), "")
.AppType = NoNull(reader("AppType"), -1)
.Archived = NoNull(reader("Archived"), False)
End With
Note that this function requires you to pass the value which should be used if the value is DbNUll as the second Parameter.
You can take advantage of the IsDBNull method of the SqlDataReader and use the VB.NET ternary operator to assign a default value to your poco object
.StatusXML = If(reader.IsDBNull(reader.GetOrdinal("StatusXML")), _
"",reader.GetString(reader.GetOrdinal("StatusXML")))
It is just one line, not very elegant because you need to call two times the GetOrdinal method.
Public Function NotNull(Of T)(ByVal Value As T, ByVal DefaultValue As T) As T
If Value Is Nothing OrElse IsDBNull(Value) Then
Return DefaultValue
Else
Return Value
End If
End Function