浅析Content Negotation在Nancy的实现和使用

原文:浅析Content Negotation在Nancy的实现和使用

背景介绍

什么是Content Negotation呢?翻译成中文的话就是"内容协商"。当然,如果不清楚HTTP规范(RFC 2616)的话,可以对这个翻译也是一头雾水。

先来看看RFC 2616对其的定义是

The process of selecting the best representation for a given response when there are multiple representations available.

这句话是什么意思呢?可以简单的理解为:当存在多个不同的表现形式时,对给定的响应选择一个最好的表现形式的过程

其涉及到相关的请求报文头部有下面几个:

  • Accept:响应可接收的Media Type,如"application/json"等
  • Accept-Charset:可接收的字符集,如"UTF-8"等
  • Accept-Encoding:可接收的内容编码,如"gzip"等
  • Accept-Language:优先选用的自然语言,如"en-us等

本文主要用到的是Accept这个请求报文头!

注:RFC 2616在2014年被拆分成了6个单独的协议!具体可以参见下面的两个链接:

http://www.w3.org/Protocols/rfc2616/rfc2616.html

https://tools.ietf.org/html/rfc2616

前言

可能看了前面一节的背景介绍,大家可能会一脸懵逼,似乎跟本文的主题并不沾边,但是多了解一点相关的知识也是必不可少的

这样我们也可以知道Content Negotation存在的意义。才能对它的使用有一个更精确的定位。

我们都知道Nancy是基于Http协议开发的一个轻量级的Web框架,所以它的内部必然会涉及到Content Negotation的实现。

其实在Nancy的实现和在Web Api的实现可以说是大同小异,如果大家看过这两者这一块的实现,应该也会有同样的感觉。

下面主要介绍Nancy 2.0.0-clinteastwood(基于dotNet Core)。

如何实现?

对于一个web应用程序而言,它的起点一定是路由,Nancy自然也是不会例外。先来看起点!起点是位于Nancy.Route这个命名空间下面的DefaultRouteInvoker

里面有一个Invoke的方法是每个路由都会执行的!

public async Task<Response> Invoke(Route route, CancellationToken cancellationToken, DynamicDictionary parameters, NancyContext context)
{
    object result;

    try
    {
        result = await route.Invoke(parameters, cancellationToken).ConfigureAwait(false);
    }
    catch(RouteExecutionEarlyExitException earlyExitException)
    {
        context.WriteTraceLog(
            sb => sb.AppendFormat(
                    "[DefaultRouteInvoker] Caught RouteExecutionEarlyExitException - reason {0}",
                    earlyExitException.Reason));
        return earlyExitException.Response;
    }

    if (!(result is ValueType) && result == null)
    {
        context.WriteTraceLog(
            sb => sb.AppendLine("[DefaultRouteInvoker] Invocation of route returned null"));

        result = new Response();
    }

    return this.negotiator.NegotiateResponse(result, context);
}

除去在执行路由的Invoke方法抛出异常的情况,其他的都是会走NegotiateResponse这个方法!!

从而也就到了本文要讲的重点了。既然每个正常的请求都能要经过它的洗礼,有什么理由不简单的了解一下呢?

一切的开始都是源于IResponseNegotiator这个接口,这个接口也十分的简单,就一个方法的定义。

public interface IResponseNegotiator
{
    Response NegotiateResponse(dynamic routeResult, NancyContext context);
}

正如我们所知,几乎每一个模块,Nancy内部都会有一个默认的实现,正常情况下,都是以Default开头的方法,关于内容协商这一块的自然也会有其对应的默认实现。

这个默认实现位于Nancy.Responses.Negotiation这个命名空间下面!

从上面接口的方法签名可以看出,处理请求时,都需要传递当前路由处理的结果和当前的上下文。

这个上下文,其实在整个Nancy框架中占据着举足轻重的地位,与之类似的有HttpContext等。

当前路由处理的结果可谓是多种多样,只要是正常执行了一个请求里面的return,这个return的内容就是路由的处理结果。

下面通过几个简单的例子介绍一下这些处理结果。

Get("/", x =>
{
    var person = new Person
    {
        Name = "catcher",
        Gender = "man"
    };
    //return Negotiate.ReturnJsonAndXml(person);
    //return Negotiate.WithModel(person);
    //return person;
    //return View["person"];
    //return Response.AsJson(person);
    //return Response.AsRedirect("/person");
    //return HttpStatusCode.RequestTimeout;
    return "";
});

这几个例子中,我们比较常用到的应该是Response.AsJson、View和Respnse.AsRedirect这3个。

