[Turn] ASP.NET Core Web API Best Practices Guide

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

Transfer from

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:

Copypublic 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:

Copypublic 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:

Copypublic 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:

img

Based on the environment setting #

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:

img

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:

img

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.

When we write DAL, we should be as a stand-alone service to create. In .NET Core project, This is important, because when we DAL as a stand-alone service, we can be injected directly into the IOC (Inversion of Control) containers. IOC is .NET Core built-in feature. In this way, we can use a constructor by way of injection in any controller.

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

Controller #

CONTROLLERS

The controller should always be kept clean and tidy. We should not be placed in any business logic.

Therefore, we should be injected through the controller constructor embodiment receives the service instance, and method of operation of the tissue 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)
    {
    }
}

Our Action should try to keep it simple, their mandate should include handling HTTP requests, validate the model, catches the exception and returns a response.

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");
    }
}

In most cases, our action should be IActonResultused as the return type (sometimes we want to return to a particular type or JsonResult...). By this way, we can make good use of the built in .NET Core return value and status codes.

Most used method is:

  • 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

Global exception handling #

HANDLING ERRORS GLOBALLY

In the example above, we have action inside a try-catchblock of code. This is important, we need to deal with all exceptions (including unprocessed) in our action method body. Some developers used in the action try-catchblock, clearly this way without any problems. But we want to try to keep it simple action. Therefore, remove it from our action in try-catchand place a centralized place would be a better way. .NET Core provides us with a way to deal with global exception, requiring only minor modifications, you can use the built-in sound and middleware. We need to do is modify the Startupmodified class Configuremethod:

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

We can also achieve our custom exception handling by creating a custom middleware:

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

After that, we only need to inject it into the request pipeline application to:

Copypublic void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseCustomExceptionMiddleware();
}

Using a filter to remove duplicate code #

USING ACTIONFILTERS TO REMOVE DUPLICATED CODE

ASP.NET Core filter allows us to run some code before requesting a particular state or after pipeline. So if we have replicated the action, then it may be used to simplify the verification operation.

When we deal with PUT or POST request method in action, we need to verify that our model object in line with our expectations. As a result, this will lead us to repeat the authentication code, we want to avoid this situation, (substantially, we should do all we avoid any code duplication.) We may be replaced by the use of our verification code ActionFilter Code:

Copyif (!ModelState.IsValid)
{
    //bad request and logging logic
}

We can create a filter:

Copypublic class ModelValidationAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

Then Startupthe class ConfigureServicesin the function of its injection:

Copyservices.AddScoped<ModelValidationAttribute>();

We can now be injected into the above-described filter action in our application.

Microsoft.AspNetCore.All yuan package #

MICROSOFT.ASPNETCORE.ALL META-PACKAGE

Note: If you are using 2.1 and later versions of ASP.NET Core. It recommended Microsoft.AspNetCore.App package, rather than Microsoft.AspNetCore.All. All this is for security reasons. In addition, if you are using version 2.1 WebAPI create a new project, we will automatically get AspNetCore.App package instead AspNetCore.All.

This meta-package contains all AspNetCore related packages, EntityFrameworkCore package, SignalR package (version 2.1) and the run-dependent framework of support package. In this way it is easy to create a new project, because we do not need to manually install some we may use to package.

Of course, in order to be able to use Microsoft.AspNetCore.all yuan package, you need to make sure your machine is installed .NET Core Runtime.

Route #

ROUTING

In .NET Core Web API project, we should use the attribute route instead of the traditional route, this route is because the property can help us match the actual routing parameter name and parameter method in Action. Another reason is a description of the route parameters, for us, a parameter named "ownerId" than the "id" more readable.

We can use [Route] attribute to mark the top of the controller:

Copy[Route("api/[controller]")]
public class OwnerController : Controller
{
    [Route("{id}")]
    [HttpGet]
    public IActionResult GetOwnerById(Guid id)
    {
    }
}

There is another way to create routing rules for the control and operation:

Copy[Route("api/owner")]
public class OwnerController : Controller
{
    [Route("{id}")]
    [HttpGet]
    public IActionResult GetOwnerById(Guid id)
    {
    }
}

Which would be better for these two ways of some disagreements, but we always recommend the second approach. This is the way we have been used in the project.

When we talk about the route, we need to mention naming the route. We can use a descriptive name for our operations, but for routing / node, we should instead use NOUNS VERBS.

One example of poor:

Copy[Route("api/owner")]
public class OwnerController : Controller
{
    [HttpGet("getAllOwners")]
    public IActionResult GetAllOwners()
    {
    }
    [HttpGet("getOwnerById/{id}"]
    public IActionResult GetOwnerById(Guid id)
    {
    }
}

A good example:

Copy[Route("api/owner")]
public class OwnerController : Controller
{
    [HttpGet]
    public IActionResult GetAllOwners()
    {
    }
    [HttpGet("{id}"]
    public IActionResult GetOwnerById(Guid id)
    {
    }
}

More details about Restful practice explanation, please refer to: Top REST API Best Practices

Log #

LOGGING

If we intend to publish our application to a production environment, we should add a logging mechanism in place. Logging is helpful for us to sort out run the application in a production environment.

.NET Core through inheritance ILoggerinterface of its own logging. By means of dependency injection mechanism that can be easily used.

Copypublic class TestController: Controller
{
    private readonly ILogger _logger;
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
}

Then, in our action, we can log the aid of different log level by using _logger object.

Provider for .NET Core supports a variety of logging. Therefore, we may come to realize our log logic using a different Provider in the project.

NLog is a very good logic class library can be used to log our custom, it is highly scalable. Support for structured logging, and easy to configure. We can record information to the console, or even a database file.

To learn more about the application library in the .NET Core, see: .NET Core Series - Logging With NLog.

Serilog is also a very good library, it applies to the built-in .NET Core logging system.

There is a log can improve performance tips: string concatenation recommended _logger.LogInformation("{0},{1}", DateTime.Now, "info")approach to logging, instead _logger.LogInformation($"{DateTime.Now},info").

Encryption #

CRYPTOHELPER

We do not recommend that passwords are stored in clear text in the database. For security reasons, we need to be hashed. This is beyond the scope of this guide. There are a lot of hash algorithm on the Internet, including some good ways to be hashed password.

But if you need to provide easy-to-use encryption libraries for the .NET Core applications, CryptoHelper is a good choice.

CryptoHelper is applicable to independent cryptographic hash .NET Core library, which is based PBKDF2 to achieve. By creating Data Protectionto the hashed password stack. The library is available on NuGet, and also very simple:

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 #

CONTENT NEGOTIATION

By default, .NET Core Web API will return results in JSON format. In most cases, this is what we want.

But if customers want our Web API returns in response to other formats, such as XML format it?

To solve this problem, we need to configure the server for on-demand format the results of our response:

Copypublic void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddXmlSerializerFormatters();
}

But sometimes the client will request a format we do not support the Web API, so the best way to practice is to request the untreated unified format returns 406 status code. This embodiment can also be arranged in a simple process ConfigureServices:

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

We can also create our own formatting rules.

This part is a big topic, if you want to know more, please refer to: Content Negotiation in the .NET Core

Use the JWT #

USING JWT

Today's Web development, JSON Web Tokens (JWT) is becoming increasingly popular. Thanks to built-in support for .NET Core JWT and therefore very easy to implement. JWT is a development standard, which allows us to JSON format for data transmission security on the server and the client.

We can configure ConfigureServices in JWT Certification:

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

To be able to use it in an application, we also need to call in the following piece of code in Configure:

Copypublic void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseAuthentication();
}

In addition, creating Token can be used as follows:

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)

The controller can be used in the following manner based on the user authentication Token:

Copyvar auth = await HttpContext.AuthenticateAsync();
var id = auth.Principal.Claims.FirstOrDefault(x => x.Type.Equals(ClaimTypes.NameIdentifier))?.Value;

We can also JWT for authorization section, simply add a statement to the role of JWT configuration can be.

More about .NET Core JWT authentication and authorization in part, please refer to: authentication-aspnetcore-jwt-1 and authentication-aspnetcore-jwt-2

Summary #

Read this, there may be a friend is not very much in agreement on some of these best practices, because the whole article did not talk about the project more in line with practice guidelines, such as TDD , DDD and so on. But I personally think that all of the above is the basis of best practice, only these basic mastered in order to better understand some of the higher level of practice guidelines. Oaks from little acorns, so you can see this as a best practice guide for the novice.

In this guide, our main purpose is to familiarize you with some of the best practices for using web API .NET Core development projects when. Part there is also applicable in the other frame. Therefore, mastering them useful.

Thank you to read this guide, hope it'll help you.

Guess you like

Origin www.cnblogs.com/Study-Csharp/p/12079089.html