ApiBoilerPlate:构建ASP.NET Core 3 API的新功能和改进

介绍 (Introduction)

Two months ago, ApiBoilerPlate was first released and it’s incredible to see that the template garnered hundreds of installs within a short period of time. I’m very glad that it somehow benefited some developers, so thank you for the support. I’m very excited to announce that the new version of ApiBoilerPlate has been released recently. In this post, we’ll take a look at the new features added to the template.

两个月前, ApiBoilerPlate 首次发布 ,令人惊讶的是,该模板在短时间内获得了数百次安装。 我很高兴它以某种方式使某些开发人员受益,因此感谢您的支持。 我很高兴地宣布, ApiBoilerPlate的新版本ApiBoilerPlate近期发布。 在本文中,我们将介绍添加到模板中的新功能。

什么是ApiBoilerPlate? (What ApiBoilerPlate Is?)

ApiBoilerPlate is a simple yet organized project template for building ASP.NET Core APIs using .NET Core 3.x (the latest/fastest version of .NET Core to date) with preconfigured tools and frameworks. It features most of the functionalities that an API will have such as database CRUD operations, Token-based Authorization, Http Response format consistency, Global exception handling, Logging, Http Request rate limiting, HealthChecks and many more. The goal is to help you get up to speed when setting up the core structure of your app and its dependencies when spinning up a new ASP.NET Core API project. This enables you to focus on implementing business specific code requirements without you having to copy and paste the core structure of your project, common features, and installing its dependencies all over again. This will speed up your development time while enforcing standard project structure with its dependencies and configurations for all your apps.

ApiBoilerPlate是一个简单而井井有条的项目模板,用于通过预配置的工具和框架使用.NET Core 3.x(迄今为止最新/最快的.NET Core版本)来构建ASP.NET Core API。 它具有API将具有的大多数功能,例如数据库CRUD操作,基于令牌的授权,Http响应格式一致性,全局异常处理,日志记录,Http请求速率限制,HealthChecks等。 目的是帮助您在建立新的ASP.NET Core API项目时快速设置应用程序的核心结构及其依赖项。 这使您可以专注于实现特定于业务的代码要求,而不必复制和粘贴项目的核心结构,通用功能以及重新安装其依赖项。 这将加速您的开发时间,同时通过所有应用程序的依赖关系和配置来强制执行标准项目结构。

使用的工具和框架 (Tools and Frameworks Used)

重要要点 (Key Takeaways)

Here's the list of the good stuff that you can get when using the template:

以下是使用模板时可获得的好东西的列表:

  • Configured Sample Code for database CRUD operations

    为数据库CRUD操作配置的示例代码
  • Configured Basic Data Access using Dapper

    使用Dapper配置基本数据访问
  • Configured Logging using Serilog

    使用Serilog配置的日志记录

  • Configured AutoMapper for mapping entity models to DTOs

    配置的AutoMapper用于将实体模型映射到DTO

  • Configured FluentValidation for DTO validations

    DTO验证配置的FluentValidation

  • Configured AutoWrapper for handling request exceptions and consistent Http response format

    配置的AutoWrapper用于处理请求异常和一致的Http响应格式

  • Configured AutoWrapper.Server for unwrapping the Result attribute from AutoWrapper's ApiResponse output

    配置的AutoWrapper.Server用于从AutoWrapper的ApiResponse输出中解开Result属性

  • Configured Swagger API Documentation

    配置的Swagger API文档

  • Configured CORS

    配置的CORS

  • Configured JWT Authorization and Validation

    已配置的JWT授权和验证

  • Configured Sample Code for Requesting Client Credentials Token

    用于请求客户端凭据令牌的配置示例代码
  • Configured Swagger to secure API documentation with Bearer Authorization

    配置的Swagger通过Bearer授权来保护API文档

  • Configured Sample Code for connecting Protected External APIs

    用于连接受保护的外部API的已配置示例代码
  • Configured Sample Code for implementing custom API Pagination

    用于实现自定义API分页的已配置示例代码
  • Configured HttpClient Resilience and Transient fault-handling

    配置的HttpClient弹性和瞬态故障处理

  • Configured Http Request Rate Limiter

    配置的Http请求速率限制器

  • Configured HealthChecks and HealthChecksUI

    配置的HealthChecksHealthChecksUI

  • Configured Unit Test Project

    配置的单元测试项目
  • Configured Sample Code for Worker service. For handling extensive process in the background, you may want to look at the Worker Template created by Jude Daryl Clarino. The template was also based on ApiBoilerPlate.

    为工作程序服务配置的示例代码。 为了在后台处理大量流程,您可能需要查看Jude Daryl Clarino创建的Worker模板 。 该模板也基于ApiBoilerPlate

如何获得? (How to Get It?)

Image 1

There are two ways to install the template:

有两种安装模板的方法:

For installation steps, visit the following links:

有关安装步骤,请访问以下链接:

发生了什么变化? (What Was Changed?)

I personally like keeping things simple, clean and organize. The new version (v2) of the template has been reorganized to simplify the folder structure groupings and refactored to provide much cleaner code. The main thing that was changed is moving Configurations, Extensions, Filters, Handlers, Helpers and Installers folders into a new folder called Infrastructure. A few of the folders was new for v2 and this is to organize files needed for your application without mixing them with one another to value the separation of concerns and ease of maintainability.

我个人喜欢保持简单,整洁和有条理。 模板的新版本( v2 )已进行了重组,以简化文件夹结构的分组,并进行了重构,以提供更简洁的代码。 更改的主要内容是将“ Configurations ,“ Extensions ,“ Filters ,“ Handlers ,“ Helpers Installers和“ Installers文件夹移动到名为“ Infrastructure的新文件夹中。 其中一些文件夹是v2新功能,这是为了组织您的应用程序所需的文件,而又不会将它们相互混合在一起,从而使关注点分离和易于维护变得更加重要。

Some services that were configured in the Startup.cs file were moved to a dedicated class file under the Infrastructure/Installers folder. This will keep the Startup.cs file leaner and enables you to have a dedicated file for configuring each middleware.

Startup.cs文件中配置的某些服务已移至Infrastructure / Installers文件夹下的专用类文件。 这将使Startup.cs文件保持精简,并使您可以使用专用文件来配置每个middleware

Another thing that was changed is merging the Domain folder into the Data folder to simplify things a bit. In the Entity folder, you will see a new class called "EntityBase" to provide a base class that houses common properties for your entity classes.

