Friday, May 07, 2010

Building a L2S Expression with .NET 3.5




I recently wanted to reduce some code redundancy by extracting out some code. We had a few helper methods that got results by specific fields.

For example:

public IList<HelpRequest> GetByLastName(string lastName)
{
return _dc.HelpRequest
.Where(x=>x.LastName.StartsWith(lastName))
.OrderBy(y => y.RequestDate).ToList();
}
public IList<HelpRequest> GetByFirstName(string firstName)
{
return _dc.HelpRequests
.Where(x => x.FirstName.StartsWith(firstName))
.OrderBy(y => y.RequestDate).ToList();
}

So obviously there is some redundant code here that we can get rid of and we did. First we created another helper method like this:

public IList<HelpRequest> GetByLastName(string search)
{
return GetBy(x => x.LastName.StartsWith(search));
}

public IList<HelpRequest> GetByFirstName(string search)
{
return GetBy(x => x.FirstName.StartsWith(search));
}

private IList<HelpRequest> GetBy(Expression<Func<HelpRequest, bool>> func)
{
return _dc.HelpRequests
.Where(func)
.OrderBy(y => y.RequestDate).ToList();
}

So that got rid of some of the duplication, but not all of it. We’re still doing a StartsWith in the helper and I didn’t want to do that logic there. So…now we have this and this took a long time to figure out…so I’m sure there is a better way because I basically got this working and stopped. Anyhow, here is the latest version, which has more lines of code, but less redundancy. I’m not sure which is better at this point…I’m thinking maybe the first refactoring, but this one is cool too and more flexible I think.

public IList<HelpRequest> GetByLastName(string search)
{
return GetBy(LastName => search);
}

public IList<HelpRequest> GetByFirstName(string search)
{
return GetBy(FirstName => search);
}

private IList<HelpRequest> GetBy(Func<string, string> func)
{
return _dc.HelpRequests
.Where(func.StartsWith<
HelpRequest>())
.OrderBy(y => y.RequestDate)
.ToList();
}

This one goes along with an extension method that looks like this: (Code below OUTDATED as of 5/9/2010 SEE BELOW)

public static Func<T, bool> StartsWith<T>(this Func<string, string> func)
{
var searchBy = func.Method.GetParameters()[0].Name;
var search = Expression.Constant(func(null), typeof(string));

var searchByParam = Expression.Parameter(typeof(T), searchBy);
var searchByProp = Expression.Property(searchByParam, searchBy);
var ignoreCase = Expression.Constant(StringComparison.CurrentCultureIgnoreCase);

var methodInfo = typeof(string).GetMethod("StartsWith", new[]{typeof(string), typeof(StringComparison)});
var containsExpression = Expression.Call(searchByProp, methodInfo, search, ignoreCase);

return Expression.Lambda<Func<T, bool>>(containsExpression, searchByParam).Compile();
}

So, as you can see this little extension method is kinda loaded down. I’m hoping there are some shortcuts to writing these expressions that I don’t know about…but I haven’t found any good examples. I browsed Google for a while and StackOverflow. I even submitted a question…I had one response, but it wasn’t exactly what I was looking for as you can see here. Anyhow, let’s go through this code.

First let’s talk about the GetByLastName & GetByFirstName methods…they’re REALLY simple now, but with one pretty big caveat I think. That caveat is that the property name isn’t strongly typed. If you follow my blog at all,  you know I’m a BIG fan of strongly typed things, but I haven’t found a better way in this instance. So, LastName & FirstName are properties on my HelpRequest object. I used this same method in the “Fun with Lambdas & HtmlHelpers” post. Alrighty, let’s keep going…so the GetBy(Func<string, string>) simply calls the func.StartsWith extension method and returns my IList…now to the meat of this post.

The extension method StartsWith<T> could be put into like 3 lines, but it’s way less readable when you do that…I basically set a variable for every different type of Expression object to keep it a bit readable. So the first thing I do is get my search by, which is my property name. On the next line, I create a constant, which is the right-side of the func passed in. Next on the list is the search by parameter, which sets the type to HelpRequest with the name of searchby (in this case, FirstName or LastName).  After that I set an Expression.Property for the searchBy and then I set another constant for the ignorecase that I’m going to pass into StartsWith in a bit.

Now, I find the method “StartsWith” on a string and setup the parameter definitions of what I want to pass in. In this case, a string and a StringComparison. After this, I call the startsWith on the search property with the search and ignoreCase parameters.

Finally, I return the lambda expression that is required for my Where on my LINQ to SQL statement. I also found that you have to call the Compile in order for it to work. What I do like about this extension method is that I can now use it for all my searches. Of course I’m going to continue testing it and verifying that there’s not a better way, but for now, I think it’ll work.

Please let me know if you have any suggestions or comments.

Thanks for reading!

UPDATE: 5/9/2010

So thanks to Brad commenting on using Ants or SQL Profiler, I did a little more investigating on the code above. Basically what was happening was this:

SELECT [t0].[FirstName], [t0].[LastName]
FROM [hd].[HelpRequest] AS [t0]

So, I made some adjustments to the static StartsWith<T> method and now it looks like this:

private static Expression<Func<T, bool>> StartsWith<T>(Func<string, string> func)
{
var searchBy = func.Method.GetParameters()[0].Name;
var search = Expression.Constant(func(null), typeof(string));

var searchByParam = Expression.Parameter(typeof(T), searchBy);
var searchByExp = Expression.Property(searchByParam, searchBy);
//var ignoreCase = Expression.Constant(StringComparison.CurrentCultureIgnoreCase);

var methodInfo = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });//, typeof(StringComparison)});
var containsExpression = Expression.Call(searchByExp, methodInfo, search);//, ignoreCase);

return Expression.Lambda<Func<T, bool>>(containsExpression, searchByParam);//.Compile();
}

First thing to note is the return type has been changed from Func<T, bool> to Expression<Func<T, bool>>. This allows us to do away with the Compile() at the bottom of the method. I was also able to do away with the ignoreCase and StringComparison stuff because it’s converted to SQL and in this case we’re not caring about case-sensitvitity. So now my SQL looks like this:

exec sp_executesql N'SELECT [t0].[FirstName], [t0].[LastName]
FROM [hd].[HelpRequest] AS [t0]
WHERE [t0].[FirstName] LIKE @p0',N'@p0 varchar(7)',@p0='deran %'

So, this is a good thing. Thank you Brad! Please let me know if you see anything else that could cause a problem.

Shout it

kick it on DotNetKicks.com

blog comments powered by Disqus
Related Posts Plugin for WordPress, Blogger...