Making lambda expression dynamically - vb.net

In Entity Framework I usually do something like:
modelBuilder.Entity(Of Model).HasKey(Function(item As Model) New With {item.PropertyA, item.PropertyB })
to map a composite primary key
I need to write a generic function like:
modelBuilder.Entity(Of TModelo).HasKey( MakeLambda({“PropertyA”, “PropertyB” })
Private Function MakeLambda(Of TModelo)(nameProperties As String()) As Expression(Of Func(Of TModelo, Object))
Dim type = GetType(TModelo)
Dim listProperties As New List(Of Expression)
Dim parameter = Expression.Parameter(type, "item")
For Each n As String In nameProperties
Dim refProperty = type.GetProperty(n)
listProperties.Add(Expression.MakeMemberAccess(parameter, refProperty))
Next
Dim arrayInit = Expression.NewArrayInit(GetType(Object), listProperties)
In this point the system fails creating the new expression
Dim newExpression = Expression.Lambda(Of Func(Of TModelo, Object))(arrayInit)
Return newExpression
End Function
May be somebody has another solution to this problem

This will do.
dynamic newExpression = Expression.Lambda>(arrayInit, parameter);
But this still not work for me yet.
I need something like this...
HasKey(p => new { p.FAMILY, p.CACHE_FAMILY, p.CUSTOMER_CODE, p.CCC, p.OPERATION, p.EVAL_CODE, p.VDT_FLAG, p.TEST_PLATFORM, p.PCBA_VENDOR });

Related

Custom function(s) in LINQ to Entities? How to write acceptable code?

Use of my simple function causes app to exit all the nested Using blocks and returns control to very outer End Using statement. But built-in function works fine. How to bypass this strange situation?
Public Shared ReadOnly GlobalSettingsKeyList As New HashSet(Of String)
Public Shared Function IsGlobalSetting(key As String) As Boolean
Return GlobalSettingsKeyList.Contains(key)
End Function
What doesn't work:
Using db = powerEntities.Open()
dim userID = 1
dim dbSettingsFound = (From setting In db.SETTINGS
Where setting.idUsers = If(IsGlobalSetting(setting.Name), Nothing, userID)
Where setting.Name.Contains(keyToMatch)) _
.ToDictionary(Function(x) x.Name, Function(y) y.Value)
End Using
What works fine:
Using db = powerEntities.Open()
dim userID = 1
dim dbSettingsFound = (From setting In db.SETTINGS
Where setting.idUsers = If(GlobalSettingsKeyList.Contains(setting.Name), Nothing, userID)
Where setting.Name.Contains(keyToMatch)) _
.ToDictionary(Function(x) x.Name, Function(y) y.Value)
End Using
{"LINQ to Entities does not recognize the method 'Boolean IsGlobalSetting(System.String)' method, and this method cannot be translated into a store expression."}
Done! Thanks to Jarekczek's answer here and his LinqExprHelper
So there is a way!
Private Shared Function GetDefaultKey(key As String) As String
If key.Contains("Station") Then
Return $"default{key.Substring("Station")}" 'my String Extension
Else
Return $"default{key}"
End If
End Function
Public Shared Function GetDefaultKeyAndCompareSetting(ByVal key As String) As Expression(Of Func(Of Settings, Boolean))
key = GetDefaultKey(key)
Return LinqExprHelper.NewExpr(Function(u As Settings) u.Name.Equals(key))
End Function
used like this...
Shared Sub Example(key As String)
...
Dim masterExpr = LinqExprHelper.NewExpr(Function(u As Settings, ByVal formatCompare As String) (formatCompare))
Dim isSameAsDefKey = masterExpr.ReplacePar("formatCompare", GetDefaultKeyAndCompareSetting(key).Body)
Dim result = (From def In db.Settings
Where def.idUsers Is Nothing).
Where(CType(isSameAsDefKey, Expression(Of Func(Of Settings, Boolean)))).FirstOrDefault
...
End Sub
...works like a charm

How to get IEnumerable(Of DataRow) From DataTable query

All the examples I have found of complex grouping of DataTable results that use linq query commands look to have no problems getting an IEnumerable(Of DataRow) object as the result.
However I seem to only get a AnonymousType Enumerator return that I cannot cast to DataTable.
I have workarounds, but would prefer to convert the results to a DataTable, as it looks possible and I may be doing something wrong.
It's a simple table with Many ClientID and ClientName columns and other columns with login timestamps.
Dim dtMatrix As DataTable = New DataTable()
... (populate DataTable)
Dim qClients = From row In dtMatrix
Group row By client = New With {Key .ClientID = row("ClientID"), Key .ClientName = row("ClientName")} Into Group
Select New With {Key .ClientID = client.ClientID, Key .ClientName = client.ClientName}
This returns the generic Enumerator result, however
Dim qClients As IEnumerable(Of DataRow) = From row In dtMatrix
Group row By client = New With {Key .ClientID = row("ClientID"), Key .ClientName = row("ClientName")} Into Group
Select New With {Key .ClientID = client.ClientID, Key .ClientName = client.ClientName}
Throws an exception
Unable to cast object of type... to type
'System.Collections.Generic.IEnumerable`1[System.Data.DataRow]'.
I will be happy to paste the whole error message if it will add more clarity.
My base assumption is that the DataTable should allow the cast to occur inherently as it is the object being queried. However this does not seem to be the case. Have I constructed my query incorrectly? (Framework 4.6.2)
You can use OfType on the Rows property of the DataTable:
Dim dtMatrix As DataTable = New DataTable()
'' Populate code goes here...
Dim dtRows As IEnumerable(Of DataRow) = dtMatrix.Rows.OfType(Of DataRow)()
The Rows property returns a DataRowCollection, that implements (through inheritance) the IEnumerable interface but not the IEnumerable(Of T) interface, that's why you can't use most of linq over it directly.
The following extension uses Reflection to create a new DataTable and create DataColumns in it that match the properties and fields of the type passed in. In general, if you are creating anonymous types in LINQ, you can't just convert to a DataRow which must be tied to a DataTable which must already have matching columns. I went ahead and wrote a second extension to DataTable that adds an IEnumerable<T> with matching field/property names to it.
Public Module Ext
<Extension()>
Public Function GetValue(member As MemberInfo, srcObject As Object) As Object
If TypeOf member Is FieldInfo Then
Return DirectCast(member, FieldInfo).GetValue(srcObject)
ElseIf TypeOf member Is PropertyInfo Then
Return DirectCast(member, PropertyInfo).GetValue(srcObject)
Else
Throw New ArgumentException("MemberInfo must be of type FieldInfo or PropertyInfo", Nameof(member))
End If
End Function
<Extension()>
Public Function GetMemberType(member As MemberInfo) As Type
If TypeOf member Is FieldInfo Then
Return DirectCast(member, FieldInfo).FieldType
ElseIf TypeOf member Is PropertyInfo Then
Return DirectCast(member, PropertyInfo).PropertyType
ElseIf TypeOf member Is EventInfo Then
Return DirectCast(member, EventInfo).EventHandlerType
Else
Throw New ArgumentException("MemberInfo must be of type FieldInfo, PropertyInfo or EventInfo", Nameof(member))
End If
End Function
<Extension()>
Public Function ToDataTable(Of T)(rows As IEnumerable(Of T)) As DataTable
Dim dt = New DataTable
If (rows.Any()) Then
Dim rowType = rows.First().GetType()
Dim memberInfos = rowType.GetProperties.Cast(Of MemberInfo)().Concat(rowType.GetFields).ToArray()
For Each info In memberInfos
dt.Columns.Add(New DataColumn(info.Name, info.GetMemberType()))
Next
For Each r In rows
dt.Rows.Add(memberInfos.Select(Function (i) i.GetValue(r)).ToArray())
Next
End If
Return dt
End Function
<Extension()>
Public Function AddObjects(Of T)(dt As DataTable, rows As IEnumerable(Of T))
If (rows.Any()) Then
Dim rowType = rows.First().GetType()
Dim memberInfos = rowType.GetProperties().Cast(Of MemberInfo)().Concat(rowType.GetFields()).ToArray()
For Each r In rows
Dim newRow = dt.NewRow()
For Each memberInfo In memberInfos
newRow(memberInfo.Name) = memberInfo.GetValue(r)
Next
dt.Rows.Add(newRow)
Next
End If
Return dt
End Function
End Module
Note that I write in C# and translated this from my C# extension. It is untested but compiles.
Using the extension, you should be able to get a DataTable from your qClients by:
Dim dtClients = qClients.ToDataTable()

Implementing Custom GroupBy function in VB.Net

I'm trying to implement a GroupBy function in such a way that it's generic and we don't end up having to keep adding properties every time the client wants something added/changed (yes, I know there's other options. Limited time/budget restrict the scope to this or hard-coding more properties, so I'm trying to opt for the better solution).
Currently I have the following;
<!-- language: lang-vb -->
Dim groupByColumns = New Dictionary(Of String, Boolean) From {
{"Col1", True},
{"Col2", False},
...
}
Dim groupedData = data.GroupBy(Function(r)
Dim groupingResult = New List(Of KeyValuePair(Of String, Object))
For Each column In groupByColumns
Dim groupingValue As Object = Nothing
If column.Value Then
groupingValue = TypeDescriptor.GetProperties(r).Item(grouping.Key).GetValue(r)
End If
groupingResult.Add(New KeyValuePair(Of String, Object)(column.Key, groupingValue))
Next
Return groupingResult
End Function)
If I then do an OrderBy or Select it seems to be returning an IGrouping, which is what I would expect, only the two data rows in the data object are still ungrouped, even though they are identical. I'm out of google-fu, can anybody please assist?
You could combine the fields to group by into one key (probably a string) and then group by that value:
<!-- language: lang-vb -->
Dim groupedData2 = data.GroupBy(Function(r)
Dim groupKey As New List(Of String)
For Each column In groupByColumns
If column.Value Then
Dim p = r.GetType().GetProperties().FirstOrDefault(Function(p) p.Name = column.Key)
Dim groupingValue = p?.GetValue(r)
groupKey.Add(groupingValue)
End If
Next
Return String.Join(",", groupKey)
End Function)

Entity Framework : Why this code doesn't work

I'm using Entity Framework 6.0, DbContext. I'm using this method to copy an object and some related children:
Imports System.Data.Objects
Imports System.Data.Objects.DataClasses
Imports System.Runtime.CompilerServices
Public Module Entities
<Extension()>
Public Function CloneEntity(Of T As Class)(entity As T, context As ObjectContext, Optional include As List(Of IncludeEntity) = Nothing, Optional copyKeys As Boolean = False) As T
Return CloneEntityHelper(entity, context, include, copyKeys)
End Function
Private Function CloneEntityHelper(Of T As Class)(entity As T, context As ObjectContext, Optional include As List(Of IncludeEntity) = Nothing, Optional copyKeys As Boolean = False) As T
If include Is Nothing Then include = New List(Of IncludeEntity)()
Dim myType = entity.GetType()
Dim methodInfo = context.GetType().GetMethod("CreateObject").MakeGenericMethod(myType)
Dim result = methodInfo.Invoke(context, Nothing)
Dim propertyInfo = entity.GetType().GetProperties()
For Each info In propertyInfo
Dim attributes = info.GetCustomAttributes(GetType(EdmScalarPropertyAttribute), False).ToList()
For Each attr As EdmScalarPropertyAttribute In attributes
If (Not copyKeys) AndAlso attr.EntityKeyProperty
Continue For
End If
info.SetValue(result, info.GetValue(entity, Nothing), Nothing)
Next
If info.PropertyType.Name.Equals("EntityCollection`1", StringComparison.OrdinalIgnoreCase) Then
Dim shouldInclude = include.SingleOrDefault(Function(i) i.Name.Equals(info.Name, StringComparison.OrdinalIgnoreCase))
If shouldInclude Is Nothing Then Continue For
Dim relatedChildren = info.GetValue(entity, Nothing)
Dim propertyType As Type = relatedChildren.GetType().GetGenericArguments().First()
Dim genericType As Type = GetType(EntityCollection(Of ))
Dim boundType = genericType.MakeGenericType(propertyType)
Dim children = Activator.CreateInstance(boundType)
For Each child In relatedChildren
Dim cloneChild = CloneEntityHelper(child, context, shouldInclude.Children, shouldInclude.CopyKeys)
children.Add(cloneChild)
Next
info.SetValue(result, children, Nothing)
End If
Next
Return result
End Function
Public Class IncludeEntity
Public Property Name As String
Public Property Children As New List(Of IncludeEntity)
Public Property CopyKeys As Boolean
Public Sub New(propertyName As String, ParamArray childNodes() As String)
Name = propertyName
Children = childNodes.Select(Function(n) new IncludeEntity(n)).ToList()
End Sub
End Class
End Module
Now I'm using the code like below :
Dim litm, newitm As New MyObject
Dim inc = New List(Of IncludeEntity)()
inc.Add(New IncludeEntity("Child_list"))
litm=context.MyObjects.FirstOrDefault
newitm = litm.CloneEntity(CType(context, Entity.Infrastructure.IObjectContextAdapter).ObjectContext,include:=inc)
The code is executed without errors, but nothing is copied, so newitm is empty.
I have inspected the code and found that this line on the CloneEntity function :
Dim myType = entity.GetType()
Is producing a strange type.
I'm expecting that the type will be of MyObject type, but instead this return :
MyObject_F2FFE64DA472EB2B2BDF7E143DE887D3845AD9D1731FD3107937062AC0C2E4BB
This line too :
Dim result = methodInfo.Invoke(context, Nothing)
produces the same strange type.
I don't know if this is the problem, but this is the only strange thing I have noticed.
Can you help me to find out why this code doesn't work?
Thank you!
Entity framework, like many other ORMs will build a proxy type for your entities so that it can intercept calls to:
Lazy load the contents of any collection contained as properties within your entity, when you access those collection properties.
Detect that you have made changes to the properties of an instance as part of dirty checking, so that it will know which objects are dirty and need to be saved to the database when you invoke SaveChanges.
Refer for example to EF returning proxy class instead of actual entity or Working with Proxies.
If you want to find out the underlying type of your entity that is wrapped by the proxy, i.e. the one that would match with the type you are looking for (e.g. MyObject), you can do that using a method in the object context:
var underlyingType = ObjectContext.GetObjectType(entity.GetType());

Type of Generic seems worthless

Please see the code below:
Public Function ExecuteDynamicQuery(Of T As New)(ByVal sql As String, ByVal type As T) As List(Of T) Implements IGenie.ExecuteDynamicQuery
Dim iConnectionBLL As iConnectionBLL = New clsConnectionBLL
Dim paramValues() As DbParameter = New clsParameterValues().getParameterValues()
Using conn As DbConnection = iConnectionBLL.getDatabaseTypeByDescription("Genie2"), _
rdr As DbDataReader = clsDatabaseHelper.ExecuteReader(conn, CommandType.Text, sql, paramValues)
Dim list As List(Of T) = New List(Of T)
While rdr.Read()
Dim hello As New T
Dim method As MethodInfo = GetType(clsType).GetMethod("PopulateDataReader")
method.Invoke(hello, New Object() {rdr})
list.Add(hello)
End While
Return list
End Using
End Function
Is there a way of executing the SQL statement above without passing in type as an arguement. It seems a bit pointless - the only reason it is there is to let the function know the type of the generic.
Well you can change the method to not have the second parameter:
Public Function ExecuteDynamicQuery(Of T As New)(ByVal sql As String) As List(Of T) Implements IGenie.ExecuteDynamicQuery
However:
You'd need to change IGenie as well
The caller would then need to explicitly specify the type argument, instead of letting the compiler infer it on the basis of the argument (which would no longer be present)