一个多月之后,由浅入深表达式系列的最后一篇终于要问世了。想对所有关注的朋友说声:“对不起,我来晚了!” 希望最后一篇的内容对得起这一个月时间的等待。在学习完表达式树的创建和遍历之后,我们要利用它的特性来写一个我们自己的Linq Provider。人家都有Linq to Amazon为什么我们不能有Linq to cnblogs呢?今天我们就来一步一步的打造自己的Linq Provider,文章未尾已附上源码下载地址。如果对于表达式树的创建和遍历还是熟悉的话,建议先看前面两篇:

  创建表达式树 http://www.cnblogs.com/jesse2013/p/expressiontree-part1.html

  遍历表达式树 http://www.cnblogs.com/jesse2013/p/expressiontree-part2.html

更新:之前没有描述清楚本篇博客的意图,导致很多朋友的误解表示抱歉。本系列重在理解表达式目录树,以及Linq Provider。最后一篇是Linq Provider的实现,之所有会写这么多的代码去做一件简单的事(拉取博客园首页文章列表)完全是为了有一个生动的例子去展示如何实现自己的Linq Provider。和我们项目中的三层架构,或者直接序列化到本地是没有可比性的。

  当然,表达式目录树以及Linq Provider的强大也远非这个小小的Demo能体现得了的,如果你真正知道Linq Provider和表达式树目录树是什么,用来干什么的,也许你就能明白本篇博客的意图了。如果不了解的,建议读完前面两篇之后再做评论。因为你在自己不理解的情况下就直接去评论其它的领域,你就失去了一个了解它的机会。:)

实现目标

  我们实现的目标就像Linq to SQL一样,可以用Linq查询语句来查询数据,我们这里面的数据用到了博客园官方的Service去查询到最新的发布到首页的博客信息。看下面的代码:

var provider = new CnblogsQueryProvider();
var queryable = new Query<Post>(provider);

var query =
    from p in queryable
    where p.Diggs >= 10 &&
    p.Comments > 10 &&
    p.Views > 10 &&
    p.Comments < 20
    select p;

var list = query.ToList();

   作为实时访问远程的Service,我们还应该具体以下几点要求:

  • 延迟加载,即到最后使用的时候才真正的去请求数据
  • 只返回需要的数据,不能把所有的数据全下载过来再到本地过滤,那就没有意义了

最后实现的结果:

由浅入深表达式树(三)

数据准备

  根据博客园公开的API显示,获取首页文章列表非常容易,大家可以点下面的URL来体检一把。我们最后给的参数是100000,当然真实返回肯定是没有那么多的,我们是希望把能够取回来的都取回来。

http://wcf.open.cnblogs.com/blog/sitehome/recent/100000

由浅入深表达式树(三)

  点击完上面的URL之后呢,问题就来了,它只有一个参数。我并不能传给它查询条件,比如说根据标题来搜索,或者根据评论数,浏览量来过滤。难道我的计划就此要泡汤了么,刚开始我很不开心,为什么博客园就不能提供灵活一点的Service呢?但是事实就是这样,咋是程序员呀,需求摆在这,怎么着还得实现是不?没有办法,我给它封装了一层。在它的基础上做了一个自己的Service。

封装博客园Service

  我们如何在博客园公开Service的基础上加一层实现条件查询呢?主要思路是这样的:

  • 为文章建立实体类(Post)
  • 将博客园Service返回的数据解析成Post的集合,我们可以加上自己的缓存机制,可以采用1分钟才到博客园取一次数据
  • 把我们上面创建的post集合当作数据库,建立查询Service

  我们首先要做的就是为博客园的博客建立实体类。

public class Post
{
    // Id
    public int Id { get; set; }

    // 标题
    public string Title { get; set; }

    // 发布时间
    public DateTime Published { get; set; }

    // 推荐数据
    public int Diggs { get; set; }

    // 访问人数
    public int Views { get; set; }

    // 评论数据
    public int Comments { get; set; }

    // 作者
    public string Author { get; set; }

    // 博客链接
    public string Href { get; set; }

    // 摘要
    public string Summary { get; set; }
}

接着,我们要将博客园返回给我们的XML数据转换成我们要的post集合,所以我们要用到Linq to XML。

_list = new List<CnblogsLinqProvider.Post>();
var document = XDocument.Load(
    "http://wcf.open.cnblogs.com/blog/sitehome/recent/100000"
    );

var elements = document.Root.Elements();
var result = from entry in elements
                where entry.HasElements == true
                select new CnblogsLinqProvider.Post
                {
                    Id = Convert.ToInt32(entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "id").Value),

                    Title = entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "title").Value,

                    Published = Convert.ToDateTime(entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "published").Value),

                    Diggs = Convert.ToInt32(entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "diggs").Value),

                    Views = Convert.ToInt32(entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "views").Value),

                    Comments = Convert.ToInt32(entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "comments").Value),

                    Summary = entry.Elements()
                        .SingleOrDefault(x=>x.Name.LocalName=="summary").Value,

                    Href = entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "link")
                        .Attribute("href").Value,

                    Author = entry.Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "author")
                        .Elements()
                        .SingleOrDefault(x => x.Name.LocalName == "name").Value
                };

