使用 SharePoint 客户端对象模型高效编程





5.00/5 (19投票s)
如何在您的 SharePoint 应用程序中充分利用客户端对象模型
引言
由于 SharePoint 2013 中已弃用沙盒化解决方案,并且 Microsoft 正在推广 SharePoint Online 和应用模型,因此许多开发人员发现他们必须从服务器端对象模型迁移到**客户端对象模型 (CSOM)**。随之而来的是一系列挑战:客户端对象模型缺少许多功能,许多曾经简单的任务现在变得困难。
可以预见,随着时间的推移,客户端对象模型将继续成熟,这些问题也会得到改善。然而,有一点不会改变,那就是编写高效代码的要求:毕竟,我们现在是在通过网络进行通信——我们的代码通常在另一台机器上执行,而我们曾经直接在 SharePoint 服务器上执行。如果您的经验与我相似,您很快就会花时间改进应用的延迟和其他性能问题。
在本文中,我将概述一些技术,以确保您的代码尽可能快速地运行。我的示例使用托管 CSOM 编写,但许多(如果不是全部)概念也可以应用于 JavaScript 版本。如果您对此感兴趣,请在评论中告诉我,我可能会写一篇新的 JavaScript 版本。
分析代码性能
如果您遇到性能问题,那么在开始对所有可见内容进行微观优化之前,您需要进行一些测量。请使用 Stopwatch
类来执行此操作。
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
s.Start();
//Your CSOM code
s.Stop();
//Take a look at s.Elapsed in the debugger
记下您应用改进前后的性能,并利用这些信息来证明您付出的努力是值得的。
我们将从最简单、最明显的方法开始,然后逐渐增加复杂性。
仅请求您想要的内容(但一次性请求所有您想要的内容!)
您必须明确请求您想要的每个对象的每个属性。这实际上是 CSOM 编程的一个基本常识——毕竟,它被设计为跨网络使用。如果您只需要用户的 Title
和 LoginName
,那么只请求这些
var spUser = clientContext.Web.CurrentUser;
clientContext.Load(spUser, user => user.Title, user => user.LoginName);
但是,如果您稍后在代码中需要向用户发送电子邮件,请在之前的请求中添加他们的电子邮件地址。不要两次返回到服务器!与稍后往返服务器一次的成本相比,总是请求一个额外属性的成本微不足道。
谨慎调用 ExecuteQuery
这又是一个显而易见的问题。但是,有些情况下您会调用 ExecuteQuery
,但实际上并不需要。如果您不知道,ExecuteQuery
是导致您的所有请求以单个批次发送到服务器的方法,所以它很慢!
看看这种情况。如果您正在创建一个列表,您可能会认为需要编写如下代码
List list = web.Lists.Add(...);
ctx.ExecuteQuery(); //Create the list
ctx.Load(list, l => l.DefaultViewUrl); // Request the new list's URL
ctx.ExecuteQuery(); // Get the new list's DefaultViewUrl
实际上,您不需要第一个 ExecuteQuery
。这并不直观,但您可以创建列表,获取其 URL,然后一次性提交这两个请求。
List list = web.Lists.Add(...);
ctx.Load(list, l => l.DefaultViewUrl);
ctx.ExecuteQuery(); // Get the new list's DefaultViewUrl
一个稍微复杂一点的例子涉及您正在间接调用一些 CSOM 代码的情况,但它位于接口之后,并且您可能多次调用它。如何防止每次调用此接口方法时都调用 ExecuteQuery
?例如
public interface IData { }
public class MyDataClass : IData { }
public interface IDataRetriever
{
IData GetData(string id);
}
public class SPDataRetriever : IDataRetriever
{
public IData GetData(string id)
{
//Make whatever CSOM requests you need
ListItem li = _list.GetItemById(id);
ctx.ExecuteQuery();
return new MyDataClass(li);
}
}
在我们的场景中,它的使用方式如下
data = ids.Select(id => dataRetriever.GetData(id));
显然,这是非常低效的,因为 ExecuteQuery
会对可枚举的每个项都进行调用。让我们重构代码以删除 ExecuteQuery
调用
public interface IDataRetriever
{
void RequestData(string id);
IEnumerable<IData> GetAvailableData();
}
public class SPDataRetriever : IDataRetriever
{
private Queue<ListItem> _queue = new Queue<ListItem>();
public void RequestData(string id)
{
//Make whatever CSOM requests you need
ListItem li = _list.GetItemById(id);
_queue.Enqueue(li);
}
public IEnumerable<IData> GetAvailableData()
{
var result = _queue.Select(li => new MyDataClass(li)).ToArray();
_queue.Clear();
return result;
}
}
您可以看到它现在被分成了两个方法:RequestData
,它“排队”等待发送到服务器的请求;以及 GetAvailableData
,它在假定 ExecuteQuery
已被调用后返回数据。
我们的使用方式如下
IDataRetriever dataRetriever1 = new SPDataRetriever(ctx);
IDataRetriever dataRetriever2 = new SPDataRetriever(ctx);
foreach (string id in new[] { "id1", "id2" })
dataRetriever1.RequestData(id);
foreach (string id in new[] { "id1", "id2" })
dataRetriever2.RequestData(id);
ctx.ExecuteQuery(); //Single call to execute query
IEnumerable<IData> allData = dataRetriever1.GetAvailableData().Concat(dataRetriever2.GetAvailableData());
这是通过最小化调用 ExecuteQuery
的次数来帮助防止性能问题的一种创意方式的示例。
在会话中缓存数据
如果您的应用中的每个页面都请求相同的 SharePoint 数据,那么您可以将其临时存储在用户会话缓存中。这将使您不必在每次页面请求时都往返 SharePoint 服务器。此外,由于它在用户会话缓存中,因此它针对每个用户单独作用域。如果您想缓存应用程序范围的数据,可以将其存储在应用程序缓存中。有关更多信息,请参阅此 MSDN 文章。
所以,让我们假设一个场景,其中每个页面都根据用户是否为站点管理员来检查用户是否有权访问它
public bool CheckPrivileges()
{
var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
using (var clientContext = spContext.CreateUserClientContextForSPHost())
{
var currentUser = clientContext.Web.CurrentUser;
clientContext.Load(currentUser, u => u.IsSiteAdmin);
clientContext.ExecuteQuery();
return currentUser.IsSiteAdmin;
}
}
我们可以简单地将 CheckPrivileges
方法包装在一个执行会话缓存的方法中
public bool CheckPrivilegesWithSessionCaching(HttpContextBase httpContext)
{
string key = "IsSiteAdmin";
var keys = httpContext.Session.Keys.Cast<string>().ToList();
if(keys.Contains(key))
{
return (bool)httpContext.Session[key];
}
else
{
bool result = CheckPrivileges(httpContext);
httpContext.Session[key] = result;
return result;
}
}
请注意,如果您存储大量数据,此解决方案的扩展性不会很好(因为它是一种“内存中”缓存),您可以将缓存的数据存储在数据库中。
此外,您不能假设数据将在会话缓存中可用——ASP.NET 随时可能清除它,或者由于负载均衡,它可能在不同的服务器上有不同的缓存。只要您在必要时返回 SharePoint 检索数据,这就不应该成为问题。
使用 CAML 查询过滤列表项的检索
如果您正在从列表中检索一些项,很容易检索所有项然后从代码中过滤它们。然而,您可以使用 CAML 查询在服务器端执行过滤。它可能有点笨拙(以 XML 编码),但对于潜在的速度提升来说是值得的,尤其是对于大型列表。
例如,这是获取列表项的懒惰方法
CamlQuery query = CamlQuery.CreateAllItemsQuery();
var items = list.GetItems(query);
这是一个带有 where
子句的格式化 CAML 查询
CamlQuery query = new CamlQuery()
{
ViewXml = string.Format("<View><Query><Where><Eq><FieldRef Name='{0}' /><Value Type='String'>{1}</Value></Eq></Where></Query></View>",
"FirstName", "Eric")
};
var items = list.GetItems(query);
注意 'View
' 外层标签,当使用 CSOM 进行查询时是必需的,这与服务器对象模型版本不同。
这里还有另一个技巧——通过指定 RecursiveAll
,您实际上可以在单个查询中获取文档库中的所有文件夹、子文件夹和/或文件。
CamlQuery allFoldersCamlQuery = new CamlQuery()
{
ViewXml = "<View Scope='RecursiveAll'>"
+ "<Query>"
+ "<Where>"
+ "<Eq><FieldRef Name='FSObjType' /><Value Type='Integer'>1</Value></Eq>"
+ "</Where>"
+ "</Query>"
+ "</View>"
};
在上面的查询中,Scope
设置为 RecursiveAll
。此外,我设置了字段 FSObjType=1
——这意味着只返回文件夹。如果您只想获取项,请设置 FSObjType=0
。如果您想要文件和文件夹,则完全省略它。
实际上,您可以更进一步——通过枚举列表并对每个列表使用 CAML 查询,检索多个列表中的所有项。重要的是,您只在最后调用一次 ExecuteQuery
。
高级:并行和异步代码
如果您使用的是 JavaScript 或 Silverlight 或 Windows Phone 中的 CSOM 库,您会发现您可以访问 ExecuteQueryAsync
。不幸的是,对于我们 .NET 用户来说,只有一个 ExecuteQuery
方法——同步的。我不知道为什么。
更新:SharePoint Online CSOM 程序集现在支持 ExecuteQueryAsync。
如果您想同时发出数据库请求、其他 Web 请求、获取用户输入或其他操作,同时发出 CSOM 请求,该怎么办?拥有那个 ExecuteQueryAsync
会很方便,对吧?让我们看看我们能否创建一个
public static class CSOMExtensions
{
public static Task ExecuteQueryAsync(this ClientContext clientContext)
{
return Task.Factory.StartNew(() =>
{
clientContext.ExecuteQuery();
});
}
}
这会启动一个新线程,然后该线程本身也会被阻塞。这对于避免阻塞 UI 线程来说是可以接受的,例如。
请注意,此代码可能需要针对您的具体情况进行一些优化。例如,使用 Task.Factory.StartNew
并不总是创建一个新线程。如果您大量并发使用这些,您可能希望避免使用线程池。您可以 在此阅读有关 Task 的更多信息。
更新:对于真正的异步版本,请尝试此操作
https://gist.github.com/johnnycardy/9e1671cf5087dcd8f4e7892fc3c2cfb8
现在我们可以做超级酷的并行和异步操作了!看看这个
public async Task<ActionResult> Index()
{
var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
using (var clientContext = spContext.CreateUserClientContextForSPHost())
{
if (clientContext != null)
{
var currentUser = clientContext.Web.CurrentUser;
clientContext.Load(currentUser, u => u.Title);
Task t1 = clientContext.ExecuteQueryAsync();
clientContext.Load(currentUser, u => u.Email);
Task t2 = clientContext.ExecuteQueryAsync();
await t1.ContinueWith((t) =>
{
ViewBag.UserName = currentUser.Title;
});
await t2.ContinueWith((t) =>
{
ViewBag.Email = currentUser.Email;
});
}
}
return View();
}
好的,这是一个牵强且无意义的例子,因为如果您注意听,您会知道我们应该只调用一次 ExecuteQuery
!但让我们一步步来看
- 首先,方法签名已更改。这个控制器方法现在是异步的,并且返回一个 Task。这意味着我们现在可以在方法中使用
await
关键字。 - 在正文中,我们正在加载
Title
和Email
。我们正在调用ExecuteQueryAsync
,它会启动(并返回)新的Task
对象。 - 我们对
Task
对象调用ContinueWith
,以便在它完成时运行代码——即使用 CSOM 代码请求的属性。 - 我们使用
await
关键字来表示代码是异步的,并且 Index 控制器方法应该依赖于此代码的结果。
请看下一个例子,这是一个名为 List
的控制器方法。它更合理:我们同时检索数据库数据和 SharePoint 列表项,并将它们合并到 ViewModel
对象中返回给客户端。
public async Task<ActionResult> List()
{
List<ViewModel> result = new List<ViewModel>();
var spContext = SharePointContextProvider.Current.GetSharePointContext(HttpContext);
using (var clientContext = spContext.CreateUserClientContextForSPHost())
{
//Form the query for ListItems
var listItems = clientContext.Web.Lists.GetByTitle(listTitle).GetItems(camlQuery);
//Send the queries
var clientTask = DB.Clients.ToListAsync();
var spTask = clientContext.ExecuteQueryAsync();
//Wait for both to complete
await Task.WhenAll(clientTask, spTask);
result = clientTask.Result.Select(c => new ViewModel(listItems)).ToList();
}
return View(result);
}
在上面的代码中,我们只有一个阻塞调用而不是两个,并且我们可能将该方法的速度加倍。
好了,我目前就这些了。显然,客户端对象模型很容易在性能方面被滥用,而且很容易突然意识到性能是一个问题。希望本文能为您保持应用快速响应提供一些想法和灵感。如果您有任何关于 CSOM 的建议或性能技巧,请在评论中告诉我!