ASP.Net Core開發(踩坑)指南
ASP.NET與ASP.NET Core很類似,但它們之間存在一些細微區別以及ASP.NET Core中新增特性的使用方法,在此之前也寫過一篇簡單的對比文章ASP.NET MVC應用遷移到ASP.NET Core及其異同簡介,但沒有進行深入的分析和介紹,在真正使用ASP.NET Core進行開發時,如果忽略這些細節可能會出現奇怪的問題,特此將這些細節進行分享。
本文主要內容有:
- 無處不在的依賴注入(Dependency Injection, DI)
- Configuration&Options
- ASP.NET Core 請求管道建立
注:本文基於ASP.Net Core 2.1版本,.Net Core SDK版本需要2.1.401+。長篇預警( ╯□╰ )
無處不在的依賴注入
ASP.NET與ASP.NET Core之間最大區別之一就是內建了依賴注入機制,雖然ASP.NET中也有DI機制,但沒有內建容器,一般都需要使用第三方的容器來提供服務,另外依賴注入的概念也不像ASP.NET Core中這樣無處不在。
簡單來說依賴注入的目的是為了讓程式碼解耦以提高程式碼的可維護性,同時也要求程式碼設計符合依賴導致原則使得程式碼更加靈活,而其原理實際上就是在應用程式中新增一個物件容器,在應用初始化時將實際的服務“放”到容器中,然後當需要相應服務時從容器中獲取,由容器來組裝服務。
服務的註冊
ASP.NET Core的Startup(注:Startup僅僅只是約定名稱,實際使用是在Program型別中建立 WebHost時使用的),該型別中包含兩個方法分別是ConfigureServices和Configure,其中ConfigureServices的主要作用就是用來將服務“放”置到容器中
程式碼來自: ofollow,noindex">https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1
替換預設的依賴注入容器
ASP.NET Core的預設容器僅提供了構造注入功能,如果需要使用屬性注入等功能或者在遷移時原有應用依賴於其它容器,那麼可以通過使用第三方容器實現。
將預設容器替換為其它容器僅需三步:
1. 將ConfigureServices方法的返回型別改為IServiceProvider。
2. 將ASP.NET Core中的服務註冊到第三方容器中。
3. 使用第三方容器實現IServiceProvider介面並返回。
官方文件以Autofac為例,Autofac已經實現了ASP.NET Core服務註冊到Autofac容器中,以及Autofac容器的IServiceProvider介面封裝,僅需安裝Autofac以及Autofac.Extensions.DependencyInjection包即可。
使用windsor或其它容器可以參考:
https://stackoverflow.com/questions/47672614/how-to-use-windsor-ioc-in-asp-net-core-2將Controller註冊為服務
雖然Controller在啟用時是通過容器來獲取Controller的依賴(即構造方法需要的引數),在程式碼執行的時候給人一種Controller是從容器中組裝的錯覺,但是實際上預設情況下Controller的組裝過程不是直接由容器組裝的,如果要讓Controller從容器組裝,那麼在配置MVC服務時需要通過.AddControllerAsServices()方法將Controller註冊到容器中:
注:一般情況下是否將Controller註冊為服務對Controller的開發和程式碼的執行並沒有很大區別,但是如果當容器變更為其它容器,並且使用了容器提供的如屬性注入等功能時,如果沒有將Controller註冊為服務,那麼相應的屬性注入的過程也不會被觸發,簡單來說就是隻有將Controller註冊為服務,那麼例項化Controller的工作才會由容器完成,才會觸發或者使用到容器提供的其它特性。
服務的獲取
前面介紹了服務的註冊,現在來介紹一下在ASP.NET Core中有哪些方法可以獲取服務:
1. Controller構造方法引數。
2. 通過Controller注入IServiceProvider型別,通過IServiceProvider來獲取服務:
3. 在Action方法或者Mvc過濾器(過濾器的上下文引數中包含HttpContext)中通過HttpContext的RequestServices物件獲取服務:
4. 在View上通過@inject注入服務:
5. 在Action方法中,通過FormServices特性注入:
注:一般來說盡可能顯式的標明型別的依賴(即通過構造引數的方式聲明當前型別所依賴的元件),上面的2和3兩點分別都是通過服務提供器在方法內部來獲取依賴,這樣做依賴對於外界來說是不可知的,可能會對程式碼的可維護、可測試性等造成一定影響,這種模式被稱為Service Locator模式,在開發過程中儘可能避免Service Locator模式的使用。
常用的服務
ASP.NET Core相對於ASP.NET來說取消了一些常用的靜態型別,比如HttpContext、ConfigurationManager等,取而代之的是通過將類似的元件以服務的形式註冊到容器中,使用時通過容易來獲取相應的服務元件,這些常用的服務有:
1. IHostingEnvironment:包含了環境名稱、應用名稱以及當前應用程式所處的根目錄及Web靜態內容的根目錄(預設wwwroot)。
2. IHttpContextAccessor:從名字可以看出,它用來訪問當前請求的HttpContext。
3. IConfiguration:ASP.NET Core配置資訊物件。
4. IServiceProvider: ASP.NET Core服務提供器。
5. DbContext: 這裡的DbContext指的是EFCore的DbContext,在ASP.NET Core中,EFCore的DbContext也是在ConfigureServices方法中進行配置並新增到容器,使用時直接從容器中獲取(但要注意的是對於分層結構的開發風格來說,DbContext不會直接被Controller依賴,而是被Controller中依賴的業務服務型別所以來,就是說編寫Controller程式碼的時候不會直接與DbContext發生直接互動)。
Configuration&Options
在ASP.NET的開發中,通常某個變數需要從配置檔案讀取,一般都是在相應型別的構造方法中,通過靜態型別ConfigurationManager的AppSettings方法來讀取並初始化變數。雖然ASP.NET Core也可以在型別中注入IConfiguration例項來直接讀取配置檔案,但該方法由於Options模式的出現已經不再建議使用,使用元件通過依賴相應的元件Options可以做到關注點分離,提高程式的靈活性、可拓展性,Options使用方法見文件: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-2.1
ASP.NET Core 請求管道建立
ASP.NET由於是基於IIS請求管道的,ASP.NET應用程式僅僅是管道中的一個處理環節,管道中還包含如身份驗證、靜態檔案處理等環節,但ASP.NET Core不一樣,它脫離了IIS處理管道,所以整個管道的建立均需要靠程式自身完成,而ASP.NET Core建立管道的程式碼就是Startup型別的Configure方法,該方法通過IApplicationBuilder例項來新增不同功能的中介軟體,通過中介軟體的串聯形成處理管道,下圖是ASP.NET Mvc模板生成的管道程式碼:
該管道主要包含了錯誤處理(開發環境顯示異常資訊,其它環境跳轉錯誤頁面)中介軟體、靜態檔案處理中介軟體以及Mvc中介軟體。
更多中介軟體可參考文件: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index?view=aspnetcore-2.1
ASP.NET Core Mvc
ASP.NET Core Mvc與ASP.NET Mvc相比整體上區別不大,但仍然有很多細節上的變化,下面就開始一一介紹:
路由的作用是將請求根據Url對映到“對應”的處理器上,在Mvc中請求的終點就是Controller的Action方法,而這裡所謂的“對應”指的是Url與路由模板的匹配,ASP.NET Core Mvc通過以下的方式新增路由模板:
上圖中的路由模板是最常用的路由模板,使用花括號內的內容為路由引數及其預設值,Url中通過路由引數控制器名稱、活動方法名稱來匹配到相應控制器的活動方法。
在註冊路由時可以為相應路由新增預設值、路由引數約束以及對應路由的相關附加資料(datatokens):
路由的功能除了處理請求匹配外,還具有連結生成的功能,特別是Mvc程式的View中使用IUrlHelper或TagHelper來生成頁面的超連結:
其生成原理是通過連結引數(如上圖所示的Controller和Action)去路由表中匹配,然後使用匹配結果中的第一個路由(可能會匹配到多個路由物件,具體內容在後續Area章節介紹)來生成連結。
更多路由資訊及路由模板定義參考文件: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.1
ASP.NET Core Mvc的Controller一般繼承Controller型別實現,基類Controller中包含了Mvc中常用的返回方法(如Json以及View等)以及用於資料儲存的ViewBag、ViewData、TempData。
Area是Mvc應用中用來進行功能拆分或分組的一種方式,Area一般有自己的名稱空間和目錄結構,一般Area的預設目錄結構如下:
ASP.NET Core Mvc和ASP.NET Mvc中的概念和用法基本上是一致的,但也存在一些區別:
1. Area下面的Controller需要使用Area特性標明當前Controller屬於哪一個Area:
注:Area的目錄結構不是必須的,只需要通過特性標記的Controller都會被正確識別,但目錄結構的改變會導致無法找到View,關於View的查詢路徑會在後續介紹。
2. Area的路由註冊也是在UseMvc方法中完成:
注:攜帶Area的路由模板需要放在前面,否則在生成通過IUrlHelper或TagHelper生成連結時,由於Controller以及action會匹配到沒有area的模板並使用該模板生成連結,導致area引數被忽略,而生成類似:/controller/action?area=area的結果(在生成Url時,ASP.NET Core會將多餘的路由引數放置到查詢字串中)
View是基於Razor的HTML模板,Razor的詳細語法參考文件:
https://docs.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-2.1
ASP.NET Core Mvc的View與ASP.NET Mvc中的使用方法基本一致,主要區別如下:
1. 引入了TagHelper,使用TagHelper可以讓View的程式碼更接近Html。更多TagHelper資訊參考文件: https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.1
2. Controller將引數傳輸到View的方法添加了ViewData特性,使用方法如下:
View中訪問被ViewData標記的方式:
3. 新增View元件: https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-2.1
配置View的查詢路徑:
ASP.NET Core可以在ConfigureServices方法中對RazorViewEngineOptions進行配置,如下圖所示,在預設查詢位置基礎上添加了View以及AreaView的查詢路徑:
模型繫結
模型繫結指的是ASP.NET Core Mvc將請求攜帶的資料繫結到Action引數的過程,ASP.NET Core Mvc的模型繫結資料來源預設使用Form Values、Route Values以及Query Strings,所有值都以Name-Value的形式存在,模型繫結時主要通過引數名稱、引數名稱.屬性名稱、引數名稱[索引]等方式與資料來源的Name進行匹配。
除了預設的資料來源之外還可以從Http請求Header、Http請求Body甚至從依賴注入容器中獲取資料,要從這些資料來源中獲取資料需要在相應引數上使用[FromHeader]、[FromBody]、[FromServices]特性。
如果需要獲取的資料在不同資料來源中都存在時(Name存在於多個數據源中),還可以通過特性指明從哪一個資料來源中獲取,如[FromForm]、[FromQuery]及[FromRoute]。
需要注意的是[FromBody]預設只支援Json格式的內容,如果需要支援其它格式,如XML需要新增相應的格式化器,新增方法如下圖所示:
更多模型繫結及驗證內容請參考文件: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.1
https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.1
其中模型驗證的使用方式與ASP.NET Mvc一致,仍然是通過相應的驗證特性對模型或模型屬性進行標記。
Action的返回值與Json序列化
說完Action方法引數的繫結,再來看一下Action方法的返回型別,在ASP.NET Mvc中Controller提供了返回頁面內容的View方法以及返回Json內容的Json方法(當然還有檔案、重定向、404等等其它內容返回方法,詳見Controller與ControllerBase型別)。
這裡有一個需要注意的地方是當使用Json方法返回一個物件例項時,預設使用首字母小寫的駝峰命名方式序列化例項的屬性名稱,如下圖所示:
訪問結果:
要使用大寫駝峰形式命名需要在配置Mvc服務時新增以下程式碼來修改Json預設的序列化配置:
注:同樣的問題也存在於WebAPI的Ok方法以及Signalr的Json格式協議。
靜態資源
由於ASP.NET Core已經不再使用IIS請求管道,所以對於靜態資源的訪問來說需要在請求管道中新增相應的處理中介軟體來完成:
預設的無參UseStaticFiles方法將wwwroot目錄作為靜態資源存放目錄,如果要新增其它靜態內容目錄可以再次使用UseStaticFiles方法,並通過StaticFileOptions對目錄的訪問路徑以及實際路徑進行配置:
注:由於ASP.NET Core可以在Linux下執行,所以對於Linux來說路徑是大小寫敏感的,另外由於Windows和Linux類系統的路徑分隔符也不一致,所以為了保證路徑的統一,可以使用Path.Combine方法,該方法會根據作業系統的不同對路徑進行不同的處理。
另外對於css及js資原始檔的打包、壓縮功能,最新版本(ASP.NET Core 2.1)的應用模板以及不會自動新增相關功能,需要在拓展工具中新增Bunlder& Minifier拓展:
然後通過右鍵js等資原始檔來建立bundleconfig.json檔案:
API控制器的建立
ASP.NET Core將Mvc和WebAPI進行了合併,它們的實現都直接或間接繼承了ControllerBase型別,只不過Mvc的基類Controller在ControllerBase的基礎上添加了一些用於處理View的功能。
用ASP.NET Core開發WebAPI時,Controller型別直接繼承ControllerBase。然後這個API的Controller就具有了基類的特性,返回一個結果僅需要使用Ok方法即可,如下圖所示:
然後在路由表中新增路由:
即可通過/api/default/index訪問到這個API:
但對於REST風格的API來說,它需要通過ApiController特性對Controller型別進行標記,並且通過Route特性來設定路由:
然後就可以通過HTTP謂詞來訪問API:
但要注意的是在ASP.NET Core中實現的REST風格的Controller,它不會再根據action方法的名稱來匹配謂詞,所以存在多個方法時會,那怕對方法進行了命名,但仍然會出現以下錯誤:
為了解決這個問題,需要通過新增謂詞特性解決:
模型繫結
WebAPI中的模型繫結與MVC存在一些區別,首先當使用ApiController標記Controller型別時,如果模型繫結驗證未通過,會直接返回400錯誤,不會執行Action方法(免去了使用!ModelState.IsValid進行判斷):
執行結果:
其次使用ApiController標記的Controller在執行模型繫結時會使用預設的推斷規則,該規則分別從Body、Form、Header、Query、Route、Services(它們分別對應FromBody、FromForm、FromHeader、FromQuery、FromRoute、FromServices特性)中推斷獲取資料並繫結,為什麼說推斷?
因為有一些特殊的規則:
1. FromBody用於複雜型別推斷,如果不是複雜型別(如int、string等)以及特殊的內建型別(IFormCollection文件例子),則不會從Body中獲取資料,除非通過[FromBody]特性指明,例子如下:
請求結果:
當使用[FormBody]指明引數資料來源後可以正常訪問:
注:當請求引數為簡單型別時,請求體內容型別需要為application/json,內容不能為Json字串,使用引數值作為內容即可(上圖id沒有提供的異常並不是因為Json格式問題,而是沒有指明從body中獲取資料導致的)。
2. 只能存在一個引數從Body中獲取資料,如果出現多個引數時,只能保證一個引數從Body中獲取資料,其它引數需要指明獲取資料的位置:
該API的呼叫方式如下:
3. FromForm預設只推斷檔案(IFormFile)及檔案集合型別(IFormFileCollection),其餘型別預設均不會從Form中獲取。
4. 使用FromForm特性時會推斷multipart/form-data請求內容型別。
以上推斷行為可以通過如下配置禁用:
更多資訊參考文件: https://docs.microsoft.com/zh-cn/aspnet/core/web-api/?view=aspnetcore-2.1
SignalR
SignalR是用於客戶端伺服器實時通訊的工具庫,從ASP.NET中就具有該功能,ASP.NET Core中的SignalR概念與用法與原來基本一致,但也存在一些區別:
1. 支援更多的客戶端,.Net客戶端、Java客戶端、Js客戶端以及非官方的C++客戶端、Swift客戶端。
2. 當連結SignalR並通過身份驗證後,SignalR會儲存當前使用者連結SignalR的ID以及通過驗證後的使用者名稱,可以通過使用者名稱向用戶客戶端推送訊息。
3. 在應用程式中可以通過IHubContext<HubType>方式,對SignalR上下文進行注入,並且可以直接通過該上下文推送資料給已經連結的客戶端,IHubContext<HubType>實際上是GlobalHost.ConnectionManager.GetHubContext<HubType>()的替代方式。
4. ASP.NET Core中通過app.UserSignalR以及route引數來對映一個Hub,每一個Hub擁有獨立的上下文,因此如果要使用IHubContext<HubType>來向客戶端推送資訊,那麼必須準確註明Hub的型別,如下圖程式碼應該使用IHubContext<ChatHub>,不能使用除ChatHub以外的型別(基類也不行)。
5. SignalR預設使用Json協議傳輸資料,預設情況下使用首字母小寫的駝峰命名方式序列化物件,要更改該預設行為需要通過一下程式碼,替換預設的序列化行為:
6. ASP.NET Core的客戶端程式碼(特指Js客戶端)有變更,需要對應版本使用。
關於更多SignalR內容請參考文件: https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-2.1
本文主要介紹了ASP.NET Core中Mvc、WebAPI以及SignalR開發時與原來ASP.NET中的一些細小區別和新特性,整體來說ASP.NET Core與ASP.NET從使用方式上基本上是一致的,這也使得從ASP.NET遷移到ASP.NET Core變得更加容易,但可能因為這些細小的問題往往會向程式碼中埋入一些坑,所以特別編寫了本文來解釋這些問題。
總的來說ASP.NET Core的文件相當齊全,本文中大部分內容實際都是文件中提到的,所以建議大家在使用ASP.NET Core開發時,首先第一步就是熟讀文件,避免遺漏細節。希望本篇文章對大家有幫助(*^_^*)
參考:
https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-2.1