本文3.0版本文章
https://mp.weixin.qq.com/s/U2-zxmYyRNTt97Dk5bA-2A
前言(不要求能看懂)
至於為什么要搭建.Net Core 平台,這個網上的解釋以及鋪天蓋地,想了想,還是感覺重要的一點,跨平台,嗯!沒錯,而且比.Net 更容易搭建,速度也更快,所有的包均有Nuget提供,不再像以前的單純引入組件,

已經沒有了之前的Assemblies和COM的引入,初次使用感覺會很別扭,不過使用多了,發現還是很方便的,所以你一定要會使用Nuget,真的很強大,這點兒設計思路感覺更像Linux了。
下邊這幾點,是先對 .net core 有一個初步的認識,看得懂或者看不懂都沒有關系,以后大家肯定都會明白的:
1、.net core 框架性能測試
http://www.techempower.com/benchmarks/ 我們可以通過這個web框架性能測試來看看 aspcore 的性能
2、.net core 執行過程
3、中間件執行過程
啟動的時候先執行該中間件類的構造函數,然后一路 Next() ;下去,返回的時候,正好是反向的,執行的是該類的邏輯部分:
4、AOP切面
5、整體框架結構與數據庫表UML
一、創建第一個Core
說了從零開始,就得從零開始,老生常談,開始。
1、SDK 安裝
當然,前提是你得安裝.Net Core,VS 2015也是可以,只不過需要單獨安裝.Net Core,首先你得裝個vs2015 並且保證已經升級至 update3及以上。
我的 VS 是 2017,我這里只說2017,有不會的網友可以留言,只要在Visual Studio Installer 中安裝下圖中的Core 平台即可。
下載 SDK 地址 :https://dotnet.microsoft.com/download
選擇指定的平台即可安裝:
這里說下,SDK 和 RunTime 的區別: 1、SDK 是用來開發 NetCore 的,內部捆綁了 Runtime 運行時; 2、但是如何只想運行 NetCore 項目的話,只需要在服務器中安裝 Runtime 運行時即可;
怎么判斷安裝成功了呢?直接運行命令,如果有結果證明成功了:
2、新建項目

1、File --> Project (記得文件名不要是中文,不然,你懂的)

2、然后選擇.Net Core 版本和項目類型,我選擇相對穩定的ASP.NET Core 2.2,然后選擇API的項目類型
至於其他的,大家可以自己玩一玩,還有就是是否Docker支持,這兩年Docker着實很火,我也會在以后的時間里,補上這塊兒的使用。。。

Duang ,然后就出現了,特別簡單的一個.Net Core API就這么誕生了,嗯不錯,基本的分成這幾個部分,是不是特別像一個控制台程序?而且真是簡潔了不少。
這里要注意下,關於Https選項問題,有很多小伙伴在以后的接口調用中,勾選了這個,但是還是一直使用 http 協議去訪問,導致找不到響應的接口地址。
1、是你的項目創建的時候,勾選了 Https 選項,如果你還沒有創建,那就可以不要勾選那個 HTTPS選項。
2、如果你的項目已經創建好了,每次訪問都是HTTPS的,但是你不想這么做,可以在 launthSettings.json 文件中,把sslPort 端口號改成0即可
2、新建項目(3.0SDK)
點擊下一步。
然后創建模板:
3、項目整體結構分析
接下來咱們看看這個項目都包含了哪些東西:

