Linq Lambda
Expression Func
▌Background
I
was refactoring the codes and normalizing the database on a website these days.
I
found that there were many duplicated columns in different tables, and they all
used the SAME query conditions in the linq or lambda expression!
That
made the system VERY HARD to maintain!
For
example, we have 2 or more tables like this …
Employee
|
Uid
|
Name
|
PhoneNumber
|
IsEnabled
|
CreatedBy
|
CreatedOn
|
Product
|
ProdId
|
Title
|
Price
|
IsEnabled
|
CreatedBy
|
CreatedOn
|
All
of them have the three columns in same. The scenario is that we want to query
all the tables with the conditions:
IsEnabled
== true and CreatedBy == “JB”
And
the codes may like this
var employees = this._employees.Where(x => x.IsEnabled == true &&
x.CreatedBy == "JB");
var products = this._products.Where(x => x.IsEnabled == true &&
x.CreatedBy == "JB");
|
Consider that you
are querying 100 tables, and you will know the problem.
The problem is
when we want to change the condition, such as “IsEnabled ==
false”, and there are 100 lambda expressions we have to change. Thaz a big
waste of time and the codes are not clean as well.
So in this
article, I am trying to pull the query condition out from the lambda
expression, and make it a reused query condition.
▌Environment
l Windows 7 Enterprise
l Visual Studio 2015 Ent.
l Linqkit 1.1.3.1
▌Implement
▋Create an Interface
The first idea
came to my mind is using interface. So I refactored my models as following.
public interface IMetaData
{
bool IsEnabled { get; set; }
string CreatedBy { get; set; }
DateTime CreatedOn { get; set; }
}
public class Employee : IMetaData
{
public int Uid { get; set; }
public string Name { get; set; }
public string PhoneNumber { get; set; }
public bool IsEnabled { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
}
public class Product : IMetaData
{
public int ProdId { get; set; }
public string Title { get; set; }
public int Price { get; set; }
public bool IsEnabled { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
}
|
The idea is
simple, we use the interface to set our query condition, and then put it in
every lambda expression of the models.
Func<IMetaData, bool>
filterCondition = (x => x.IsEnabled == true && x.CreatedBy == "JB");
var employees = this._employees.Where(filterCondition
as Func<Employee, bool>);
|
However, we CANNOT
DIRECTLY CONVERT Func<IMetaData, bool> to Func<Employee, bool>!
The query condition after converted will be NULL!
▋Convert Func Expression
Refer to the
solution of this article
We can convert
Func Expression by some tricks. Here is the sample code of the covert function.
public static class ConvertService
{
public static Func<Employee, bool> ConvertFuncExpression(Expression<Func<IMetaData, bool>>
predicate)
{
Expression<Func<Employee, IMetaData>> convert =
obj => new Employee
{
IsEnabled =
obj.IsEnabled,
CreatedBy =
obj.CreatedBy,
CreatedOn = obj.CreatedOn
};
var param = Expression.Parameter(typeof(Employee), "obj");
var body = Expression.Invoke(predicate, Expression.Invoke(convert,
param));
var lambda = Expression.Lambda<Func<Employee, bool>>(body, param);
// test with LINQ-to-Objects for
simplicity
var func = lambda.Compile();
return func;
}
}
|
▋Unit Test
I made an unit
test to verify the function.
private List<Employee> _employees = null;
public
UnitTestConvertService()
{
this._employees = new List<Employee>()
{
new Employee() { Uid=1, Name="JB", PhoneNumber="09XXXXX", IsEnabled=true, CreatedBy="Admin", CreatedOn=DateTime.Now },
new Employee() { Uid=2, Name="Lily", PhoneNumber="09XXXXX", IsEnabled=false, CreatedBy="Admin", CreatedOn=DateTime.Now },
new Employee() { Uid=3, Name="Leia", PhoneNumber="09XXXXX", IsEnabled=true, CreatedBy="JB", CreatedOn=DateTime.Now },
new Employee() { Uid=4, Name="Hachi", PhoneNumber="09XXXXX", IsEnabled=false, CreatedBy="JB", CreatedOn=DateTime.Now },
new Employee() { Uid=5, Name="Stan", PhoneNumber="09XXXXX", IsEnabled=true, CreatedBy="Admin", CreatedOn=DateTime.Now },
};
}
[TestMethod]
public void
TestConvertFuncExpression()
{
Func<IMetaData, bool> filterCondition = (x =>
x.IsEnabled == true
&& x.CreatedBy == "JB");
var employees = this._employees.Where(filterCondition
as Func<Employee, bool>);
//Set the reused query condition
(By Linqkit)
var predicate = PredicateBuilder.True<IMetaData>();
predicate = predicate.And(x =>
x.IsEnabled == true);
predicate = predicate.And(x =>
x.CreatedBy == "JB");
//Set the reused query condition
(By Func expression)
//Expression<Func<IMetaData,
bool>> predicate =
// (x => x.IsEnabled == true &&
x.CreatedBy == "JB");
//Convert the reused query
condition
var filter = ConvertService.ConvertFuncExpression(predicate);
//Query
var filteredEmployees = this._employees.Where(filter).ToList();
//Verify
Assert.IsTrue(filteredEmployees.Count ==
1);
Assert.AreEqual(3,
filteredEmployees[0].Uid);
Assert.AreEqual("Leia", filteredEmployees[0].Name);
}
|
▋Convert Func Expression (Generic function)
Last but not least, we have to rewrite the convert function as the Generic
function to support every model that implements IMetaData.
public static Func<C, bool> ConvertFuncExpression<C>(Expression<Func<IMetaData, bool>>
predicate) where C : IMetaData, new()
{
Expression<Func<C, IMetaData>> convert =
obj => new C
{
IsEnabled =
obj.IsEnabled,
CreatedBy =
obj.CreatedBy,
CreatedOn = obj.CreatedOn
};
var param = Expression.Parameter(typeof(C), "obj");
var body = Expression.Invoke(predicate, Expression.Invoke(convert,
param));
var lambda = Expression.Lambda<Func<C, bool>>(body, param);
// test with LINQ-to-Objects for
simplicity
var func = lambda.Compile();
return func;
}
|
▌Conclusion
This
article shows how to refactor the codes with lots of identical query condition
in lambda expression.
Although
it’s a simple trick for refactoring, I think it’s all in the details.
▌Reference
沒有留言:
張貼留言