淺析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/xml
和text/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/json
和appliaction/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中是如何實現的,以及我們平時的開發中是如何使用的。
當然其中有許多相關的細節在文中也沒有特別體現出來,如果園友們覺得與這一塊密切相關且有必要說明的
可以在評論中指出,也可以私信給我,便於我在後期增加上去。
同樣用一張思維導圖概括本文:
淺析Content Negotation在Nancy的實現和使用