2016年1月30日 星期六

[Lambda Expression] Create a reused query condition

 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




沒有留言:

張貼留言