适用于SharePoint的上下文框架(ExContext)

在进行SharePoint开发时,我们经常会用到SPContext这个对象,它可以帮助我们十分方便地获取当前请求相关的所有信息,能在一定程度上提升代码的健壮性和性能。但它也不是万金油,它没有(也不可能)提供我们需要的所有信息(尤其是那些与业务相关的信息),而且当项目复杂到一定程度时使用SPContext会带来一些新的问题(比如在Timer Job里是没有SPContext的)。

本文会介绍一种适用于SharePoint的上下文框架(ExContext),它既保留了SPContext的优点,又能弥补SPContext的一些不足,并且经过一个非常复杂的SharePoint项目的实际验证,对性能提升、代码可读性和编码效率方面都有明显的帮助。

首先来明确一下我们所期望的上下文需要满足的要求:

  • 能够完全代替SPContext:我们需要从SPContext中获取的所有资源都可以被添加到该上下文中;
  • 适用于任何场景:无论是Web场景(HTTP请求)还是非Web场景(Timer Job、Console以及Windows Form程序)都能使用该上下文对象,并且不存在互相干扰的问题;
  • 方便获取:只需要一条语句就能获取到该上下文对象;
  • 特定于HTTP请求:每一个HTTP请求都有且只有一个上下文对象,该对象随HTTP请求而生,随HTTP请求而亡(之所以这样做因为跨HTTP请求缓存SPRequest会引发一些问题,而且也不容易控制);
  • 按需提供属性:该上下文对象中提供的属性应当能按需生成,籍此来避免不必要的性能开销;
  • 特定于用户:在Web场景中,HTTP请求和上下文是一一对应的,而每个HTTP请求实际上也只有一个发起用户,所以也可以认为每个上下文对应于一个用户(可以体现在SPWeb.CurrentUser上)。当然,上下文提供的资源也会有一些是可以服务于任何用户的;
  • 支持更换用户:在非Web场景中,经常需要进行一些批处理操作,此时就有可能需要模拟不同的用户进行操作,如果为每个用户都去构造新的上下文显然不太实际,又与上下文的初衷相违背,所以需要该上下文对象能提供更换用户的功能,以便消费方能够及时刷新上下文中与特定于用户的那些属性。

看完以上的要求,可能您会感到疑惑:为什么需要完全代替SPContext?这是因为当代码复杂到一定程度时,我们势必会对代码进行某种规则的组织和分层以最大程度地实现复用。暂且先将这些可能会被复用的代码称作“业务代码”,如果在业务代码中使用了SPContext,那么如果想要在Timer Job等非Web场景中使用该业务代码时,就会遇到问题,因为SPContext是特定于HTTP请求的,在非Web场景中是不存在SPContext的。

那么我们就需要将业务代码与SPContext解耦,可以采取的方法之一就是以参数的形式将所需的资源传给业务代码。这样业务代码便不再依赖于SPContext,从而可以运行在任何场景中。

这也引出了相对于业务代码的另一个概念:“入口代码”。入口代码是指位于所有请求或操作起点处的代码。在Web场景里,它可能是UserControl的Page_Load事件的第一行代码;在非Web场景中,它可能是Timer Job的Excute事件的第一行代码。

在ExContext框架中,入口代码的作用是获取ExContext实例。ExContext就绪之后,就可以将它传递给业务代码,业务代码再基于上下文中提供的资源来完成相应的操作。

获取ExContext的方法很简单,根据使用场景不同使用静态方法Get()的不同重载即可。

在Web场景中可以使用无参数的重载:

var context = ExContext.Get();

在非Web场景中则需要传递网站的URL:

var context = ExContext.Get(url);

之所以需要通过静态方法来获取,而不是像SPContext那样使用静态单例属性Current,是为了照顾非Web场景。因为在非Web场景中,消费方没有明确唯一的标识符,很难区分两次调用是否来自同一消费方的同一次调用,也很难确定何时释放资源,所以最简单可靠的方式就是每次调用Get方法时都返回一个全新的上下文对象,将确保该对象唯一以及合适释放的职责转交给消费方。

下面来看看Get方法的实现逻辑:

static readonly string CONTEXT_KEY = "ExContext";