NegotiateResponse方法的第一个参数routeResult不单单包含上面提到的正常的响应信息,

还有一些错误类的信息,如404、500等,这个时候routeResult就会是一个Nancy.ErrorHandling.DefaultStatusCodeHandler.DefaultStatusCodeHandlerResult对象了。

这个对象承载了我们的各种错误类的响应。

下面来看看具体做了什么内容!

在这个方法中执行的第一步就是先判断我们在Module中返回的结果是不是一个Response对象,

如果是一个Response对象就直接将这个对象返回了。具体的片段代码如下:

Response response;
if (TryCastResultToResponse(routeResult, out response))
{
    context.WriteTraceLog(sb =>
        sb.AppendLine("[DefaultResponseNegotiator] Processing as real response"));

    return response;
}

这个时候可能就会有这样的一个疑问,什么样的返回结果是一个Response对象,什么样的返回结果不是呢?

  • Response.AsXXX 这一类的返回结果就属于一个Response对象,这些以As开头的都是一些返回Response对象的扩展方法。
  • Negotiator对象 这一类的返回结果就不是Response对象,所以这一类返回结果是还要继续下面的层层审判!

到这里已经过滤掉了一部分"不属于"Content Negotation处理的请求了!需要注意的是View是属于Negotiator对象这一类的!

第二步是拿到NegotiationContext这个上下文

第三步就是处理Accept这个请求头的内容了

开始这一步的内容之前要先来简单了解一下Accept:

Accept首部字段可以通知服务器,用户代理能够处理的媒体类型及媒体类型的相对优先级。具体的使用形式为:type/subtype,当然也可以一次指定多种媒体类型。

如果想给显示的媒体类型添加优先级,那么就要使用q因子来额外表示该媒体类型的优先级(权重值),具体使用形式为:type/subtype;q=0.8

这个权重的取值范围是0~1(可精确到小数点后三位),最大值为1,并且当没有指定权重的时候,默认的权重就是为1。

所以,当服务器提供了多种不同的内容时,就会先返回权重最高的那个媒体类型。

下面拿一个具体的例子来看一下:

当我们访问博客园时,浏览器的Accept头为text/html, application/xhtml+xml, image/jxr, */*

这就表明浏览器想告诉服务器,“我支持这些媒体类型,你最好返回这些Media Type的数据给我。”

当服务器处理好了之后,就会在响应头中的Content-Type表现出要展示什么的内容。

OK,了解完毕,下面来看看Nancy是怎么处理的。

要处理Accept,肯定会有一个定义,从我们上面的了解中,也知道这肯定会是一个集合,每个集合的项包含两个内容:Media Type和权重值。下面来验证一下

public IEnumerable<Tuple<string, decimal>> Accept
{
    get { return this.GetWeightedValues("Accept"); }
    set { this.SetHeaderValues("Accept", value, GetWeightedValuesAsStrings); }
}

Nancy把集合的项定义成了元组(省去定义一个类那么麻烦),元组的第一个元素就是Media Type,第二个就是这个Media Type对应的权重值。

需要注意的是在get的时候,根据权重值对Media Type做了一个降序,后面的处理就直接是按照权重高的优先处理

private IEnumerable<Tuple<string, decimal>> GetWeightedValues(string headerName)
{
    return this.cache.GetOrAdd(headerName, r =>
    {
        var values = this.GetValue(r);
        var result = new List<Tuple<string, decimal>>();

        foreach (var header in values)
        {
            //....
        }

        return result.OrderByDescending(x => x.Item2);
    });
}

请求的相关信息都是会记录在Nancy上下文的Request属性中,所以想要处理Accept,NancyContext肯定是必不可少的。

这一步主要的处理是把Nancy上下文中的Accept信息强制转化成一个方便后续处理的集合对象。

前面的这三步可以说是铺垫,后面的处理才是重头戏。

第四步,获取合适的Media Type

var compatibleHeaders = this.GetCompatibleHeaders(coercedAcceptHeaders, negotiationContext, context).ToArray();

Nancy是如何来处理这一块的呢

首先是取到合法的Media Type:

当前negotiationContext的PermissableMediaRanges属性如果包含 */*这个Media Type,就直接把权重大于0的Media Type返回

这里也可以间接说返回的是Accept的所有内容,应该不会有人那么无聊弄个负数或者其他吧?

大部分情况下,权重大于0的就是合法的媒体类型。

拿到合法的媒体类型之后,还要根据媒体类型去拿到对应的内容。如:application/json ,返回一个序列化的Person对象,这个Person对象就是对应的内容。