更改的另一件事是将Domain文件夹合并到Data文件夹中,以简化操作。 在Entity文件夹中,您将看到一个名为"EntityBase"的新类,以提供一个包含您的实体类的通用属性的基类。

The DTO (a.k.a Data Transfer Object) folder has been reorganized as well to split Request and Response objects. This means that each request dto should have its own class and each response dto should have it own class and specific validation rules as well. This is to decouple them from the entity class (a.k.a Models) so that when a requirement changes or if your entity properties change, they won't be affected and wont break your API. Your entity classes should only be used for database related process and your DTOs are for mapping the requests and response objects from your entity classes and only expose properties that you want your client to see.

DTO (aka数据传输对象)文件夹也已重新组织,以拆分RequestResponse对象。 这意味着每个请求dto应该具有自己的类,并且每个响应dto也应该具有自己的类和特定的验证规则。 这是为了将它们与实体类(也称为Models )分离,以便在需求发生变化或您的实体属性发生变化时,它们不会受到影响并且不会破坏您的API 。 您的实体类仅应用于与数据库相关的过程,而DTO则应用于映射来自实体类的请求和响应对象,并且仅公开您希望客户端看到的属性。

I’ve also added a couple of methods in PersonManager.cs class to demonstrate paging and executing queries with transaction.

我还在PersonManager.cs类中添加了两个方法来演示分页和执行事务查询。

Finally, all Nuget dependencies have been updated to most recent versions.

最后,所有Nuget依赖项都已更新为最新版本。

添加了什么? (What Was Added?)

I put together all requests I gathered in version 1 from the community feedback and added a few more features for version 2 as well. Here’s the list of newly added features:

我将社区反馈中收集的所有请求汇总到版本1中,并为版本2添加了更多功能。 以下是新添加的功能列表:

  • Enable CORS

    启用CORS

  • JWT Authorization and Validation

    JWT授权和验证

  • Sample Code for Requesting Client Credentials Token

    请求客户端凭证令牌的示例代码
  • Swagger to secure API documentation with Bearer Authorization

    Swagger以通过Bearer授权保护API文档

  • Sample Code for connecting Protected External APIs

    连接受保护的外部API的示例代码
  • Sample Code for implementing custom API Pagination

    用于实现自定义API分页的示例代码
  • HttpClient Resilience and Transient fault-handling

    HttpClient弹性和瞬时故障处理

  • Http Request Rate Limiter

    Http请求速率限制器

  • HealthChecks and HealthChecksUI

    HealthChecksHealthChecksUI

  • Unit Test Project

    单元测试项目

启用CORS (Enable CORS)

Cross-Origin Resource Sharing (a.k.a. CORS) enable clients that are hosted in different domains/ports accessing your API endpoints. The template was configured to allow any origin, header and method as shown in the code below:

跨域资源共享(aka CORS)使托管在不同域/端口中的客户端访问API端点。 模板已配置为允许任何源,标头和方法,如下面的代码所示:

services.AddCors(options =>  
{
    options.AddPolicy("AllowAll",
    builder =>
    {
        builder.AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod();
    });
});

You may need to change the default policy configuration to allow only specific origins, headers and methods based on your business requirements. For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core.

您可能需要更改默认策略配置,以根据业务需求仅允许特定的来源,标头和方法。 有关更多信息,请参见在ASP.NET Core中启用跨域请求(CORS)

IdentityServer4 JWT身份验证 (IdentityServer4 JWT Authentication)

The template uses IdentityServer4 to authenticate and validate access tokens. You can find the code that configures IdentityServer Authentication under Installers/RegisterIdentityServerAuthentication.cs file. Here’s the code snippet:

该模板使用IdentityServer4来验证和验证访问令牌。 您可以在Installers / RegisterIdentityServerAuthentication.cs文件下找到配置IdentityServer验证的代码。 这是代码片段:

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)  
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = config["ApiResourceBaseUrls:AuthServer"];
        options.RequireHttpsMetadata = false;
        options.ApiName = "api.boilerplate.core";
});

The code above adds Authentication support using "Bearer" as the default scheme. It then configures IdentityServer Authentication handler. The Authority is the base Url to where your IdentityServer is hosted. The ApiName should be registered in your IdentityServer as an Audience. The RequireHttpsMetadata property is turned off by default and you should turn it on when you deploy the app in production.

上面的代码使用"Bearer"作为默认方案添加了身份验证支持。 然后,它配置IdentityServer验证处理程序。 Authority是承载IdentityServer的基本URL。 ApiName应该在您的IdentityServer注册为Audience 。 默认情况下, RequireHttpsMetadata属性是关闭的,在生产环境中部署应用程序时应将其打开。

When your APIs are decorated with the [Authorize] attribute, then the requesting clients should provide the access token generated from IdentityServer and pass it as a Bearer Authorization Header before they can be granted access to your API endpoints. For more information, see: IdentityServer: Protecting APIs.

当您的API用[Authorize]属性修饰时,发出请求的客户端应提供从IdentityServer生成的访问令牌,并将其作为Bearer Authorization Header传递,然后才能授予它们对API端点的访问权限。 有关更多信息,请参见: IdentityServer:保护API

请求客户端凭证令牌的示例代码 (Sample Code for Requesting Client Credentials Token)

It occurred to me that accessing protected internal/external services are pretty much a common scenario and so I have decided to include a sample code demonstrating how to do it in ASP.NET Core. In version 2, you can see a new folder called "Services" and under it, you can find a class called AuthServerConnect with the following code:

在我看来,访问受保护的内部/外部服务几乎是一种常见情况,因此,我决定包括一个示例代码,演示如何在ASP.NET Core中进行操作。 在版本2中,您可以看到一个名为" Services "的新文件夹,并且在其下可以找到带有以下代码的名为AuthServerConnect的类:

public class AuthServerConnect : IAuthServerConnect  
{
    private readonly HttpClient _httpClient;
    private readonly IDiscoveryCache _discoveryCache;
    private readonly ILogger<AuthServerConnect> _logger;
    private readonly IConfiguration _config;

