Best Practices Guide ASP.NET Core Web API

Original Address: ASP.NET-Core-Web-API-Best-Practices-Guide

Introduction

When we write a project, our main goal is to make it run on schedule, and meet all the needs of users as possible.

But, do not you think that can work to create a project that enough? At the same time the project should also not maintainable and readable it?

It turns out that we need to put more focus put on the readability and maintainability of our project. The main reason behind this is that we may not be the only writer of this project. Once we have completed, others most likely will be added to this come inside.

Therefore, we should put the focus where it?

In this is a guide, on the development of .NET Core Web API project, we will describe some of us thought would be the best way to practice. And then make our projects better and more maintainable.

Now, let's start thinking about some of the best practices can be applied to some of the ASP.NET Web API project.

Startup classes and services configuration

STARTUP CLASS AND THE SERVICE CONFIGURATION

In Startupclass, there are two methods: ConfigureServicesfor service registration, Configureis added to the request pipeline middleware application.

Therefore, the best way is to keep the ConfigureServicesmethod is simple and readable as possible. Of course, we need to write the code inside the method to register the service, but we can use 扩展方法to make our code more readable and maintainable manner.

For example, let's look at a bad way CORS registration services:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options => 
    {
        options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials());
    });
}

While this approach looks good, can normally be successfully registered CORS service. But imagine the length of the body after more than a dozen registered service this method.

Such is not readable.

A good way is by creating an extended class static method:

public static class ServiceExtensions
{
    public static void ConfigureCors(this IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
        });
    }
}

Then, you only need to call this extension method:

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureCors();
}

Read more about .NET Core project configuration, please see: .NET Core Project the Configuration

Project Organization

PROJECT ORGANIZATION

We should try to split our application into multiple small projects. In this way, we can get the best project organization, and can separation of concerns (SoC). Business logic of our entities, contracts, access to database operations, log information or send e-mail should always be placed in a separate .NET Core class library project.

Each application should include small projects in multiple folders used to organize the business logic.

Here is a simple example to show how a complex project should be organized:

Set on the environment

ENVIRONMENT BASED SETTINGS

When we develop an application, it is in the development environment. But once we released, it will be in a production environment. Therefore, each configured to isolate the environment is often a good way to practice.

In .NET Core, which is very easy to implement.

Once we create a good project, there is already a appsettings.jsonfile, expand it when we will see the appsettings.Development.jsonfile:

All settings in this file will be used for the development environment.

We should add another file appsettings.Production.json, which is used in the production environment:

Production file will be located under development files.

After setting changes, we will be able to load files by different appsettings different configurations, depending on the application is currently in our environment, .NET Core will provide us with the correct settings. For more on this topic, please refer to: Multiple Environments in ASP.NET Core.

Data Access Layer

DATA ACCESS LAYER

In some different examples tutorial, we may see DAL implementation of the main project, and each controller in both instances. We do not recommend it.

当我们编写 DAL 时,我们应该将其作为一个独立的服务来创建。在 .NET Core 项目中,这一点很重要,因为当我们将 DAL 作为一个独立的服务时,我们就可以将其直接注入到 IOC(控制反转)容器中。IOC 是 .NET Core 内置功能。通过这种方式,我们可以在任何控制器中通过构造函数注入的方式来使用。

public class OwnerController: Controller
{
    private IRepository _repository;
    public OwnerController(IRepository repository)
    {
        _repository = repository;
    }
}

控制器

CONTROLLERS

控制器应该始终尽量保持整洁。我们不应该将任何业务逻辑放置于内。

因此,我们的控制器应该通过构造函数注入的方式接收服务实例,并组织 HTTP 的操作方法(GET,POST,PUT,DELETE,PATCH...):

public class OwnerController : Controller
{
    private ILoggerManager _logger;
    private 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 中内置方法的返回值和状态码。

使用最多的方法是:

  • 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 方法:

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 元包

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

使用 JWT

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.UtcNow.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-1authentication-aspnetcore-jwt-2

总结

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

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

非常感谢你能阅读这份指南,希望它能对你有所帮助。

Guess you like

Origin www.cnblogs.com/hippieZhou/p/11966373.html
Recommended