問題
ASP.NET Web API 構建 Web 應用程序時,要求使用 Session 在服務器存儲一些用戶特定的信息
解決方案
ASP.NET Web API 不支持 Session,因為 API 根本不依賴于System.Web。他想試圖擺脫偽造 Session,非 HTTP這樣的概念。
然而,如果我們 在 ASP.NET 運行時中運行 ASP.NET Web API,還想啟用 Session。我們可以通過兩種方式來做:
-
全局:應用于整個 API
-
局部:應用于指定路由
啟用全局方式,我們需要在 Global.asax 中 通過 SesssionStateBehavior.Required顯示的設置啟用 Session 行為。
1
2
3
4
|
protected void Application_PostAuthorizeRequest() { HttpContext.Current.SetSessionStateBehavior(SessionStateBehavior.Required); } |
啟用指定路由(局部方式),我們可以通過過使用路由處理器,讓路由處理器繼承自 IRequiresSessionState。然后,我們可以在指定的路由上附加處理器,這個就在請求指定路由的時候啟用了 Session。
工作原理
默認的 ASP.NET Web API 模板,會幫我們在 WebApiConfig 靜態類中使用 HttpConfiguration 定義默認路由,因為,框架附帶的擴展方法是支持我們使用 System.Web.RouteCollection,在定義 MVC 路由的地方定義 Web API路由。
雖然 MapHttpRoute 的多個重載方法經常被使用,但是這些重載方法都是 void (無返回值)方法,實際上,方法還是返回了一個最新聲明路由的實例,只是方法的調用結果一般都是被拋棄掉的。在使用Syste.Web.RouteCollection 直接定義路由的情況下,返回值是 System.Web.Route 的對象,我們可以將其賦值給 IrouteHandler。
當運行在 ASP.NET 的時候,ASP.NET Web API 框架使用同樣的機制來確保 Api 請求可以準確到達,他會賦值HttpControllerRouteHandler 給每一個 Web API 路由,HttpControllerRouteHandler 是 GetHttpHandler 方法返回的一個HttpControllerHandler 實例,這是 ASP.NET Web API 管道的入口點。HttpControllerHandler(WEB API 的核心)雖然很復雜,但是究其原理也就是一個傳統的 IHttpAsyncHandler(舊的 IHttpHandler 的一個異步的版本)。
我們可以通過實現IRequiresSessionState 的接口,來強制在 IHttpHandler 中使用 Session。ASP.NET 將會顯示的為每一個實現了這個接口路由啟用 Session。
另外要在全局范圍內調用 HttpContext.Current.SetSessionStateBehavior 方法和傳遞 SessionStateBehavior,需要為當前的 HttpContext 顯示的啟用 Session。SetSessionStateBehavior方法必須在 AcquireRequestState 事件之前調用。
代碼演示
繼承兩個類:
-
HttpControllerHandler
-
HttpControllerRouteHandler
我們將創建兩個自定義類
-
SessionHttpControllerHandler:實現 IRequiresSessionState
-
SessionHttpControllerRouteHandler:只是代替默認類型,來充當返回 SessionHttpControllerHandler 的工廠
如清單 1-26 所示。
清單 1-26. 定制 HttpControllerHandler和 HttpControllerRouteHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class SessionControllerHandler : HttpControllerHandler, IRequiresSessionState { public SessionControllerHandler(RouteData routeData) : base (routeData) { } } public class SessionHttpControllerRouteHandler : HttpControllerRouteHandler { protected override IHttpHandler GetHttpHandler(RequestContext requestContext) { return new SessionControllerHandler(requestContext.RouteData); } } |
現在我們需要將我們的注意力從 WebApiConfig 類轉移到 RouteConfig 類,因為我們需要執行RouteCollection。接下來,我們應該在創建路由的時候,將 SessionHttpControllerRouteHandler 賦值給RouteHandler。如清單 1-27 所示。
清單 1-27. 在System.Web.RouteCollection 中注冊 Web API 路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" ); //Web API routes.MapHttpRoute( name: "DefaultApi" , routeTemplate: "api/{controller}/{id}" , defaults: new { id = RouteParameter.Optional } ).RouteHandler = new SessionHttpControllerRouteHandler(); //MVC routes.MapRoute( name: "Default" , url: "{controller}/{action}/{id}" , defaults: new { controller = "Home" , action = "Index" , id = UrlParameter.Optional } ); } |
如果想激進一點,還是有其他的方式來執行這個功能的。不再需要跑到 RouteConfig 中注冊 Api 的路由,而是需要使用 WebApiConfig 中的 HttpConfiguration 做一些必要的處理,可以同樣達對所有 Web API 路由啟用Session。
當我們通過 Web API 的配置注冊路由的時候,路由是被注冊在 RouteTable 中,同時,使用一個單利HttpControllerRouteHandler.Instance 處理器來處理路由。這樣,我們可以讓 ASP.NET 所有調用轉到Web API路由,進入到 Web API 的管道。這里說到的單例其實就是一個 Lazy<HttpControllerRputeHandler>。我們可以在應用程序啟動的地方使用自己的類似 SessionHttpControllerRouteHandler 的類實例,然后繼續注冊路由到HttpConfiguration,同時,這樣可以確保每一個 Web API 路由都使用了SessionHttpControllerRouteHandler,也就是說所有的路由都可以訪問Session。這個簡單的代碼如清單 1-28 所示。
清單 1-28. 配置 Session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var httpControllerRouteHandler = typeof (HttpControllerRouteHandler).GetField( "_instance" , BindingFlags.Static | BindingFlags.NonPublic); if (httpControllerRouteHandler != null ) { httpControllerRouteHandler.SetValue( null , new Lazy<HttpControllerRouteHandler>(() => new SessionHttpControllerRouteHandler(), true )); } config.Routes.MapHttpRoute( name: "DefaultApi" , routeTemplate: "api/{controller}/{id}" , defaults: new { id = RouteParameter.Optional } ); config.MapHttpAttributeRoutes(); } } |
現在,我們需要證明這個起作用了,需要做一個簡單模擬擲骰子的 ApiController 例子。首先,生成一個 1 到6 之間的隨機數,將 Session 中上一次的值使用當前投擲的值賦值。
清單 1-29. 使用 Session 的 ApiController 簡單例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public class DiceResult { public int NewValue { get ; set ; } public int LastValue { get ; set ; } } public class DiceController : ApiController { public DiceResult Get() { var newValue = new Random().Next(1, 7); object context; if (Request.Properties.TryGetValue( "MS_HttpContext" , out context)) { var httpContext = context as HttpContextBase; if (httpContext != null && httpContext.Session != null ) { var lastValue = httpContext.Session[ "LastValue" ] as int ?; httpContext.Session[ "LastValue" ] = newValue; return new DiceResult { NewValue = newValue, LastValue = lastValue ?? 0 }; } } return new DiceResult { NewValue = newValue }; } } |
值得注意的是,我們剛剛獲取的 HttpContext 是從 HttpRequestMessage 屬性字典中通過“MS_HttpContext”獲取的。這個比直接從 System.HttpContext.Current中獲取更具可測性。
文章列表