    public AuthServerConnect(HttpClient httpClient, IConfiguration config, 
           IDiscoveryCache discoveryCache, ILogger<AuthServerConnect> logger)
    {
        _httpClient = httpClient;
        _config = config;
        _discoveryCache = discoveryCache;
        _logger = logger;
    }
    public async Task<string> RequestClientCredentialsTokenAsync()
    {

        var endPointDiscovery = await _discoveryCache.GetAsync();
        if (endPointDiscovery.IsError)
        {
            _logger.Log(LogLevel.Error, $"ErrorType: {endPointDiscovery.ErrorType} 
                        Error: {endPointDiscovery.Error}");
            throw new HttpRequestException
            ("Something went wrong while connecting to the AuthServer Token Endpoint.");
        }

        var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync
                            (new ClientCredentialsTokenRequest
        {
            Address = endPointDiscovery.TokenEndpoint,
            ClientId = _config["Self:Id"],
            ClientSecret = _config["Self:Secret"],
            Scope = "SampleApiResource"
        });

        if (tokenResponse.IsError)
        {
            _logger.Log(LogLevel.Error, $"ErrorType: {tokenResponse.ErrorType} 
                                         Error: {tokenResponse.Error}");
            throw new HttpRequestException
            ("Something went wrong while requesting Token to the AuthServer.");
        }

        return tokenResponse.AccessToken;
    }
}

The code snippet above requests an access token from IndentityServer Token endpoint by passing the registered client_id, client_secret and scope.

上面的代码段通过传递已注册的client_idclient_secretscopeIndentityServer Token端点请求访问令牌。

The RequestClientCredentialsTokenAsync() method will then be called each time you issue an Http Requests via HttpClient. This process is encapsulated in a custom bearer token DelegatingHandler class called ProtectedApiBearerTokenHandler. Here’s the code snippet:

每次您通过HttpClient发出Http Request时,都会调用RequestClientCredentialsTokenAsync()方法。 此过程封装在名为ProtectedApiBearerTokenHandler的自定义承载令牌DelegatingHandler类中。 这是代码片段:

public class ProtectedApiBearerTokenHandler : DelegatingHandler  
{
    private readonly IAuthServerConnect _authServerConnect;

    public ProtectedApiBearerTokenHandler(IAuthServerConnect authServerConnect)
    {
        _authServerConnect = authServerConnect;
    }

    protected override async Task<HttpResponseMessage> SendAsync
         (HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // request the access token
        var accessToken = await _authServerConnect.RequestClientCredentialsTokenAsync();

        // set the bearer token to the outgoing request as Authentication Header
        request.SetBearerToken(accessToken);

        // Proceed calling the inner handler, 
        // that will actually send the request to our protected API
        return await base.SendAsync(request, cancellationToken);
    }
}

We can then register the ProtectedApiBearerTokenHandler as a Transient service in onfigureServices() method in Startup.cs:

然后,我们可以在Startup.cs的 onfigureServices()方法中将ProtectedApiBearerTokenHandler注册为Transient服务:

services.AddTransient<ProtectedApiBearerTokenHandler>();  

访问受保护的内部/外部API的示例代码 (Sample Code for Accessing Protected Internal/External APIs)

Under Services folder, you can find the SampleApiConnect.cs class that houses a couple of methods for requesting external services or APIs as shown in the following code:

在“ 服务”文件夹下,您可以找到SampleApiConnect.cs类,该类包含一些用于请求外部服务或API的方法,如以下代码所示:

namespace ApiBoilerPlate.Services  
{
    public class SampleApiConnect: IApiConnect
    {
      private readonly HttpClient _httpClient;
      private readonly ILogger<SampleApiConnect> _logger;
      public SampleApiConnect(HttpClient httpClient,ILogger<SampleApiConnect> logger)
        {
            _httpClient = httpClient;
            _logger = logger;
        }

        public async Task<SampleResponse> 
        PostDataAsync<SampleResponse, SampleRequest>(string endPoint, SampleRequest dto)
        {
            var content = new StringContent(JsonSerializer.Serialize(dto), 
                          Encoding.UTF8, HttpContentMediaTypes.JSON);
            var httpResponse = await _httpClient.PostAsync(endPoint, content);

            if (!httpResponse.IsSuccessStatusCode)
            {
                _logger.Log(LogLevel.Warning, $"[{httpResponse.StatusCode}] 
                            An error occured while requesting external api.");
                return default(SampleResponse);
            }

            var jsonString = await httpResponse.Content.ReadAsStringAsync();
            var data = Unwrapper.Unwrap<SampleResponse>(jsonString);

            return data;
        }

        public async Task<SampleResponse> GetDataAsync<SampleResponse>(string endPoint)
        {
            var httpResponse = await _httpClient.GetAsync(endPoint);

            if (!httpResponse.IsSuccessStatusCode)
            {
                _logger.Log(LogLevel.Warning, $"[{httpResponse.StatusCode}] 
                            An error occured while requesting external api.");
                return default(SampleResponse);
            }

            var jsonString = await httpResponse.Content.ReadAsStringAsync();
            var data = Unwrapper.Unwrap<SampleResponse>(jsonString);

            return data;
        }
    }
}

To take advantage of Dependency Injection, we can then register a typed instance of HttpClientFactory for SampleApiConnect class and then pass in the ProtectedApiBearerTokenHandler as a Message Handler. Here’s the code snippet that you can find under Infrastructure/Installer/ RegisterApiResources.cs file:

为了利用依赖注入,我们可以为SampleApiConnect类注册一个HttpClientFactory的类型化实例,然后将ProtectedApiBearerTokenHandler传递为消息处理程序。 您可以在Infrastructure / Installer / RegisterApiResources.cs文件下找到以下代码片段:

services.AddHttpClient<IApiConnect, SampleApiConnect>(client =>  
{
    client.BaseAddress = new Uri(config["ApiResourceBaseUrls:SampleApi"]);
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add
           (new MediaTypeWithQualityHeaderValue(HttpContentMediaTypes.JSON));
})
.AddHttpMessageHandler<ProtectedApiBearerTokenHandler>();

This way, when we issue an Http Requests to SampleApiConnect endpoints, it automatically generates an access token for us and sets it as a Bearer Authentication header every time we invoke an Http call.

这样,当我们向SampleApiConnect端点发出Http请求时,每次我们调用Http调用时,它将自动为我们生成访问令牌并将其设置为Bearer Authentication标头。

Here’s the code for calling the SampleApiConnect methods that you can find under API/v1/SampleApiControlle.cs file:

以下是用于调用SampleApiConnect方法的代码,您可以在API / v1 / SampleApiControlle.cs文件下找到该代码:

public class SampleApiController : ControllerBase  
{
    private readonly ILogger<SampleApiController> _logger;
    private readonly IApiConnect _sampleApiConnect;

    public SampleApiController
           (IApiConnect sampleApiConnect, ILogger<SampleApiController> logger)
    {
        _sampleApiConnect = sampleApiConnect;
        _logger = logger;
    }

    [Route("{id:long}")]
    [HttpGet]
    public async Task<ApiResponse> Get(long id)
    {
        if (ModelState.IsValid)
            return new ApiResponse
            (await _sampleApiConnect.GetDataAsync<SampleResponse>($"/api/v1/sample/{id}"));
        else
            throw new ApiException(ModelState.AllErrors());
    }

    [HttpPost]
    public async Task<ApiResponse> Post([FromBody] SampleRequest dto)
    {
        if (ModelState.IsValid)
            return new ApiResponse(await _sampleApiConnect.PostDataAsync
                   <SampleResponse, SampleRequest>("/api/v1/sample", dto));
        else
            throw new ApiException(ModelState.AllErrors());
    }
}

HttpClient的弹性和瞬时故障处理 (HttpClient Resilience and Transient Fault-Handling)

When you call internal or external services within your API app, there is the ever-present risk when communicating with services over a transport such as Http that a transient fault will occur. A transient fault may prevent your request from being completed, but is also likely to be a temporary problem.

当您在API应用程序中调用内部或外部服务时,通过Http的传输方式与服务进行通信时,始终存在不断出现风险。 暂时性故障可能会阻止您的请求完成,但也可能是暂时的问题。

The template uses Polly to enable us to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. The template uses the following features:

该模板使用Polly使我们能够以流畅和线程安全的方式表达策略,例如重试,断路器,超时,隔板隔离和回退。 该模板使用以下功能:

  • Retry - maybe it's a network blip

    Retry -可能是网络故障

  • Circuit-breaker - Try a few times, but stop so you don't overload the system

    Circuit-breaker -尝试几次,但要停止操作,以免系统过载

  • Timeout - Try, but give up after n seconds/minutes

    Timeout -尝试,但在n秒/分钟后放弃

You can find how Polly was configured under Infrastructure/Installer/RegisterApiResources.cs file. Here’s the code snippet:

您可以在Infrastructure / Installer / RegisterApiResources.cs文件下找到如何配置Polly 。 这是代码片段:

var policyConfigs = new HttpClientPolicyConfiguration();  
config.Bind("HttpClientPolicies", policyConfigs);

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>
                    (TimeSpan.FromSeconds(policyConfigs.RetryTimeoutInSeconds));

var retryPolicy = HttpPolicyExtensions  
    .HandleTransientHttpError()
    .OrResult(r => r.StatusCode == HttpStatusCode.NotFound)
    .WaitAndRetryAsync(policyConfigs.RetryCount, _ => TimeSpan.FromMilliseconds
                      (policyConfigs.RetryDelayInMs));

var circuitBreakerPolicy = HttpPolicyExtensions  
   .HandleTransientHttpError()
   .CircuitBreakerAsync(policyConfigs.MaxAttemptBeforeBreak, 
                  TimeSpan.FromSeconds(policyConfigs.BreakDurationInSeconds));

var noOpPolicy = Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>();


services.AddTransient<ProtectedApiBearerTokenHandler>();


services.AddHttpClient<IApiConnect, SampleApiConnect>(client =>  
{
    client.BaseAddress = new Uri(config["ApiResourceBaseUrls:SampleApi"]);
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add
           (new MediaTypeWithQualityHeaderValue(HttpContentMediaTypes.JSON));
})
.SetHandlerLifetime(TimeSpan.FromMinutes(policyConfigs.HandlerTimeoutInMinutes))
.AddHttpMessageHandler<ProtectedApiBearerTokenHandler>()
.AddPolicyHandler(request => request.Method == HttpMethod.Get? retryPolicy : noOpPolicy)
.AddPolicyHandler(timeoutPolicy)
.AddPolicyHandler(circuitBreakerPolicy);

The code snippet above defines a set of Polly polices for Retries, CircuitBreaker and Timeout. In this example, we’ve applied Retry policy for GET request only for idempotency reason. It will trigger a Retry every 500 milliseconds for 3 times. We’ve also applied circuitbreaker to allow 3 attempts and blocks execution for 30 seconds on the 4th attempt. Finally, we’ve setup a Timeout when the execution goes beyond a certain threshold, in this case, a 5 second timeout. This guarantees the caller won't have to wait beyond the timeout.

上面的代码段为RetriesCircuitBreaker和Timeout定义了一组Polly策略。 在此示例中,仅出于幂等原因,我们对GET请求应用了重试策略。 它将每500毫秒触发一次重试3次。 我们还应用了断路器,允许进行3次尝试,并在第4 尝试中阻止执行30秒。 最后,我们设置一个Timeout当执行超过一定的阈值,在这种情况下,5秒超时。 这保证了呼叫者不必等待超时。

Here’s the HttpClientPolicies configuration which can be found within appsettings.TEST.json file:

这是HttpClientPolicies配置,可以在appsettings.TEST.json文件中找到:

"HttpClientPolicies": {
    "RetryCount": 3,
    "RetryDelayInMs": 500,
    "RetryTimeoutInSeconds": 5,
    "BreakDurationInSeconds": 30,
    "MaxAttemptBeforeBreak": 3,
    "HandlerTimeoutInMinutes": 5
}

Http请求速率限制器 (Http Request Rate Limiter)

In order to prevent your API endpoints from being abused, we usually enforce a rate limit on the number of requests that a client can consume over a time period. Throttling the API endpoint on the server side can protect our system from overloading resources which deteriorates the performance of the API endpoint.

为了防止您的API端点被滥用,我们通常会对一段时间内客户端可以使用的请求数量实施速率限制。 在服务器端限制API端点可以保护我们的系统避免资源过载,从而降低API端点的性能。

The template uses AspNetCoreRateLimit that provides a solution designed to control the rate of requests that clients can make to your APIs based on IP address or client ID. You can find how it was implemented under Infrastructure/Installer/RegisterRequestRateLimiter.cs file. Here’s the code snippet:

该模板使用AspNetCoreRateLimit提供的解决方案旨在根据IP地址或客户端ID控制客户端可以向您的API发出请求的速率。 您可以在Infrastructure / Installer / RegisterRequestRateLimiter.cs文件下找到它的实现方式。 这是代码片段:

internal class RegisterRequestRateLimiter : IServiceRegistration  
{
    public void RegisterAppServices(IServiceCollection services, IConfiguration config)
    {
        // needed to load configuration from appsettings.json
        services.AddOptions();
        // needed to store rate limit counters and ip rules
        services.AddMemoryCache();

        //load general configuration from appsettings.json
        services.Configure<IpRateLimitOptions>(config.GetSection("IpRateLimiting"));

        // inject counter and rules stores
        services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
        services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();

        // https://github.com/aspnet/Hosting/issues/793
        // the IHttpContextAccessor service is not registered by default.
        // the clientId/clientIp resolvers use it.
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        // configuration (resolvers, counter key builders)
        services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
    }
}

And here’s the IpRateLimiting configuration in appsettings.TEST.json file:

这是appsettings.TEST.json文件中的IpRateLimiting配置:

"IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "GeneralRules": [
      {
        "Endpoint": "*:/api/*",
        "Period": "1s",
        "Limit": 2
      }
    ]
}

The configuration above defines the rule for every endpoint requests that contains the segment "/api/". The client can only request an endpoint 2 times within a 1 second period. Of course, you are free to change the configuration that best suits your needs.

上面的配置为每个包含段"/api/"端点请求定义了规则。 客户端在1秒内只能请求2次端点。 当然,您可以自由更改最适合您需要的配置。

The EnableEndpointRateLimiting needs to be set to true so that IP rate limits are applied to specific endpoints like "*:/api/*" rather than all endpoints ("*"). In the GeneralRules section, we set one rate limiting rule. The rule says, for endpoint like "*:/api/*", only allow 2 requests within every 1 second. The format of Endpoint means that for any Http verb ("*:"), all URLs that start with "/api/" and end with anything ("*") will comply with the rule.

需要将EnableEndpointRateLimiting设置为true以便将IP速率限制应用于特定的端点,例如"*:/api/*"而不是所有端点( "*" )。 在GeneralRules部分中,我们设置了一个速率限制规则。 规则说,对于像"*:/api/*"这样的终结点,每1秒只允许2个请求。 Endpoint的格式意味着对于任何Http动词( "*:" ),所有以"/api/"开头并以任何内容( "*" )结束的URL都将遵守该规则。

Here’s a sample screenshot of the captured requests of 200 and 429 Http StatusCodes.

这是捕获的200429 Http StatusCodes请求的屏幕截图示例。

Image 2

For the failed API request, the response contains an exception message of "API calls quota exceeded! maximum admitted 2 per 1s." and an Http status code 429 Too Many Requests. The response headers include a key-value pair of "Retry-After: 1", which instructs consumers to retry after 1 second in order to overcome the rate limit.

对于失败的API请求,响应包含一个异常消息“ API calls quota exceeded! maximum admitted 2 per 1s. ”和Http状态码429 Too Many Requests。 响应标头包含键值对“ Retry-After: 1 ”,该键值对指示消费者在1秒后重试以克服速率限制。

For more information, see: https://github.com/stefanprodan/AspNetCoreRateLimit and Changhui Xu's excellent article about Requests Rate Limiting.

有关更多信息,请参见: https : //github.com/stefanprodan/AspNetCoreRateLimit和Xu Changhui的关于请求速率限制的出色文章

HealthChecks和HealthChecksUI (HealthChecks and HealthChecksUI)

Great systems are built to anticipate and handle unexpected issues, rather than just silently failing.

出色的系统旨在预测和处理意外问题,而不仅仅是默默失败。

The template uses HealthChecks to monitor the health of the app. This enable us to monitor the status of our application dependencies such as database connection, external services and many more. HealthChecks keep us alerted as soon as something isn't functioning well or some services are unavailable, rather than hearing the issues from a customer.

该模板使用HealthChecks监视应用程序的运行状况。 这使我们能够监视应用程序依赖项的状态,例如数据库连接,外部服务等等。 当某些设备运行不正常或某些服务不可用时, HealthChecks向我们发出警报,而不是听取客户的问题。

You can find a sample HealthChecks configuration under Infrastructure/Installers/RegisterHealthChecks.cs file. Here’s the code snippet:

您可以在Infrastructure / Installers / RegisterHealthChecks.cs文件下找到一个示例HealthChecks配置。 这是代码片段:

//Register HealthChecks and UI
services.AddHealthChecks()  
        .AddCheck("Google Ping", new PingHealthCheck("www.google.com", 100))
        .AddCheck("Bing Ping", new PingHealthCheck("www.bing.com", 100))
        .AddUrlGroup(new Uri(config["ApiResourceBaseUrls:AuthServer"]),
                    name: "Auth Server",
                    failureStatus: HealthStatus.Degraded)
        .AddUrlGroup(new Uri(config["ApiResourceBaseUrls:SampleApi"]),
                    name: "External Api",
                    failureStatus: HealthStatus.Degraded)
        .AddNpgSql(config["ConnectionStrings:PostgreSQLConnectionString"],
                    name: "PostgreSQL",
                    failureStatus: HealthStatus.Unhealthy)
        .AddSqlServer(
                    connectionString: config["ConnectionStrings:SQLDBConnectionString"],
                    healthQuery: "SELECT 1;",
                    name: "SQL",
                    failureStatus: HealthStatus.Degraded,
                    tags: new string[] { "db", "sql", "sqlserver" });

services.AddHealthChecksUI();

It uses the following Nuget packages from AspNetCore.Diagnostics.HealthChecks to perform basic HealthCheck monitoring:

它使用AspNetCore.Diagnostics.HealthChecks的以下Nuget程序包执行基本的HealthCheck监视:

  • AspNetCore.HealthChecks.SqlServer

    AspNetCore.HealthChecks.SqlServer

  • AspNetCore.HealthChecks.Npgsql

    AspNetCore.HealthChecks.Npgsql

  • AspNetCore.HealthChecks.Uris

    AspNetCore.HealthChecks.Uris

I’ve also included a simple PingHealthCheck to add as an example. Here’s the code snippet:

我还提供了一个简单的PingHealthCheck作为示例。 这是代码片段:

internal class PingHealthCheck : IHealthCheck  
{
    private string _host;
    private int _timeout;

    public PingHealthCheck(string host, int timeout)
    {
        _host = host;
        _timeout = timeout;
    }

    public async Task<HealthCheckResult> CheckHealthAsync
           (HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            using (var ping = new Ping())
            {
                var reply = await ping.SendPingAsync(_host, _timeout);
                if (reply.Status != IPStatus.Success)
                {
                    return HealthCheckResult.Unhealthy
                    ($"Ping check status [{ reply.Status }]. Host 
                    {_host} did not respond within {_timeout} ms.");
                }

                if (reply.RoundtripTime >= _timeout)
                {
                    return HealthCheckResult.Degraded
                    ($"Ping check for {_host} takes too long to respond. 
                    Expected {_timeout} ms but responded in {reply.RoundtripTime} ms.");
                }

                return HealthCheckResult.Healthy($"Ping check for {_host} is ok.");
            }
        }
        catch
        {
            return HealthCheckResult.Unhealthy
                   ($"Error when trying to check ping for {_host}.");
        }
    }
}

In the Configure() method of Startup.cs file, we can enable HealthChecks and HealthChecksUI by adding the following code below:

Startup.cs文件的Configure()方法中,我们可以通过添加以下代码来启用HealthChecksHealthChecksUI

//Enable HealthChecks and UI
app.UseHealthChecks("/selfcheck", new HealthCheckOptions  
{
      Predicate = _ => true,
      ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).UseHealthChecksUI();

The "/selfcheck" is the endpoint that you can call or your uptime monitoring would call for getting the status of the app. For example, if you run the app and navigate to "/selfcheck", it should return you the following response in JSON format:

"/selfcheck"是您可以调用的端点,或者您的正常运行时间监视将调用该端点来获取应用程序的状态。 例如,如果您运行该应用程序并导航至"/selfcheck" ,它将以JSON格式返回以下响应:

{
    "status": "Unhealthy",
    "totalDuration": "00:00:02.0427058",
    "entries": {
        "Google Ping": {
            "data": {},
            "description": "Ping check for www.google.com is ok.",
            "duration": "00:00:00.0308662",
            "status": "Healthy"
        },
        "Bing Ping": {
            "data": {},
            "description": "Ping check status [TimedOut]. 
                            Host www.bing.com did not respond within 100 ms.",
            "duration": "00:00:00.4633644",
            "status": "Unhealthy"
        },
        "Auth Server": {
            "data": {},
            "description": "No connection could be made because 
                            the target machine actively refused it.",
            "duration": "00:00:02.0204641",
            "exception": "No connection could be made because 
                          the target machine actively refused it.",
            "status": "Degraded"
        },
        "External Api": {
            "data": {},
            "description": "No connection could be made because 
                            the target machine actively refused it.",
            "duration": "00:00:02.0219353",
            "exception": "No connection could be made because 
                          the target machine actively refused it.",
            "status": "Degraded"
        },
        "PostgreSQL": {
            "data": {},
            "description": "Host can't be null",
            "duration": "00:00:00.0083434",
            "exception": "Host can't be null",
            "status": "Unhealthy"
        },
        "SQL": {
            "data": {},
            "duration": "00:00:00.0246875",
            "status": "Healthy"
        }
    }
}

And if you want to have a nice Visualization for monitoring the health status of each check, then you can simply navigate to "/healthchecks-iu" and you should be presented with something like this:

并且,如果您希望有一个很好的可视化工具来监视每个检查的运行状况,则可以简单地导航到"/healthchecks-iu"并且应该显示以下内容:

Image 3

That’s just awesome!

太棒了!

For more information about configuring ASP.NET Core HealthChecks, see https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks.

有关配置ASP.NET Core HealthChecks的更多信息,请参见https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks

通过持证人授权保护Swagger文档 (Secure Swagger Documentation with Bearer Authorization)

In Swagger, we can describe how we secure our APIs by defining one or more security schemes. Since the template uses JWT to protect the APIs, we can define a "Bearer" scheme to protect our SwaggerUI API documentation. In Infrastructure/Installers/RegisterSwagger.cs file, you can find the following code below:

Swagger ,我们可以描述如何通过定义一个或多个安全方案来保护我们的APIs 。 由于模板使用JWT保护API,因此我们可以定义"Bearer"方案以保护SwaggerUI API文档。 在Infrastructure / Installers / RegisterSwagger.cs文件中,您可以在下面找到以下代码:

services.AddSwaggerGen(options =>  
{
    options.SwaggerDoc("v1", new OpenApiInfo 
                      { Title = "ASP.NET Core Template API", Version = "v1" });

    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Scheme = "Bearer",
        Description = "Enter 'Bearer' following by space and JWT.",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,

    });

    options.OperationFilter<SwaggerAuthorizeCheckOperationFilter>();
});

The code snippet above defines a security definition using "Bearer" security scheme of type Http. The SecuritySchemeType.Http type is part of OpenApi 3 specification that is used for Basic, Bearer and other Http authentications schemes.

上面的代码段使用Http类型的"Bearer"安全方案来定义安全定义。 SecuritySchemeType.Http类型是OpenApi 3规范的一部分, OpenApi 3规范用于BasicBearer和其他Http身份验证方案。

One thing to notice in the code above is the OperationFilter injection. That line enables applying security definition for APIs that requires Bearer scheme. Here’s the code for SwaggerAuthorizeCheckOperationFilter.cs class that sits under Infrastructure/Filters folder:

上面的代码中要注意的一件事是OperationFilter注入。 该行允许对需要Bearer方案的API应用安全定义。 这是位于Infrastructure / Filters文件夹下的SwaggerAuthorizeCheckOperationFilter.cs类的代码:

internal class SwaggerAuthorizeCheckOperationFilter : IOperationFilter  
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        // Check for authorize attribute
        var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes
                           (true).OfType<AuthorizeAttribute>().Any() ||
                           context.MethodInfo.GetCustomAttributes(true).OfType
                           <AuthorizeAttribute>().Any();

        if (!hasAuthorize) return;

        operation.Responses.TryAdd
                  ("401", new OpenApiResponse { Description = "Unauthorized" });
        operation.Responses.TryAdd
                  ("403", new OpenApiResponse { Description = "Forbidden" });

        operation.Security = new List<OpenApiSecurityRequirement>
            {
               new OpenApiSecurityRequirement{
                    {
                        new OpenApiSecurityScheme{
                            Reference = new OpenApiReference{
                                Id = "Bearer",
                                Type = ReferenceType.SecurityScheme
                            }
                        },new List<string>()
                    }
            } };

    }
}

