Tuesday, October 12, 2010

Fluent Search Interface with some Func




UPDATED: 10/18/2010 11:42PM

So…how do you start a fluent interface? By typing how you’d want your code to read, at least that’s how I do it. So I created a unit test project and started typing how I’d like my interface to work. This is what I came up with:

Search<Person>.With(myPersonList).For("deran").By("FirstName").Result;

Alright well, that was simple enough. Now how do I make it work? First I’ll create my Search class with the With method:

    public class Search<T>
{
internal Search(IEnumerable<T> value)
{ }

public static ISearchRequired<T> With(IEnumerable<T> source)
{
return new Search<T>(source);
}
}

You can see here that I’m returning a ISearchRequired<T>, which looks like this:

    public interface ISearchRequired<T>
{
ISearchExtensions<T> For(string value);
ISearchExtensions<T> Not(string value);
}

The reason I have this interface is so I can control what the developer using my fluent interface sees. Each of these items returns the ISearchExtensions, which has more options. First I want to show the finished Search<T> class:

    public class Search<T> : SearchExtensions<T>, ISearchRequired<T>
{
internal Search(IEnumerable<T> value)
:
base(value)
{ }

public static ISearchRequired<T> With(IEnumerable<T> source)
{
return new Search<T>(source);
}

public ISearchExtensions<T> Not(string value)
{
_searchCriteria.Add(
new SearchCriteria { Value = value, NotEqual = true });
return this;
}

public ISearchExtensions<T> For(string value)
{
_searchCriteria.Add(
new SearchCriteria { Value = value });
return this;
}
}

Here’s the ISearchExtensions<T> interface:

    public interface ISearchExtensions<T>
{
ISearchExtensions<T> By(Expression<Func<T, string>> valueExpr);//changed from (string value)
ISearchExtensions<T> AndFor(string value);
ISearchExtensions<T> OrFor(string value);
ISearchExtensions<T> AndNot(string value);
IEnumerable<T> Result { get; }
}

You can also see in my completed Search<T> class, I have a SearchCriteria object that looks like this:

    public class SearchCriteria
{
public Expression<Func<T, string>>/*changed from string*/ By { get; set; }
public string Value { get; set; }
public LogicalOperator LogicalOperator { get; set; }
public bool NotEqual { get; set; }
}

public enum LogicalOperator
{
None, And, Or
}

Simple stuff so far…now for the fun stuff…the actual implementation, which looks like this:

public class SearchExtensions<T> : ISearchExtensions<T>
{
protected IList<SearchCriteria> _searchCriteria;
private readonly IEnumerable<T> _source;
internal SearchExtensions(IEnumerable<T> source)
{
_source = source;
_searchCriteria =
new List<SearchCriteria>();
}

public ISearchExtensions<T> By(Expression<Func<T, string>> valueExpr)
{
//REMOVED: var value = NoMagicStringHelper.GetPropertyFrom(valueExpr);

foreach (var sc in _searchCriteria.Where(x => x.By == null))
sc.By = value;

return this;
}


public ISearchExtensions<T> AndFor(string value)
{
_searchCriteria.Add(
new SearchCriteria { Value = value, LogicalOperator = LogicalOperator.And });
return this;
}

public ISearchExtensions<T> OrFor(string value)
{
_searchCriteria.Add(
new SearchCriteria { Value = value, LogicalOperator = LogicalOperator.Or });
return this;
}

public ISearchExtensions<T> AndNot(string value)
{
_searchCriteria.Add(
new SearchCriteria { Value = value, NotEqual = true });
return this;
}

public IEnumerable<T> Result
{
get
{
var predicate = PredicateBuilder.True<T>();
predicate = _searchCriteria.Where(x => !x.NotEqual)
.Aggregate(predicate, (current, criteria) =>
criteria.LogicalOperator ==
LogicalOperator.Or ?
current.Or(StartsWith(criteria.By, criteria.Value)) :
current.And(StartsWith(criteria.By, criteria.Value)));
predicate = _searchCriteria.Where(x => x.NotEqual)
.Aggregate(predicate, (current, criteria) =>
current.AndNot(StartsWith(criteria.By, criteria.Value)));
return _source.AsQueryable().Where(predicate);

//changed from:
//var predicate = PredicateBuilder.True<T>();
//predicate = _searchCriteria.Where(x => !x.NotEqual)
// .Aggregate(predicate, (current, criteria) =>
// criteria.LogicalOperator == LogicalOperator.Or ?
// current.Or(Contains(criteria.By, criteria.Value)) :
// current.And(Contains(criteria.By, criteria.Value)));
//predicate = _searchCriteria.Where(x => x.NotEqual)
// .Aggregate(predicate, (current, criteria) =>
// current.AndNot(Contains(criteria.By, criteria.Value)));
//return _source.AsQueryable().Where(predicate);
}
}






private static Expression<Func<T, bool>> StartsWith(Expression<Func<T, string>> searchBy, string searchValue)
{
var search = Expression.Constant(searchValue.ToLower(), typeof(string));
var param = Expression.Parameter(typeof(T), "x");
var containsMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
var containsExp = Expression.Call(getProperty(searchBy, param), containsMethod, search);

return Expression.Lambda<Func<T, bool>>(containsExp, param);
}

//changed from:
//private static Expression<Func<T, bool>> Contains(string searchBy, string searchValue)
//{
// var search = Expression.Constant(searchValue, typeof (string));
// var searchByParam = Expression.Parameter(typeof (T), searchBy);
// var searchByExp = Expression.Property(searchByParam, searchBy);
// var methodInfo = typeof (string).GetMethod("Contains", new[] {typeof (string)});
// var containsExpression = Expression.Call(searchByExp, methodInfo, search);
// return Expression.Lambda<Func<T, bool>>(containsExpression, searchByParam);
//}

}