///
/// If consumer is a non-web application, url is required.
///
///the url of root site
public static ExContext Get(string url = null)
{
if (SPContext.Current != null && HttpContext.Current != null)
{
if (HttpContext.Current.Items.Contains(CONTEXT_KEY))
return HttpContext.Current.Items[CONTEXT_KEY] as ExContext;
else
{
var context = new ExContext(url ?? SPContext.Current.Site.Url);
HttpContext.Current.Items[CONTEXT_KEY] = context;
return context;
}
}
else if (string.IsNullOrEmpty(url))
throw new ArgumentException("Cannot get SPContext, please specify the url argument", "url");
else
return new ExContext(url);
}

ExContext(string url)
{
this._url = new Uri(url).GetLeftPart(UriPartial.Authority);
}

Get方法会判断SPContext和HttpContext是否存在(既是否处于Web场景),如果存在的话,就检查HttpContext上下文中是否已经缓存了ExContext,没有的话就使用传入的URL或者SPContext.Current.Site.URL来构造一个新的ExContext缓存到HttpContext中,然后返回。

如果消费方是非Web应用程序,那么就使用传入的URL来构造ExContext并返回。

ExContext的构造函数是私有的,这意味着调用方不能直接实例化它,而只能通过Get方法来获取它的实例。这样做又是为了照顾Web场景,在Web场景的同一个HTTP请求中,消费方所获取到的ExContext应当总是一样的(因为它只会被构造一次,一旦构造出来,就会被保存到HttpContext上下文中)。此外,该构造函数的逻辑也很简单,它仅仅是将传入的URL保存到一个私有变量里,接下来ExContext将提供的许多属性就能够依靠这个URL来按需构造了。

需要注意的是ExContext可能会提供非常多的属性,而且每个消费方所需要的属性也不尽相同。如果在构造ExContext时就去初始化所有属性,那么它将会变成一场灾难。这些属性应当能做到按需提供,也就是说,只有在第一次访问时才被构造出来。

下面是一个简单属性的示例:

public SPSite Site
{
get
{
if (this._site == null)
{
this._site = new SPSite(this._url);
this._disposables.Add(this._site);
}

return this._site;
}
}

SPSite _site;
readonly List<IDisposable> _disposables = new List<IDisposable>();

ExContext包含一个私有的用来存放IDisposable对象的集合,当ExContext被释放的时候,其内部构造出来的所有资源也应当一并被释放,为此,ExContext也需要实现IDisposable接口:

public partial class ExContext : IDisposable
{
public void Dispose()
{
foreach (var obj in this._disposables)
{
try
{
obj.Dispose();
}
catch { }
}
}
}

前边提到过,ExContext中会有一部分属性特定于当前用户,但为了执行一些批处理操作,我们需要能更改ExContext的当前用户,这个功能的实现思路是提供一个Refresh(string loginName)方法,该方法用来重置私有变量_user_loginName、释放跟用户相关的所有资源并将相关私有变量设置为null,接下来针对这些属性的调用就会根据_user_loginName重新进行构造了:

string _user_LoginName;

public void Refresh(string loginName = null)
{
if (loginName != null)
this._user_LoginName = loginName;
this.Dispose();
this.OnRefreshing();
}

///
/// Dispose and reset user related objects
///
private void OnRefreshing()
{
this._user_RegionalSettings = null;
this._user_Profile = null;
//…
}

既然ExContext本身是一个IDisposable对象,那么它应当如何被释放呢?在非Web场景中,释放的职责属于消费方,也就是说,消费方应当自行释放通过Get方法获取的ExContext对象,一个不错的方式是使用using语句块:

using(var context = ExContext.Get(url))
{
//…
}

而在Web场景中,由于同一次HTTP请求中可能会存在多个消费方(比如页面上的每一个Web Part都算一个独立的调用方),所以调用方不应当擅自释放ExContext,否则就可能会对其他调用方造成影响。Web场景中对ExContext的释放操作应当在HttpApplication的EndRequest事件中进行,可以通过HttpModule来捕获此事件:

public class ExHttpModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.EndRequest += context_EndRequest;
}

