Skip to content

Instantly share code, notes, and snippets.

@SoulFireMage
Last active November 16, 2015 07:28
Show Gist options
  • Save SoulFireMage/08ecabf0300420ad1dd7 to your computer and use it in GitHub Desktop.
Save SoulFireMage/08ecabf0300420ad1dd7 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
/// <summary>
/// Dynamic rules engine, translated from visual basic
/// Demonstration of demonstrating how to build a generic dynamic rules engine.
/// Original purpose: arbitrary classes are generated to reflect types from tables in a database
/// Two databases were being merged and a method of checking arbitrary rows in any table was required
/// The datalayer translated 1 table = 1 class and primary keys were autonumber and could not be relied upon
/// to guarantee that a fact (table row as an instance of it's class) was identical from the original databases
/// and the new merged database. This was a legacy schema.
///
/// The rules engine below would allow for a list of any class type to be used, you had to
/// define what columns must match in order for the row from table in db1 to be equal to the row in the othogonal table
/// in DB2.
///
/// It relies on a manual definition of what the correct columns are and simply helped cut the code to be written down.
///
/// At the time of writing, the legacy codebase did not use Entity Framework or any other ORM.
///
/// Key points of the exercise:
/// Demonstrate understand of generics and the idea of a template pattern.
/// Demonstrate use of interfaces as a means of providing the type/member contracts needed.
/// Use of datastructures, code as data and linq
/// Use of reflection and expression trees.
///
/// Some methods are skipped in translation as they don't add clarity to this code (database access etc).
///
/// Important Note:
/// I would use T4 or other code gen tools plus a config file (Jason property list) for this job now and
/// over head of actually implementing this approach!
/// </summary>
class RulesEngine<T> where T : INamed
{
private List<T> _SourceList = new List<T>();
public List<T> SourceList
{
get { return _SourceList; }
}
private List<T> _mergeList = new List<T>();
public List<T> MergeList
{
get{ return _mergeList;}
}
private void BuildMatchingPairs(List<PropertyInfo> properties)
{
foreach (T t in SourceList)
{
T mergeItem = getMatchingMerge(t, properties);
if (mergeItem == null)
{
mMatchedPairs.Add(new MatchPair<T> { sourceItem = t, mergeItem = mergeItem });
}
}
}
private List<MatchPair<T>> mMatchedPairs = new List<MatchPair<T>>();
/// <summary>
/// These are the matched pairs from the left table and right table class instances.
/// We use getmatchingMerge to find our ideal partner.
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public List<MatchPair<T>> MatchedPairs(List<PropertyInfo> properties)
{
if (mMatchedPairs.Count == 0)
{
BuildMatchingPairs(properties);
}
return mMatchedPairs;
}
private Action<string, dbConnect, List<T>> _action;
private void BuildList(string query, dbConnect dbConnect, List<T> list)
{
_action.Invoke(query, dbConnect, list);
}
private Dictionary<PropertyInfo, PropertyInfo> propDict = new Dictionary<PropertyInfo, PropertyInfo>();
private readonly List<Func<T, T, bool>> _rules;
/// <summary>
/// The Dynamic Rules Engine itself
/// Here we have to pass in a class instance and the properties to be matched.
/// using reflection to get the members, put them in a list and generate a list of lambda predicates expressions
/// We compile and run these - they all have to be true to get a match. We return a match item, if one exists.
/// </summary>
private T getMatchingMerge(T item, List<PropertyInfo> matchProperties)
{
List<PropertyInfo> getPropList = item.GetType().GetProperties().Where(s =>
s.Equals(matchProperties.Where(x => x.Name == s.Name).FirstOrDefault())).ToList();
if (propDict.Count < matchProperties.Count)
{
propDict = getPropList.ToDictionary(p => matchProperties.Where(s => s.Name == p.Name).First());
}
if (_rules.Count != matchProperties.Count) {
_rules.Clear();
foreach (var lambda in from Pair in propDict
let leftProperty = Pair.Key
let rightProperty = Pair.Value
let leftParameter = Expression.Parameter(rightProperty.DeclaringType, rightProperty.DeclaringType.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<Func<T, T, bool>>(leftEqualsRight, Expression), new Parameter.Expression() { leftParameter, rightParameter }).Compile()
select Expression.Lambda<Func<T, T, bool>>((Expression)leftEqualsRight, new ParameterExpression[] { leftParameter, rightParameter }).Compile())
_rules.Add(lambda);
}
T _match = MergeList.Where(r => _rules.All(x => x.Invoke(item, r) == true)).FirstOrDefault();
return _match; //I've not covered the path where there is no match!
}
private List<T> _unMatchedSource = new List<T>();
public List<T> UnmatchedSource
{
get {
return _unMatchedSource; }
}
}
public class MatchPair<T> : INamed where T : INamed
{
public T sourceItem { get; set; }
public T mergeItem { get; set; }
public override string ToString()
{
return mergeItem.Name();
}
string INamed.Name()
{
return ToString();
}
}
public interface INamed
{
string Name();
}
//Dummy class
public class dbConnect
{
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment