Hello My LINQ World——自定义LINQ Provider实现LINQ to LDAP查询(其三)

引言

说明

由于博客园是个技术社区,所以我得显得严谨点,这里留下几点说明,我会在接下来的几篇文章中(如果有的话)重复这个说明。

其一,这篇(或者系列,如果有的话)文章是为了和大家一起入门(注意不是指导)。所以所编写的代码仅仅是示例的,或者说是处于编写中(完善中)的。

其二,至于为什么在学习的过程中就着手写这些文章,那是因为我深深觉得作为入门,这些内容还是容易的,但是常常让人却而退步。比如在一周之前,我还问博客园中的另一位博主,请求资料。那个时候我还觉得非常困难,非常苦恼。但是,经过一些摸索,一些文章的指导之后,却轻轻叩开了LINQ的门,一窥其瑰丽了。

其三,其实网上并不是没有LINQ的教程(指编写Provider)。但是“会”和不会往往隔了一点顿悟。就像“水门事件”一样。所以作为初学者来和大家一起探讨可以让彼此更同步。

其四,这真的是一个非常有挑战,非常有趣的内容。我接触了之后就忍不住和大家一起分享,邀大家一起参与冒险。

最后,这里列出所有我参考的,觉得有价值的资源。

其一,MSDN的博客: http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx

这系列文章直接和本系列文章相关。07年的帖子,13年才发现,真该面壁思过。

其二,http://weblogs.asp.net/mehfuzh/archive/2007/10/04/writing-custom-linq-provider.aspx

待会会在文章中引用到这个博主写的一个非常短小的Provider示例。

其三,博客园中某个博主的作品http://www.cnblogs.com/Terrylee/category/48778.html

大神的文章读起来有点累,所以这系列我访问了好几次,愣是没看懂怎么回事,不过里面有张图挺不错。

接上文

在上一篇文章中,我们成功的将一个lambda表达式转换为被DirectorySearcher接受的Filter字符串。煽情点说,我们已经构建了LINQ查询和LDAP查询的桥梁。“这具有重大意义!”,好比听懂了方言。其实在上一篇中我提到,最后一篇的内容不会特别多。但是如果直接附在上一篇的话,就会使那篇显得有点长(当然这只是一个借口,很大程度上是我的强迫症在作怪),所以我要紧牙关写了第三篇。同时我在编码的过程中,脑海中浮现了一些问题和想法,想和大家分享。

另外,这篇文章算得上是“暂时告别篇”。和园友留言交流的时候我多次表明了自己的决心,所以我会长期跟进这个系列,但是不会像这三篇文章那么连续了。这三篇文章算是补上LINQ Provider实现这方面的“空白”,希望能帮助到喜欢和想要了解LINQ的朋友们。另外,再强调一次,http://blogs.msdn.com/b/mattwar/archive/2007/07/30/linq-building-an-iqueryable-provider-part-i.aspx,这个帖子其实已经包含了绝大多数“实现LINQ Provider”的内容,非常有价值。

Hello World

