轉自web
當咱們編寫一個項目的時候,咱們的主要目標是使它能如期運行,並儘量地知足全部用戶需求。算法
可是,你難道不認爲建立一個能正常工做的項目還不夠嗎?同時這個項目不該該也是可維護和可讀的嗎?數據庫
事實證實,咱們須要把更多的關注點放到咱們項目的可讀性和可維護性上。這背後的主要緣由是咱們或許不是這個項目的惟一編寫者。一旦咱們完成後,其餘人也極有可能會加入到這裏面來。json
所以,咱們應該把關注點放到哪裏呢?c#
在這一份指南中,關於開發 .NET Core Web API 項目,咱們將敘述一些咱們認爲會是最佳實踐的方式。進而讓咱們的項目變得更好和更加具備可維護性。api
如今,讓咱們開始想一些能夠應用到 ASP.NET Web API 項目中的一些最佳實踐。安全
STARTUP CLASS AND THE SERVICE CONFIGURATIONapp
在 Startup
類中,有兩個方法:ConfigureServices
是用於服務註冊,Configure
方法是嚮應用程序的請求管道中添加中間件。框架
所以,最好的方式是保持 ConfigureServices
方法簡潔,而且儘量地具備可讀性。固然,咱們須要在該方法內部編寫代碼來註冊服務,可是咱們能夠經過使用 擴展方法
來讓咱們的代碼更加地可讀和可維護。
例如,讓咱們看一個註冊 CORS 服務的很差方式:
Copypublic void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); }
儘管這種方式看起來挺好,也能正常地將 CORS 服務註冊成功。可是想象一下,在註冊了十幾個服務以後這個方法體的長度。
這樣一點也不具備可讀性。
一種好的方式是經過在擴展類中建立靜態方法:
Copypublic static class ServiceExtensions { public static void ConfigureCors(this IServiceCollection services) { services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); } }
而後,只須要調用這個擴展方法便可:
Copypublic void ConfigureServices(IServiceCollection services) { services.ConfigureCors(); }
瞭解更多關於 .NET Core 的項目配置,請查看:.NET Core Project Configuration
PROJECT ORGANIZATION
咱們應該嘗試將咱們的應用程序拆分爲多個小項目。經過這種方式,咱們能夠得到最佳的項目組織方式,並能將關注點分離(SoC)。咱們的實體、契約、訪問數據庫操做、記錄信息或者發送郵件的業務邏輯應該始終放在單獨的 .NET Core 類庫項目中。
應用程序中的每一個小項目都應該包含多個文件夾用來組織業務邏輯。
這裏有個簡單的示例用來展現一個複雜的項目應該如何組織:
ENVIRONMENT BASED SETTINGS
當咱們開發應用程序時,它處於開發環境。可是一旦咱們發佈以後,它將處於生產環境。所以,將每一個環境進行隔離配置每每是一種好的實踐方式。
在 .NET Core 中,這一點很容易實現。
一旦咱們建立好了項目,就已經有一個 appsettings.json
文件,當咱們展開它時會看到 appsettings.Development.json
文件:
此文件中的全部設置將用於開發環境。
咱們應該添加另外一個文件 appsettings.Production.json
,將其用於生產環境:
生產文件將位於開發文件下面。
設置修改後,咱們就能夠經過不一樣的 appsettings 文件來加載不一樣的配置,取決於咱們應用程序當前所處環境,.NET Core 將會給咱們提供正確的設置。更多關於這一主題,請查閱:Multiple Environments in ASP.NET Core.
DATA ACCESS LAYER
在一些不一樣的示例教程中,咱們可能看到 DAL 的實如今主項目中,而且每一個控制器中都有實例。咱們不建議這麼作。
當咱們編寫 DAL 時,咱們應該將其做爲一個獨立的服務來建立。在 .NET Core 項目中,這一點很重要,由於當咱們將 DAL 做爲一個獨立的服務時,咱們就能夠將其直接注入到 IOC(控制反轉)容器中。IOC 是 .NET Core 內置功能。經過這種方式,咱們能夠在任何控制器中經過構造函數注入的方式來使用。
Copypublic class OwnerController: Controller { private readonly IRepository _repository; public OwnerController(IRepository repository) { _repository = repository; } }
CONTROLLERS
控制器應該始終儘可能保持整潔。咱們不該該將任何業務邏輯放置於內。
所以,咱們的控制器應該經過構造函數注入的方式接收服務實例,並組織 HTTP 的操做方法(GET,POST,PUT,DELETE,PATCH...):
Copypublic class OwnerController : Controller { private readonly ILoggerManager _logger; private readonly IRepository _repository; public OwnerController(ILoggerManager logger, IRepository repository) { _logger = logger; _repository = repository; } [HttpGet] public IActionResult GetAllOwners() { } [HttpGet("{id}", Name = "OwnerById")] public IActionResult GetOwnerById(Guid id) { } [HttpGet("{id}/account")] public IActionResult GetOwnerWithDetails(Guid id) { } [HttpPost] public IActionResult CreateOwner([FromBody]Owner owner) { } [HttpPut("{id}")] public IActionResult UpdateOwner(Guid id, [FromBody]Owner owner) { } [HttpDelete("{id}")] public IActionResult DeleteOwner(Guid id) { } }
咱們的 Action 應該儘可能保持簡潔,它們的職責應該包括處理 HTTP 請求,驗證模型,捕捉異常和返回響應。
Copy[HttpPost] public IActionResult CreateOwner([FromBody]Owner owner) { try { if (owner.IsObjectNull()) { return BadRequest("Owner object is null"); } if (!ModelState.IsValid) { return BadRequest("Invalid model object"); } _repository.Owner.CreateOwner(owner); return CreatedAtRoute("OwnerById", new { id = owner.Id }, owner); } catch (Exception ex) { _logger.LogError($"Something went wrong inside the CreateOwner action: { ex} "); return StatusCode(500, "Internal server error"); } }
在大多數狀況下,咱們的 action 應該將 IActonResult
做爲返回類型(有時咱們但願返回一個特定類型或者是 JsonResult
...)。經過使用這種方式,咱們能夠很好地使用 .NET Core 中內置方法的返回值和狀態碼。
使用最多的方法是:
HANDLING ERRORS GLOBALLY
在上面的示例中,咱們的 action 內部有一個 try-catch
代碼塊。這一點很重要,咱們須要在咱們的 action 方法體中處理全部的異常(包括未處理的)。一些開發者在 action 中使用 try-catch
代碼塊,這種方式明顯沒有任何問題。但咱們但願 action 儘可能保持簡潔。所以,從咱們的 action 中刪除 try-catch
,並將其放在一個集中的地方會是一種更好的方式。.NET Core 給咱們提供了一種處理全局異常的方式,只須要稍加修改,就可使用內置且完善的的中間件。咱們須要作的修改就是在 Startup
類中修改 Configure
方法:
Copypublic void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseExceptionHandler(config => { config.Run(async context => { context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; var error = context.Features.Get<IExceptionHandlerFeature>(); if (error != null) { var ex = error.Error; await context.Response.WriteAsync(new ErrorModel { StatusCode = 500, ErrorMessage = ex.Message }.ToString()); } }); }); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
咱們也能夠經過建立自定義的中間件來實現咱們的自定義異常處理:
Copy// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project public class CustomExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger<CustomExceptionMiddleware> _logger; public CustomExceptionMiddleware(RequestDelegate next, ILogger<CustomExceptionMiddleware> logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext httpContext) { try { await _next(httpContext); } catch (Exception ex) { _logger.LogError("Unhandled exception....", ex); await HandleExceptionAsync(httpContext, ex); } } private Task HandleExceptionAsync(HttpContext httpContext, Exception ex) { //todo return Task.CompletedTask; } } // Extension method used to add the middleware to the HTTP request pipeline. public static class CustomExceptionMiddlewareExtensions { public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<CustomExceptionMiddleware>(); } }
以後,咱們只須要將其注入到應用程序的請求管道中便可:
Copypublic void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseCustomExceptionMiddleware(); }
USING ACTIONFILTERS TO REMOVE DUPLICATED CODE
ASP.NET Core 的過濾器可讓咱們在請求管道的特定狀態以前或以後運行一些代碼。所以若是咱們的 action 中有重複驗證的話,可使用它來簡化驗證操做。
當咱們在 action 方法中處理 PUT 或者 POST 請求時,咱們須要驗證咱們的模型對象是否符合咱們的預期。做爲結果,這將致使咱們的驗證代碼重複,咱們但願避免出現這種狀況,(基本上,咱們應該盡咱們所能避免出現任何代碼重複。)咱們能夠在代碼中經過使用 ActionFilter 來代替咱們的驗證代碼:
Copyif (!ModelState.IsValid) { //bad request and logging logic }
咱們能夠建立一個過濾器:
Copypublic class ModelValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } }
而後在 Startup
類的 ConfigureServices
函數中將其注入:
Copyservices.AddScoped<ModelValidationAttribute>();
如今,咱們能夠將上述注入的過濾器應用到咱們的 action 中。
MICROSOFT.ASPNETCORE.ALL META-PACKAGE
注:若是你使用的是 2.1 和更高版本的 ASP.NET Core。建議使用 Microsoft.AspNetCore.App 包,而不是 Microsoft.AspNetCore.All。這一切都是出於安全緣由。此外,若是使用 2.1 版本建立新的 WebAPI 項目,咱們將自動獲取 AspNetCore.App 包,而不是 AspNetCore.All。
這個元包包含了全部 AspNetCore 的相關包,EntityFrameworkCore 包,SignalR 包(version 2.1) 和依賴框架運行的支持包。採用這種方式建立一個新項目很方便,由於咱們不須要手動安裝一些咱們可能使用到的包。
固然,爲了能使用 Microsoft.AspNetCore.all 元包,須要確保你的機器安裝了 .NET Core Runtime。
ROUTING
在 .NET Core Web API 項目中,咱們應該使用屬性路由代替傳統路由,這是由於屬性路由能夠幫助咱們匹配路由參數名稱與 Action 內的實際參數方法。另外一個緣由是路由參數的描述,對咱們而言,一個名爲 "ownerId" 的參數要比 "id" 更加具備可讀性。
咱們可使用 [Route] 屬性來在控制器的頂部進行標註:
Copy[Route("api/[controller]")] public class OwnerController : Controller { [Route("{id}")] [HttpGet] public IActionResult GetOwnerById(Guid id) { } }
還有另外一種方式爲控制器和操做建立路由規則:
Copy[Route("api/owner")] public class OwnerController : Controller { [Route("{id}")] [HttpGet] public IActionResult GetOwnerById(Guid id) { } }
對於這兩種方式哪一種會好一些存在分歧,可是咱們常常建議採用第二種方式。這是咱們一直在項目中採用的方式。
當咱們談論路由時,咱們須要提到路由的命名規則。咱們能夠爲咱們的操做使用描述性名稱,但對於 路由/節點,咱們應該使用 NOUNS 而不是 VERBS。
一個較差的示例:
Copy[Route("api/owner")] public class OwnerController : Controller { [HttpGet("getAllOwners")] public IActionResult GetAllOwners() { } [HttpGet("getOwnerById/{id}"] public IActionResult GetOwnerById(Guid id) { } }
一個較好的示例:
Copy[Route("api/owner")] public class OwnerController : Controller { [HttpGet] public IActionResult GetAllOwners() { } [HttpGet("{id}"] public IActionResult GetOwnerById(Guid id) { } }
更多關於 Restful 實踐的細節解釋,請查閱:Top REST API Best Practices
LOGGING
若是咱們打算將咱們的應用程序發佈到生產環境,咱們應該在合適的位置添加一個日誌記錄機制。在生產環境中記錄日誌對於咱們梳理應用程序的運行頗有幫助。
.NET Core 經過繼承 ILogger
接口實現了它本身的日誌記錄。經過藉助依賴注入機制,它能夠很容易地使用。
Copypublic class TestController: Controller { private readonly ILogger _logger; public TestController(ILogger<TestController> logger) { _logger = logger; } }
而後,在咱們的 action 中,咱們能夠經過使用 _logger 對象藉助不一樣的日誌級別來記錄日誌。
.NET Core 支持使用於各類日誌記錄的 Provider。所以,咱們可能會在項目中使用不一樣的 Provider 來實現咱們的日誌邏輯。
NLog 是一個很不錯的能夠用於咱們自定義的日誌邏輯類庫,它極具擴展性。支持結構化日誌,且易於配置。咱們能夠將信息記錄到控制檯,文件甚至是數據庫中。
想了解更多關於該類庫在 .NET Core 中的應用,請查閱:.NET Core series – Logging With NLog.
Serilog 也是一個很不錯的類庫,它適用於 .NET Core 內置的日誌系統。
這裏有一個能提升日誌性能的小技巧:字符串拼接建議使用
_logger.LogInformation("{0},{1}", DateTime.Now, "info")
方式來記錄日誌,而不是_logger.LogInformation($"{DateTime.Now},info")
。
CRYPTOHELPER
咱們不會建議將密碼以明文形式存儲到數據庫中。處於安全緣由,咱們須要對其進行哈希處理。這超出了本指南的內容範圍。互聯網上有大量哈希算法,其中不乏一些不錯的方法來將密碼進行哈希處理。
可是若是須要爲 .NET Core 的應用程序提供易於使用的加密類庫,CryptoHelper 是一個不錯的選擇。
CryptoHelper 是適用於 .NET Core 的獨立密碼哈希庫,它是基於 PBKDF2 來實現的。經過建立 Data Protection
棧來將密碼進行哈希化。這個類庫在 NuGet 上是可用的,而且使用也很簡單:
Copyusing CryptoHelper; // Hash a password public string HashPassword(string password) { return Crypto.HashPassword(password); } // Verify the password hash against the given password public bool VerifyPassword(string hash, string password) { return Crypto.VerifyHashedPassword(hash, password); }
CONTENT NEGOTIATION
默認狀況下,.NET Core Web API 會返回 JSON 格式的結果。大多數狀況下,這是咱們所但願的。
可是若是客戶但願咱們的 Web API 返回其它的響應格式,例如 XML 格式呢?
爲了解決這個問題,咱們須要進行服務端配置,用於按需格式化咱們的響應結果:
Copypublic void ConfigureServices(IServiceCollection services) { services.AddControllers().AddXmlSerializerFormatters(); }
但有時客戶端會請求一個咱們 Web API 不支持的格式,所以最好的實踐方式是對於未經處理的請求格式統一返回 406 狀態碼。這種方式也一樣能在 ConfigureServices 方法中進行簡單配置:
Copypublic void ConfigureServices(IServiceCollection services) { services.AddControllers(options => options.ReturnHttpNotAcceptable = true).AddXmlSerializerFormatters(); }
咱們也能夠建立咱們本身的格式化規則。
這一部份內容是一個很大的主題,若是你但願瞭解更多,請查閱:Content Negotiation in .NET Core
USING JWT
現現在的 Web 開發中,JSON Web Tokens (JWT) 變得愈來愈流行。得益於 .NET Core 內置了對 JWT 的支持,所以實現起來很是容易。JWT 是一個開發標準,它容許咱們以 JSON 格式在服務端和客戶端進行安全的數據傳輸。
咱們能夠在 ConfigureServices 中配置 JWT 認證:
Copypublic void ConfigureServices(IServiceCollection services) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = _authToken.Issuer, ValidateAudience = true, ValidAudience = _authToken.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_authToken.Key)), RequireExpirationTime = true, ValidateLifetime = true, //others }; }); }
爲了能在應用程序中使用它,咱們還須要在 Configure 中調用下面一段代碼:
Copypublic void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseAuthentication(); }
此外,建立 Token 可使用以下方式:
Copyvar securityToken = new JwtSecurityToken( claims: new Claim[] { new Claim(ClaimTypes.NameIdentifier,user.Id), new Claim(ClaimTypes.Email,user.Email) }, issuer: _authToken.Issuer, audience: _authToken.Audience, notBefore: DateTime.Now, expires: DateTime.Now.AddDays(_authToken.Expires), signingCredentials: new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_authToken.Key)), SecurityAlgorithms.HmacSha256Signature)); Token = new JwtSecurityTokenHandler().WriteToken(securityToken)
基於 Token 的用戶驗證能夠在控制器中使用以下方式:
Copyvar auth = await HttpContext.AuthenticateAsync(); var id = auth.Principal.Claims.FirstOrDefault(x => x.Type.Equals(ClaimTypes.NameIdentifier))?.Value;
咱們也能夠將 JWT 用於受權部分,只需添加角色聲明到 JWT 配置中便可。
更多關於 .NET Core 中 JWT 認證和受權部分,請查閱:authentication-aspnetcore-jwt-1 和 authentication-aspnetcore-jwt-2
讀到這裏,可能會有朋友對上述一些最佳實踐不是很認同,由於全篇都沒有談及更切合項目的實踐指南,好比 TDD 、DDD 等。但我我的認爲上述全部的最佳實踐是基礎,只有把這些基礎掌握了,才能更好地理解一些更高層次的實踐指南。萬丈高樓平地起,因此你能夠把這看做是一篇面向新手的最佳實踐指南。
在這份指南中,咱們的主要目的是讓你熟悉關於使用 .NET Core 開發 web API 項目時的一些最佳實踐。這裏面的部份內容在其它框架中也一樣適用。所以,熟練掌握它們頗有用。
很是感謝你能閱讀這份指南,但願它能對你有所幫助。