點開Controllers --> ValuesController 文件,你會發現四個方法,並且每個方法也有各自的特性,分別是HttpGet,HttpPost,HttpPut,HttpDelete,這四個就是傳說中的RESTful風格的編程。
為什么會有這種風格呢:
RESTful 風格接口實際情況是,我們在前后端在約定接口的時候,可以約定各種風格的接口,但是,RESTful 接口是目前來說比較流行的,並且在運用中比較方便和常見的接口。
雖然它有一些缺陷,目前 github 也在主推 GraphQL 這種新的接口風格,但目前國內來說還是 RESTful 接口風格比較普遍。並且,在掌握了 RESTful 接口風格之后,會深入的理解這種接口的優缺點,到時候,你自然會去想解決方案,並且在項目中實行新的更好的理念,所以,我這系列的博文,依然采用 http://cnodejs.org/ 網站提供的 RESTful 接口來實戰。
了解程序開發的都應該知道,我們所做的大多數操作都是對數據庫的四格操作 “增刪改查” 對應到我們的接口操作分別是:post 插入新數據delete 刪除數據put 修改數據get 查詢數據
注意,這里是我們約定,並非這些動作只能干這件事情。從表層來說,除get外的其他方法,沒有什么區別,都是一樣的。從深層來說包括 get在內的所有方法都是一模一樣的,沒有任何區別。但是,我們約定,每種動作對應不同的操作,這樣方便我們統一規范我們的所有操作。
假設,我們的接口是 /api/v1/love 這樣的接口,采用 RESTful 接口風格對應操作是如下的:get 操作 /api/v1/love獲取 /api/v1/love 的分頁列表數據,得到的主體,將是一個數組,我們可以用數據來遍歷循環列表post 操作 /api/v1/love我們會往 /api/v1/love 插入一條新的數據,我們插入的數據,將是JOSN利用對象傳輸的。get 操作 /api/v1/love/1我們獲取到一個 ID 為 1 的數據,數據一般為一個對象,里面包含了 1 的各項字段信息。put 操作 /api/v1/love/1我們向接口提交了一個新的信息,來修改 ID 為 1 的這條信息delete 操作 /api/v1/love/1我們向接口請求,刪除 ID 為 1 的這一條數據
由上述例子可知,我們實現了5種操作,但只用了兩個接口地址, /api/v1/love 和 /api/v1/love/1 。所以,采用這種接口風格,可以大幅的簡化我們的接口設計。

然后 F5 運行,就會看到接口地址,以及對應的內容,你可以根據自己的需要進行各種配置合適的路由,
這里要注意下,如果出現特性相同,方法同名,參數一樣的,編譯會報錯,起名字很重要。
還有,這里會自動跳轉到默認地址 api/values,當然是可以配置的,就在 Properties --> launchSettings.json 中

接下來點開 appsettings.json 文件,這里就是整個系統app的配置地址,更類似以前的web.config,以后大家會用到。
這里還要說一下[HttpGet("{id}"),Name="Get"] ,這個有時候會帶 Name,很多小伙伴不知道這是干啥的,我就簡單說一下:
[HttpGet("{id}", Name = "GetTodo")] public IActionResult GetById(long id)
"{id}"
是 todo
項 的 ID 的占位符變量。 調用 GetById
時,它會將 URL 中“{id}”的值分配給方法的 id
參數。Name = "GetTodo"
創建一個命名的路由,使你能夠 HTTP 響應中鏈接到此路由。 稍后將使用示例進行解釋。 有關詳細信息,請參閱路由到控制器操作,還有這個Attribute Routing in Web API 2
一般來說,路由名稱都是和路由url一一對應的,盡量不要重復,不過也很少有人寫這個,沒啥用,所以一般不要寫。
繼續往下,打開Startup.cs 文件這里是整個項目的啟動文件,所有的啟動相關的都會在這里配置,比如 依賴注入,跨域請求,Redis緩存等,更多詳情在以后的文章中都會有所提起

