Dynamically Sort Datetime with reflection and expression lambda - vb.net

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

Related

Generic Function and action/return depending on Type

I have a function which deserializes some custom serialization sent by an API.
I want to build a generic function so that the deserialized object is not of type Object but of the correct type.
The strings which contain the serialized object can be deserialized into one of the following types:
A String,
an IList(Of String),
an IDictionnary(Of String),
one of many SomeNameContainer classes, all derived from a
BaseContainer class,
an IList(Of SomeNameContainer), or
an IDictionnary(Of SomeNameContainer).
I would like to have a single Function Deserialize(Of T)(MyString as String) as T.
Inside this function, I tried to run some Select Case T: GetType(String):Etc tests in order to separate the different actions to run on MyString, depending on the expected object to create from the deserialization.
For example, deserializing into a SomeNameContainer is normally done via another generic function: Dim Deserialized as SomeNameContainer = GetFromContainer(SomeNameContainer)(MyString)
However, I get quickly limited, mainly because:
I cannot return a String type, because it is unable to cast it
into T.
String is a value type, whilst SomeNameContainer are classes. So it is not possible to add an (Of T As {New}) constraint. Which means I am unable to do something like Dim NameContainer as New T: If TypeOf NameContainer Is BaseContainer in order to apply the same operation to all the classes derived from BaseContainer.
One track I have found is to use CTypeDynamic(Of T)(obj as object), which casts at run-time. That might fix problem 1, but problem 2 is still on.
Function Deserialize(Of T)(MyString as String) as T
Select Case GetType(T)
Case GetType(String)
Return SomeFunction(String) '<- Only run-time casting allowed: Return CTypeDynamic(Of String)(SomeFunction(String))
Case GetType(IList(Of String)
Return SomeOtherFunction(String)
Case GetType(...)
'...
Case Else
Dim MyContainer as New T '<- Not Allowed to use New
if TypeOf MyContainer Is T then
Return GetFromContainer(Of T)(String)
else
'...
End If
End Select
End Function
I could decide to split each Type into a separate function. I would like to avoid so that I do not end up with 6 functions. That is because I also need to run some other operations on the string before it is deserialized. For the story, the strings come under various encoding/encryption formats. So if I have 4 formats, that is now 4x6=24 functions I would need to deal with.
I would love to have the luxury of encapsulating all the decoding/deserialization into a single function: Dim MyObject as Something = Deserialize(Of Something)(StringFromAPI, MyEncodingEnumOptions.Option42)
Many thanks in advance!
Performing a specific action depending on the type of a specific variable: that feels similar to Overloading, except that here instead of performing the action based on the type of the input variables, it should be base on the type of the output variables.
Unfortunately, it is not possible to overload the TypeName of a generic function. For example, Function MyFunction(Of T as New)(SomeParameter as String) as T and Function MyFunction(Of T as Structure)(SomeParameter as String) as T cannot coexist in the same namespace.
An alternative is to pass the expected output type as an input argument, so that regular overloading can be performed: Sub MyFunction(ByVal SomeParameter as String, ByRef OutputVar as SomeType). Each overload including a different SomeType TypeName.
The output of the "function" is stored into OutputVar, which is passed ByRef and retrieved after running the Sub:
Dim MyObject as Something = Deserialize(Of Something)(StringFromAPI, MyEncodingEnumOptions.Option42)
Becomes
Sub Deserialize(ByRef MyObject as String, ByVal MyString As String, ByVal EncodingOption As MyEncodingEnumOptions)
MyString = SomeDecoding(MyString, EncodingOption)
MyObject = SomeFunction(MyString)
End Sub
Sub Deserialize(ByRef MyObject as IList(Of String), ByVal MyString As String, ByVal EncodingOption As MyEncodingEnumOptions)
MyString = SomeDecoding(MyString, EncodingOption)
MyObject = SomeOtherFunction(MyString)
End Sub
'...
Dim MyObject as Something
Deserialize(MyObject, StringFromAPI, MyEncodingEnumOptions.Option42)
'Now MyObject has been filled with the relevant data.
An alternative is to use late binding / runtime object initilization, using Activator.CreateInstance(Of T). A typical switch over T would then look like:
Public Function GetDeserializedObject(Of T)(ByVal MyString As String) As T
Select Case GetType(T)
Case GetType(String)
Return CTypeDynamic(MyString, GetType(T)) '<-- Runtime Casting
Case Else
If Not MyString.IsDeserializable Then Throw New ArgumentException(String.Format("Unable to deserialize to a {0} object: The provided string is not valid.", GetType(T).ToString))
Select Case GetType(T)
Case GetType(IList(Of String))
Return CollectionString.ToStringList(MyString)
Case Else
Dim MyReturn As T = Activator.CreateInstance(Of T) '<-- Object instantiation to the type provided at Runtim
If TypeOf MyReturn Is BaseContainer Then '<-- Now we can use TypeOf ... Is ... which will return True for all Object derived from BaseContainer
Return Activator.CreateInstance(GetType(T), MyString)
ElseIf TypeOf MyReturn Is IList(Of BaseContainer) Then
Dim MyCollectionString As CollectionString = MyString
Return MyCollectionString.ExportToContainerList(MyReturn.GetType)
Else
Throw New ArgumentException(String.Format("Unable to deserialize to a {0} object: This type of object is not supported.", GetType(T).ToString))
End If
End Select
End Select
End Function

Public member 'ToCSVValue' on type 'Integer' not found for VB Extension method

I am trying to write a ToCSV() extension in VB based on Scott Hanselman's blog. It could be that my C# to VB is not correct, but it all seems right.
I added a module with:
<System.Runtime.CompilerServices.Extension>
Public Function ToCSV(Of T)(items As IEnumerable(Of T)) As String
Try
Dim csvBuilder = New StringBuilder()
Dim properties = GetType(T).GetProperties()
For Each item As T In items
'' Test Code
Dim newline As String = ""
For Each l2 As Reflection.PropertyInfo In properties
' This works
newline &= l2.GetValue(item, Nothing)
' This works too
Dim int As Integer = 1234
Dim s As String = int.ToCSVValue()
'This works
Dim nl = l2.GetValue(item, Nothing)
' This blows up with "Public member 'ToCSVValue' on type 'Integer' not found."
' The Debugger type shows "Object {Integer}" which I assume to mean that the debugger interprets the object as an integer.
nl = nl.ToCSVValue()
Next
' Original code
Dim line As String = String.Join(",", properties.Select(Function(p) p.GetValue(item, Nothing).ToCSVValue()).ToArray())
csvBuilder.AppendLine(line)
Next
Return csvBuilder.ToString()
Catch ex As Exception
Throw
End Try
End Function
<System.Runtime.CompilerServices.Extension>
Private Function ToCSVValue(Of T)(item As T) As String
If item Is Nothing Then
Return """"""
End If
If TypeOf item Is String Then
Return String.Format("""{0}""", item.ToString().Replace("""", "\"""))
End If
Dim dummy As Double
If Double.TryParse(item.ToString(), dummy) Then
Return String.Format("{0}", item)
End If
Return String.Format("""{0}""", item)
End Function
When I call it with something like:
Dim s As String = ctx.Customers.Where(Function(x) x.CustomerID = 123456).Select(Function(x) New With {.CustomerID = x.CustomerID, .CustomerName = x.CustomerName}).ToCSV()
it gets to the function ToCSV just fine. It recognizes the items passed in. It pulls out the first item and sees that there are the 2 fields in it. All good!
The GetValue() works just fine.
If I create a static integer and call ToCSVValue on it, it works fine.
If I create a static string and call ToCSVValue on it, it works fine.
When I call ToCSVValue on the GetValue() I get:
Public member 'ToCSVValue' on type 'Integer' not found.
Likewise, if I have just strings in the dataset, I get:
Public member 'ToCSVValue' on type 'String' not found.
Ideally this would work as it is in the "Original code" section and I can kill all this other test code.
Can anyone tell me what is happening and why the "(Of T)" is not working the get GetValue() types, but it is for the directly cast types?
You need to have 'Option Infer On'.
When I use Option Infer On, it works fine.
If you don't use this, then VB is using 'Object' whenever you leave off the type.
Also, although this isn't causing your problem, the proper conversion of the ToCSV method is:
Public Function ToCSV(Of T As Class)(items As IEnumerable(Of T)) As String
The short answer is that calling it as a method ToCSVValue(p.GetValue(item, Nothing)) will work as in the C# version.
The longer answer is that you can't call extension methods on Object in VB. In VB Object is treated more like dynamic in C#. For example:
<Extension()> Function toStr(Of T)(item As T) As String
Return item.ToString
End Function
then this will result in compile-time Warning "Late bound resolution; runtime errors could occur." and a run-time Error "Public member 'toStr' on type 'Integer' not found.", but it will work in C#:
Dim i As Object = 123
Dim s = i.toStr

Unable to cast object of type 'System.Linq.Expressions.InstanceMethodCallExpressionN' to type 'System.Linq.Expressions.MemberExpression

This is the code that I am using:
Private Function ExpresionNullable(contenedor As ParameterExpression, prop As String) As MemberExpression
Dim member As Expression
member = Expression.PropertyOrField(contenedor, prop)
Dim typeIfNullable As Type = Nullable.GetUnderlyingType(member.Type)
If typeIfNullable IsNot Nothing Then
Dim callexpression As MethodCallExpression = Expression.Call(member, "GetValueOrDefault", Type.EmptyTypes)
member = callexpression
End If
Return member
End Function
I have a model that has this property: Public Property price As Decimal? and when i try to use this: exprPropiedad = Expression.Property(exprPropiedad, prop) I get an error because is it's allow nulls, so i found that function but have problems to cast the MethodCallExpression to MethodCallExpression.
This is the code where i call that function:
Function GetExpresion(cont As ParameterExpression, props As String, subObject As IEnumerable(Of String)) As MemberExpression
Dim exprProp As MemberExpression = Nothing
If subObject IsNot Nothing Then
For Each subObjeto In subObject
If exprProp Is Nothing Then
exprProp = Expression.Property(cont, subObject)
Else
exprProp = Expression.Property(exprProp, subObjeto)
End If
Next
End If
If exprProp Is Nothing Then
exprProp = ExpresionNullable(cont, props)
Else
exprProp = Expression.Property(exprProp, props)
End If
Return exprProp
End Function
in general what i need is something simple, need to use this
Expression.Property(exprPropiedad, subObjeto)
when the model has a property that allow null values.
Instead of having the return type of the ExpresionNullable method be MemberExpression, make it Expression.
The signature of the method should look like this:
Private Function ExpresionNullable(
contenedor As ParameterExpression,
prop As String) As Expression
You are trying to create an expression that calls the GetValueOrDefault method on the nullable property object using Expression.Call. This creates a method call expression (MethodCallExpression) which is not a MemberExpression (it is not assignable to MemberExpression and this is why you are getting the cast exception). MemberExpression is only for expressions accessing fields or properties.
Such proposed change will also require you to do changes in the GetExpresion method. You have to change the type of the return value to Expression. Also you have to change the type of the exprProp variable to Expression.
Depending on the rest of your code, such changes might effect other parts of your code. And you might need to make similar changes there.
UPDATE:
In my answer I am assuming that you are calling GetValueOrDefault intentionally because you want to get the default value of the nullable property type if the value of the property is null.
In case you need the expression to access the nullable property and not its value (or the default if it is null), then simply delete the ExpresionNullable method because you don't need it and replace any call to it with Expression.Property(....

Expression Lambda to order fields with null value

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
)

Calling a method on a Object says "disallows late binding"

I have a calls called DtaDate that stores an integer "key", a string name, a string for the date, and a Date object that is created from that string.
I have some code that needs to accept a date-like object. I'd like the user to be able to pass in anything date like - a Date object which I'll extract the information from, another DtaDate, a string with the date in it, or even the key, which I'll use to look up the DtaDate from a collection.
So I have this:
Friend Sub New(NameIn As String, DateFormulaIn As String, Optional FromDateIn As Object = Nothing)
[stuff that works]
[check that we got a FromDateIn...]
If TypeOf FromDateIn Is DtaDate Then
fdk = FromDateIn.Key
Ans.FromDate = fdk
VB tells me that "Option Strict On disallows late binding". The other cases, where a string or integer is the TypeOf, I use CInt or CStr. But this is the first time I've actually run into a case where the casting is a non-base type. What's the trick?
Your signature defines FromDateIn As Object. Later on, you're trying to treat it as a DtaDate by calling FromDateIn.Key. As far as the compiler knows, FromDateIn is just an Object, and doesn't have a Key property, and option strict prohibits late binding.
It looks like you're checking the type of FromDateIn and then acting based on that, so all you need to do is cast FromDateIn to a DtaDate. There are a few ways to do that.
fdk = CType(FromDateIn, DtaDate).Key
fdk = DirectCast(FromDateIn, DtaDate).Key
fdk = TryCast(FromDateIn, DtaDate).Key
DirectCast will convert the given variable if it is of the given type, or inherits or implements it.
CType does the same, but will also check to see if there is a conversion from its current type to the specified type. Both of these will throw an InvalidCastException on failure.
TryCast only works for reference types, and works like DirectCast, but returns Nothing on failure rather than throwing an exception.
Another alternative would be to provide three separate constructors, each of which takes a strongly typed variable, e.g.
Friend Sub New(NameIn As String, DateFormulaIn As String)
'...handle case where no date is provided
End Sub
Friend Sub New(NameIn As String, DateFormulaIn As String, FromDateIn As DateTime)
'...handle case where a DateTime is passed in
End Sub
Friend Sub New(NameIn As String, DateFormulaIn As String, FromDateIn As DtaDate)
'...handle case where a DtaDate is passed in
End Sub
Friend Sub New(NameIn As String, DateFormulaIn As String, FromDateIn As String)
'...handle case where a string is passed in
End Sub
This would be more work, but is also safer because the type being passed to the constructor can be checked at compile-time rather than at run-time.
I would use the TryCast() method. If it can cast the source object to the specified type then it will, otherwise it sets it to Nothing.
Inside the If block reference the DtaFromDate, which will be the FromDateIn cast to DtaDate.
Friend Sub New(NameIn As String, DateFormulaIn As String, Optional FromDateIn As Object = Nothing)
[stuff that works]
[check that we got a FromDateIn...]
Dim DtaFromDate As DtaDate
TryCast(FromDateIn, DtaDate)
If DtaFromDate IsNot Nothing Then
fdk = DtaFromDate.Key
Ans.FromDate = fdk
End If