技术开发 频道

ASP.NET三大核心对象及基础功能解析

  HttpResponse

  我们处理HTTP请求的最终目的只有一个:向客户端返回结果。而所有需要向客户端返回的操作都要调用HttpResponse的方法。它提供的功能集中在操作HTTP响应部分,如:响应流,响应头。

  我把一些认为很重要的成员简单列举了一下:

// 获取网页的缓存策略(过期时间、保密性、变化子句)。
public HttpCachePolicy Cache { get; }

// 获取或设置输出流的 HTTP MIME 类型。默认值为“text/html”。
public string ContentType { get; set; }

// 获取响应 Cookie 集合。
public HttpCookieCollection Cookies { get; }

// 获取或设置一个包装筛选器对象,该对象用于在传输之前修改 HTTP 实体主体。
public Stream Filter { get; set; }

// 启用到输出 Http 内容主体的二进制输出。
public Stream OutputStream { get; }

// 获取或设置返回给客户端的输出的 HTTP 状态代码。默认值为 200 (OK)。
public int StatusCode { get; set; }

// 将 HTTP 头添加到输出流。
public void AppendHeader(string name, string value);

// 将当前所有缓冲的输出发送到客户端,停止该页的执行,并引发EndRequest事件。
public void End();

// 将客户端重定向到新的 URL。指定新的 URL 并指定当前页的执行是否应终止。
public void Redirect(string url, bool endResponse);

// 将指定的文件直接写入 HTTP 响应输出流,而不在内存中缓冲该文件。
public void TransmitFile(string filename);

// 将 System.Object 写入 HTTP 响应流。
public void Write(object obj);

   这些成员都有简单的解释,应该了解它们。

  这里请关注一下属性StatusCode。我们经常用JQuery来实现Ajax,比如:使用ajax()函数,虽然你可以设置error回调函数,但是,极有可能在服务端即使抛黄页了,也不会触发这个回调函数,除非是设置了dataType="json",这时在解析失败时,才会触发这个回调函数,如果是dataType="html",就算是黄页了,也能【正常显示】。

  怎么办?在服务端发生异常不能返回正确结果时,请设置StatusCode属性,比如:Response.StatusCode = 500;

  HttpContext

  终于轮到大人物出场了。

  应该可以这么说:有了HttpRequest, HttpResponse分别控制了输入输出,就应该没有更重要的东西了。但我们用的都是HttpRequest, HttpResponse的实例,它们在哪里创建的呢,哪里保存有它们最原始的引用呢?答案当然是:HttpContext 。没有老子哪有儿子,就这么个关系。更关键的是:这个老子还很牛,【在任何地方都能找到它】,而且我前面提到另二个实力不错的选手(HttpServerUtility和Cache),也都是它的手下。因此,任何事情,找到它就算是有办法了。你说它是不是最牛。

  不仅如此,在Asp.net的世界,还有黑白二派。Module像个土匪,什么请求都要去“检查”一下,Handler更像白道上的人物,点名了只做某某事。有趣的是:HttpContext真像个大人物,黑白道的人物有时都要找它帮忙。帮什么忙呢?可怜的土匪没有仓库,它有东西没地方存放,只能存放在HttpContext那里,有时惹得Handler也盯上了它,去HttpContext去拿土匪的战利品。

  这位大人物的传奇故事大致就这样。我们再来从技术的角度来观察它的功能。

  虽然HttpContext也公开了一些属性和方法,但我认为最重要的还是上面提到的那些对象的引用。

  这里再补充二个上面没提到的实例属性:User, Items

  User属性保存于当前请求的用户身份信息。如果判断当前请求的用户是不是已经过身份认证,可以访问:Request.IsAuthenticated这个实例属性。

  前面我在故事中提到:“可怜的土匪没有仓库,它有东西没地方存放,只能存放在HttpContext那里”,其实这些东西就是保存在Items属性中。这是个字典,因此适合以Key/Value的方式来访问。如果希望在一次请求的过程中保存一些临时数据,那么,这个属性是最理想的存放容器了。它会在下次请求重新创建,因此,不同的请求之间,数据不会被共享。

  如果希望提供一些静态属性,并且,只希望与一次请求关联,那么建议借助HttpContext.Items的实例属性来实现。

  我曾经见过有人用ThreadStaticAttribute来实现这个功能,然后在Page.Init事件中去修改那个字段。

  哎,哥啊,MSDN上说:【用 ThreadStaticAttribute 标记的 static 字段不在线程之间共享。每个执行线程都有单独的字段实例,并且独立地设置及获取该字段的值。如果在不同的线程中访问该字段,则该字段将包含不同的值。】 注意了:一个线程可以执行多次请求过程,且Page.Init事件在Asp.net的管道中属于较中间的事件啊,要是请求不使用Page呢,您再想想吧。

  前面我提到HttpContext有种超能力:【在任何地方都能找到它】,是的,HttpContext有个静态属性Current,你说是不是【在任何地方都能找到它】。千万别小看这个属性,没有它,HttpContext根本牛不起来。

  也正是因为这个属性,在Asp.net的世界里,您可以在任何地方访问Request, Response, Server, Cache,还能在任何地方将一些与请求有关的临时数据保存起来,这绝对是个非常强大的功能。Module的在不同的事件阶段,以及与Handler的”沟通“有时就通过这个方式来完成。

  还记得我上篇博客【Session,有没有必要使用它?】 中提到的事情吗:每个页面使用Session的方式是使用Page指令来说明的,但Session是由SessionStateModule来实现的, SessionStateModule会处理所有的请求,所以,它不知道当前要请求的要如何使用Session,但是,HttpContext提供了一个属性Handler让它们之间有机会沟通,才能处理这个问题。

  这个例子反映了Module与Handler沟通的方式,我再来举个Module自身沟通的例子,就说UrlRoutingModule吧,它订阅了二个事件:

protected virtual void Init(HttpApplication application)
{
    application.PostResolveRequestCache
+= new EventHandler(this.OnApplicationPostResolveRequestCache);
    application.PostMapRequestHandler
+= new EventHandler(this.OnApplicationPostMapRequestHandler);
}

   在OnApplicationPostResolveRequestCache方法中,最终做了以下调用:

public virtual void PostResolveRequestCache(HttpContextBase context)
{
    
// ...............
    RequestData data2
= new RequestData {
        OriginalPath
= context.Request.Path,
        HttpHandler
= httpHandler
    };
    context.Items[_requestDataKey]
= data2;
    context.RewritePath(
"~/UrlRouting.axd");

}

   再来看看OnApplicationPostMapRequestHandler方法中,最终做了以下调用:

public virtual void PostMapRequestHandler(HttpContextBase context)
{
    RequestData data
= (RequestData)context.Items[_requestDataKey];
    
if( data != null ) {
        context.RewritePath(data.OriginalPath);
        context.Handler
= data.HttpHandler;
    }
}

   看到了吗,HttpContext.Items为Module在不同的事件中保存了临时数据,而且很方便。

  强大的背后也有麻烦事

  前面我们看到了HttpContext的强大,而且还提供HttpContext.Current这个静态属性。这样一来,的确是【在任何地方都能找到它】。想想我们能做什么?我们可以在任何一个类库中都可以访问QueryString, Form,够灵活吧。我们还可以在任何地方(比如BLL中)调用Response.Redirect()让请求重定向,是不是很强大?

  不过,有个很现实的问题摆在面前:到处访问这些对象会让代码很难测试。原因很简单:在测试时,这些对象没法正常工作,因为HttpRuntime很多幕后的事情还没做,没有运行它们的环境。是不是很扫兴?没办法,现在的测试水平很难驾驭这些功能强大的对象。

  很多人都说WebForms框架搞得代码没法测试,通常也是的确如此。

  我看到很多人在页面的CodeFile中写了一大堆的控件操作代码,还混有很多调用业务逻辑的代码,甚至在类库项目中还中访问QueryString, Cookie。再加上诸如ViewState, Session这类【有状态】的东西大量使用,这样的代码是很难测试。

  换个视角,看看MVC框架为什么说可测试性会好很多,理由很简单,你很少会需要使用HttpRequest, HttpRespons,从Controller开始,您需要的数据已经给您准备好了,直接用就可以了。但MVC框架并不能保证写的代码就一定能方便的测试,比如:您继续使用HttpContext.Current.XXXXX而不使用那些HttpXxxxxBase对象。

  一般说来,很多人会采用三层或者多层的方式来组织他们的项目代码。此时,如果您希望您的核心代码是可测试的,并且确实需要使用这些对象,那么应该尽量集中使用这些强大的对象,应该在最靠近UI层的地方去访问它们。可以把调用业务逻辑的代码再提取到一个单独的层中,比如就叫“服务层”吧,由服务层去调用下面的BLL(假设BLL的API的粒度较小),服务层由表示层调用,调用服务层的参数由表示层从HttpRequest中取得。需要操作Response对象时,比如:重定向这类操作,则应该在表示层中完成。

  记住:只有表示层才能访问前面提到的对象,而且要让表示层尽量简单,简单到不需要测试,真正需要测试的代码(与业务逻辑有关)放在表示层以下。如此设计,您的表示层将非常简单,以至于不用测试(MVC框架中的View也能包含代码,但也没法测试,是一样的道理)。甚至,服务层还可以单独部署。

  如果您的项目真的采用分层的设计,那么,就应该可以让界面与业务处理分离。比如您可以这样设计:

  ①表示层只处理输入输出的事情,它应该仅负责与用户的交互处理,建议这层代码简单到可以忽略测试。

  ②处理请求由UI层以下的逻辑层来完成,它负责请求的具体实现过程,它的方法参数来自于表示层。

  为了检验您的分层设计是否符合这个原则,有个很简单的方法:

  写个console小程序模拟UI层调用下层方法,能正常运行,就说明您的分层是正确的,否则,建议改进它们。

  换一种方式使用Asp.net框架

  前面我提到HttpRequest有个InputStream属性, HttpResponse有一个OutputStream属性,它们对应的是输入输出流。直接使用它们,我们可以非常简单地提供一些服务功能,比如:我希望直接使用JSON格式来请求和应答。如果采用这种方案来设计,我们只需要定义好输入输出的数据结构,并使用这们来传输数据就好了。当然了,也有其它的方法能实现,但它们不是本文的主题,我也比较喜欢这种简单又直观地方式来解决某些问题。

  2007年我做过一个短信的接口,人家就提供几个URL做为服务的地址,调用参数以及返回值就直接通过HTTP请求一起传递。

  2009年做过一个项目是调用Experian Precise ID服务(Java写的),那个服务也直接使用HTTP协议,数据格式采用XML,输出输入的数据结构由他们定义的自定义类型。

  2010年,我做过一个数据访问层服务,与C++的客户端通信,采用Asp.net加JSON数据格式的方式。

  基本上这三个项目都有一个共同点:直接使用HTTP协议,数据结构有着明确的定义格式,直接随HTTP一起传递。就这么简单,却非常有用,而且适用性很广,基本上什么语言都能很好地相互调用。

  下面我以一个简单的示例演示这二个属性的强大之处。

  在示例中,服务端要求数据的输入输出采用JSON格式,服务的功能是一个订单查询功能,输入输出的类型定义如下:

// 查询订单的输入参数
public sealed class QueryOrderCondition
{
    
public int? OrderId;
    
public int? CustomerId;
    
public DateTime StartDate;
    
public DateTime EndDate;
}

// 查询订单的输出参数类型
public sealed class Order
{
    
public int OrderID { get; set; }
    
public int CustomerID { get; set; }
    
public string CustomerName { get; set; }
    
public DateTime OrderDate { get; set; }
    
public double SumMoney { get; set; }
    
public string Comment { get; set; }
    
public bool Finished { get; set; }
    
public List<OrderDetail> Detail { get; set; }
}

public sealed class OrderDetail
{
    
public int OrderID { get; set; }
    
public int Quantity { get; set; }
    
public int ProductID { get; set; }
    
public string ProductName { get; set; }
    
public string Unit { get; set; }
    
public double UnitPrice { get; set; }
}

   服务端的实现:创建一个QueryOrderService.ashx,具体实现代码如下:

public class QueryOrderService : IHttpHandler
{
    
public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType
= "application/json";

        
string input = null;
        JavaScriptSerializer jss
= new JavaScriptSerializer();

        
using( StreamReader sr = new StreamReader(context.Request.InputStream) ) {
            
input = sr.ReadToEnd();
        }

        QueryOrderCondition query
= jss.Deserialize<QueryOrderCondition>(input);

        
// 模拟查询过程,这里就直接返回一个列表。        
        List
<Order> list = new List<Order>();
        
for( int i = 0; i < 10; i++ )
            list.Add(DataFactory.CreateRandomOrder());

        
string json = jss.Serialize(list);
        context.Response.Write(json);
    }

   代码很简单,经过了以下几个步骤

   从Request.InputStream中读取客户端发送过来的JSON字符串,

  反序列化成需要的输入参数,

   执行查询订单的操作,生成结果数据,

  将结果做JSON序列化,转成字符串,

   写入到响应流。

  很简单吧,我可以把它看作是一个服务吧,但它没有其它服务框架的种种约束,而且相当灵活,比如我可以让服务采用GZIP的方式来压缩传输数据:

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType
= "application/json";

    
string input = null;
    JavaScriptSerializer jss