二、重要文件說明
1、Program.cs
namespace CoreBackend.Api { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .Build(); } }
這個Program是程序的入口, 看起來很眼熟, 是因為asp.net core application實際就是控制台程序(console application).
它是一個調用asp.net core 相關庫的console application.
Main方法里面的內容主要是用來配置和運行程序的.
因為我們的web程序需要一個宿主, 所以 BuildWebHost這個方法就創建了一個WebHostBuilder. 而且我們還需要Web Server.
asp.net core 自帶了兩種http servers, 一個是WebListener, 它只能用於windows系統, 另一個是kestrel, 它是跨平台的.
kestrel是默認的web server, 就是通過UseKestrel()這個方法來啟用的.
但是我們開發的時候使用的是IIS Express, 調用UseIISIntegration()這個方法是啟用IIS Express, 它作為Kestrel的Reverse Proxy server來用.
如果在windows服務器上部署的話, 就應該使用IIS作為Kestrel的反向代理服務器來管理和代理請求.
如果在linux上的話, 可以使用apache, nginx等等的作為kestrel的proxy server.
當然也可以單獨使用kestrel作為web 服務器, 但是使用iis作為reverse proxy還是有很多有優點的: 例如,IIS可以過濾請求, 管理證書, 程序崩潰時自動重啟等.
UseStartup<Startup>(), 這句話表示在程序啟動的時候, 我們會調用Startup這個類.
Build()完之后返回一個實現了IWebHost接口的實例(WebHostBuilder), 然后調用Run()就會運行Web程序, 並且阻止這個調用的線程, 直到程序關閉.
如果想要對AspNetCore源碼進行研究,可以查看源碼,這里提供兩個方法:
1、F12,當然這個不能看到詳細的,你需要安裝一個組件,VS2017 Resharper
2、查看Github 開源源碼 https://github.com/aspnet/MetaPackages/tree/master/src/Microsoft.AspNetCore
2、Startup.cs
namespace CoreBackend.Api { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
//判斷是否是環境變量 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //這個就是一個簡單的中間件寫法 app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } } }
其實Startup算是程序真正的切入點.
ConfigureServices方法是用來把services(各種服務, 例如identity, ef, mvc等等包括第三方的, 或者自己寫的)加入(register)到container(asp.net core的容器)中去, 並配置這些services. 這個container是用來進行dependency injection的(依賴注入). 所有注入的services(此外還包括一些框架已經注冊好的services) 在以后寫代碼的時候, 都可以將它們注入(inject)進去. 例如上面的Configure方法的參數, app, env, loggerFactory都是注入進去的services.
Configure方法是asp.net core程序用來具體指定如何處理每個http請求的, 例如我們可以讓這個程序知道我使用mvc來處理http請求, 那就調用app.UseMvc()這個方法就行. 但是目前, 所有的http請求都會導致返回"Hello World!".
看一看我們項目的最后,Configure方法是如何配置的:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { // 在開發環境中,使用異常頁面,這樣可以暴露錯誤堆棧信息,所以不要放在生產環境。 app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // 在非開發環境中,使用HTTP嚴格安全傳輸(or HSTS) 對於保護web安全是非常重要的。 // 強制實施 HTTPS 在 ASP.NET Core,配合 app.UseHttpsRedirection //app.UseHsts(); } #region Swagger app.UseSwagger(); app.UseSwaggerUI(c => { //之前是寫死的 //c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1"); //c.RoutePrefix = "";//路徑配置,設置為空,表示直接在根域名(localhost:8001)訪問該文件,注意localhost:8001/swagger是訪問不到的,去launchSettings.json把launchUrl去掉 //根據版本名稱倒序 遍歷展示 typeof(ApiVersions).GetEnumNames().OrderByDescending(e => e).ToList().ForEach(version => {
// 注意這個 ApiName 和 要和上邊 ConfigureServices 中配置swagger的name要大小寫一致,具體查看我的blog.core源碼 c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{ApiName} {version}"); }); }); #endregion #region Authen //app.UseMiddleware<JwtTokenAuth>();//注意此授權方法已經放棄,請使用下邊的官方驗證方法。但是如果你還想傳User的全局變量,還是可以繼續使用中間件 app.UseAuthentication(); #endregion #region CORS //跨域第二種方法,使用策略,詳細策略信息在ConfigureService中 app.UseCors("LimitRequests");//將 CORS 中間件添加到 web 應用程序管線中, 以允許跨域請求。 //跨域第一種版本,請要ConfigureService中配置服務 services.AddCors(); // app.UseCors(options => options.WithOrigins("http://localhost:8021").AllowAnyHeader() //.AllowAnyMethod()); #endregion // 跳轉https app.UseHttpsRedirection(); // 使用靜態文件 app.UseStaticFiles(); // 使用cookie app.UseCookiePolicy(); // 返回錯誤碼 app.UseStatusCodePages();//把錯誤碼返回前台,比如是404 app.UseMvc(); }
3、調試方法
.net core 調試的兩種方法
1、通過IIS調試
2、項目自帶的Kestrel web應用調式
三、注冊並使用MVC
因為asp.net core 2.0使用了一個大而全的metapackage, 所以這些基本的services和middleware是不需要另外安裝的.
首先, 在ConfigureServices里面向Container注冊MVC: services.AddMvc();
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); // 注冊MVC到Container }
然后再Configure里面告訴程序使用mvc中間件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc();
注意順序, 應該在處理異常的middleware后邊調用app.UseMvc(), 所以處理異常的middleware可以在把request交給mvc之間就處理異常, 更重要的是它還可以捕獲並處理返回MVC相關代碼執行中的異常.
然后別忘了把app.Run那部分代碼去掉. 然后改回到Develpment環境, 跑一下, 試試效果:
Chrome顯示了一個空白頁, 按F12, 顯示了404 Not Found錯誤.
這是因為我只添加了MVC middleware, 但是它啥也沒做, 也沒有找到任何可用於處理請求的代碼, 所以我們要添加Controller來返回數據/資源等等
四、核心知識點
1、Routing 路由
路由有兩種方式: Convention-based (按約定), attribute-based(基於路由屬性配置的).
其中convention-based (基於約定的) 主要用於MVC (返回View或者Razor Page那種的).
Web api 推薦使用attribute-based.
這種基於屬性配置的路由可以配置Controller或者Action級別, uri會根據Http method然后被匹配到一個controller里具體的action上.
常用的Http Method有:
- Get, 查詢, Attribute: HttpGet, 例如: '/api/product', '/api/product/1'
- POST, 創建, HttpPost, '/api/product'
- PUT 整體修改更新 HttpPut, '/api/product/1'
- PATCH 部分更新, HttpPatch, '/api/product/1'
- DELETE 刪除, HttpDelete, '/api/product/1
還有一個Route屬性(attribute)也可以用於Controller層, 它可以控制action級的URI前綴.
以下不是本系列,就看思路即可,不用敲代碼
//以下不是本系列教程,就看思路即可,不用敲代碼
namespace CoreBackend.Api.Controllers { //[Route("api/product")] [Route("api/[controller]")] public class ProductController: Controller { [HttpGet] public JsonResult GetProducts() { return new JsonResult(new List<Product> { new Product { Id = 1, Name = "牛奶", Price = 2.5f }, new Product { Id = 2, Name = "面包", Price = 4.5f } }); } } }
使用[Route("api/[controller]")], 它使得整個Controller下面所有action的uri前綴變成了"/api/product", 其中[controller]表示XxxController.cs中的Xxx(其實是小寫).
也可以具體指定, [Route("api/product")], 這樣做的好處是, 如果ProductController重構以后改名了, 只要不改Route里面的內容, 那么請求的地址不會發生變化.
然后在GetProducts方法上面, 寫上HttpGet, 也可以寫HttpGet(). 它里面還可以加參數,例如: HttpGet("all"), 那么這個Action的請求的地址就變成了 "/api/product/All".
2、內容協商 Content Negotiation
如果 web api提供了多種內容格式, 那么可以通過Accept Header來選擇最好的內容返回格式: 例如:
application/json, application/xml等等
如果設定的格式在web api里面沒有, 那么web api就會使用默認的格式.
asp.net core 默認提供的是json格式, 也可以配置xml等格式.
目前只考慮 Output formatter, 就是返回的內容格式.
如果想輸出xml格式,就配置這里:
3、創建Post Action
以下不是本系列,就看思路即可,不用敲代碼
//以下不是本系列教程,就看思路即可,不用敲代碼
[Route("{id}", Name = "GetProduct")] public IActionResult GetProduct(int id) { var product = ProductService...(x => x.Id == id); if (product == null) { return NotFound(); } return Ok(product); } [HttpPost] public IActionResult Post([FromBody] ProductCreation product) { if (product == null) { return BadRequest(); } var maxId = ProductService.Max(x => x.Id); var newProduct = new Product { Id = ++maxId, Name = product.Name, Price = product.Price }; ProductService.Add(newProduct); return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct); }
[HttpPost] 表示請求的謂詞是Post. 加上Controller的Route前綴, 那么訪問這個Action的地址就應該是: 'api/product'
后邊也可以跟着自定義的路由地址, 例如 [HttpPost("create")], 那么這個Action的路由地址就應該是: 'api/product/create'.
[FromBody] , 請求的body里面包含着方法需要的實體數據, 方法需要把這個數據Deserialize成ProductCreation, [FromBody]就是干這些活的.
客戶端程序可能會發起一個Bad的Request, 導致數據不能被Deserialize, 這時候參數product就會變成null. 所以這是一個客戶端發生的錯誤, 程序為讓客戶端知道是它引起了錯誤, 就應該返回一個Bad Request 400 (Bad Request表示客戶端引起的錯誤)的 Status Code.
傳遞進來的model類型是 ProductCreation, 而我們最終操作的類型是Product, 所以需要進行一個Map操作, 目前還是挨個屬性寫代碼進行Map吧, 以后會改成Automapper.
返回 CreatedAtRoute: 對於POST, 建議的返回Status Code 是 201 (Created), 可以使用CreatedAtRoute這個內置的Helper Method. 它可以返回一個帶有地址Header的Response, 這個Location Header將會包含一個URI, 通過這個URI可以找到我們新創建的實體數據. 這里就是指之前寫的GetProduct(int id)這個方法. 但是這個Action必須有一個路由的名字才可以引用它, 所以在GetProduct方法上的Route這個attribute里面加上Name="GetProduct", 然后在CreatedAtRoute方法第一個參數寫上這個名字就可以了, 盡管進行了引用, 但是Post方法走完的時候並不會調用GetProduct方法. CreatedAtRoute第二個參數就是對應着GetProduct的參數列表, 使用匿名類即可, 最后一個參數是我們剛剛創建的數據實體.
運行程序試驗一下, 注意需要在Headers里面設置Content-Type: application/json.
4、Validation 驗證
針對上面的Post方法, 如果請求沒有Body, 參數product就會是null, 這個我們已經判斷了; 如果body里面的數據所包含的屬性在product中不存在, 那么這個屬性就會被忽略.
但是如果body數據的屬性有問題, 比如說name沒有填寫, 或者name太長, 那么在執行action方法的時候就會報錯, 這時候框架會自動拋出500異常, 表示是服務器的錯誤, 這是不對的. 這種錯誤是由客戶端引起的, 所以需要返回400 Bad Request錯誤.
驗證Model/實體, asp.net core 內置可以使用 Data Annotations進行:
//以下不是本系列教程,就看思路即可,不用敲代碼
using System; using System.ComponentModel.DataAnnotations; namespace CoreBackend.Api.Dtos { public class ProductCreation { [Display(Name = "產品名稱")] [Required(ErrorMessage = "{0}是必填項")] // [MinLength(2, ErrorMessage = "{0}的最小長度是{1}")] // [MaxLength(10, ErrorMessage = "{0}的長度不可以超過{1}")]
[StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")] public string Name { get; set; } [Display(Name = "價格")] [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")] public float Price { get; set; } } }
這些Data Annotation (理解為用於驗證的注解), 可以在System.ComponentModel.DataAnnotation找到, 例如[Required]表示必填, [MinLength]表示最小長度, [StringLength]可以同時驗證最小和最大長度, [Range]表示數值的范圍等等很多.
[Display(Name="xxx")]的用處是, 給屬性起一個比較友好的名字.
其他的驗證注解都有一個屬性叫做 ErrorMessage (string), 表示如果驗證失敗, 就會把ErrorMessage的內容添加到錯誤結果里面去. 這個ErrorMessage可以使用參數, {0}表示Display的Name屬性, {1}表示當前注解的第一個變量, {2}表示當前注解的第二個變量.
在Controller里面添加驗證邏輯:
//以下不是本系列教程,就看思路即可,不用敲代碼
[HttpPost] public IActionResult Post([FromBody] ProductCreation product) { if (product == null) { return BadRequest(); } if (!ModelState.IsValid) { return BadRequest(ModelState); } var maxId = ProductService.Max(x => x.Id); var newProduct = new Product { Id = ++maxId, Name = product.Name, Price = product.Price }; ProductService.Add(newProduct); return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct); }
ModelState: 是一個Dictionary, 它里面是請求提交到Action的Name和Value的對們, 一個name對應着model的一個屬性, 它也包含了一個針對每個提交的屬性的錯誤信息的集合.
每次請求進到Action的時候, 我們在ProductCreationModel添加的那些注解的驗證, 就會被檢查. 只要其中有一個驗證沒通過, 那么ModelState.IsValid屬性就是False. 可以設置斷點查看ModelState里面都有哪些東西.
如果有錯誤的話, 我們可以把ModelState當作 Bad Request的參數一起返回到前台.
5、PUT請求
put應該用於對model進行完整的更新.
首先最好還是單獨為Put寫一個Dto Model, 盡管屬性可能都是一樣的, 但是也建議這樣寫, 實在不想寫也可以.
ProducModification.cs
public class ProductModification { [Display(Name = "產品名稱")] [Required(ErrorMessage = "{0}是必填項")] [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")] public string Name { get; set; } [Display(Name = "價格")] [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")] public float Price { get; set; } }
然后編寫Controller的方法:
//以下不是本系列教程,就看思路即可,不用敲代碼
[HttpPut("{id}")] public IActionResult Put(int id, [FromBody] ProductModification product) { if (product == null) { return BadRequest(); } if (product.Name == "產品") { ModelState.AddModelError("Name", "產品的名稱不可以是'產品'二字"); } if (!ModelState.IsValid) { return BadRequest(ModelState); } var model = ProductService.SingleOrDefault(x => x.Id == id); if (model == null) { return NotFound(); } model.Name = product.Name; model.Price = product.Price; // return Ok(model); return NoContent(); }
按照Http Put的約定, 需要一個id這樣的參數, 用於查找現有的model.
由於Put做的是完整的更新, 所以把ProducModification整個Model作為參數.
進來之后, 進行了一套和POST一模一樣的驗證, 這地方肯定可以改進, 如果驗證邏輯比較復雜的話, 到處寫同樣驗證邏輯肯定是不好的, 所以建議使用FluentValidation.
然后, 把ProductModification的屬性都映射查詢找到給Product, 這個以后用AutoMapper來映射.
返回: PUT建議返回NoContent(), 因為更新是客戶端發起的, 客戶端已經有了最新的值, 無須服務器再給它傳遞一次, 當然了, 如果有些值是在后台更新的, 那么也可以使用Ok(xxx)然后把更新后的model作為參數一起傳到前台.
五、結語
好啦,項目搭建就這么愉快的解決了,而且你也應該簡單了解了.Net Core API是如何安裝,創建,各個文件的意義以及如何運作,如何配置等,但是既然是接口,那一定是要前后端一起進行配置,使用,交流的平台,從上文看出,每次都特別麻煩,而且不直觀,UI 不友好,怎么辦呢?
下一節我們就使用一個神器 Swagger,一個快速,輕量級的項目RESTFUL接口的文檔在線自動生成+功能測試功能軟件。
Github && Gitee
https://github.com/anjoy8/Blog.Core.git
NOTE
如何不會使用Git,可以參考
https://www.jianshu.com/p/2b666a08a3b5