I think most of these are pretty self explanatory, with the exception of Result & Contains. Result takes advantage of a PredicateBuilder that I’ll show in a minute. Basically, it grabs all the search criteria that is equal and checks whether or not the command is or or and and builds the predicate. The next part grabs all the not equal criteria and builds its predicate. Finally it returns the course AsQueryable() where predicate. The AsQueryable is important because it’s what allows us not to have to call Compile() anywhere in our expression building.

Thanks to Rob White commenting below, I also added a NoMagicStringHelper class mentioned above in the new By() method. Here it is:

private static Expression getProperty(Expression<Func<T, string>> searchBy, Expression param)
{
Expression propExp = null;
var first = true;
foreach (var s in searchBy.Body.ToString().Split('.').Skip /*parameter*/(1))
if (first)
{
propExp =
Expression.Property(param, s);
first =
false;
}
else
{
if (s.StartsWith("ToString"))
propExp =
Expression.Call(propExp, "ToString", null);
else
propExp = Expression.Property(propExp, s);
}

return Expression.Call(Expression.Coalesce(propExp, Expression.Constant(String.Empty)), "ToLower", null);
}

//DELETED:
//public class NoMagicStringHelper
//{
// public static string GetPropertyFrom<T>(Expression<Func<T, string>> expr)
// {
// MemberExpression me = null;
// var body = expr.Body;
// if (body is MemberExpression) me = body as MemberExpression;
// else if (body is UnaryExpression)
// {
// var ue = body as UnaryExpression;
// me = ue.Operand as MemberExpression;
// }
// if (me == null) throw new NotImplementedException("Only Member and Unary Expressions implemented.");
// return me.Member.Name;
// }
//}

The Contains method just builds the Contains expression based on the search criteria. I used this same approach back in May, for more details, check it out.

The PredicateBuilder is pretty cool, I found it here on the C# 4.0/3.01 in a Nutshell site. It looks like this:

public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T>() { return f => true; }
public static Expression<Func<T, bool>> False<T>() { return f => false; }

public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());

return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
}

public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());

return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
}

public static Expression<Func<T, bool>> AndNot<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, Expression.Not(invokedExpr)), expr1.Parameters);
}
}

I added the AndNot<T> method to handle the Not and NotFor methods of my interface.

And of course, I have some tests to go along: I added more tests, but this post is long enough. You can download the source and check them out though.

    [TestFixture]
public class SearchTests
{
private List<Person> myPersonList;

[
TestFixtureSetUp]
public void Setup()
{
myPersonList =
new List<Person>();
myPersonList.Add(
new Person() { FirstName = "pappa", LastName = "racer" });
myPersonList.Add(
new Person() { FirstName = "speed", LastName = "racer" });
myPersonList.Add(
new Person() { FirstName = "george", LastName = "jetson" });
}

[
Test]
public void Result_should_return_search_results()
{
var result = Search<Person>.With(myPersonList).For("speed").By(x=>x.FirstName).Result;

Assert.That(result.Count(), Is.EqualTo(1));
}
[
Test]
public void Result_should_return_search_results_not_containing_speed()
{
var result = Search<Person>.With(myPersonList).Not("speed").By(x => x.FirstName).Result;

Assert.That(result.Count(), Is.EqualTo(2));
}
[
Test]
public void Result_should_return_search_results_containing_speed_or_pappa()
{
var result = Search<Person>.With(myPersonList).For("speed").OrFor("pappa").By(x => x.FirstName).Result;

Assert.That(result.Count(), Is.EqualTo(2));
}
[
Test]
public void Result_should_return_search_results_containing_pappa_or_speed_by_firstname_or_jetson_by_lastname()
{
var result = Search<Person>.With(myPersonList).For("pappa").OrFor("speed").By(x => x.FirstName).OrFor("jetson").By(x=>x.LastName).Result;

Assert.That(result.Count(), Is.EqualTo(3));
}
[
Test]
public void Result_should_return_search_results_containing_pappa_or_george_by_firstname_and_not_racer_by_lastname()
{
var result = Search<Person>.With(myPersonList).For("pappa").OrFor("george").By(x => x.FirstName).AndNot("racer").By(x=>x.LastName).Result;

Assert.That(result.Count(), Is.EqualTo(1));
}
}

internal class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

You can download the source code here.

Thanks for reading and as always, please let me know what you think!

Shout it

kick it on DotNetKicks.com

Related Posts Plugin for WordPress, Blogger...