API endpoints that don’t require authorization will be ignored and the filter will only be applied based on the presence of the AuthorizeAttribute.

不需要授权的API端点将被忽略,并且仅根据AuthorizeAttribute的存在来应用过滤器。

Here's a sample screenshot of the secured SwaggerUI:

这是受保护的SwaggerUI的示例屏幕截图:

Image 4

Notice that the authorization is only applied to specific API endpoints. In this case, only the SampleApi controller is protected with the Bearer scheme authorization. Clicking the Authorize button or any of the SampleApi endpoints should prompt you the following dialog:

请注意,授权仅适用于特定的API端点。 在这种情况下,只有SampleApi控制器受到Bearer方案授权的保护。 单击“ Authorize按钮或任何SampleApi端点应提示您以下对话框:

Image 5

Once you supply a valid access token, you should be able to test the secured endpoints from SwaggerUI.

一旦提供了有效的访问令牌,就应该能够从SwaggerUI测试安全端点。

API分页的示例代码 (Sample Code for API Pagination)

If you value performance, then you may want to implement pagination in your API to limit the amount of data to the requesting client. You may have a GET endpoint that returns all the data to clients and when the data that your API serves grow, then your API might end up being unusable.

如果您重视性能,那么您可能希望在API中实现分页,以将数据量限制到发出请求的客户端。 您可能有一个GET端点,该端点将所有数据返回给客户端,并且当您的API提供的数据增长时,您的API可能最终变得不可用。