_list.AddRange(result);

   然后就到了我们的查询了,实际上我们有了IEnumrable<Post>的数据就可以直接在本地用Linq去查询它了。但是这不是我们想要的,因为我们上面的步骤是把所有的数据一次性全部下载下来了,而不是根据我们的需求返回数据。另外我们这里面是在博客园Service的基础上做一层封装,实现通过Url直接查询首页的文章。为什么要通过Url来查询?因为我们最后会通过我们自己的LinqProvider将Linq查询语句直接翻译成Url这样就能够实现远程的返回数据了。来看看我们对Url参数的定义:

由浅入深表达式树(三)

利用JsonResult 返回json数据来创建我们的Service

  作为Service,我们返回Json或者XML格式的数据都是可以的。当然实现这个需求的方法有很多种,我们这里面有选了一种最简单方便又比较适合我们需求方式。不需要WCF Service也不需要Web API,直接用MVC里面的Action返回JsonResult就可以了。

[HttpGet]
public JsonResult Index(SearchCriteria criteria = null)
{
    var result = PostManager.Posts;
    if (criteria != null)
    {
        if (!string.IsNullOrEmpty(criteria.Title))
            result = result.Where(
                p => p.Title.IndexOf(criteria.Title, StringComparison.OrdinalIgnoreCase) >= 0);

        if (!string.IsNullOrEmpty(criteria.Author))
            result = result.Where(p => p.Author.IndexOf(criteria.Author, StringComparison.OrdinalIgnoreCase) >= 0);

        if (criteria.Start.HasValue)
            result = result.Where(p => p.Published >= criteria.Start.Value);

        if (criteria.End.HasValue)
            result = result.Where(p => p.Published <= criteria.End.Value);

        if (criteria.MinComments > 0)
            result = result.Where(p => p.Comments >= criteria.MinComments);

        if (criteria.MinDiggs > 0)
            result = result.Where(p => p.Diggs >= criteria.MinDiggs);

        if (criteria.MinViews > 0)
            result = result.Where(p => p.Diggs >= criteria.MinViews);

        if (criteria.MaxComments > 0)
            result = result.Where(p => p.Comments <= criteria.MaxComments);

        if (criteria.MaxDiggs > 0)
            result = result.Where(p => p.Diggs <= criteria.MaxDiggs);

        if (criteria.MaxViews > 0)
            result = result.Where(p => p.Diggs <= criteria.MaxViews);
    }
    return Json(result, JsonRequestBehavior.AllowGet);
}

  利用Action来做这种Service还有一个好处就是我们不需要一个一个的声明查询参数,只需要把所有的参数放到一个model中就可以了。剩下的事就交给Model Binder吧。

 public class SearchCriteria
    {
        public string Title { get; set; }
        public string Author { get; set; }
        
        public DateTime? Start { get; set; }
        public DateTime? End { get; set; }

        public int MinDiggs { get; set; }
        public int MaxDiggs { get; set; }
        public int MinViews { get; set; }
        public int MaxViews { get; set; }
        public int MinComments { get; set; }
        public int MaxComments { get; set; }
    }

  如果大家想更熟悉这个Service的功能,可以参考上面的参数自己去体验一下(用IE会直接下载.json的文件,用Chrom是可以直接在浏览器里面看数据的)。但是我没有做任何安全性的措施,希望大侠高抬贵手,别把网站整挂了就行。

认识IQueryable和IQueryProvider接口

   有了上面的Service之后,我们要做的事情就简单多了,但是在我们真正开始动手写自己的Linq Provider之前,先来看看IQueryable和IQueryProvider这两个重要的接口。

IQueryable

  IQueryable本身并没有包含多少的东西,它只有三个属性:

由浅入深表达式树(三)
  • ElementType 代表当然这个Query所对应的类型
  • Expression 包含了我们当然Query所执行的所有查询或者是其它的操作
  • IQueryProvider则是负责处理上面的Expression的实现

  更为重要的是,在IQueryable这个接口之上,.net为我们提供了很多的扩展方法:

由浅入深表达式树(三)

  我们平常用到的Where,Select,Max,Any都包括在其中,具体的方法大家可以到System.Linq.Queryable这个静态类下去看。大家注意一下,传给Where方法的正是我们现在学习的Expression。

    在另外一个很重要的接口IEnumrable下,也有着同样的扩展方法:

由浅入深表达式树(三)

  这些扩展方法来自System.Linq.Enumrable这个静态类下。我们可以看到两组扩展方法的不同之处在于IQueryable下传入的Expression类型,而IEnumrable下传入的是委托。这样做的用意是什么呢?您请接着往下看。

IQueryProvider

  我们上面讲到了Enumrable和Queryable这两个静态类下的扩展方法,对于Enumrable下的扩展方法来说他们传入的是委托,对于委托而言直接执行就可以了。

