介绍#

当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求。

但是,你难道不认为创建一个能正常工作的项目还不够吗?同时这个项目不应该也是可维护和可读的吗?

事实证明,我们需要把更多的关注点放到我们项目的可读性和可维护性上。这背后的主要原因是我们或许不是这个项目的唯一编写者。一旦我们完成后,其他人也极有可能会加入到这里面来。

因此,我们应该把关注点放到哪里呢?

在这一份指南中,关于开发 .NET Core Web API 项目,我们将叙述一些我们认为会是最佳实践的方式。进而让我们的项目变得更好和更加具有可维护性。

现在,让我们开始想一些可以应用到 ASP.NET Web API 项目中的一些最佳实践。

Startup 类 和 服务配置#

STARTUP CLASS AND THE SERVICE CONFIGURATION

在 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 中内置方法的返回值和状态码。

使用最多的方法是:

  • OK => returns the 200 status code
  • NotFound => returns the 404 status code
  • BadRequest => returns the 400 status code
  • NoContent => returns the 204 status code
  • Created, CreatedAtRoute, CreatedAtAction => returns the 201 status code
  • Unauthorized => returns the 401 status code
  • Forbid => returns the 403 status code
  • StatusCode => returns the status code we provide as input

处理全局异常#

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();            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 projectpublic class CustomExceptionMiddleware{    private readonly RequestDelegate _next;    private readonly ILogger _logger;    public CustomExceptionMiddleware(RequestDelegate next, ILogger 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();    }}

之后,我们只需要将其注入到应用程序的请求管道中即可:

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();

现在,我们可以将上述注入的过滤器应用到我们的 action 中。

Microsoft.AspNetCore.All 元包#

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 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 passwordpublic string HashPassword(string password){    return Crypto.HashPassword(password);}// Verify the password hash against the given passwordpublic 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().AddXmlDataContractSerializerFormatters();}

但有时客户端会请求一个我们 Web API 不支持的格式,因此最好的实践方式是对于未经处理的请求格式统一返回 406 状态码。这种方式也同样能在 ConfigureServices 方法中进行简单配置:

Copypublic void ConfigureServices(IServiceCollection services){     services.AddControllers(option => option.ReturnHttpNotAcceptable = true).AddXmlDataContractSerializerFormatters();}

我们也可以创建我们自己的格式化规则。

这一部分内容是一个很大的主题,如果你希望了解更多,请查阅:Content Negotiation in .NET Core

使用 JWT#

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

总结#

读到这里,可能会有朋友对上述一些最佳实践不是很认同,因为全篇都没有谈及更切合项目的实践指南,比如 TDDDDD 等。但我个人认为上述所有的最佳实践是基础,只有把这些基础掌握了,才能更好地理解一些更高层次的实践指南。万丈高楼平地起,所以你可以把这看作是一篇面向新手的最佳实践指南。

在这份指南中,我们的主要目的是让你熟悉关于使用 .NET Core 开发 web API 项目时的一些最佳实践。这里面的部分内容在其它框架中也同样适用。因此,熟练掌握它们很有用。

delphi7下实现http的post_ASP.NET Core Web API 实现过程相关推荐

  1. 重温.NET下Assembly的加载过程 ASP.NET Core Web API下事件驱动型架构的实现(三):基于RabbitMQ的事件总线...

    重温.NET下Assembly的加载过程 最近在工作中牵涉到了.NET下的一个古老的问题:Assembly的加载过程.虽然网上有很多文章介绍这部分内容,很多文章也是很久以前就已经出现了,但阅读之后发现 ...

  2. 【壹刊】Azure AD 保护的 ASP.NET Core Web API (下)

    一,引言 上一节讲到如何在我们的项目中集成Azure AD 保护我们的API资源,以及在项目中集成Swagger,并且如何把Swagger作为一个客户端进行认证和授权去访问我们的WebApi资源的?本 ...

  3. ASP.NET Core Web API下事件驱动型架构的实现(四):CQRS架构中聚合与聚合根的实现

    在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅.通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现.接下来对于事件驱动型架构的讨论,就需 ...

  4. ASP.NET Core Web API下事件驱动型架构的实现(三):基于RabbitMQ的事件总线

    在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容.下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了. 其 ...

  5. ASP.NET Core Web API下事件驱动型架构的实现(二):事件处理器中对象生命周期的管理

    在ASP.NET Core Web API下事件驱动型架构的实现(一):一个简单的实现中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发.订阅和处理的流程.这种实现太简单了,百十行 ...

  6. ASP.NET Core Web API下事件驱动型架构的实现(一):一个简单的实现

    很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构.这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地 ...

  7. Docker容器环境下ASP.NET Core Web API应用程序的调试

    本文主要介绍通过Visual Studio 2015 Tools for Docker – Preview插件,在Docker容器环境下,对ASP.NET Core Web API应用程序进行调试.在 ...

  8. core webapi缩略图_在ASP.NET Core Web API 项目里无法访问(wwwroot)下的文件

    新建 ASP.NET Core Web API 项目 -- RESTFul 风格 Hello World! 一.创建一个空项目 请查看 新建 .NET Core 项目 -- Hello World!  ...

  9. 如何测试ASP.NET Core Web API

    在本文中,我们将研究如何测试你的ASP .NET Core 2.0 Web API解决方案.我们将了解使用单元测试进行内部测试,使用全新的ASP .NET Core的集成测试框架来进行外部测试. 本文 ...

最新文章

  1. Hexo集成Valine实现评论留言
  2. 面试--js实现继承的几种方式
  3. php生成图片表格自动换行_实用的财务做账表格,点击即可一键生成报表,并自动结转...
  4. ASP.NET MVC Core的TagHelper (高级特性)
  5. Py之cupy:cupy的简介、安装、使用方法之详细攻略
  6. AEF横空出世——几个重要的概念
  7. 树莓派使用STEP1:装系统
  8. 李洋疯狂C语言之将”you are come from shanghai ”倒置为”shanghai from come are you”,将句子中的单词位置倒置,而不改变单词内部结构
  9. MindSpore实践:对篮球运动员目标的检测
  10. Linux 平台下 Tomcat 的安装与优化
  11. linux查看schema版本,Schema 日期
  12. python 代码行数统计工具_Python实现一个代码行数统计工具
  13. Python 导入通讯录:将.csv文件转换为.vcf文件
  14. C库-atoi(), atol(), atoll(), atof(), ceil(), ceilf(), ceill()
  15. CE 开启 DBVM
  16. vue 跳转到指定路由地址 (可附带参数)
  17. MySQL 3306端口被占用 ERROR 1043 (08S01): Bad handshake
  18. python学习笔记---函数【廖雪峰】
  19. 学习:浏览器设置Burpsuite代理后打不开网页
  20. 最新精美UI外卖小生鲜配送程序源码+支持优惠券

热门文章

  1. linux下vi编辑器的命令大全,linux下VI编辑器命令大全(超级完整版)
  2. 读写文件--with open
  3. servlet ---- 简单案例
  4. mysql乱码utfmb4_MySQL乱码问题以及utf8mb4字符集
  5. 学习笔记之grub应用
  6. java主函数_《左手 Java 右手 Python 》之 Java 的安装与初识(1)
  7. h5和web前端的区别
  8. nginx tomcat spring-boot 对json等数据压缩
  9. MYSQL5.6 设置root用户密码
  10. n卡eth挖矿设置_“挖矿”再度兴起,N卡停产遇到ETH大涨,显卡会不会涨到18年那样...