Paging refers to getting partial results from an API. Imagine having millions of results in the database and having your application try to return all of them at once.

分页是指从API获得部分结果。 想象一下,数据库中有数百万个结果,而您的应用程序尝试一次返回所有结果。

Not only would that be an extremely ineffective way of returning the results, but it could also possibly have devastating effects on the application itself or the hardware it runs on. Moreover, every client has limited memory resources and it needs to restrict the number of shown results.

这不仅是返回结果的极其无效的方法,而且还可能对应用程序本身或运行它的硬件产生破坏性影响。 此外,每个客户端的内存资源都有限,因此需要限制显示结果的数量。

Pagination helps performance and scalability in a number of ways:

分页可通过多种方式帮助提高性能和可伸缩性:

  • The number of page read I/Os is reduced when SQL Server grabs the data.

    当SQL Server获取数据时,页面读取I / O的数量将减少。
  • The amount of data transferred from the database server to the web server is reduced.

    从数据库服务器传输到Web服务器的数据量减少了。
  • The amount of memory used to store the data on the web server in our object model is reduced.

    在我们的对象模型中,用于在Web服务器上存储数据的内存量减少了。
  • The amount of data transferred from the web server to the client is reduced.

    从Web服务器传输到客户端的数据量减少了。
  • This all adds up to potentially a significant positive impact – particularly for large collections of data.

    所有这些加在一起可能会产生重大的积极影响,尤其是对于大量数据。