public static IEnumerable<T> Where<T>(this IEnumerable<T> list, Func<T, bool> predicate)
{
    var result = new List<T>();
    foreach (var element in list)
    { 
        // 调用委托是验证这个元素是否符合条件
        if(predicate(element))
            result.Add(element);
    }
    return result;
}

  上面的代码给大家作一个参考,相信不难理解,所以Enumrable下的静态方法都是操作本地数据的。而对于Queryable下的静态方法而言,他们接收的是表达式,还记得表达式的最大特征吗?可以在运行时去遍历解释然后执行,那么这样就可以将表达式转换成各种其它的方式去获取数据,伟大的Linq to SQL就是这么实现的。而这背后的大功臣就是我们的Linq Provider了,而IQueryProvider就是LinqProvider的接口。

由浅入深表达式树(三)

  IQueryProvider只有两个操作,CreateQuery和Execute分别有泛型版本和非泛型版本。 CreatQuery用于构造一个IQueryable<T>的对象,这个类其实没有任何实现,只是继承了IQueryable和IEnumrable接口。主要用于计算指定表达式目录树所表示的查询,返回的结果是一个可枚举的类型。 而Execute会执行指定表达式目录树所表示的查询,返回指定的结果。所有的内幕就在这个Execute方法里面,拿我们要进行的Linq to cnblogs方法来举例,我们将把传入的表达式目录树翻译成一个URL就是指向我们封装好的Service的URL,通过发起web request到这个URL,拿到response进行解析,最终得到我们所要的数据,这就是我们Linq to cnblogs的思路。

Linq to cnblogs的实现

  有了前面的数据准备和一些实现的大致思路以后,我们就可以着手开始实现我们的CnblogsQueryProvider了。我们的思路大致是这样的:

  1. 实现自己的ExpressionVisitor类去访问表达式目录数,将其翻译成可以访问Service的Url
  2. 调用WebRequest去访问这个Url
  3. 将上面返回的Response解析成我们要的对象

实现PostExpressionVisitor

  关于表达式树的访问,我们在第二篇中已经有了比较详细的介绍。如果对于表达式树的遍历不清楚的,可以去第二篇《遍历表达式》中查阅。在这里,我们创建一个我们自己的ExpressionVisitor类,去遍历表达式树。我们暂时只需要生成一个SearchCriteria(我们上面已经定义好了,对于查询条件建的模)对象即可。

  1 public class PostExpressionVisitor
  2 {
  3 private SearchCriteria _criteria;
  4 
  5 // 入口方法
  6 public SearchCriteria ProcessExpression(Expression expression)
  7 {
  8     _criteria = new SearchCriteria();
  9     VisitExpression(expression);
 10     return _criteria;
 11 }
 12 
 13 private void VisitExpression(Expression expression)
 14 {
 15     switch (expression.NodeType)
 16     { 
 17         // 访问 &&
 18         case ExpressionType.AndAlso:
 19             VisitAndAlso((BinaryExpression)expression);
 20             break;
 21         // 访问 等于
 22         case ExpressionType.Equal:
 23             VisitEqual((BinaryExpression)expression);
 24             break;
 25         // 访问 小于和小于等于
 26         case ExpressionType.LessThan:
 27         case ExpressionType.LessThanOrEqual:
 28             VisitLessThanOrEqual((BinaryExpression)expression);
 29             break;
 30         // 访问大于和大于等于
 31         case ExpressionType.GreaterThan:
 32         case ExpressionType.GreaterThanOrEqual:
 33             GreaterThanOrEqual((BinaryExpression)expression);
 34             break;
 35         // 访问调用方法,主要有于解析Contains方法,我们的Title会用到
 36         case ExpressionType.Call:
 37             VisitMethodCall((MethodCallExpression)expression);
 38             break;
 39         // 访问Lambda表达式
 40         case ExpressionType.Lambda:
 41             VisitExpression(((LambdaExpression)expression).Body);
 42             break;
 43     }
 44 }
 45 
 46 // 访问  &&
 47 private void VisitAndAlso(BinaryExpression andAlso)
 48 {
 49     VisitExpression(andAlso.Left);
 50     VisitExpression(andAlso.Right);
 51 }
 52 
 53 // 访问 等于
 54 private void VisitEqual(BinaryExpression expression)
 55 {
 56     // 我们这里面只处理在Author上的等于操作
 57     // Views, Comments, 和 Diggs 我们都是用的大于等于,或者小于等于
 58     if ((expression.Left.NodeType == ExpressionType.MemberAccess) &&
 59         (((MemberExpression)expression.Left).Member.Name == "Author"))
 60     {
 61         if (expression.Right.NodeType == ExpressionType.Constant)
 62             _criteria.Author = 
 63                 (String)

相关文章:

  • 2021-11-17
  • 2021-06-17
  • 2022-02-05
  • 2022-12-23
  • 2021-06-22
  • 2022-12-23
  • 2022-12-23
  • 2021-06-21
猜你喜欢
  • 2021-05-04
  • 2021-07-29
相关资源
相似解决方案