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

  • 相关阅读:
    立一个flag。
    详解 fcntl 记录上锁。
    [转] 虚拟机中生成与物理机的共享文档。
    Shell实现1.0
    深剖malloc、new
    由命名管道的实现联想到 read 和 fread 的区别。
    僵尸进程与孤儿进程。
    Vim引申以及Linux下彩色进度条实现
    各大排序八方齐聚!
    Docker三剑客:Compose、Machine和Swarm
  • 原文地址:https://www.cnblogs.com/lightluomeng/p/2883324.html
Copyright © 2020-2023  润新知