还要对媒体类型处理,最后返回一个CompatibleHeader集合。

第五步,判断是否有合适的媒体类型,如果没有就直接返回406。

从这一步也得知,当客户端向服务器请求一种服务器无法处理的媒体类型时,就会返回406(Not Acceptable)!

第六步,创建当前请求的Response对象

在Nancy中,请求的最后都是以Response对象的形式呈现在我们面前,所以在创建好一个Response之前 ,Negotiate是属于不完善的!

下面看看是如何创建Response对象的:

首先是用NegotiateResponse方法创建了一个Response对象

var response = NegotiateResponse(compatibleHeaders, negotiationContext, context);    

在NegotiateResponse方法中,通过遍历前面得到的媒体类型集合。

根据每一个媒体类型去拿到对应的一个优先级列表

最后在优先级列表中根据 MediaRange , mediaRangeModel , NancyContext 这三个来判断能否生成一个Respone对象

如果能生成就返回上面创建好的这个Response对象,不能就只好返回null了。

由于这里的Response对象还是有可能为空,所以当其为空的时候,还是应该要向上面那样处理成406

后面就是处理一些响应头部的信息并最终返回这个Response。

下面是完整的NegotiateResponse方法:

public Response NegotiateResponse(dynamic routeResult, NancyContext context)
{
    Response response;
    if (TryCastResultToResponse(routeResult, out response))
    {
        context.WriteTraceLog(sb =>
            sb.AppendLine("[DefaultResponseNegotiator] Processing as real response"));

        return response;
    }

    context.WriteTraceLog(sb =>
        sb.AppendLine("[DefaultResponseNegotiator] Processing as negotiation"));

    NegotiationContext negotiationContext = GetNegotiationContext(routeResult, context);

    var coercedAcceptHeaders = this.GetCoercedAcceptHeaders(context).ToArray();

    context.WriteTraceLog(sb => GetAccepHeaderTraceLog(context, negotiationContext, coercedAcceptHeaders, sb));

    var compatibleHeaders = this.GetCompatibleHeaders(coercedAcceptHeaders, negotiationContext, context).ToArray();

    if (!compatibleHeaders.Any())
    {
        context.WriteTraceLog(sb =>
            sb.AppendLine("[DefaultResponseNegotiator] Unable to negotiate response - no headers compatible"));

        return new NotAcceptableResponse();
    }

    return CreateResponse(compatibleHeaders, negotiationContext, context);
}

上面大致履了一下相应的实现

对于它的大致实现,有了一定的了解,下面来看看具体是要怎么用

如何使用?

平时我们如果用Negotiate的话,基本都是用的Negotiator的扩展方法,输入Negotiator后,可以看到一堆扩展方法,这堆扩展方法就是我们经常用到的。

我们先尝试用Negotiate处理一个MIME Type为application/json的请求!

下面是具体的示例代码:

Get("/", x =>
{
    var person = new
    {
        Name = "catcher",
        Gender = "man"
    };

    return Negotiate.WithMediaRangeModel(new MediaRange("application/json"),person);
});

定义了一个匿名对象,并通过WithMdeiaRangeModel这个扩展方法来处理MIME Type和这个匿名对象。

此时,我们希望能够得到结果是对匿名对象进行json序列化后结果,和Response.AsJson得到的应该是基本一致的。

当然,这个时候我们在浏览器打开这个URL时,结果并不是我们所期望的那样!

不管三七二十一,来看看这个扩展方法做了一些什么操作!

//直接调用
public static Negotiator WithMediaRangeModel(this Negotiator negotiator, MediaRange range, object model)
{
    return negotiator.WithMediaRangeModel(range, () => model);
}
//间接调用
public static Negotiator WithMediaRangeModel(this Negotiator negotiator, MediaRange range, Func<object> modelFactory)
{
    negotiator.NegotiationContext.PermissableMediaRanges.Add(range);
    negotiator.NegotiationContext.MediaRangeModelMappings.Add(range, modelFactory);
    return negotiator;
}

幡然醒悟,它是往我们的当前NegotiationContext的PermissableMediaRanges添加了application/json这个媒体类型!

并且此时PermissableMediaRanges集合就包含了两个对象:一个是*/*,一个是appliaction/json

我们用了一种错误的方式来请求这个URL!!因为我们是直接用浏览器打开的,而这个时候默认的Accept头是

Accept: text/html, application/xhtml+xml, image/jxr, */*

它并不包含我们所接收请求的application/json,所以它在生成Response对象的时候会抛出异常,然后就看到那个500的错误页面了。

