当咱们编写一个项目的时候,咱们的主要目标是使它能如期运行,并尽量地知足全部用户需求。算法
可是,你难道不认为建立一个能正常工做的项目还不够吗?同时这个项目不该该也是可维护和可读的吗?数据库
事实证实,咱们须要把更多的关注点放到咱们项目的可读性和可维护性上。这背后的主要缘由是咱们或许不是这个项目的惟一编写者。一旦咱们完成后,其余人也极有可能会加入到这里面来。json
所以,咱们应该把关注点放到哪里呢?c#
在这一份指南中,关于开发 .NET Core Web API 项目,咱们将叙述一些咱们认为会是最佳实践的方式。进而让咱们的项目变得更好和更加具备可维护性。api
如今,让咱们开始想一些能够应用到 ASP.NET Web API 项目中的一些最佳实践。安全
STARTUP CLASS AND THE SERVICE CONFIGURATIONapp
在 Startup
类中,有两个方法:ConfigureServices
是用于服务注册,Configure
方法是向应用程序的请求管道中添加中间件。框架
所以,最好的方式是保持 ConfigureServices
方法简洁,而且尽量地具备可读性。固然,咱们须要在该方法内部编写代码来注册服务,可是咱们能够经过使用 扩展方法
来让咱们的代码更加地可读和可维护。async
例如,让咱们看一个注册 CORS 服务的很差方式:
public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); }
尽管这种方式看起来挺好,也能正常地将 CORS 服务注册成功。可是想象一下,在注册了十几个服务以后这个方法体的长度。
这样一点也不具备可读性。
一种好的方式是经过在扩展类中建立静态方法:
public static class ServiceExtensions { public static void ConfigureCors(this IServiceCollection services) { services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); } }
而后,只须要调用这个扩展方法便可:
public 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 内置功能。经过这种方式,咱们能够在任何控制器中经过构造函数注入的方式来使用。
public class OwnerController: Controller { private readonly IRepository _repository; public OwnerController(IRepository repository) { _repository = repository; } }
CONTROLLERS
控制器应该始终尽可能保持整洁。咱们不该该将任何业务逻辑放置于内。
所以,咱们的控制器应该经过构造函数注入的方式接收服务实例,并组织 HTTP 的操做方法(GET,POST,PUT,DELETE,PATCH...):
public 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 请求,验证模型,捕捉异常和返回响应。
[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
方法:
public 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(); }); }
咱们也能够经过建立自定义的中间件来实现咱们的自定义异常处理:
// 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>(); } }
以后,咱们只须要将其注入到应用程序的请求管道中便可:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseCustomExceptionMiddleware(); }
USING ACTIONFILTERS TO REMOVE DUPLICATED CODE
ASP.NET Core 的过滤器可让咱们在请求管道的特定状态以前或以后运行一些代码。所以若是咱们的 action 中有重复验证的话,可使用它来简化验证操做。
当咱们在 action 方法中处理 PUT 或者 POST 请求时,咱们须要验证咱们的模型对象是否符合咱们的预期。做为结果,这将致使咱们的验证代码重复,咱们但愿避免出现这种状况,(基本上,咱们应该尽咱们所能避免出现任何代码重复。)咱们能够在代码中经过使用 ActionFilter 来代替咱们的验证代码:
if (!ModelState.IsValid) { //bad request and logging logic }
咱们能够建立一个过滤器:
public class ModelValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } }
而后在 Startup
类的 ConfigureServices
函数中将其注入:
services.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] 属性来在控制器的顶部进行标注:
[Route("api/[controller]")] public class OwnerController : Controller { [Route("{id}")] [HttpGet] public IActionResult GetOwnerById(Guid id) { } }
还有另外一种方式为控制器和操做建立路由规则:
[Route("api/owner")] public class OwnerController : Controller { [Route("{id}")] [HttpGet] public IActionResult GetOwnerById(Guid id) { } }
对于这两种方式哪一种会好一些存在分歧,可是咱们常常建议采用第二种方式。这是咱们一直在项目中采用的方式。
当咱们谈论路由时,咱们须要提到路由的命名规则。咱们能够为咱们的操做使用描述性名称,但对于 路由/节点,咱们应该使用 NOUNS 而不是 VERBS。
一个较差的示例:
[Route("api/owner")] public class OwnerController : Controller { [HttpGet("getAllOwners")] public IActionResult GetAllOwners() { } [HttpGet("getOwnerById/{id}"] public IActionResult GetOwnerById(Guid id) { } }
一个较好的示例:
[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
接口实现了它本身的日志记录。经过借助依赖注入机制,它能够很容易地使用。
public 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 内置的日志系统。
CRYPTOHELPER
咱们不会建议将密码以明文形式存储到数据库中。处于安全缘由,咱们须要对其进行哈希处理。这超出了本指南的内容范围。互联网上有大量哈希算法,其中不乏一些不错的方法来将密码进行哈希处理。
可是若是须要为 .NET Core 的应用程序提供易于使用的加密类库,CryptoHelper 是一个不错的选择。
CryptoHelper 是适用于 .NET Core 的独立密码哈希库,它是基于 PBKDF2 来实现的。经过建立 Data Protection
栈来将密码进行哈希化。这个类库在 NuGet 上是可用的,而且使用也很简单:
using 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 格式呢?
为了解决这个问题,咱们须要进行服务端配置,用于按需格式化咱们的响应结果:
public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddXmlSerializerFormatters(); }
但有时客户端会请求一个咱们 Web API 不支持的格式,所以最好的实践方式是对于未经处理的请求格式统一返回 406 状态码。这种方式也一样能在 ConfigureServices 方法中进行简单配置:
public 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 认证:
public 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 中调用下面一段代码:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseAuthentication(); }
此外,建立 Token 可使用以下方式:
var 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 的用户验证能够在控制器中使用以下方式:
var 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 项目时的一些最佳实践。这里面的部份内容在其它框架中也一样适用。所以,熟练掌握它们颇有用。
很是感谢你能阅读这份指南,但愿它能对你有所帮助。