void context_EndRequest(object sender, EventArgs e)
{
Business.ExContext.Clear();
}
}

Clear方法的实现如下:

///
/// DO NOT call this method manually, it will be called in the EndRequest event of HttpModule
///
public static void Clear()
{
try
{
if (HttpContext.Current != null
&& HttpContext.Current.Items.Contains(CONTEXT_KEY))
{
(HttpContext.Current.Items[CONTEXT_KEY] as ExContext).Dispose();
HttpContext.Current.Items.Remove(CONTEXT_KEY);
}
}
catch { }
}

以上就是ExContext的大体实现思路,我们已经知道ExContext必须在入口代码处获取,那么接下来业务代码该如何消费它呢?如果想要方便的消费ExContext,我们必须将ExContext通过某种方式传递给业务代码,我采取的方法是通过构造函数来传递ExContext:

///
/// This is the base class of any class which provide functionalities and using context.
///
public abstract class ContextProsumer: IDisposable
{
protected ExContext Context { get; private set; }

public ContextProsumer(ExContext context)
{
this.Context = context;
}

public virtual void Dispose()
{
}
}

所有业务代码类都需要从这个ContextProsumer基类派生而来,Context Prosumer表示它既是Context的消费方,又是一个功能的提供方。ContextProsumer的子类可以随时通过this.Context来访问ExContext及其提供的所有资源。

所有这一切就绪后,就可以非常方便地使用ExContext带来的好处了,假设我们有一个业务类名为ActivityRecordManager,用来记录用户的点击行为,就可以使用如下的代码来调用它:

var context = Business.ExContext.Get();
var recordManager =  new ActivityRecordManager(context);
recordManager.RecordClick(HttpContext.Current.Session.SessionID,
context.User_LoginName,
url,
DateTime.UtcNow);

如果ActivityRecordManager使用范围很广的话,我们就可以将其以属性的形式加入到ExContext中来简化调用行为:

var context = Business.ExContext.Get();
context.ActivityRecordManager.RecordClick(HttpContext.Current.Session.SessionID,
context.User_LoginName,
url,
DateTime.UtcNow);

以上就是ExContext的大体实现思路,在项目中引入ExContext之后,我们切身体会到的好处有:

  1. 能够控制整个请求中需要构造的资源数量,无论页面上有多少个Web Part,它们获取的ExContext总是同一个,使用的资源也不需要重复构造,从而达到提升性能的目的;
  2. 虽然我们也会对项目进行组织和分层,但是总会“不小心”在展现层编写一些实际上属于业务层的代码,使得代码变得难以阅读和维护。而ExContext是和业务紧密相关的,使用ExContext时会自然而然的对代码进行重组和分层,展现层(或Timer Job)的代码会变得十分精简,通常只包含获取ExContext、调用业务方法和控件值绑定这几个操作,业务功能全部交由业务层处理。
  3. 因为业务代码不再遍布各处,并且有了比较一致的使用方式,无论Web Part、Timer Job、还是用来执行一些临时任务的Console程序,都能使用同样的业务代码。在项目中引入单元测试就变得十分容易了,有了单元测试助力,代码的健壮性也有了保证。
  4. 我们实际的开发过程中,其实大部分工作都是在编写业务类,ExContext的目标就是提供业务类需要的各种资源,开发人员专注于业务代码的编写,而不用再担心如何构造和释放所需的资源,能显著提高开发效率。

ExContext并不是一个特别神奇的东西,它的实现其实并不复杂,本文也仅仅是简单的介绍了实现思路和手段(可能并不是十分完美)。每个项目都有其特殊性,ExContext上下文框架可能并不适合所有项目,也可能需要些许改造才能完美匹配你的项目。这些都不重要,真正重要的是在SharePoint项目趋于复杂时,如果我们不能有效的利用资源,不能合理的组织代码,项目就可能沦为泥淖,如果我们不想陷进去,最好还是想想办法。

我相信ExContext只是诸多方法其中的一种。

One Comment

  1. AaronHan

    赞下Windie Chai的设计和逻辑,在可能取多个SPSite和SPWeb的场景下,确实高效了不少

发表评论

电子邮件地址不会被公开。 必填项已用*标注