这个时候我们应该借助工具来探讨,可以使用Fiddler、Postman和Charles等工具。

这里我用的是Fiddler,当我们在Composer中添加Accept请求头后,就能正常返回我们想要的结果了。

不知道大家是否有留意到这样子返回和用Response.AsJson这样返回有什么区别?

当然,这种写法是只能处理application/json的请求,并不能处理其他MIME Type的请求!

下面我们继续改进一下,让这个请求可以同时接收处理application/xmltext/html这两种MIME Type

Get("/", x =>
{
    var person = new
    {
        Name = "catcher",
        Gender = "man"
    };

    return Negotiate
    .WithMediaRangeModel(new MediaRange("application/json"), person)
    .WithMediaRangeModel(new MediaRange("application/xml"), person)
    .WithView("person")
    .WithModel(person);
});

下面来看看Accept为application/xml的试试:

可以看到,它并没有返回我们想要的结果,但是请求却是成功的!这里的问题出在我们定义的那个匿名对象!

这里默认处理的序列化XML的方法是不支持匿名对象的,具体可以参考Nancy对XML处理的方法。

修改匿名对象为实体对象后,它就能把数据正常返回给我们了。

var person = new Person
{
    Name = "catcher",
    Gender = "man"
};

可以看到,我们刚才的改造已经能够同时支持json和xml了!对于前面提到的匿名类的问题,如果有需要可以实现一个支持匿名类的序列化方法以达到对匿名类的适配。

前面我们直接在浏览器打开这个URL时,提示我们500错误,现在改进后再来看看能否返回一个正常页面给浏览器!

这个时候我们并没有编写对应的视图,所以得到的必然还是500错误(ViewNotFound)。下面就要处理这个错误。

我们在根目录添加一个person.html文件,并设置它的Copy to Output Directory属性为Copy always

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>

    <p>Name:@Model.Name</p>
    <br />
    <p>Gender:@Model.Gender</p>

</body>
</html>

我们的页面比较简单,就把刚才实体对象的内容展示一下。此时再运行就可以发现页面已经能正常显示了!

其实还有一种比较直接的方法也是用Content Negotation实现的!

不知道大家是否记得在创建一个WEB API项目后,生成的valuecontroller,里面的方法都是直接返回一个数组或字符串。

在Nancy中也可以直接返回这样的一个对象!!来看看下面的这个例子:

Get("/", x =>
{
    var person = new Person
    {
        Name = "catcher",
        Gender = "man"
    };
    return person;
    //对等的写法
    //return Negotiate.WithModel(person);
}

上面的示例代码中也给出了一种等价的写法,最终它是给NegotiationContext的DefaultModel属性赋值为这个对象。

这样直接返回一个对象的写法,似乎就没有那么灵活,我想到的一个用来形容的词就是"任人宰割"

而用Negotiate就可以适当的加上一些控制,毕竟有那么多的扩展方法可以用。如果觉得不够用,那就自己加扩展,加到自己满意为止。

好比说,现在某个api只对MIME类型为application/jsonappliaction/xml的请求进行处理,其他的一概不理。

这个时候,常规有效的做法就是直接用WithMediaRangeModel这个扩展方法

return Negotiate
        .WithMediaRangeModel(new MediaRange("application/json"), person)
        .WithMediaRangeModel(new MediaRange("application/xml"), person);     

这样的写法并没有什么问题,但是并不那么简洁,这个时候我们就可以通过写扩展来让它变得简洁一些。

public static class NegotiateExtensions
{
    public static Negotiator ReturnJson(this Negotiator negotiator, object model)
    {
        return negotiator.WithMediaRangeModel(new MediaRange("application/json"), model);
    }

    public static Negotiator ReturnXml(this Negotiator negotiator, object model)
    {
        return negotiator.WithMediaRangeModel(new MediaRange("application/xml"), model);
    }

    public static Negotiator ReturnJsonAndXml(this Negotiator negotiator, object model)
    {
        return negotiator.ReturnJson(model).ReturnXml(model);
    }
}

使用的时候:

return Negotiate.ReturnJsonAndXml(person);

这样是不是很方便和简洁呢?

总结

内容协商的作用可大可小,如果能多加利用,或许能成为一把利刃。

本文简单的分析了一下内容协商在Nancy中是如何实现的,以及我们平时的开发中是如何使用的。

当然其中有许多相关的细节在文中也没有特别体现出来,如果园友们觉得与这一块密切相关且有必要说明的

可以在评论中指出,也可以私信给我,便于我在后期增加上去。

同样用一张思维导图概括本文:

原文地址:https://www.cnblogs.com/lonelyxmas/p/9068169.html

时间: 2024-11-14 06:47:09

浅析Content Negotation在Nancy的实现和使用的相关文章

Nancy - 管理静态内容

TL;DR: stick stuff in /Content ... done. In Nancy parlance "Static Content" is things like javascript files, css, images etc, but can actually be anything, and Nancy uses a convention based approach for figuring out what static content it is abl

浅析如何在Nancy中使用Swagger生成API文档

原文:浅析如何在Nancy中使用Swagger生成API文档 前言 上一篇博客介绍了使用Nancy框架内部的方法来创建了一个简单到不能再简单的Document.但是还有许许多多的不足. 为了能稍微完善一下这个Document,这篇引用了当前流行的Swagger,以及另一个开源的Nancy.Swagger项目来完成今天的任务! 注:Swagger是已经相对成熟的了,但Nancy(2.0.0-clinteastwood)和Nancy.Swagger(2.2.6-alpha)是基于目前的最新版本,但目

浅析requests库响应对象的text和content属性

在做爬虫时请求网页的requests库是必不可少的,我们常常会用到 res = resquests.get(url) 方法,在获取网页的html代码时常常使用res的text属性: html = res.text,在下载图片或文件时常常使用res的content属性: with open(filename, 'wb') as fp: fp.write(res.content) 下面我们来看看 'text' 和 'content' 的不同之处: 输出本博客的响应对象的 text import re

浅析vanish

浅析 VANISH --一种cache 第一部分:理解vanish的准备工作 1.对CDN的小剖析 CDN  content  delivery  network  内容分发(推送)网络,是在现有的Internet中增加一层新的网络架构,将网络内容发布到最接近用户的网络边缘(边缘服务器),使用户最近取得所需内容,解决网络拥挤状态,提高用户访问网站的速度. CDN网络架构主要有两部分组成,中心和边缘两部分,中心指CDN网管中心和DNS重定向解析中心,负责全局负载均衡.边缘主要指异地节点,CDN分发

Nancy

Nancy Nancy 是一个轻量级的,简单粗暴的framework用来构建基于HTTP的各种服务,兼容.Net和Mono.Nancy的整套设计理念是基于"super-duper-happy-path",这是一个作者杜撰的单词,个人觉得翻译过来基本就是简单粗暴,行之有效的意思. 简单的例子: public class Module : NancyModule { public Module() { Get["/greet/{name}"] = x => { r

Volley框架源码浅析(一)

尊重原创http://blog.csdn.net/yuanzeyao/article/details/25837897 从今天开始,我打算为大家呈现关于Volley框架的源码分析的文章,Volley框架是Google在2013年发布的,主要用于实现频繁而且粒度比较细小的Http请求,在此之前Android中进行Http请求通常是使用HttpUrlConnection和HttpClient进行,但是使用起来非常麻烦,而且效率比较地下,我想谷歌正式基于此种原因发布了Volley框架,其实出了Voll

Android L 漫游浅析

这篇文章主要是分析在Android L 源代码中对手机漫游的处理.当然我这里所说的漫游指的是国际漫游.通常我们判断手机是否在国际漫游,第一个想法就是比较网络上获取的MCC+MNC是否与手机中的IMSI相同,如果不同就判断为漫游了.如果是漫游的话,手机上最直观的可以看到就是两个地方了: a . 手机的屏幕的状态拦上手机信号角标的左下方是否有"R"显示. b . Setting --->About phone --->Status --->Roming 当然这是最粗略的比

Android中常用的三种存储方法浅析

Android中常用的三种存储方法浅析 Android中数据存储有5种方式: [1]使用SharedPreferences存储数据 [2]文件存储数据 [3]SQLite数据库存储数据 [4]使用ContentProvider存储数据 [5]网络存储数据 在这里我只总结了三种我用到过的或即将可能用到的三种存储方法. 一.使用SharedPreferences存储数据 SharedPreferences是Android平台上一个轻量级的存储类,主要是保存一些常用的配置信息比如窗口状态,它的本质是基

第五章 Nancy 路由

在Nancy中,最为神奇的莫过于路由了,定义路由模块是构成Nancy应用的骨架.在Nancy中定义路由,和在 ASP.NET MVC那些类似的框架中有着非常大的区别. 以 ASP.NET MVC 为例,通常情况需要创建一个控制类.多数情况下,这个类提供了路由的约定.通过定义您的控制器类名和该类中的方法的名称,就能定义了该代码所处理的"路由" 请看下面的例子: using System; using System.Linq; using System.Web.Mvc; namespace