You can find the paging example under Data/DataManager/PersonsManager.cs file. Here’s the code snippet:

您可以在Data / DataManager / PersonsManager.cs文件下找到分页示例。 这是代码片段:

public async Task<(IEnumerable<Person> Persons, Pagination Pagination)> 
       GetPersonsAsync(UrlQueryParameters urlQueryParameters)  
{
    IEnumerable<Person> persons;
    int recordCount = 0;

    var query = @"SELECT ID, FirstName, LastName FROM Person
                            ORDER BY ID DESC
                            OFFSET @Limit * (@Offset -1) ROWS
                            FETCH NEXT @Limit ROWS ONLY";

    var param = new DynamicParameters();
    param.Add("Limit", urlQueryParameters.PageSize);
    param.Add("Offset", urlQueryParameters.PageNumber);

    if (urlQueryParameters.IncludeCount)
    {
        query += " SELECT COUNT(ID) FROM Person";
        var pagedRows = await DbQueryMultipleAsync<Person>(query, param);

        persons = pagedRows.Data;
        recordCount = pagedRows.RecordCount;
    }
    else
    {
        persons = await DbQueryAsync<Person>(query, param);
    }

    var metadata = new Pagination
    {
        PageNumber = urlQueryParameters.PageNumber,
        PageSize = urlQueryParameters.PageSize,
        TotalRecords = recordCount

    };

    return (persons, metadata);

}

