2013年8月21日 星期三

效能大戰番外編 - 性格乖僻扭曲的傢伙 為了摘一朵花 翻越整個青藏高原 之 使用 Expression Trees (運算式樹狀架構) 動態 操作 LINQ 消除重複程式片段 這真令人心情莫名興奮愉悅

標題是想模仿 這個 的說 但是好像還是不夠長





如果你是個無法忍受任何原因任何方式出現相同 OR 相似重複程式碼片段 性格乖僻扭曲的傢伙的話

當你在玩 LINQ + entity framework 的時候 一定遇過這個困擾

        public void Kill(int id)
        {
            using (TransactionScope tran = new TransactionScope())
            {
                UserInfo UserInfo = db.UserInfo.FirstOrDefault(x => x.UserID == id);
                if (UserInfo != null)
                {
                    db.UserInfo.Remove(UserInfo);
                }
                UserAccount userAccount = db.UserAccount.FirstOrDefault(x => x.UserID == id);
                if (userAccount != null)
                {
                    db.UserAccount.Remove(userAccount);
                }
                db.SaveChanges();
                tran.Complete();
            }

        }

如果你覺得這段非常自然毫無異常 恭喜 你不是個 性格乖僻扭曲的傢伙





性格乖僻扭曲的傢伙 會說 如果今天這樣怎辦

重複片段 不能接受啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊~

當下決定 反射吧!男孩

        public void Kill(int id)
        {
            using (TransactionScope tran = new TransactionScope())
            {
                dynamic DbSetArr = new dynamic[]
                {
                    db.UserInfo,
                    db.UserAccount
                };
                foreach (var dbSet in DbSetArr)
                {
                    RemoveByUserID(id, "UserID", dbSet);
                }
                db.SaveChanges();
                tran.Complete();
            }
        }

        private void RemoveByUserID<T>(int id, string p, DbSet<T> dbSet) where T : class
        {
            T target = dbSet.FirstOrDefault(
                x => (int)x.GetType()
                    .GetProperty("UserID")
                    .GetValue(dbSet) == id
            );
            if (target != null)
            {
                dbSet.Remove(target);
            }

        }

you usually right, but this time you are left

你通常是正確的 但是這次不行

沒錯 LINQ To Entity 有後天性功能缺乏症候群 你沒辦法在 FirstOrDefault() 這類 LINQ 方法內 搞些卑鄙手段





就在不信邪試盡各種伎倆鼻青臉腫之後 果斷決定 stackoverflow 上的各位 借給我一點元氣吧!!!

        public void Kill(int id)
        {
            using (TransactionScope tran = new TransactionScope())
            {
                DbSet[] DbSetArr = new DbSet[]
                {
                    db.UserInfo,
                    db.UserAccount
                };
                foreach (var dbSet in DbSetArr)
                {
                    RemoveByUserID(id, "UserID", dbSet);
                }
                db.SaveChanges();
                tran.Complete();
            }
        }

        private void RemoveByUserID(int id, string columnName, DbSet dbSet)
        {
            Type type = dbSet.GetType().GetGenericArguments()[0];
            ParameterExpression parameter = Expression.Parameter(type, "x");
            //x.ColumnName
            MemberExpression left = Expression.Property(parameter, columnName);
            //id (Constant Value)
            ConstantExpression right = Expression.Constant(id);
            //x.ColumnName == id
            BinaryExpression filter = Expression.Equal(left, right);
            //x => x.ColumnName == id
            LambdaExpression firstOrDefaultMetod = Expression.Lambda(filter, parameter);
            MethodCallExpression resultExp = Expression.Call(
                typeof(Queryable),
                "FirstOrDefault",
                new Type[] { type },
                dbSet.AsQueryable().Expression,
                Expression.Quote(firstOrDefaultMetod)
            );
            object target = dbSet.AsQueryable().Provider.Execute(resultExp);
            if (target != null)
            {
                dbSet.Remove(target);
            }

        }




太讚了 I don't know WTF is it, but it works









----工作人員清單----













----番外編的番外編----

我們都知道 泛型 是利於泛用 而非利於重用

所以如果上面的方法是個泛型方法...

        public void Kill(int id)
        {
            using (TransactionScope tran = new TransactionScope())
            {
                RemoveByUserID<UserInfo>(id, "UserID", db.UserInfo);
                RemoveByUserID<UserAccount>(id, "UserID", db.UserAccount);
                db.SaveChanges();
                tran.Complete();
            }
        }

        private void RemoveByUserID<T>(int id, string columnName, DbSet<T> dbSet) where T : class
        {
            ParameterExpression parameter = Expression.Parameter(typeof(T), "x");
            T target = dbSet.AsQueryable().Provider.Execute<T>(Expression.Call(
                typeof(Queryable),
                "FirstOrDefault",
                new Type[] { typeof(T) },
                dbSet.AsQueryable().Expression,
                Expression.Quote(
                    Expression.Lambda(
                        Expression.Equal(
                            Expression.Property(parameter, columnName),
                            Expression.Constant(id)
                        ),
                        parameter
                    )
                )
            ));
            if (target != null)
            {
                dbSet.Remove(target);
            }

        }

效果自然是一樣 但是由於 泛型的限制

RemoveByUserID<UserInfo>(id, "UserID", db.UserInfo);

RemoveByUserID<UserAccount>(id, "UserID", db.UserAccount);

這段變得無法跑 foreach






這時候就能用反射囉 (請注意是能用 但是這不代表這是個好方法 過度使用反射未必會得到好結果 ._.)

        public void Kill(int id)
        {
            using (TransactionScope tran = new TransactionScope())
            {
                object[] DbSetArr = new object[]
                {
                    db.UserInfo,
                    db.UserAccount
                };
                foreach (var dbSet in DbSetArr)
                {
                    RemoveByUserID(id, dbSet);
                }
                db.SaveChanges();
                tran.Complete();
            }
        }

        private void RemoveByUserID(int id, object dbSet)
        {
            Type type = dbSet.GetType().GetGenericArguments()[0];
            //此處必須加上 BindingFlags 才能反射到 private method
            MethodInfo removeMethod = this.GetType().GetMethod(
                "Remove",
                BindingFlags.NonPublic | BindingFlags.Instance
            ).MakeGenericMethod(type);
            removeMethod.Invoke(this, new object[] { id, "UserID", dbSet });
        }

        private void Remove<T>(int id, string columnName, DbSet<T> dbSet) where T : class
        {
            Type type = typeof(T);
            ParameterExpression parameter = Expression.Parameter(type, "x");
            //x.ColumnName
            MemberExpression left = Expression.Property(parameter, columnName);
            //x.ColumnName 7
            ConstantExpression right = Expression.Constant(id);
            //x.ColumnName == 7
            BinaryExpression filter = Expression.Equal(left, right);
            //x => x.ColumnName == 7
            LambdaExpression firstOrDefaultMetod = Expression.Lambda(filter, parameter);
            MethodCallExpression resultExp = Expression.Call(
                typeof(Queryable),
                "FirstOrDefault",
                new Type[] { type },
                dbSet.AsQueryable().Expression,
                Expression.Quote(firstOrDefaultMetod)
            );
            T target = dbSet.AsQueryable().Provider.Execute<T>(resultExp);
            if (target != null)
            {
                dbSet.Remove(target);
            }

        }