= new JavaScriptSerializer();

    
using( GZipStream gzip = new GZipStream(context.Request.InputStream, CompressionMode.Decompress) ) {
        
using( StreamReader sr = new StreamReader(gzip) ) {
            
input = sr.ReadToEnd();
        }
    }

    QueryOrderCondition query
= jss.Deserialize<QueryOrderCondition>(input);

    
// 模拟查询过程,这里就直接返回一个列表。        
    List
<Order> list = new List<Order>();
    
for( int i = 0; i < 10; i++ )
        list.Add(DataFactory.CreateRandomOrder());

    
string json = jss.Serialize(list);

    
using( GZipStream gzip = new GZipStream(context.Response.OutputStream, CompressionMode.Compress) ) {
        
using( StreamWriter sw = new StreamWriter(gzip) ) {
            context.Response.AppendHeader(
"Content-Encoding", "gzip");
            sw.Write(json);
        }
    }
}

   修改也很直观,在输入输出的地方,加上Gzip的操作就可以了。

  如果您想加密传输内容,也可以在读写之间做相应的处理,或者,想换个序列化方式,也简单,我想您应该懂的。

  总之,如何读写数据,全由您来决定。喜欢怎样处理就怎样处理,这就是自由。

  不仅如此,我还可以让服务端判断客户端是否要求使用GZIP方式来传输数据,如果客户端要求使用GZIP压缩,服务就自动适应,最后把结果也做GZIP压缩处理,是不是更酷?

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType
= "application/json";

    
string input = null;
    JavaScriptSerializer jss
= new JavaScriptSerializer();

    bool enableGzip
= (context.Request.Headers["Content-Encoding"] == "gzip");
    
if( enableGzip )
        context.Request.Filter
= new GZipStream(context.Request.Filter, CompressionMode.Decompress);

    
using( StreamReader sr = new StreamReader(context.Request.InputStream) ) {
        
input = sr.ReadToEnd();
    }

    QueryOrderCondition query
= jss.Deserialize<QueryOrderCondition>(input);

    
// 模拟查询过程,这里就直接返回一个列表。        
    List
<Order> list = new List<Order>();
    
for( int i = 0; i < 10; i++ )
        list.Add(DataFactory.CreateRandomOrder());

    
string json = jss.Serialize(list);

    
if( enableGzip ) {
        context.Response.Filter
= new GZipStream(context.Response.Filter, CompressionMode.Compress);
        context.Response.AppendHeader(
"Content-Encoding", "gzip");
    }

    context.Response.Write(json);
}

   注意:这次我为了不想写二套代码,使用了Request.Filter属性。前面我就说过这是个功能强大的属性。这个属性实现的效果就是装饰器模式,因此您可以继续对输入输出流进行【装饰】,但是要保证输入和输出的装饰顺序要相反。所以使用多次装饰后,会把事情搞复杂,因此,建议需要多次装饰时,做个封装可能会好些。不过,这个属性的更强大之处或许在这里体现的并不明显,要谈它的强大之处已不是本文的主题,我以后再说。

  想想:我这几行代码与此服务完全没有关系,而且照这种做法,每个服务都要写一遍,是不是太麻烦了?

bool enableGzip = (context.Request.Headers["Content-Encoding"] == "gzip");
if( enableGzip )
    context.Request.Filter
= new GZipStream(context.Request.Filter, CompressionMode.Decompress);

// .............................................................

if( enableGzip ) {
    context.Response.Filter
= new GZipStream(context.Response.Filter, CompressionMode.Compress);
    context.Response.AppendHeader(
"Content-Encoding", "gzip");
}

   其实,岂止是这一个地方麻烦。照这种做法,每个服务都要创建一个ahsx文件,读输入,写输出,也是重复劳动。但是,如何改进这些地方,就不是本文的主题了,我将在后面的博客中改进它们。今天的主题是展示这些对象的强大功能。

  从以上的示例中,您有没有发现:只要使用这几个对象就可以实现一个服务所必需的基础功能!

0
相关文章