The GetPersonsAsync() takes a UrlQueryParameters object and returns a named Tuple called Persons and Pagination.

GetPersonsAsync()接受一个UrlQueryParameters对象,并返回一个名为Persons and Pagination的命名Tuple

The code snippet above performs pagination based on the page numbers and page size supplied by the requesting client. However, there are cases that the client app also requires your API to include the total number of records in the response so they can also present paginated data in the UI. Including the total record count can potentially impose performance penalty as we need to perform 2 SQL queries in the database: First is to get the chunk of data and second is to get the total record count.

上面的代码段根据请求客户端提供的页码和页面大小执行分页。 但是,在某些情况下,客户端应用程序还需要您的API在响应中包括记录总数,因此它们也可以在UI中显示分页数据。 包括总记录数可能会导致性能下降,因为我们需要在数据库中执行2条SQL查询:首先是获取数据块,其次是获取总记录数。

This is the reason why we've added the IncludeCount as part of the UrlQueryParameters so that we can turn off this feature by default. When the client set IncludeCount = true, we use the Dapper's QueryMultipleAsync() method to perform multiple queries and reads multiple result set in a single database round trip.

这就是为什么我们将IncludeCount作为UrlQueryParameters一部分添加的原因,以便我们可以默认关闭此功能。 当客户端设置IncludeCount = true ,我们使用Dapper的QueryMultipleAsync()方法执行多个查询并在一次数据库往返中读取多个结果集。

You can find how the UrlQueryParameters and Pagination classes are defined under the Data folder. Here’s how the method GetPersonsAsync() is being called in the PersonsController class:

您可以找到如何在Data文件夹下定义UrlQueryParametersPagination类。 这是在PersonsController类中调用GetPersonsAsync()方法的方式:

[Route("paged")]
[HttpGet]
public async Task<IEnumerable<PersonResponse>> 
       Get([FromQuery] UrlQueryParameters urlQueryParameters)  
{
    var data = await _personManager.GetPersonsAsync(urlQueryParameters);
    var persons = _mapper.Map<IEnumerable<PersonResponse>>(data.Persons);

    Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(data.Pagination));

    return persons;
}

Now when you issue the following GET request:

现在,当您发出以下GET请求时:

https://localhost:44321/api/v1/persons/paged?pagenumber=1&pagesize=3&includecount=true

https://localhost:44321/api/v1/persons/paged?pagenumber=1&pagesize=3&includecount=true

The response output should return something like this:

响应输出应返回如下内容:

{
    "message": "Request successful.",
    "isError": false,
    "result": [
        {
            "id": 14002,
            "firstName": "Vianne Maverich",
            "lastName": "Durano",
            "dateOfBirth": "2019-11-26T00:00:00",
            "fullName": "Vianne Maverich Durano"
        },
        {
            "id": 13002,
            "firstName": "Vynn Markus",
            "lastName": "Durano",
            "dateOfBirth": "2019-11-26T00:00:00",
            "fullName": "Vynn Markus Durano"
        },
        {
            "id": 12002,
            "firstName": "Michelle",
            "lastName": "Durano",
            "dateOfBirth": "1990-11-03T00:00:00",
            "fullName": "Michelle Durano"
        }
    ]
}