接下来我们就要完成这个Hello World并使之运行了,虽然我们已经耗费了很长的时间,但是值得。还记得高一的电脑课上,第一次输入VB代码的时候因为拼写错误抓狂了半天(也是高中唯一一次编码)。在第一篇中,我们把7个需要实现的方法缩减为1个。现在就去实现那个方法。从第二篇到这一篇,我已经对部分代码做了较大程度的更改。比如,为User类型提供了构造函数以保存结果。为Provider类型和Context类型提供了Username,Password,Path等属性以方便访问AD,同时还为他们提供了PropertiesToLoad只读属性用来加快LDAP搜索(关于这点,可以参考我的另外一篇文章:http://www.cnblogs.com/lightluomeng/archive/2013/01/18/2867019.html),另外为User提供了GetDirectory方法来增强实用性。下面是User类型,贴出来作为示例。

View Code
    [Category("User")]
    public class User
    {
        public User(SearchResult result)
        {
            this.result = result;
        }

        private SearchResult result;

        /*模型类型,用来构造查询,和存储结果*/
        [Property("userPrincipalName")]
        public string UserPrincipalName
        {
            get { return this["userPrincipalName"][0].ToString(); }
        }
        [Property("cn")]
        public string Name {
            get
            {
                if (this["cn"].Count == 0)
                    throw new ArgumentException("未定义加载的属性!参数:cn。");
                else
                    return this["cn"][0].ToString();
            } 
        }

这些工作都是为了实现抽象类(见第一篇),QueryProvider的Excute方法准备的。这个方法的实现目前是这样的。

View Code
protected override object excute(System.Linq.Expressions.Expression expression)
        {/*最核心的部分,将表达式目录树转换为目标查询,执行查询并返回结果*/
            string queryString = translator.Translate(expression);

            /*加载属性*/
            DirectoryEntry root = new DirectoryEntry(Path, Username, Password);
            DirectorySearcher searcher = new DirectorySearcher(root);

            if (PropertiesToLoad.Count == 0)
            {//如果没有指定要加载哪些属性的话,则根据表达式计算需要加载的属性的最小集合
                string ptm = @"[\w]+(?=[=<>])";
                var matches = Regex
                    .Matches(queryString, ptm)
                    .Cast<Match>()
                    .Select(i => i.Value);
                foreach (var i in matches) searcher.PropertiesToLoad.Add(i);
            }
            else
            {
                foreach (var i in PropertiesToLoad) searcher.PropertiesToLoad.Add(i);
            }
           
            searcher.Filter = queryString;
            searcher.SearchScope = SearchScope.Subtree;
            searcher.PageSize = 10;

            var result = searcher.FindAll();


            return searcher.FindAll()
                .Cast<SearchResult>()
                //.ToArray()
                .Select(i => new User(i));
 
            //foreach (var item in searcher.FindAll())
            //{
            //    yield return (new User(item));
            //}
        }

如果没有指定PropertiesToLoad的话,为了快速搜索,就根据表达式的值去指定PropertiesToLoad,不管何时,加载全部属性都是不明智的,对于一个需要详细信息的单个条目,宁愿调用GetDirecotryEntry方法来获取DirectoryEntry。还是要声明一下,上面的代码是出于“生长中”的,最明显的是,我把返回结果直接封装成了User,实际上应该根据lambda的ElementType来生成结果。或许还有朋友注意到了我注释掉的代码,这个我的几个想法有关,容后考虑。现在,暂且欣赏我们的成果吧。下面是调用示例。

View Code
    class Program
    {
        static void Main(string[] args)
        {
            Context context = new Context("LDAP://searchAD", @"search\Ouwsearch", "OuweiSoft0123");

            context.PropertiesToLoad.Add("cn");
            context.PropertiesToLoad.Add("userPrincipalName");
            //var query1 = context.Users.Where(i => i.UserPrincipalName.StartsWith("c"));
            /*注意upn可是带了完整域名的(@)*/
            var query1 = from u in context.Users where u.UserPrincipalName.Contains("xc") select u;
            var query2 = context.Groups;
            var query3 = context.Users;

            foreach (var u in query1)
            {
                Console.Write("{0},", u.Name);
            }

            /*因为只是胚胎代码,这个查询甚至都无法输出呢*/
            //foreach (var g in query2)
            //{
            //    //Console.Write("{0},",
            //}

            Console.ReadLine();
        }

以及靓照一张:

很开心,但又是最淡淡的开心。因为这一切并不是“你突然掌握了一种魔术”,而是“按照计划,达到了预期”。 

 一些想法

或许你注意到了代码中注释掉的ToArray(在Excute方法实现中),这正是我关心的地方。从延时查询说吧。

延时查询——缓存和流式

“LINQ中的缓存和流式都属于延时查询”(《深入C#》P229)。书中选用了排序这个操作作为比较,因为排序总是要访问了所有元素之后才能得出结果。然而对于数据库而言,排序简直是家常便饭,LINQ to SQL也不可能真正的做到“每次请求一个元素,然后提交给迭代器”(那样的请求是要多频繁啊)。但是如果指定语义为“SELECT * FROM TABLE”的查询的话呢?难道真的要返回所有记录?可不可以内置一种方式,只有在需要的时候才加载?不然的话,Excute方法一被调用,就要处理所有记录并转化,代价太高了。我的想法是在枚举器中进行处理。

对于这一点我进行了测试,编写一个查询,使用foreach进行迭代,在foreach方法块的第一个花括号设置断点。程序执行到断点后就去修改SQL表中的值。这里是简单的查询迭代。

View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {//<--断点
            var context = new QueryDataContext();
            var query = context.Master;

            foreach (var i in query)
            {
                Console.WriteLine(i.Name);
            }

            Console.ReadLine();
        }
}

我一共有2W条记录,修改了最后一条,结果输出的竟然是“修改过的值”!由此可见,MS进行了优化,防止一次性返回太多的数据,从而提升性能。但是,我认为绝对不是逐行求值,那将是灾难。所以呢,我尝试着去修靠前的几条值来验证。结果如何呢?有趣的事情发生了,我得到了这个提示。经过尝试,我发现这个锁定区域是30左右(我的环境中),也就是说,前30条被锁定无法更改。所以我修改了第35条,然后输出验证,发现第35条的值已经被更改。然后我尝试修改第一条,通过了;修改第36-65条(大概这个区间),又提示如下。所以呢,数据库的返回也是分段的,而不是一股脑的,当然作为程序员的我们倒不需要太在意(除了,在实现LINQ to SQL的Excute方法中千万不要ToArray!)不过很有意思是吧?

LDAP中也同样存在这个问题,请看下文。

在我们的例子中

LDAP查询支持分页,但是不像数据库哪样明确。DirecotrySearcher可以设置PageSize属性,但是没用PageIndex。设置好PageSize属性之后,进行查询输输出,在控制台中,可以看到输出一段,停一小会(非常微妙,有时无法看出),在输出一段...这样的循环直到输出完毕。这也是一种流式(不是一个一个元素,而是一段一段序列)。流式的好处是相应速度快(第一次加载),而且能够控制流量——只加载必要的信息。但是调用了.ToArray之后,就等于加载所有数据进行处理。为了弄清楚这个,先写一段纯粹的LDAP查询做测试,在测试环境中导入了1000条记录,以延长时间。我们只需要弄清楚,调用ToArray和不调用ToArray的区别,就能初步说明问题。这里是测试代码。

View Code
static void Main(string[] args)
        {
            DirectoryEntry entry = new DirectoryEntry("LDAP://dc.lsow.ow", @"lsow\exadmin", "1qaz@WSXEx");
            DirectorySearcher searcher1 = new DirectorySearcher(entry);
            searcher1.SearchScope = SearchScope.Subtree;
            searcher1.Filter = "(objectclass=user)";

            DirectorySearcher searcher2 = new DirectorySearcher(entry);
            searcher2.SearchScope = SearchScope.Subtree;
            searcher2.Filter = "(objectclass=user)";

            DirectorySearcher searcher3 = new DirectorySearcher(entry);
            searcher3.SearchScope = SearchScope.Subtree;
            searcher3.Filter = "(objectclass=user)";

            PrintTime();
            var result1 = searcher1.FindAll();
            PrintTime();
            var result2 = searcher2.FindAll().Cast<SearchResult>();
            PrintTime();
            var result3 = searcher3.FindAll().Cast<SearchResult>().ToArray();
            PrintTime();
            foreach (var i in result1)
            {
                Console.Write(".");
            }
            PrintTime();
            foreach (var i in result2)
            {
                Console.Write(".");
            }
            PrintTime();
            foreach (var i in result3)
            {
                Console.Write(".");
            }
            PrintTime();

            Console.ReadLine();
        }

比较让我惊讶的是Cast方法调用的那一段...也太省力了点。我刚才说“一段一段”这个效果有点微妙,但在这个示例中就非常明显了(注意对比第三段的ToArray版本的),是不是很好玩?:)现在我要做一些转换,使用一个方法将Cast而且执行了Select的结果以object返回,然后再重新转换为IEnumerable接口的对象,再使用foreach进行迭代。如果还是有“分段”的效果的话,我心里的石头就落下了。

View Code
        static object getObjectEnumerable(IEnumerable<SearchResult> source)
        {
            return source.Select(i => new User(i));
        }

方法很简单,对应的用是:

View Code
            foreach (var i in (IEnumerable<SearchResult>) getObjectEnumerable<SearchResult>( result2))
            {
                Console.Write(".");
            }

这其实是过度担心的做法,前面的例子已经说明了,调用ToArray和不调用ToArray的区别了。不过还是请大家亲手试试。LINQ的延时查询非常完美的支持了“分段求值”(大家或许还注意到我注释掉的那个yield return,我害怕Cast<>和Select破坏了原有的枚举,现在看来是担心过度了)。

备注

我按照原样将我的想法和验证步骤(当然如果我验证过程中出错,我自动纠正,只提供我认为“正确”的做法,以缩减篇幅)写在这里,欢迎各位朋友一起讨论。

结语

OK,这个系列到这里暂停了。2号就准备回家,宽带被我妈迁到她工作的地方了,没打算迁回家。原本还纠结假期怎么发博客呢,现在总算不用担心了。如大家所见,我比较快的给这个系列开完了头,但是接下来的事情就不那么容易了,我们要实现更多的操作符/更丰富的查询,还要进行优化。我会坚持更新文章,同时希望对这方面感兴趣的朋友也推出自己的文章,方便大家学习讨论。:)

posted @ 2013-01-31 10:35  LibraJM  阅读(1279)  评论(4编辑  收藏  举报