惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
月光博客
月光博客
AI
AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CXSECURITY Database RSS Feed - CXSecurity.com
WordPress大学
WordPress大学
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
O
OpenAI News
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
W
WeLiveSecurity
L
LINUX DO - 最新话题
人人都是产品经理
人人都是产品经理
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
Arctic Wolf
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Scott Helme
Scott Helme
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
The Hacker News
The Hacker News
Y
Y Combinator Blog
Latest news
Latest news
SecWiki News
SecWiki News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题

博客园 - Windie Chai

为SharePoint顶部链接开发自定义数据源 SharePoint 2010 JavaScript技巧两则 Windows Phone Background Agent杂谈 Windows Phone自定义主题 Windows Phone WebBrowser的技巧 《Visual Studio程序员箴言》笔记 HTML5学习碎片 Windows Phone 7 UI设计和交互规范随笔(2) Windows Phone 7 UI设计和交互规范随笔(1) 使用SharePoint Client OM来查询列表的注意事项(2) 使用SharePoint Client OM来查询列表的注意事项 小心Windows Live Writer插件偷走你的博客密码 [C#]增强响应性,用加载窗体(Splash)来载入主窗体 WindStyle ExifInfo for Windows Live Writer发布 WF4.0活动模型(1):工作流既活动 泛谈SharePoint 2010无代码工作流 如何在SharePoint 2010项目中引用UserProfiles.dll Windstyle SlugHelper for Windows Live Writer发布 让代码看起来更舒服(2):选择适合的字体
Linq to SharePoint,看上去很美
Windie Chai · 2012-04-12 · via 博客园 - Windie Chai

Linq to SharePoint是SharePoint 2010引入的一组新API,在这之前,如果我们想要按照条件过滤SharePoint列表中的数据,只能通过CAML。

但使用CAML并不是件令人身心愉悦的事情,至少我是这么认为的。我觉得在代码中嵌入一块冗长的XML字符串非常破坏美感,我尤其喜欢强类型,所以一直很难接受SPListItem用字符串作为键值去获取Field值的方式,更别提这些值都是Object类型,还得再经过一次转换。

所以我比较喜欢将SPListItem转换成实体类来使用,只不过一直以来的做法都是自己写实体类和转换方法。而Linq to SharePoint则可以自动将列表映射为实体类,并且可以使用Linq语句来进行查询,看上去很美!

那么Linq to SharePoint能不能帮我彻底摆脱CAML呢,趁着重构代码的机会研究了一下,在这里简单总结一下。

前面说过Linq to SharePoint可以自动生成列表的实体类,这是通过一个叫做SPMetal的工具来实现的,具体的用法请查阅这里

SPMetal会根据实际的列名来生成实体类中的属性名,所以如果你的列名是中文的话(譬如你安装了中文版SharePoint),你会得到一份非常诡异且冗长的代码文件。

当然,如果你能接受中英文混排的代码的话,这倒也不是什么问题。

SPMetal生成的属性大多是下面这个样子的:

[ColumnAttribute(Name = "Body", Storage = "_body", FieldType = "Note")]
public string Body
{
  get
  {
    return this._body;
  }
  set
  {
    if ((value != this._body))
    {
       this.OnPropertyChanging("Body", this._body);
       this._body = value;
       this.OnPropertyChanged("Body");
    }
  }
}
protected string _body;

Body属性被附加了一个ColumnAttribute,它的作用是将属性和SharePoint中的某一列关联起来。在它的命名参数中,Name表示的就是SharePoint中的列名,FieldType指列的类型,Storage表示的是实体类中用来存放列值的变量,可以看到这里为它指定的是一个变量,而不是Body属性,也就是说,在初始化这一实体的时候,该列的值会直接赋给_body变量,而不经过Body属性。那么Body属性的set访问器又是用来干什么的呢?实际上它的作用只是为了提供一种更改列值的机制,这一点从它复杂的内部流程也能看出端倪。

如果你只是为了查询方便,并不需要修改和提交数据的话,完全可以使用下面的只读版本:

[Column(Name = "Body", Storage = "_body", FieldType = "Note")]
public string Body
{
  get
  {
    return this._body;
  }
}
protected string _body;

此外,如果列表的包含一些设置为可空值的列的话,它们会被映射成一个Nullable<T>类型,如下所示:

[ColumnAttribute(Name="RatingCount", Storage="_ratingCount", FieldType="Number")]
public System.Nullable<double> RatingCount{
  get {
    return this._ratingCount;
  }
  set {
    if ((value != this._ratingCount)) {
      this.OnPropertyChanging("RatingCount", this._ratingCount);
      this._ratingCount= value;
      this.OnPropertyChanged("RatingCount");
    }
  }
}
private System.Nullable<double> _ratingCount;

虽然可以理解这么做的原因,但是却很难接受这种代码。尤其是在HTML中做绑定时,你不得不针对Nullable属性额外写一些代码来处理它的非空情况。

好在我们可以将属性本身改成非空的类型,然后在get访问器里根据情况返回真实的值或者默认值:

[Column(Name="RatingCount", Storage="_ratingCount", FieldType="Counter")]
public double RatingCount{
  get {
    return this._ratingCount ?? 0;
  }
}
private double? _ratingCount;

但要注意Storage指向的变量还得是Nullable类型,以保存列的真实的值(包括空值);属性的类型虽然可以改为非空类型,但要注意类型一定要和对应的变量相同,因为ColumnAttribute会在初始化时检查属性的实际类型。我曾尝试写过下面这样的属性,结果只收获了一个异常:

[Column(Name="RatingCount", Storage="_ratingCount", FieldType="Counter")]
public int RatingCount{
  get {
    return this._id ? (int)this._id.Value : 0;
  }
}
private double? _ratingCount;

为什么想要这么做呢?因为我实在想不出投票总数为什么会是一个小数?

如果你刚巧需要使用Linq语句查询列表,而且查询条件刚巧也是一个包含可空值的列的话,就不能用上面提到的方法来修改属性的类型了,否则Linq to SharePoint将无法生成CAML,结果也是以异常告终。

如果想让可空值列的映射属性既能在Linq语句里作为条件,又能让调用者方便使用的话,只能像下面这样定义它们,对,是它们:

[ColumnAttribute(Name = "RatingCount", Storage = "_ratingCount", FieldType = "Number")]
public double? RatingCountField
{
  get { return _ratingCount; }
}
protected double? _ratingCount;
public int RatingCount { get { return this._ratingCount.HasValue ? (int)this._ratingCount.Value : 0; } }

平常使用int类型的RatingCount,在Linq语句里查询时使用double?类型的RatingCountField。

坦白说,我很讨厌这样的代码,两个含义相同的属性必然会让其他阅读者感到困惑。

此外,如果列是一个查阅项(譬如Author列),我们可以做到映射这个查阅项的完整字符串(譬如“12;#windstyle\chai”),或者查阅项的ID(譬如“12”),或者查阅项的值(譬如“windstyle\chai”),所做的仅仅是在ColumnAttribute里指定IsLookupId或IsLookupValue(如果要拿到完整字符串,则什么都别指定):

[Column(Name = "Author", Storage = "_authorId", FieldType = "Text", IsLookupId = true)]
public int AuthorId
{
  get { return _authorId; }
}
protected int _authorId;

而且如果指定了IsLookupId,就可以在Linq语句中使用这一属性来做查询了。

以上提到的都是关于列与属性的映射,然而有一些列很难通过简单的映射变成属性,那就需要另外一种机制:自定义映射。

自定义映射需要实体类实现ICustomMapping接口,并实现它的三个成员方法MapFrom、MapTo和Resolve,我们这里只讨论只读实体类的情况,只需实现MapFrom即可:

[CustomMapping(Columns = new string[] { Attachments" })]
public override void MapFrom(object listItem)
{
  var item = listItem as Microsoft.SharePoint.SPListItem;
  this.IsRootPost = item["IsRootPost"].ToString();
  if (this.IsRootPost == "1")
    this.Url = item.Web.Url + "/" + item.Folder.Url;
  else
    this.Url = new Uri(new Uri(item.Web.Url), item.ParentList.DefaultDisplayFormUrl + "?id=" + item.ID).ToString();
}

MapFrom方法包含一个listItem参数,可以通过它来拿到SPListItem的列值,具体能拿到哪些列,需要在修饰MapFrom的CustomMappingAttribute中指定。

在使用Lambda表达式进行查询时,CustomMappingAttribute中指定的列名以及之前属性映射时指定的列名都会成为ViewFields的一员。

但需要注意的是,如果你在Linq语句中使用了通过MapFrom映射而来的属性,那么它将不会出现在CAML的Query语句中,Linq to SharePoint采取的方法是把所有SPlistItem都获取并转换成实体类,然后通过Linq to Objects来进行第二次查询(而普通的映射属性则不存在这个问题)。

这当然是极大的性能隐患,然而在Linq to SharePoint中,类似的性能隐患还不止这一处,而且稍不注意就会中招。

譬如根据ID来查找某一Item,我们通常会写出这样的代码:

var item = list.First(i => i.Id == root.ID);

或者

var item = list.Where(i => i.Id == root.ID).First();

或者

var item = list.Where(i => i.Id == root.ID).Single();

这三行代码看起来没有任何问题,而且最终也会被翻译成一模一样的CAML(我省去了ViewFields):

<View>
  <Query>
    <Where>
      <Eq>
        <FieldRef Name="ID" />
        <Value Type="Counter">16</Value>
      </Eq>
    </Where>
  </Query>
  <RowLimit Paged="TRUE">2147483647</RowLimit>
</View>

注意RowLimit,它的值居然是2147483647,这表示查询会返回列表中的所有条目,并将它们都转换成实体类,然后再使用Linq to Objects来进行查询。

MSDN的这篇文档中的“Additional Performance Considerations”一节虽然明确了哪些方法会导致这种行为,但First和Single这两个方法居然都被标记为Efficient。

那么正确的获取单个条目的查询表达式该怎么写呢?使用Take方法,只有Take方法才会正确的翻译成RowLimit:

this.Root = list.Where(i => i.Id == root.ID).Take(1).Single();

它会被翻译成(同样省去了ViewFields):

<View>
  <Query>
    <Where>
      <Eq>
        <FieldRef Name="ID" />
        <Value Type="Counter">16</Value>
      </Eq>
    </Where>
  </Query>
  <RowLimit Paged="TRUE">1</RowLimit>
</View>

同样的试验还可以参考这里

此外,我们知道Linq可以使用Take和Skip方法来进行数据分页,但在Linq to SharePoint中,Skip方法并不会翻译成CAML的分页语句,它还是会拿到所有条目。

写到这里,基本上已经把我所遇到的所有难以接受的部分都介绍完了,其实前面几条,若不是有严重的代码洁癖的话,也可以不必在意。

最后提到的性能问题才是关键所在,而且你很难通过阅读代码来发现问题所在,微软的官方文档对性能问题的解释也模棱两可,一会儿说性能很棒,一会儿又说可能会导致极差的性能。所以最好还是检查一下每一条查询语句生成的CAML是否有问题(DataContext的Log属性会输出所有翻译后的CAML)。

现在你应该明白标题的含义了。