And the custom header X-Pagination should be added in the response headers that contains the metadata for paging as shown in the figure below:

并且自定义标头X-Pagination应该添加到响应标头中,该标头包含用于分页的metadata ,如下图所示:

Image 6

The API response uses AutoWrapper to automatically format the Http response.

API响应使用AutoWrapper自动格式化Http响应。

单元测试项目 (Unit Test Project)

The template also includes a Unit Test project using xUnit and Moq. Here’s a sample code snippet of the test class:

该模板还包括一个使用xUnitMoq的单元测试项目。 这是测试类的示例代码片段:

public class PersonsControllerTests  
{
    private readonly Mock<IPersonManager> _mockDataManager;
    private readonly PersonsController _controller;

    public PersonsControllerTests()
    {
        var logger = Mock.Of<ILogger<PersonsController>>();

        var mapperProfile = new MappingProfileConfiguration();
        var configuration = new MapperConfiguration(cfg => cfg.AddProfile(mapperProfile));
        var mapper = new Mapper(configuration);

        _mockDataManager = new Mock<IPersonManager>();

        _controller = new PersonsController(_mockDataManager.Object, mapper, logger);
    }

    private IEnumerable<Person> GetFakePersonLists()
    {
        return new List<Person>
            {
                new Person()
                {
                    ID = 1,
                    FirstName = "Vynn Markus",
                    LastName = "Durano",
                    DateOfBirth = Convert.ToDateTime("01/15/2016")
                },
                new Person()
                {
                    ID = 2,
                    FirstName = "Vianne Maverich",
                    LastName = "Durano",
                    DateOfBirth = Convert.ToDateTime("02/15/2016")
                }
            };
    }

    private CreatePersonRequest FakeCreateRequestObject()
    {
        return new CreatePersonRequest()
        {
            FirstName = "Vinz",
            LastName = "Durano",
            DateOfBirth = Convert.ToDateTime("02/15/2016")
        };
    }

    private UpdatePersonRequest FakeUpdateRequestObject()
    {
        return new UpdatePersonRequest()
        {
            FirstName = "Vinz",
            LastName = "Durano",
            DateOfBirth = Convert.ToDateTime("02/15/2016")
        };
    }

    private CreatePersonRequest FakeCreateRequestObjectWithMissingAttribute()
    {
        return new CreatePersonRequest()
        {
            FirstName = "Vinz",
            LastName = "Durano"
        };
    }

    private CreatePersonRequest FakeUpdateRequestObjectWithMissingAttribute()
    {
        return new CreatePersonRequest()
        {
            FirstName = "Vinz",
            LastName = "Durano"
        };
    }

    [Fact]
    public async Task GET_All_RETURNS_OK()
    {
        // Arrange
        _mockDataManager.Setup(manager => manager.GetAllAsync())
           .ReturnsAsync(GetFakePersonLists());

        // Act
        var result = await _controller.Get();

        // Assert
        var persons = Assert.IsType<List<PersonResponse>>(result);
        Assert.Equal(2, persons.Count);
    }

    [Fact]
    public async Task GET_ById_RETURNS_OK()
    {
        long id = 1;

        _mockDataManager.Setup(manager => manager.GetByIdAsync(id))
           .ReturnsAsync(GetFakePersonLists().Single(p => p.ID.Equals(id)));

        var person = await _controller.Get(id);
        Assert.IsType<PersonResponse>(person);
    }

    [Fact]
    public async Task GET_ById_RETURNS_NOTFOUND()
    {
        var apiException = await Assert.ThrowsAsync<ApiException>(() => _controller.Get(10));
        Assert.Equal(404, apiException.StatusCode);
    }
}

The test project should include tests for POST, PUT and DELETE methods. I’ve just trimmed down the test code for simplicity. Here’s the screenshot of the tests:

测试项目应包括POSTPUTDELETE方法的测试。 为了简化起见,我刚刚精简了测试代码。 这是测试的屏幕截图:

Image 7

StringExtensions的样本方法 (Sample Methods for StringExtensions)

It also occurred to me that converting strings to type datetime, int and long are pretty much common when parsing data so I thought I would include a few sample methods to handle that. Here are the StringExtension methods:

我还想到,在解析数据时,将string s转换为type datetimeintlong类型非常普遍,因此我认为我将包括一些用于处理该问题的示例方法。 这是StringExtension方法:

namespace ApiBoilerPlate.Infrastructure.Extensions
{
    public static class StringExtensions
    {
        public static DateTime ToDateTime(this string dateString)
        {
            DateTime resultDate;
            if (DateTime.TryParse(dateString, out resultDate))
                return resultDate;

            return default;
        }
        public static DateTime? ToNullableDateTime(this string dateString)
        {
            if (string.IsNullOrEmpty((dateString ?? "").Trim()))
                return null;

            DateTime resultDate;
            if (DateTime.TryParse(dateString, out resultDate))
                return resultDate;

            return null;
        }

        public static int ToInt32(this string value, int defaultIntValue = 0)
        {
            int parsedInt;
            if (int.TryParse(value, out parsedInt))
            {
                return parsedInt;
            }

            return defaultIntValue;
        }

        public static int? ToNullableInt32(this string value)
        {
            if (string.IsNullOrEmpty(value))
                return null;

            return value.ToInt32();
        }

        public static long ToInt64(this string value, long defaultInt64Value = 0)
        {
            long parsedInt64;
            if (Int64.TryParse(value, out parsedInt64))
            {
                return parsedInt64;
            }

            return defaultInt64Value;
        }

        public static long? ToNullableInt64(this string value)
        {
            if (string.IsNullOrEmpty(value))
                return null;

            return value.ToInt64();
        }
    }
}

That’s it!. Feel free to request an issue on Github if you find bugs or request a new feature. Your valuable feedback is much appreciated to better improve this project. If you find this useful, please give it a star to show your support for this project. Thank you!

而已!。 如果发现错误或请求新功能,请随时在Github上提出问题。 非常感谢您宝贵的反馈意见,以更好地改进该项目。 如果您觉得这很有用,请给它加星号,以表示您对该项目的支持。 谢谢!

参考文献 (References)

翻译自: https://www.codeproject.com/Articles/5252536/ApiBoilerPlate-New-Features-and-Improvements-for-B

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值