Skip to content

Instantly share code, notes, and snippets.

@SoulFireMage
Last active December 21, 2015 11:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SoulFireMage/6300552 to your computer and use it in GitHub Desktop.
Save SoulFireMage/6300552 to your computer and use it in GitHub Desktop.
A generic constrained class that dynamically generates a rule engine based on reflection and specified properties-an expression tree is built for each property, compiled and added to a list of rules - this is generate but once for the lifetime of the instance. This is intended for use with a database migration testing engine.
Imports System.Reflection
Imports System.Linq.Expressions
''' <summary>
'''This generic class encapsulates the main algorithms for building a rule engine and a list of matched source/merge '''"rows"To prove whether a row in source and target are equivalent.
''' A property in the classes we may be using represent columns in a table or query.
'''Say we have an instance thus:
'''SourcePerson = New Person with {.Name = "ommitted db query",
''' .DOB =" ommitted db query"}
'''MergedPerson = New Person with {.Name = "ommitted db query",
''' .DOB =" ommitted db query"}
'''We add these instances to a List(of Person) so we get SourceList(of Person), MergeList(Of Person).
'''Then we would like to build some list where each item is a match pair of Persons. The engine below can be told to match '''on .Name and .DOB; it will generate the rules as needed and use them to decide if instances from each list are in fact, '''the same "fact".
'''
'''So we get a List(of MatchedPair(Of Person)) where MatchPair has members .SourcePerson and .MergePerson. In addition, '''using the IName, the MatchedPair instance will guarantee an Overrides ToString that returns a member from MergePerson (in '''this case).
'''
'''There is a weakness - it relies on the programmer to build adequate calling classes that use properties that adequately '''describe unique rows. Since some columns in a migration (unique ID's, some foreign keys) may be shifted, this is the next '''best thing.
'''
'''Using reflection we get the types and the propertylist passed by the calling class.
'''Expression trees are used to generate a lambda rule per property to test for equality.
'''The generic constraint provides the ToString member contract
'''The calling class can use the source, merge and Matched pair collections.
''' </summary>
'''<typeparam name="T"></typeparam>
'''<remarks></remarks>
Class BuildGenericMergeList(Of T As INamed)
Private Property _SourceList As New List(Of T)
Public ReadOnly Property SourceList As List(Of T)
Get
Return _SourceList
End Get
End Property
Private Property _MergeList As New List(Of T)
Public ReadOnly Property Mergelist As List(Of T)
Get
Return _MergeList
End Get
End Property
Private Property _unMatchedSource As New List(Of T)
Public ReadOnly Property unMatchedSource As List(Of T)
Get
Dim temp = mMatchedPairs.Select(Function(x) x.sourceItem).ToList
_unMatchedSource = temp.Except(_SourceList).ToList
Return _unMatchedSource
End Get
End Property
Private ReadOnly Property mDictBuild As Boolean
Get
Return _SourceList.Count = 0 And _MergeList.Count = 0
End Get
End Property
Public Sub New(query As String, action As Action(Of String, dbConnect, List(Of T))) 'may need an overload for a dictionary as well.
_action = action
If query.Length > 0 Then BuildList(query)
End Sub
Private Sub BuildMatchingPairs(properties As List(Of PropertyInfo))
For Each t In SourceList
Dim mergeItem As T = getMatchingMerge(t, properties)
If IsNothing(mergeItem) = False Then mMatchedPairs.Add(New MatchPair(Of T) With {.sourceItem = t, .mergeItem = mergeItem})
Next
End Sub
Private Property mMatchedPairs As New List(Of MatchPair(Of T))
Public ReadOnly Property MatchedPairs(properties As List(Of PropertyInfo)) As List(Of MatchPair(Of T))
Get
If mMatchedPairs.Count = 0 Then BuildMatchingPairs(properties)
Return mMatchedPairs
End Get
End Property
Private Sub BuildList(query As String)
Dim dbset = New testingDBSet
If mDictBuild Then
'This implementation uses 3 databases.
For Each dbConnect In {dbset.getDbConnect(dbset.SourceDB), dbset.getDbConnect(dbset.TargetDB)}
BuildList(query, dbConnect, SourceList)
Next
BuildList(query, dbset.getDbConnect(dbset.MergedDB), Mergelist)
End If
End Sub
Private Property _action As Action(Of String, dbConnect, List(Of T))
Private Sub BuildList(ByVal query As String, ByVal dbConnect As dbConnect, ByVal list As List(Of T))
_action.Invoke(query, dbConnect, list)
End Sub
Private Property propDict As New Dictionary(Of PropertyInfo, PropertyInfo)
Private ReadOnly _rules As New List(Of Func(Of T, T, Boolean))
Public Function getMatchingMerge(item As T, matchProperties As List(Of PropertyInfo)) As T
Dim getPropList = item.GetType().GetProperties.Where(Function(s) s.Equals(matchProperties.Where(Function(x) x.Name = s.Name).Select(Function(a) a).FirstOrDefault)).ToList
If propDict.Count < matchProperties.Count Then propDict = getPropList.ToDictionary(Function(p) matchProperties.Where(Function(s) s.Name = p.Name).First)
If _rules.Count <> matchProperties.Count Then
_rules.Clear()
For Each Lambda In From pair In propDict
Let leftProperty = pair.Key
Let rightProperty = pair.Value
Let leftParameter = Expression.Parameter(item.GetType, item.GetType.Name)
Let rightParameter = Expression.Parameter(rightProperty.DeclaringType, rightProperty.DeclaringType.Name)
Let left = Expression.Property(leftParameter, leftProperty)
Let right = Expression.Property(rightParameter, rightProperty)
Let leftEqualsRight = Expression.Equal(left, right)
Select Expression.Lambda(Of Func(Of T, T, Boolean))(DirectCast(leftEqualsRight, Expression), New ParameterExpression() {leftParameter, rightParameter}).Compile
_rules.Add(Lambda)
Next
End If
Dim _match = Mergelist.Where(Function(r) _rules.All(Function(x) x.Invoke(item, r) = True)).FirstOrDefault
Return _match
End Function
End Class
Public Class MatchPair(Of T As INamed)
Implements INamed
Property sourceItem As T
Property mergeItem As T
Overrides Function ToString() As String Implements INamed.Name
Return mergeItem.Name
End Function
End Class
Public Interface INamed
Function Name() As String
End Interface
'Used by calling classes to build a property list.
Module ReflectionUtility
Function GetMemberInfo(Of T)(expression As Expression(Of Func(Of T))) As MemberInfo
Dim member = TryCast(expression.Body, MemberExpression)
If member IsNot Nothing Then Return member.Member
Throw New ArgumentException("Expression is not a member", "expression")
End Function
End Module
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment