下载source from GitHub

对ASP进行深度重构和优化。NET Core WEB API应用程序代码

介绍

第1部分。创建一个测试的RESTful WEB API应用程序。

第2部分。增加了ASP。NET Core WEB API应用程序的生产力。

在第3部分中,我们将回顾以下内容:

为什么我们需要重构和改进代码?在try/catch/finally中的不重复(DRY)原则异常处理阻止了我们的异常处理中间件在。net Core统一异常消息格式中记录到文件的需求。net Core异常处理中间件实现业务逻辑层应该返回给控制器什么类型的结果?自定义异常处理中间件使用类型的客户提供httpclientfactory处理应用程序的设置缓存关注分离的通用异步DistributedCache库内存和数据库内分页在实体框架控制器vs controllerbase定义Id参数验证过滤器和属性自定义分页参数模型验证过滤器跨源资源共享(歌珥)

使歌珥ASP。NET core没有CORS报头发送的情况下的HTTP错误如何发送HTTP 4xx-5xx响应与CORS报头在一个ASPNET。核心web应用程序
API版本控制

控制内部HTTP调用中的API版本控制错误消息格式版本控制
解析DNS名称本地文档。net核心API应用程序

XML为RESTful api注释OpenApi文档,使用swagger swagger响应示例标记和属性来形成OpenApi文档
摆脱不使用或重复的NuGet包Microsoft.AspNetCore。所有和Microsoft.AspNetCore。从ASP迁移应用程序元打包。NET Core 2.2到3.0的有趣点

为什么我们需要重构和改进代码?

第1部分的目标是创建一个非常简单的基本应用程序,我们可以从中开始。主要关注的是如何使应用和检查不同的方法、修改代码和检查结果变得更容易。

第二部分是关于生产力的。实现了多种方法。与第一部分相比,代码变得更加复杂。

现在,在选择并实现这些方法之后,我们可以将应用程序作为一个整体来考虑。很明显,代码需要深度重构和细化,以满足良好编程风格的各种原则。

不要重复自己(干)原则

根据DRY原则,我们应该消除代码的重复。因此,让我们检查一下ProductsService代码,看看它是否有任何重复。我们可以立即看到,下面的片段在所有返回ProductViewModel或IEnumerable的方法中重复了好几次。类型的值:

隐藏,复制Code…
new ProductViewModel()
{
Id = p.ProductId,
Sku = p.Sku,
Name = p.Name
}

我们总是从一个产品类型对象创建一个ProductViewModel类型对象。将ProductViewModels对象的字段初始化移动到它的构造函数中是合乎逻辑的。让我们在ProductViewModel类中创建一个构造函数方法。在构造函数中,我们用Product参数的适当值填充对象的字段值:

隐藏,复制Codepublic ProductViewModel(Product product)
{
Id = product.ProductId;
Sku = product.Sku;
Name = product.Name;
}

现在我们可以重写复制的代码在FindProductsAsync和GetAllProductsAsync方法的ProductsService:

隐藏,复制Code…
return new OkObjectResult(products.Select(p => new ProductViewModel()
{
Id = p.ProductId,
Sku = p.Sku,
Name = p.Name
}));

return new OkObjectResult(products.Select(p => new ProductViewModel(p)));

修改ProductsService类的GetProductAsync和DeleteProductAsync方法:

隐藏,复制Code…
return new OkObjectResult(new ProductViewModel()
{
Id = product.ProductId,
Sku = product.Sku,
Name = product.Name
});

return new OkObjectResult(new ProductViewModel(product));

对PriceViewModel类重复同样的操作。

隐藏,复制Code…
new PriceViewModel()
{
Price = p.Value,
Supplier = p.Supplier
}

尽管我们在PricesService中只使用了一次片段,但最好还是将PriceViewModel的字段初始化封装在其构造函数中的类中。

让我们创建一个PriceViewModel类构造函数

隐藏,复制Code…
public PriceViewModel(Price price)
{
Price = price.Value;
Supplier = price.Supplier;
}

然后改变片段:

隐藏,复制Code…
return new OkObjectResult(pricess.Select(p => new PriceViewModel()
{
Price = p.Value,
Supplier = p.Supplier
})
.OrderBy(p => p.Price)
.ThenBy(p => p.Supplier));

return new OkObjectResult(pricess.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier));

try/catch/finally块中的异常处理

下一个需要解决的问题是异常处理。在整个应用程序中,所有可能导致异常的操作都在try-catch构造中被调用。这种方法在调试过程中非常方便,因为它允许我们在异常发生的特定位置检查异常。但是这种方法也有代码重复的缺点。ASP中更好的异常处理方法。NET Core是在中间件或异常过滤器中全局地处理它们。

我们将创建异常处理中间件,通过日志记录和生成用户友好的错误消息来集中异常处理。

我们的异常处理中间件的需求

将详细信息记录到日志文件中;调试时详细的错误信息和生产时友好的信息;统一错误信息格式

在。net Core中登录到一个文件

在。net Core应用的主要方法中,我们创建并运行了web服务器。

隐藏,复制Code…
BuildWebHost(args).Run();

此时,将自动创建ILoggerFactory的一个实例。现在可以通过depend访问它注入并在代码中的任何位置执行日志记录。但是,使用标准的ILoggerFactory,我们不能将日志记录到文件中。为了克服这个限制,我们将使用Serilog库,它扩展了ILoggerFactory并允许将日志记录到文件中。”

让我们安装serilog . extension . logging。文件NuGet包第一:

我们应该添加使用微软。extension。logging;语句模块,我们将在其中应用日志记录。

Serilog库可以以不同的方式配置。在我们的简单示例中,要为Serilog设置日志记录规则,我们应该在Configure方法的Startup类中添加下一段代码

隐藏,复制Code…
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddFile(“Logs/log.txt”);

这意味着,日志记录器将写入相对的\日志目录,日志文件的名称格式将为:log- yyyymd .txt

统一异常消息格式

在工作期间,我们的应用程序可以生成不同类型的异常消息。我们的目标是统一这些消息的格式,以便它们可以由客户机应用程序的某种通用方法进行处理。

让所有消息具有以下格式:

隐藏,复制Code{
“message”: “Product not found”
}

格式非常简单。对于像我们这样的简单应用程序来说,这是可以接受的。但是我们应该预见到扩大它的机会并且集中在一个地方。为此,我们将创建一个ExceptionMessage类,它将封装消息格式化过程。我们将在任何需要生成异常消息的地方使用这个类。

让我们在我们的项目中创建一个文件夹异常,并添加一个类ExceptionMessage:>

隐藏,复制Codeusing Newtonsoft.Json;

namespace SpeedUpCoreAPIExample.Exceptions
{
public class ExceptionMessage
{
public string Message { get; set; }

    public ExceptionMessage() {}public ExceptionMessage(string message){Message = message;}public override string ToString(){return JsonConvert.SerializeObject(new { message = new string(Message) });}
}

}

现在我们可以创建ExceptionsHandlingMiddleware了

.NET核心异常处理中间件实现

在异常文件夹中创建一个类ExceptionsHandlingMiddleware:

隐藏,收缩,复制Codeusing Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Net;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Exceptions
{
public class ExceptionsHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;

    public ExceptionsHandlingMiddleware(RequestDelegate next, ILogger<ExceptionsHandlingMiddleware> logger){_next = next;_logger = logger;}public async Task InvokeAsync(HttpContext httpContext){try{await _next(httpContext);}catch (Exception ex){await HandleUnhandledExceptionAsync(httpContext, ex);}}private async Task HandleUnhandledExceptionAsync(HttpContext context,Exception exception){_logger.LogError(exception, exception.Message);if (!context.Response.HasStarted){int statusCode = (int)HttpStatusCode.InternalServerError; // 500string message = string.Empty;

#if DEBUG
message = exception.Message;
#else
message = “An unhandled exception has occurred”;
#endif
context.Response.Clear();
context.Response.ContentType = “application/json”;
context.Response.StatusCode = statusCode;

            var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);}}
}

}

这个中间件在调试(#if调试)或不进行调试的用户友好的消息时拦截未处理的异常,记录异常的详细信息并发出详细消息。

注意,我们是如何使用ExceptionMessage类来格式化结果的。

现在,我们应该在启动时将这个中间件添加到应用程序HTTP请求管道中。在app.UseMvc()之前配置方法;声明。

隐藏,复制Codeapp.UseMiddleware();;

app.UseMvc();

让我们来看看它是如何工作的。为此,我们将更改ProductsRepository中的存储过程名称。对于不存在的GetProductsBySKUError方法的FindProductsAsync方法。

隐藏,复制Codepublic async Task<IEnumerable> FindProductsAsync(string sku)
{
return await _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKUError @sku = {0}", sku).ToListAsync();
}

并从ProductsService中删除Try-Catch块。FindProductsAsync方法

隐藏,复制Codepublic async Task FindProductsAsync(string sku)
{
try
{
IEnumerabler products = await _productsRepository.FindProductsAsync(sku);

}
catch
{
return new ConflictResult();
}

}

让我们运行应用程序并检查结果

打电话到http://localhost:49858/api/products/find/aa

我们将有500 Http响应代码和一条消息:

让我们检查一下日志文件

现在我们有了带有文件的日志文件夹

在文件中我们有详细的异常描述:

隐藏,复制Code…
“”[dbo].GetProductsBySKUError @sku = @p0" (627a98df)
System.Data.SqlClient.SqlException (0x80131904): Could not find stored procedure ‘dbo.GetProductsBySKUError’.

我们声称,我们的异常处理中间件应该在调试模式下生成详细的错误消息,在生产模式下生成友好的消息。让我们检查一下。为此,我们将在工具栏中更改发布的活动解决方案配置:

或在配置管理器:

然后再次调用不正确的API。如我们所料,结果将是:

因此,我们的异常处理程序如我们预期的那样工作。

注意!如果我们没有删除Try-Catch块,我们将永远不会让这个处理程序工作,因为未处理的豁免将由Catch语句中的代码处理。

不要忘记恢复正确的存储过程名称GetProductsBySKU!

现在我们可以移除product service和PricesService clacces中的所有Try-Catch块。

注意!为了简洁起见,我们省略了删除Try-Catch块实现的代码。

我们唯一还需要尝试的地方是产品和服务。PreparePricesAsync PricesService。PreparePricesAsync方法。正如我们在第2部分中所讨论的那样,我们不希望在这些地方中断应用程序工作流

删除了Try-Catch块之后,代码变得更加简单和直接。但是当我们返回时,在大多数服务的方法中仍然有一些重复

隐藏,复制Codereturn new NotFoundResult();

让我们也改进这一点。

在所有方法中,查找值的集合,如ProductsService。GetAllProductsAsync ProductsService。FindProductsAsync PricesService。我们有两个问题。

第一个是检查从存储库接收的集合是否为空。为此我们使用а声明

隐藏,复制Code…
if (products != null)

但是在我们的例子中,集合永远不会是空的(除非存储库中发生了处理过的异常)。由于所有异常现在都在服务和存储库之外的专用中间件中处理,所以我们总是会收到一个值集合(如果没有找到任何东西,则为空)。所以,检查结果的正确方法是

隐藏,复制Codeif (products.Any())

隐藏,复制Code(products.Count() > 0)

GetPricesAsync方法中的PricesService类也是如此:change

隐藏,复制Code…
if (pricess != null)

if (pricess.Any())

第二个问题lem是空集合应该返回的结果。到目前为止,我们已经返回了NotFoundResult(),但它也不是真正正确的。例如,如果我们创建另一个API,它应该返回一个由产品及其价格组成的值,那么一个空的价格集合将在JSON结构中表示为一个空的massive,而StatusCode将为200——好的。所以,为了保持一致,我们应该重写上述方法的代码,删除空集合的NotFoundResult:

隐藏,复制Codepublic async Task FindProductsAsync(string sku)
{
IEnumerable products = await _productsRepository.FindProductsAsync(sku);

if (products.Count() == 1)
{//only one record found - prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{PreparePricesAsync(products.FirstOrDefault().ProductId);});
};return new OkObjectResult(products.Select(p => new ProductViewModel(p)));

}

public async Task GetAllProductsAsync()
{
IEnumerable products = await _productsRepository.GetAllProductsAsync();

return new OkObjectResult(products.Select(p => new ProductViewModel(p)));

}

而在PricesService

隐藏,复制Codepublic async Task GetPricesAsync(int productId)
{
IEnumerable pricess = await _pricesRepository.GetPricesAsync(productId);

return new OkObjectResult(pricess.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier));

}

代码变得非常简单,但另一个问题仍然存在:这是从服务返回IActionResult的正确解决方案吗?

业务逻辑层应该向控制器返回什么类型的结果?

通常,业务层的方法向控制器返回一个POCO(普通的旧CLR对象)类型的值,然后控制器使用适当的StatusCode形成适当的响应。例如,产品服务。GetProductAsync方法应该返回ProductViewModel对象或null(如果没有找到产品)。控制器应该分别生成OkObjectResult(ProductViewModel)或NotFound()响应。

但这种方法并不总是可行的。实际上,我们可以有不同的理由从服务返回null。例如,让我们设想一个用户可以访问某些内容的应用程序。这些内容可以是公开的、私有的或预付的。当用户请求一些内容时,ISomeContentService可以返回ISomeContent或null。有一些可能的原因,这个空:

隐藏,复制Code401 Unauthorized
402 Payment Required
403 Forbidden
404 Not Found

原因在服务内部变得很清楚。如果一个方法只返回null值,服务如何通知控制器这个原因?对于控制器来说,这还不足以创建适当的响应。为了解决这个问题,我们使用IActionResult类型作为服务-业务层的返回类型。这种方法非常灵活,与IActionResult result一样,我们可以将所有内容传递给控制器。但是业务层应该形成API的响应,执行控制器的工作吗?它会不会打破关注点分离的设计原则?

在业务层摆脱IActionResult的一种可能方法是使用自定义异常来控制应用程序的工作流和生成正确的响应。为了提供这一点,我们将增强异常处理中间件,使其能够处理定制异常。

自定义异常处理中间件

让我们创建一个简单的HttpException类,它继承自Exception。并增强了out异常处理程序中间件来处理HttpException类型的异常。

在HttpException文件夹中添加类HttpException

隐藏,复制Codeusing System;
using System.Net;

namespace SpeedUpCoreAPIExample.Exceptions
{
// Custom Http Exception
public class HttpException : Exception
{
// Holds Http status code: 404 NotFound, 400 BadRequest, …
public int StatusCode { get; }
public string MessageDetail { get; set; }

    public HttpException(HttpStatusCode statusCode, string message = null, string messageDetail = null) : base(message){StatusCode = (int)statusCode;MessageDetail = messageDetail;}
}

}

并更改ExceptionsHandlingMiddleware类代码

隐藏,收缩,复制Code…
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (HttpException ex)
{
await HandleHttpExceptionAsync(httpContext, ex);
}
catch (Exception ex)
{
await HandleUnhandledExceptionAsync(httpContext, ex);
}
}


private async Task HandleHttpExceptionAsync(HttpContext context, HttpException exception)
{
_logger.LogError(exception, exception.MessageDetail);

if (!context.Response.HasStarted)
{int statusCode = exception.StatusCode;string message = exception.Message;context.Response.Clear();context.Response.ContentType = "application/json";context.Response.StatusCode = statusCode;var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);
}

}

在中间件中,我们在处理一般异常类型之前处理HttpException类型的异常,调用HandleHttpExceptionAsync方法。如果提供,我们会记录详细的异常消息。

现在,我们可以重写产品和服务。GetProductAsync和ProductsService.DeleteProductAsync

隐藏,收缩,复制Code…
public async Task GetProductAsync(int productId)
{
Product product = await _productsRepository.GetProductAsync(productId);

if (product == null)throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");ThreadPool.QueueUserWorkItem(delegate
{PreparePricesAsync(productId);
});return new OkObjectResult(new ProductViewModel(product));

}

public async Task DeleteProductAsync(int productId)
{
Product product = await _productsRepository.DeleteProductAsync(productId);

if (product == null)throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");return new OkObjectResult(new ProductViewModel(product));

}

在这个版本中,我们不是用IActionResult返回404 Not Found from the services,而是抛出一个定制的HttpException,异常处理中间件会返回一个正确的响应给用户。让我们通过使用productid调用API来检查它是如何工作的,它显然不在Products表中:

http://localhost:49858/api/products/100

我们的通用异常处理中间件工作得很好。

由于我们已经创建了一种替代方法来传递任何StatucCode和来自业务层的消息,所以我们可以轻松地将返回值类型从IActionResult更改为合适的POCO类型。为此,我们必须重写以下接口:

隐藏,复制Codepublic interface IProductsService
{
Task GetAllProductsAsync();
Task GetProductAsync(int productId);
Task FindProductsAsync(string sku);
Task DeleteProductAsync(int productId);

Task<IEnumerable<ProductViewModel>> GetAllProductsAsync();
Task<ProductViewModel> GetProductAsync(int productId);
Task<IEnumerable<ProductViewModel>> FindProductsAsync(string sku);
Task<ProductViewModel> DeleteProductAsync(int productId);

}

和改变

隐藏,复制Codepublic interface IPricesService
{
Task<IEnumerable> GetPricesAsync(int productId);

Task<IEnumerable<PriceViewModel>> GetPricesAsync(int productId);


}

我们还应该在ProductsService和PricesService类中重新声明适当的方法,方法是将IActionResult类型从接口更改为类型。通过删除OkObjectResult语句,也改变了它们的返回语句。例如,在产品服务中。GetAllProductsAsync方法:

新版本将是:

隐藏,复制Codepublic async Task<IEnumerable> GetAllProductsAsync()
{
IEnumerable products = await _productsRepository.GetAllProductsAsync();

return products.Select(p => new ProductViewModel(p));

}

最后一个任务是更改控制器的操作,以便它们创建OK响应。它总是200 OK,因为NotFound将被ExceptionsHandlingMiddleware返回

例如,对于product service。返回语句应该更改为:

隐藏,复制Code// GET /api/products
[HttpGet]
public async Task GetAllProductsAsync()
{
return await _productsService.GetAllProductsAsync();
}

:

隐藏,复制Code// GET /api/products
[HttpGet]
public async Task GetAllProductsAsync()
{
return new OkObjectResult(await _productsService.GetAllProductsAsync());
}

您可以在所有ProductsController的操作和PricesService中执行此操作。GetPricesAsync行动。

使用HttpClientFactory的类型化客户端

我们以前的HttpClient实现有一些问题,我们可以改进。首先,我们必须注入IHttpContextAccessor以在GetFullyQualifiedApiUrl方法中使用它。IHttpContextAccessor和GetFullyQualifiedApiUrl方法只专用于HttpClient,从未在产品服务的其他地方使用。如果我们想在其他服务中应用相同的功能,我们将不得不编写几乎相同的代码。因此,最好是在HttpClient周围创建一个单独的helper类包装器,并将所有必要的HttpClient调用业务逻辑封装在这个类中。

我们将使用另一种处理HttpClientFactory类型的客户机类的方法。

在接口文件夹中创建一个ISelfHttpClient intetface:

隐藏,复制Codeusing System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Interfaces
{
public interface ISelfHttpClient
{
Task PostIdAsync(string apiRoute, string id);
}
}

我们只声明了一个方法,它使用HttpPost方法和Id参数调用任何控制器的动作

让我们创建一个助手文件夹,并在那里添加一个新的类SelfHttpClient继承自ISelfHttpClient接口:

隐藏,收缩,复制Codeusing Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Helpers
{
// HttpClient for application’s own controllers access
public class SelfHttpClient : ISelfHttpClient
{
private readonly HttpClient _client;

    public SelfHttpClient(HttpClient httpClient, IHttpContextAccessor httpContextAccessor){string baseAddress = string.Format("{0}://{1}/api/",httpContextAccessor.HttpContext.Request.Scheme,httpContextAccessor.HttpContext.Request.Host);_client = httpClient;_client.BaseAddress = new Uri(baseAddress);}// Call any controller's action with HttpPost method and Id parameter.// apiRoute - Relative API route.// id - The parameter.public async Task PostIdAsync(string apiRoute, string id){try{var result = await _client.PostAsync(string.Format("{0}/{1}", apiRoute, Id), null).ConfigureAwait(false);}catch (Exception ex){//ignore errors}}
}

}

在这个类中,我们获得了要在类构造函数中调用的API的一个基地址。在PostIdAsync方法中,我们通过HttpPost方法的相对apiRoute路由调用API,并将Id作为响应参数传递。注意,我们只发送null,而不是创建一个空的HttpContent

我们应该在启动时声明这个类。ConfigureServices方法:

隐藏,复制Code…
services.AddHttpClient();
services.AddHttpClient<ISelfHttpClient, SelfHttpClient>();

现在我们可以在应用程序的任何地方使用。在ProductsService service中,我们应该在类构造函数中注入它。我们可以删除IHttpContextAccessor和IHttpClientFactory,因为我们不再使用它们了,我们可以删除GetFullyQualifiedApiUrl方法。

新版本的ProductsService构造函数将是:

隐藏,复制Codepublic class ProductsService : IProductsService
{
private readonly IProductsRepository _productsRepository;
private readonly ISelfHttpClient _selfHttpClient;

public ProductsService(IProductsRepository productsRepository, ISelfHttpClient selfHttpClient)
{_productsRepository = productsRepository;_selfHttpClient = selfHttpClient;
}

}

让我们更改PreparePricesAsync方法。首先,我们将它重命名为CallPreparePricesApiAsync,因为这个名称更有信息,并且方法:

隐藏,复制Codeprivate async void CallPreparePricesApiAsync(string productId)
{
await _selfHttpClient.PostIdAsync(“prices/prepare”, productId);
}

当我们在ProductsService中调用这个方法时,不要忘记将PreparePricesAsync更改为CallPreparePricesApiAsync。还要考虑到,在CallPreparePricesApiAsync中,我们使用了字符串类型的productId参数

可以看到,我们将API URL的尾部部分作为PostIdAsync参数传递。新的SelfHttpClient是真正可重用的。例如,如果我们有一个API /products/prepare,我们可以这样调用API:

隐藏,复制Codeprivate async void CallPrepareProductAPIAsync(string productId)
{
await _selfHttpClient.PostIdAsync(“products/prepare”, productId);
}

处理应用程序的设置

在前面的部分中,我们通过注入IConfiguration来访问应用程序的设置。然后,在类构造器中,我们创建了设置类,在其中解析适当的设置变量并应用默认值。这种方法很适合调试,但是在调试之后,使用简单的POCO类访问应用程序的设置似乎更可取。让我们稍微改变一下appsets .json。我们将为产品和价格服务设置两个部分:

隐藏,复制Code “Caching”: {
“PricesExpirationPeriod”: 15
}

“Products”: {
“CachingExpirationPeriod”: 15,
“DefaultPageSize”: 20
},
“Prices”: {
“CachingExpirationPeriod”: 15,
“DefaultPageSize”: 20
},

注意!在本文中,我们将使用DefaultPageSize值。

让我们创建设置POCO类。创建一个设置文件夹与以下文件:

隐藏,复制Codenamespace SpeedUpCoreAPIExample.Settings
{
public class ProductsSettings
{
public int CachingExpirationPeriod { get; set; }
public int DefaultPageSize { get; set; }
}
}

隐藏,复制Codenamespace SpeedUpCoreAPIExample.Settings
{
public class PricesSettings
{
public int CachingExpirationPeriod { get; set; }
public int DefaultPageSize { get; set; }
}
}

尽管这些类仍然相似,但在实际应用程序中,不同服务的设置可能会有很大差异。因此,我们将使用这两个类,以便以后不分割它们。

现在,我们使用这些类所需要的就是在start . configureservices中声明它们:

隐藏,复制Code…
//Settings
services.Configure(Configuration.GetSection(“Products”));
services.Configure(Configuration.GetSection(“Prices”));

//Repositories

在此之后,我们可以在应用程序的任何地方注入设置类,我们将在下面几节中演示

缓存担心分离

在PricesRepository中,我们使用IDistributedCache缓存实现了缓存。缓存在存储库中的想法是完全关闭数据存储源的业务层细节。在这种情况下,不知道服务是否通过了缓存阶段的数据。这个解决方案真的好吗?

存储库负责使用DbContext,即从数据库中获取数据或将数据保存到数据库中。但是缓存肯定是出于这个考虑。此外,在更复杂的系统中,在从数据库接收到原始数据之后,可能需要在将数据传递给用户之前对其进行修改。将数据缓存到最终状态是合理的。根据这一点,最好将缓存应用于服务中的业务逻辑层。

注意!PricesRepository。GetPricesAsync PricesRepository。用于缓存的代码几乎是相同的。从逻辑上讲,我们应该将这些代码移到一个单独的类中,以避免重复。

通用异步分布式缓存存储库

其思想是创建一个存储库来封装IDistributedCache业务逻辑。存储库将是通用的,并能够缓存任何类型的对象。这是它的界面

隐藏,复制Codeusing Microsoft.Extensions.Caching.Distributed;
using System;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IDistributedCacheRepository
{
Task GetOrSetValueAsync(string key, Func<Task> valueDelegate, DistributedCacheEntryOptions options);
Task IsValueCachedAsync(string key);
Task GetValueAsync(string key);
Task SetValueAsync(string key, T value, DistributedCacheEntryOptions options);
Task RemoveValueAsync(string key);
}
}

这里唯一有趣的地方是作为GetOrSetValueAsync方法的第二个参数的异步委托。它将在实现部分进行讨论。在Repositories文件夹中创建一个新的类DistributedCache存储库:

隐藏,收缩,复制Codeusing Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using SpeedUpCoreAPIExample.Interfaces;
using System;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Repositories
{
public abstract class DistributedCacheRepository : IDistributedCacheRepository where T : class
{
private readonly IDistributedCache _distributedCache;
private readonly string _keyPrefix;

    protected DistributedCacheRepository(IDistributedCache distributedCache, string keyPrefix){_distributedCache = distributedCache;_keyPrefix = keyPrefix;}public virtual async Task<T> GetOrSetValueAsync(string key, Func<Task<T>> valueDelegate, DistributedCacheEntryOptions options){var value = await GetValueAsync(key);if (value == null){value = await valueDelegate();if (value != null)await SetValueAsync(key, value, options ?? GetDefaultOptions());}return null;}public async Task<bool> IsValueCachedAsync(string key){var value = await _distributedCache.GetStringAsync(_keyPrefix + key);return value != null;}public async Task<T> GetValueAsync(string key){var value = await _distributedCache.GetStringAsync(_keyPrefix + key);return value != null ? JsonConvert.DeserializeObject<T>(value) : null;}public async Task SetValueAsync(string key, T value, DistributedCacheEntryOptions options){await _distributedCache.SetStringAsync(_keyPrefix + key, JsonConvert.SerializeObject(value), options ?? GetDefaultOptions());}public async Task RemoveValueAsync(string key){await _distributedCache.RemoveAsync(_keyPrefix + key);}protected abstract DistributedCacheEntryOptions GetDefaultOptions();
}

}

这个类是抽象的,因为我们不打算直接创建它的实例。相反,它将是PricesCacheRepository和ProductsCacheRepository类的基类。注意,GetOrSetValueAsync有一个虚拟修饰符—我们将在继承的类中重写这个方法。GetDefaultOptions方法也是如此,在这种情况下,它被声明为抽象,因此它将在派生类中实现。当它在父DistributedCacheRepository类中调用时,将调用从派生类继承的方法。

GetOrSetValueAsync方法的第二个参数声明为异步委托:valueDelegate。在GetOrSetValueAsync方法中,我们首先尝试从缓存中获取一个值。如果它还没有缓存,我们通过调用valueDelegate函数获得它,然后缓存值。

让我们从DistributedCacheRepository创建具有明确类型的继承类。

隐藏,复制Codeusing Microsoft.Extensions.Caching.Distributed;
using SpeedUpCoreAPIExample.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IPricesCacheRepository
{
Task<IEnumerable> GetOrSetValueAsync(string key, Func<Task<IEnumerable>> valueDelegate, DistributedCacheEntryOptions options = null);
Task IsValueCachedAsync(string key);
Task RemoveValueAsync(string key);
}
}

隐藏,复制Codeusing Microsoft.Extensions.Caching.Distributed;
using SpeedUpCoreAPIExample.Models;
using System;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IProductCacheRepository
{
Task GetOrSetValueAsync(string key, Func<Task> valueDelegate, DistributedCacheEntryOptions options = null);
Task IsValueCachedAsync(string key);
Task RemoveValueAsync(string key);
Task SetValueAsync(string key, Product value, DistributedCacheEntryOptions options = null);
}
}

然后我们将在Repositories文件夹中创建两个类

隐藏,收缩,复制Codeusing Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.Settings;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Repositories
{
public class PricesCacheRepository : DistributedCacheRepository<IEnumerable>, IPricesCacheRepository
{
private const string KeyPrefix = "Prices: ";
private readonly PricesSettings _settings;

    public PricesCacheRepository(IDistributedCache distributedCache, IOptions<PricesSettings> settings): base(distributedCache, KeyPrefix){_settings = settings.Value;}public override async Task<IEnumerable<Price>> GetOrSetValueAsync(string key, Func<Task<IEnumerable<Price>>> valueDelegate, DistributedCacheEntryOptions options = null){return base.GetOrSetValueAsync(key, valueDelegate, options);}protected override DistributedCacheEntryOptions GetDefaultOptions(){//use default caching options for the class if they are not defined in options parameterreturn new DistributedCacheEntryOptions(){AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_settings.CachingExpirationPeriod)};}
}

}

隐藏,收缩,复制Codeusing Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.Settings;
using System;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Repositories
{
public class ProductCacheRepository : DistributedCacheRepository, IProductCacheRepository
{
private const string KeyPrefix = "Product: ";
private readonly ProductsSettings _settings;

    public ProductCacheRepository(IDistributedCache distributedCache, IOptions<ProductsSettings> settings) : base(distributedCache, KeyPrefix){_settings = settings.Value;}public override async Task<Product> GetOrSetValueAsync(string key, Func<Task<Product>> valueDelegate, DistributedCacheEntryOptions options = null){return await base.GetOrSetValueAsync(key, valueDelegate, options);}protected override DistributedCacheEntryOptions GetDefaultOptions(){//use default caching options for the class if they are not defined in options parameterreturn new DistributedCacheEntryOptions(){AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_settings.CachingExpirationPeriod)};}
}

}

注意!GetDefaultOptions的实现在ProductCacheRepository和PricesCacheRepository类中是相等的,似乎可以移到基类中。但在真实的应用程序中,缓存策略可能因对象的不同而不同,如果我们将GetDefaultOptions的一些通用实现移动到基类中,当派生类的缓存逻辑发生变化时,我们将不得不更改基类。这将违反“启闭”设计原则。这就是我们在派生类中实现GetDefaultOptions方法的原因。

在Startup类中声明存储库

隐藏,复制Code…
services.AddScoped<IPricesCacheRepository, PricesCacheRepository>();
services.AddScoped<IProductCacheRepository, ProductCacheRepository>();

现在,我们可以从PricesRepository删除缓存,使其尽可能简单:

隐藏,复制Codeusing Microsoft.EntityFrameworkCore;
using SpeedUpCoreAPIExample.Contexts;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Repositories
{
public class PricesRepository : IPricesRepository
{
private readonly DefaultContext _context;

    public PricesRepository(DefaultContext context){_context = context;}public async Task<IEnumerable<Price>> GetPricesAsync(int productId){return await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync();}
}

}

我们还可以重写PricesService类。我们注入了IPricesCacheRepository,而不是IDistributedCache。

隐藏,收缩,复制Codeusing SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.ViewModels;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Services
{
public class PricesService : IPricesService
{
private readonly IPricesRepository _pricesRepository;
private readonly IPricesCacheRepository _pricesCacheRepository;

    public PricesService(IPricesRepository pricesRepository, IPricesCacheRepository pricesCacheRepository){_pricesRepository = pricesRepository;_pricesCacheRepository = pricesCacheRepository;}public async Task<IEnumerable<PriceViewModel>> GetPricesAsync(int productId){IEnumerable<Price> pricess = await _pricesCacheRepository.GetOrSetValueAsync(productId.ToString(), async () =>await _pricesRepository.GetPricesAsync(productId));return pricess.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier);}public async Task<bool> IsPriceCachedAsync(int productId){return await _pricesCacheRepository.IsValueCachedAsync(productId.ToString());}public async Task RemovePriceAsync(int productId){await _pricesCacheRepository.RemoveValueAsync(productId.ToString());}public async Task PreparePricesAsync(int productId){try{await _pricesCacheRepository.GetOrSetValueAsync(productId.ToString(), async () => await _pricesRepository.GetPricesAsync(productId));}catch{}}
}

}

在GetPricesAsync和PreparePricesAsync方法中,我们使用了PricesCacheRepository的GetOrSetValueAsync方法。如果期望的值不在缓存中,则调用异步方法GetPricesAsync。

我们还创建了IsPriceCachedAsync和RemovePriceAsync方法,它们将在后面使用。不要忘记在IPricesService接口中声明它们:

隐藏,复制Codeusing SpeedUpCoreAPIExample.ViewModels;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IPricesService
{
Task<IEnumerable> GetPricesAsync(int productId);
Task IsPriceCachedAsync(int productId);
Task RemovePriceAsync(int productId);
Task PreparePricesAsync(int productId);
}
}

让我们来看看新的缓存方法是如何工作的。为此,在GetPricesAsync方法中设置一个断点:

使用Swagger Inspector扩展调用http://localhost:49858/api/prices/1 api两次:

在第一次调用期间,调试器到达断点。这意味着,GetOrSetValueAsync方法无法在缓存中找到结果,因此必须调用_pricesRepository.GetPricesAsync(productId)方法,该方法作为委托传递给GetOrSetValueAsync。但是在第二次调用时,应用程序工作流不会在断点处停止,因为它从缓存中获取一个值。

现在我们可以在ProductService中使用通用缓存机制

隐藏,收缩,复制Codenamespace SpeedUpCoreAPIExample.Services
{
public class ProductsService : IProductsService
{
private readonly IProductsRepository _productsRepository;
private readonly ISelfHttpClient _selfHttpClient;
private readonly IPricesCacheRepository _pricesCacheRepository;
private readonly IProductCacheRepository _productCacheRepository;
private readonly ProductsSettings _settings;

    public ProductsService(IProductsRepository productsRepository, IPricesCacheRepository pricesCacheRepository,IProductCacheRepository productCacheRepository, IOptions<ProductsSettings> settings, ISelfHttpClient selfHttpClient){_productsRepository = productsRepository;_selfHttpClient = selfHttpClient;_pricesCacheRepository = pricesCacheRepository;_productCacheRepository = productCacheRepository;_settings = settings.Value;}public async Task<ProductsPageViewModel> FindProductsAsync(string sku){IEnumerable<product> products = await _productsRepository.FindProductsAsync(sku);if (products.Count() == 1){//only one record foundProduct product = products.FirstOrDefault();string productId = product.ProductId.ToString();//cache a product if not in cache yetif (!await _productCacheRepository.IsValueCachedAsync(productId)){await _productCacheRepository.SetValueAsync(productId, product);}//prepare pricesif (!await _pricesCacheRepository.IsValueCachedAsync(productId)){//prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{CallPreparePricesApiAsync(productId);});}};return new OkObjectResult(products.Select(p => new ProductViewModel(p)));}…public async Task<ProductViewModel> GetProductAsync(int productId){Product product = await _productCacheRepository.GetOrSetValueAsync(productId.ToString(), async () => await _productsRepository.GetProductAsync(productId));if (product == null){throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");}//prepare pricesif (!await _pricesCacheRepository.IsValueCachedAsync(productId.ToString())){//prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{CallPreparePricesApiAsync(productId.ToString());});}return new ProductViewModel(product);}…public async Task<ProductViewModel> DeleteProductAsync(int productId){Product product = await _productsRepository.DeleteProductAsync(productId);if (product == null){throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");}//remove product and its prices from cacheawait _productCacheRepository.RemoveValueAsync(productId.ToString());await _pricesCacheRepository.RemoveValueAsync(productId.ToString());return new OkObjectResult(new ProductViewModel(product));}        …

实体框架中的内存分页和数据库分页

您可能已经注意到,ProductsController的方法GetAllProductsAsync和FindProductsAsync以及PricesController的GetPricesAsync方法返回产品和价格的集合,根据集合的大小,这些集合没有限制。这意味着,在真实的数据库庞大的应用程序中,某些API的响应可能会返回大量数据,以至于客户端应用程序无法在合理的时间内处理甚至接收这些数据。为了避免这个问题,一个好的实践是建立API结果的分页。

有两种组织分页的方法:在内存中和在数据库中。例如,当我们收到一些产品的价格时,我们会将结果缓存到Redis缓存中。所以,我们已经有了整套的价格,可以建立内存分页,这是更快的方法。

另一方面,在GetAllProductsAsync方法中使用内存分页不是一个好主意,因为要在内存中进行分页,我们应该将整个产品集合从数据库读入内存。这是一个非常缓慢的操作,会消耗很多资源。因此,在这种情况下,最好根据页面大小和索引在数据库中过滤必要的数据集。

对于分页,我们将创建一个通用的PaginatedList类,它将能够处理任何数据类型的集合,并支持内存和数据库中的分页方法。

让我们创建一个通用的PaginatedList 继承自List 在helper文件夹中

隐藏,收缩,复制Codeusing Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Helpers
{
public class PaginatedList : List
{
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }

    public PaginatedList(IEnumerable<T> source, int pageSize, int pageIndex = 1){TotalCount = source.Count();PageIndex = pageIndex;PageSize = pageSize == 0 ? TotalCount : pageSize;TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);this.AddRange(source.Skip((PageIndex - 1) * PageSize).Take(PageSize));}private PaginatedList(IEnumerable<T> source, int pageSize, int pageIndex, int totalCount) : base(source){PageIndex = pageIndex;PageSize = pageSize;TotalCount = totalCount;TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);}public static async Task<PaginatedList<T>> FromIQueryable(IQueryable<T> source, int pageSize, int pageIndex = 1){int totalCount = await source.CountAsync();pageSize = pageSize == 0 ? totalCount : pageSize;int totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);if (pageIndex > totalPages){//return empty listreturn new PaginatedList<T>(new List<T>(), pageSize, pageIndex, totalCount);}if (pageIndex == 1 && pageSize == totalCount){//no paging needed}else{source = source.Skip((pageIndex - 1) * pageSize).Take(pageSize);};List<T> sourceList = await source.ToListAsync();return new PaginatedList<T>(sourceList, pageSize, pageIndex, totalCount);}
}

}

我们需要第一个constructor,用于任何类型的内存数据收集。第二个构造函数也用于内存中的集合,但前提是已经知道页面大小和页面数量。我们将它标记为私有,因为它只在FromIQueryable类本身中使用。

FromIQueryable用于建立数据库内分页。源参数具有IQueryable类型。使用IQueryable在执行对数据库的实际请求之前,我们不会处理物理数据,如source.CountAsync()或source.ToListAsync()。因此,我们能够格式化一个适当的分页查询,并且在一个请求中只接收一小组过滤后的数据。

让我们也调整一下ProductsRepository。GetAllProductsAsync ProductsRepository。FindProductsAsync方法,以便它们能够处理数据库内分页。现在它们应该返回IQueryable,而不是以前的IEnumerable。

隐藏,复制Codenamespace SpeedUpCoreAPIExample.Interfaces
{
public interface IProductsRepository
{

Task<IEnumerable> GetAllProductsAsync();
Task<IEnumerable> FindProductsAsync(string sku);

    IQueryable<Product> GetAllProductsAsync();IQueryable<Product> FindProductsAsync(string sku);


}
}

在ProductsRepository类中正确的方法代码

隐藏,复制Code…
public async Task<IEnumerable> GetAllProductsAsync()
{
return await _context.Products.AsNoTracking().ToListAsync();
}

    public IQueryable<Product> GetAllProductsAsync(){return  _context.Products.AsNoTracking();}public async Task<IEnumerable<Product>> FindProductsAsync(string sku){return await _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKU @sku = {0}", sku).ToListAsync();}public IQueryable<Product> FindProductsAsync(string sku){return _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKU @sku = {0}", sku);}

让我们定义一些类,在这些类中我们将把分页结果返回给用户。在ViewModels文件夹中创建一个基类PageViewModel

隐藏,复制Codenamespace SpeedUpCoreAPIExample.ViewModels
{
public class PageViewModel
{
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalPages { get; private set; }
public int TotalCount { get; private set; }

    public bool HasPreviousPage => PageIndex > 1;public bool HasNextPage => PageIndex < TotalPages;public PageViewModel(int pageIndex, int pageSize, int totalPages, int totalCount){PageIndex = pageIndex;PageSize = pageSize;TotalPages = totalPages;TotalCount = totalCount;}
}

}

ProductsPageViewModel和PricesPageViewModel类,继承自PageViewModel

隐藏,复制Codeusing SpeedUpCoreAPIExample.Helpers;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Linq;

namespace SpeedUpCoreAPIExample.ViewModels
{
public class ProductsPageViewModel : PageViewModel
{
public IList Items;

    public ProductsPageViewModel(PaginatedList<Product> paginatedList) :base(paginatedList.PageIndex, paginatedList.PageSize, paginatedList.TotalPages, paginatedList.TotalCount){this.Items = paginatedList.Select(p => new ProductViewModel(p)).ToList();}
}

}

隐藏,复制Codeusing SpeedUpCoreAPIExample.Helpers;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Linq;

namespace SpeedUpCoreAPIExample.ViewModels
{
public class PricesPageViewModel : PageViewModel
{
public IList Items;

    public PricesPageViewModel(PaginatedList<Price> paginatedList) :base(paginatedList.PageIndex, paginatedList.PageSize, paginatedList.TotalPages, paginatedList.TotalCount){this.Items = paginatedList.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier).ToList();}
}

}

在PricesPageViewModel中,我们对PriceViewModel的分页列表应用了额外的排序

现在我们应该改变产品和服务。GetAllProductsAsync ProductsService。FindProductsAsync,以便它们返回ProductsPageViewMode

隐藏,复制Codepublic interface IProductsService

Task<IEnumerable> GetAllProductsAsync();
Task<IEnumerable> FindProductsAsync(string sku);

Task<ProductsPageViewModel> GetAllProductsAsync(int pageIndex, int pageSize);
Task<ProductsPageViewModel> FindProductsAsync(string sku, int pageIndex, int pageSize);

隐藏,收缩,复制Code public class ProductsService : IProductsService
{
private readonly IProductsRepository _productsRepository;
private readonly ISelfHttpClient _selfHttpClient;
private readonly IPricesCacheRepository _pricesCacheRepository;
private readonly IProductCacheRepository _productCacheRepository;
private readonly ProductsSettings _settings;

    public ProductsService(IProductsRepository productsRepository, IPricesCacheRepository pricesCacheRepository,IProductCacheRepository productCacheRepository, IOptions<ProductsSettings> settings, ISelfHttpClient selfHttpClient){_productsRepository = productsRepository;_selfHttpClient = selfHttpClient;_pricesCacheRepository = pricesCacheRepository;_productCacheRepository = productCacheRepository;_settings = settings.Value;}public async Task<ProductsPageViewModel> FindProductsAsync(string sku, int pageIndex, int pageSize){pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize;PaginatedList<Product> products = await PaginatedList<Product>.FromIQueryable(_productsRepository.FindProductsAsync(sku), pageIndex, pageSize);if (products.Count() == 1){//only one record foundProduct product = products.FirstOrDefault();string productId = product.ProductId.ToString();//cache a product if not in cache yetif (!await _productCacheRepository.IsValueCachedAsync(productId)){await _productCacheRepository.SetValueAsync(productId, product);}//prepare pricesif (!await _pricesCacheRepository.IsValueCachedAsync(productId)){//prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{CallPreparePricesApiAsync(productId);});}};return new ProductsPageViewModel(products);}public async Task<ProductsPageViewModel> GetAllProductsAsync(int pageIndex, int pageSize){pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize;PaginatedList<Product> products = await PaginatedList<Product>.FromIQueryable(_productsRepository.GetAllProductsAsync(), pageIndex, pageSize);return new ProductsPageViewModel(products);}

注意,如果没有将有效参数PageIndex和PageSize传递给PaginatedList构造器,则使用默认值——PageIndex = 1和PageSize =整个datatable大小。为了避免返回所有的产品和价格表的记录,我们将使用默认值DefaultPageSize从ProductsSettings和PricesSettings相应地。

更改PricesServicePricesAsync返回PricesPageViewModel

隐藏,复制Codepublic interface IPricesService

Task<IEnumerable GetPricesAsync(int productId);

Task<PricesPageViewModel> GetPricesAsync(int productId, int pageIndex, int pageSize);

隐藏,复制Code public class PricesService : IPricesService
{
private readonly IPricesRepository _pricesRepository;
private readonly IPricesCacheRepository _pricesCacheRepository;
private readonly PricesSettings _settings;

    public PricesService(IPricesRepository pricesRepository, IPricesCacheRepository pricesCacheRepository, IOptions<PricesSettings> settings){_pricesRepository = pricesRepository;_pricesCacheRepository = pricesCacheRepository;_settings = settings.Value;}public async Task<PricesPageViewModel> GetPricesAsync(int productId, int pageIndex, int pageSize){IEnumerable<Price> prices = await _pricesCacheRepository.GetOrSetValueAsync(productId.ToString(), async () =>await _pricesRepository.GetPricesAsync(productId));            pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize;return new PricesPageViewModel(new PaginatedList<Price>(prices, pageIndex, pageSize));}

现在我们可以重写ProductsController和PricesController,以便它们能够使用新的分页机制

让我们更改ProductsController。GetAllProductsAsync ProductsController。FindProductsAsync方法。新版本将是:

隐藏,复制Code[HttpGet]
public async Task GetAllProductsAsync(int pageIndex, int pageSize)
{
ProductsPageViewModel productsPageViewModel = await _productsService.GetAllProductsAsync(pageIndex, pageSize);

return new OkObjectResult(productsPageViewModel);

}

[HttpGet(“find/{sku}”)]
public async Task FindProductsAsync(string sku, int pageIndex, int pageSize)
{
ProductsPageViewModel productsPageViewModel = await _productsService.FindProductsAsync(sku, pageIndex, pageSize);

return new OkObjectResult(productsPageViewModel);

}

和PricesController。GetPricesAsync方法:

隐藏,复制Code[HttpGet("{Id:int}")]
public async Task GetPricesAsync(int id, int pageIndex, int pageSize)
{
PricesPageViewModel pricesPageViewModel = await _pricesService.GetPricesAsync(id, pageIndex, pageSize);

return new OkObjectResult(pricesPageViewModel);

}

如果我们有一些客户端使用旧版本的api,它仍然可以使用新版本,因为如果我们错过pageIndex或pageSize参数,它们的值将为0,我们的分页机制可以正确处理pageIndex=0和/或pageSize=0的情况。

既然我们已经在代码重构中达到了控制器,就让我们留在这里,把所有最初的混乱整理出来吧。

控制器vs ControllerBase

您可能已经注意到,在我们的解决方案中,ProductsController继承自Controller类,而PricesController继承自ControllerBase类。两个控制器都工作得很好,那么我们应该使用哪个版本呢?控制器类支持视图,因此它应该用于创建使用视图的web站点。对于WEB API服务,ControllerBase更可取,因为它更轻量级,因为它没有我们在WEB API中不需要的特性。

因此,我们将从ControllerBase继承我们的控制器,并使用属性[ApiController],它支持诸如自动模型验证、属性路由等有用特性

因此,更改ProductsController的声明为:

隐藏,复制Code…
[Route(“api/[controller]”)]
[ApiController]
public class ProductsController : ControllerBase
{

让我们看看模型验证是如何使用ApiController属性的。为此,我们将调用一些带有无效参数的api。例如,下面的操作期望整数Id,但我们发送一个字符串代替:

http://localhost:49858/api/products/aa

结果将是:

状态:400错误请求

隐藏,复制Code{
“id”: [
“The value ‘aa’ is not valid.”
]
}

在这种情况下,当我们有意声明参数的类型[HttpGet("{Id:int}")]情况更糟:

http://localhost:49858/api/prices/aa

状态:404未找到,没有任何关于Id参数类型不正确的消息。

因此,首先,我们将从PricesController中的HttpGet属性中删除Id类型声明。GetPricesAsync方法:

隐藏,复制Code[HttpGet("{Id:int}")]
[HttpGet("{id}")]

这将给我们一个标准的400错误请求和一个类型不匹配的消息。

另一个直接关系到应用程序生产力的问题是消除无意义的工作。例如,http://localhost:49858/api/prices/-1 api显然将返回404 Not Found,因为我们的数据库永远不会有任何负Id值。

我们在应用程序中多次使用正整数Id参数。我们的想法是创建一个Id验证过滤器,并在有Id参数时使用它。

自定义Id参数验证过滤器和属性

在解决方案中,创建一个过滤器文件夹和一个新类ValidateIdAsyncActi在它onFilter:

隐藏,收缩,复制Codeusing Microsoft.AspNetCore.Mvc.Filters;
using SpeedUpCoreAPIExample.Exceptions;
using System.Linq;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Filters
{
// Validating Id request parameter ActionFilter. Id is required and must be a positive integer
public class ValidateIdAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
ValidateParameter(context, “id”);

        await next();}private void ValidateParameter(ActionExecutingContext context, string paramName){string message = $"'{paramName.ToLower()}' must be a positive integer.";var param = context.ActionArguments.SingleOrDefault(p => p.Key == paramName);if (param.Value == null){throw new HttpException(System.Net.HttpStatusCode.BadRequest, message, $"'{paramName.ToLower()}' is empty.");}var id = param.Value as int?;if (!id.HasValue || id < 1){throw new HttpException(System.Net.HttpStatusCode.BadRequest, message,param.Value != null ? $"{paramName}: {param.Value}" : null);}
}

}

在筛选器中,我们检查请求是否只有一个Id参数。如果Id参数丢失或没有正整数值,筛选器将生成BadRequest HttpException。抛出HttpException涉及到我们的exceptionshandling中间件,以及它的所有好处,比如日志记录、统一的消息格式等等。

为了能够在控制器的任何位置应用这个过滤器,我们将在相同的过滤器文件夹中创建一个ValidateIdAttribute:

隐藏,复制Codeusing Microsoft.AspNetCore.Mvc;

namespace SpeedUpCoreAPIExample.Filters
{
public class ValidateIdAttribute : ServiceFilterAttribute
{
public ValidateIdAttribute() : base(typeof(ValidateIdAsyncActionFilter))
{
}
}
}

在ProductsController中添加引用过滤器类名称空间

隐藏,复制Code…
using SpeedUpCoreAPIExample.Filters;

并将[ValidateId]属性添加到所有需要Id参数的GetProductAsync和DeleteProductAsync动作:

隐藏,复制Code…
[HttpGet("{id}")]
[ValidateId]
public async Task GetProductAsync(int id)
{

[HttpDelete("{id}")]
[ValidateId]
public async Task DeleteProductAsync(int id)
{

我们可以将ValidateId属性应用到整个PricesController控制器,因为它的所有动作都需要一个Id参数。此外,我们需要纠正PricesController类名称空间中的错误——它显然应该是namespace SpeedUpCoreAPIExample。控制器,而不是命名空间SpeedUpCoreAPIExample.Contexts

隐藏,复制Codeusing Microsoft.AspNetCore.Mvc;
using SpeedUpCoreAPIExample.Filters;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.ViewModels;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Contexts
namespace SpeedUpCoreAPIExample.Controllers
{
[Route(“api/[controller]”)]
[ApiController]

public class PricesController : ControllerBase
{

最后一步是在Startup.cs中声明过滤器

隐藏,复制Codeusing SpeedUpCoreAPIExample.Filters;

public void ConfigureServices(IServiceCollection services)

services.AddSingleton();

让我们检查一下新的过滤器是如何工作的。为此,我们将再次错误地调用API http://localhost:49858/api/prices/-1。结果将完全符合我们的期望:

状态:400错误请求

隐藏,复制Code{
“message”: “‘Id’ must be a positive integer.”
}

注意!我们使用了ExceptionMessage类,现在消息通常满足我们的格式约定,但并不总是这样!如果我们再次尝试http://localhost:49858/api/prices/aa,仍然会得到标准的400错误请求消息。这是因为[ApiController]属性。当它被应用时,框架自动注册一个ModelStateInvalidFilter,它将在ValidateIdAsyncActionFilter过滤器之前工作,并生成自己格式的消息。

我们可以在启动类的ConfigureServices方法中抑制这种行为:

隐藏,复制Code…
services.AddMvc();
services.AddApiVersioning();

services.Configure(options =>
{
options.SuppressModelStateInvalidFilter = true;
});

在此之后,只有我们的过滤器工作,我们可以控制模型验证消息的格式。但是现在我们有义务组织控制器动作的所有参数的显式验证。

分页参数自定义模型验证过滤器

我们在简单的应用程序中使用了分页树时间。让我们看看如果参数不正确会发生什么。为此,我们将调用http://localhost:49858/api/products?pageindex=-1

结果将是:

状态:500内部服务器错误

隐藏,复制Code{
“message”: “The offset specified in a OFFSET clause may not be negative.”
}

这条消息确实令人困惑,因为没有服务器错误,它是一个纯粹的坏请求。如果你不知道它是关于分页的,那么文本本身就是神秘的。

我们希望得到一个答复:

状态:400错误请求

隐藏,复制Code{
“message”: “‘pageindex’ must be 0 or a positive integer.”
}

另一个问题是在哪里应用参数检查。注意,如果省略任何一个或两个参数,分页机制工作得很好——它使用默认值。我们应该只控制负面参数。在PaginatedList级别的id上抛出HttpException不是一个好主意,因为代码应该在不改变它的情况下可重用,并且下一次PaginatedList将不一定在ASP中使用。网络应用程序。在服务级别检查参数更好,但是需要重复验证代码或创建其他具有验证方法的公共助手类。

如果分页参数来自外部,那么在传递给分页过程之前,最好在控制器中组织它们的检查。

因此,我们必须创建另一个模型验证过滤器,它将验证PageIndex和PageSize参数。验证的思想略有不同——可以省略任何或两个参数,可以等于零或大于零的整数。

在相同的过滤器文件夹中创建一个新的类ValidatePagingAsyncActionFilter:

隐藏,收缩,复制Codeusing Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Threading.Tasks;

namespace SpeedUpCoreAPIExample.Filters
{
// Validating PageIndex and PageSize request parameters ActionFilter. If exist, must be 0 or a positive integer
public class ValidatePagingAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
ValidateParameter(context, “pageIndex”);
ValidateParameter(context, “pageSize”);

        await next();}private void ValidateParameter(ActionExecutingContext context, string paramName){var param = context.ActionArguments.SingleOrDefault(p => p.Key == paramName);if (param.Value != null){var id = param.Value as int?;if (!id.HasValue || id < 0){string message = $"'{paramName.ToLower()}' must be 0 or a positive integer.";throw new HttpException(System.Net.HttpStatusCode.BadRequest, message,param.Value != null ? $"{paramName}: {param.Value}" : null);}}}
}

}

然后创建ValidatePagingAttribute类:

隐藏,复制Codeusing Microsoft.AspNetCore.Mvc;

namespace SpeedUpCoreAPIExample.Filters
{
public class ValidatePagingAttribute : ServiceFilterAttribute
{
public ValidatePagingAttribute() : base(typeof(ValidatePagingAsyncActionFilter))
{
}
}
}

然后在start .cs中声明过滤器

隐藏,复制Code…
public void ConfigureServices(IServiceCollection services)

services.AddSingleton();

最后,添加[ValidatePaging]属性到ProductsController。GetAllProductsAsync ProductsController。FindProductsAsync方法:

隐藏,复制Code…
[HttpGet]
[ValidatePaging]
public async Task GetAllProductsAsync(int pageIndex, int pageSize)
{

[HttpGet(“find/{sku}”)]
[ValidatePaging]
public async Task FindProductsAsync(string sku, int pageIndex, int pageSize)
{

和PricesController。GetPricesAsync方法:

隐藏,复制Code…
[HttpGet("{id}")]
[ValidatePaging]
public async Task GetPricesAsync(int id, int pageIndex, int pageSize)
{

现在我们有了针对所有敏感参数的自动验证机制,并且我们的应用程序能够正常工作(至少在本地)

跨源资源共享

在实际的应用程序中,我们将把一些域名绑定到web服务,其URL类似于http://mydomainname.com/api/

同时,使用我们服务的api的客户机应用程序可以驻留在不同的域上。如果客户端(例如web站点)对API请求使用AJAX,并且响应不包含value = *(所有域都允许)的Access-Control-Allow-Origin头文件,或者不包含与orig相同的主机在(客户端主机)中,支持CORS的浏览器会出于安全原因阻止响应。

让我们确保。构建并发布我们的应用程序到IIS,绑定它与测试URL(在我们的例子中是mydomainname.com),并调用任何API https://resttesttest.com/ -在线工具API检查:

使歌珥ASP。网络核心

要强制应用程序发送正确的标头,我们应该启用CORS。为此,请安装Microsoft.AspNetCore。Cors NuGet包(如果你仍然没有安装它与其他包像微软。aspnetcore。MVC或Microsoft.AspNetCore.All)

启用CORS的最简单方法是在Startup.cs中添加以下代码:

隐藏,复制Code…
public void Configure(

app.UseCors(builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());

app.UseMvc();

这样我们就允许从任何主机访问我们的API。我们还可以添加. allowcredentials()选项,但是在AllowAnyOrigin中使用它是不安全的。

之后,重新构建,将应用程序重新发布到IIS,并使用resttest.com或其他工具测试它。乍一看,一切工作良好- CORS错误消息消失。但是这只在我们的ExceptionsHandlingMiddleware进入游戏之前有效。

没有CORS头发送的情况下,HTTP错误

这是因为实际上,当HttpException或任何其他异常发生并且中间件处理它时,response headers集合是空的。这意味着没有向客户机应用程序传递任何访问控制允许原点头,从而出现CORS问题。

如何发送HTTP 4xx-5xx响应与CORS头在一个ASPNET。核心web应用程序

为了克服这个问题,我们应该稍微不同地启用CORS。在启动。ConfigureServices输入以下代码:

隐藏,复制Code…
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(“Default”, builder =>
{
builder.AllowAnyOrigin();
builder.AllowAnyMethod();
builder.AllowAnyHeader();
});
});

而在Startup.Configure:

隐藏,复制Code…
public void Configure(

app.UseCors(“Default”);

app.UseMvc();

通过这种方式启用CORS,我们可以通过依赖注入在应用程序的任何位置访问CorsOptions。其思想是用取自CorsOptions的CORS策略在ExceptionsHandlingMiddleware中重新填充响应头。

ExceptionsHandlingMiddleware类的正确代码:

隐藏,收缩,复制Codeusing Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Net;
using System.Threading.Tasks;

namespace SCARWebService.Exceptions
{
public class ExceptionsHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly ICorsService _corsService;
private readonly CorsOptions _corsOptions;

    public ExceptionsHandlingMiddleware(RequestDelegate next, ILogger<ExceptionsHandlingMiddleware> logger,ICorsService corsService, IOptions<CorsOptions> corsOptions){_next = next;_logger = logger;_corsService = corsService;_corsOptions = corsOptions.Value;}


private async Task HandleHttpExceptionAsync(HttpContext context, HttpException exception)
{
_logger.LogError(exception, exception.MessageDetail);

        if (!context.Response.HasStarted){int statusCode = exception.StatusCode;string message = exception.Message;context.Response.Clear();//repopulate Response header with CORS policy_corsService.ApplyResult(_corsService.EvaluatePolicy(context, _corsOptions.GetPolicy("Default")), context.Response);context.Response.ContentType = "application/json";context.Response.StatusCode = statusCode;var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);}}private async Task HandleUnhandledExceptionAsync(HttpContext context, Exception exception){_logger.LogError(exception, exception.Message);if (!context.Response.HasStarted){int statusCode = (int)HttpStatusCode.InternalServerError; // 500string message = string.Empty;

#if DEBUG
message = exception.Message;
#else
message = “An unhandled exception has occurred”;
#endif
context.Response.Clear();

            //repopulate Response header with CORS policy_corsService.ApplyResult(_corsService.EvaluatePolicy(context, _corsOptions.GetPolicy("Default")), context.Response);context.Response.ContentType = "application/json";context.Response.StatusCode = statusCode;var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);}}

如果我们重新构建并重新发布我们的应用程序,当它的api被从任何主机调用时,它将工作得很好,没有任何CORS问题。

API版本控制

在公开我们的应用程序之前,我们必须考虑如何使用它的api。一段时间后,需求可能会发生变化,我们将不得不重写应用程序,以便其API将返回不同的数据集。如果我们发布有新变化的web服务,但不更新使用api的客户机应用程序,那么客户机-服务器兼容性将会出现大问题。

为了避免这些问题,我们应该建立API版本控制。例如,旧版本的产品API会有一个路径:

http://mydomainname.com/api/v1.0/products/

新版本会有一条路线

http://mydomainname.com/api/v2.0/products/

在这种情况下,即使是旧的客户机应用程序也将继续正常工作,直到它们被更新为可以在版本2.0中正常工作的版本为止

在我们的应用程序中,我们将实现基于URL路径的版本控制,其中版本号是api URL的一部分,就像上面的示例一样。

在。net Core微软。aspnetcore。mvc中。版本控制包负责版本控制。所以,我们应该先安装包:

然后将services.AddApiVersioning()添加到启动的类ConfigureServices方法中:

隐藏,复制Code…
services.AddMvc();
services.AddApiVersioning();

最后,为两个控制器添加ApiVersion和正确路由属性:

隐藏,复制Code…
[ApiVersion(“1.0”)]
[Route("/api/v{version:apiVersion}/[controller]/")]

现在我们有了版本控制。这样做之后,如果我们想在2.0版本中增强应用程序,例如,我们可以在控制器中添加[ApiVersion(“2.0”)]属性:

隐藏,复制Code…
[ApiVersion(“1.0”)]
[ApiVersion(“2.0”)]

然后创建一个操作,我们希望只使用2.0版本,并添加add [MapToApiVersion(“2.0”)]属性到操作。

版本控制机制完美的几乎没有任何编码,但像往常一样,美中不足之处:如果我们不小心使用了错误的版本的API URL (http://localhost: 49858 / API / v10.0 /价格/ 1),我们将有一个错误消息在以下格式:

状态:400错误请求

隐藏,复制Code{
“error”: {
“code”: “UnsupportedApiVersion”,
“message”: “The HTTP resource that matches the request URI ‘http://localhost:49858/api/v10.0/prices/1’ does not support the API version ‘10.0’.”,
“innerError”: null
}
}

这是标准的错误响应格式。它的信息量大得多,但与我们想要的格式相去甚远。因此,如果我们想对所有类型的消息使用统一格式,就必须在详细的标准错误响应格式和我们为应用程序设计的简单格式之间做出选择。

要应用标准错误响应格式,我们只需扩展ExceptionMessage类。幸运的是,我们已经预见到这个机会,这并不困难。但是这种格式的消息比我们想传递给用户的还要详细。在一个简单的应用程序中,这样的去talization可能并不真正相关。所以,为了不让事情复杂化,我们将使用简单的格式。

控制API版本控制错误消息format

让我们在异常文件夹中创建一个VersioningErrorResponseProvider类:

隐藏,收缩,复制Codeusing Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;

namespace SpeedUpCoreAPIExample.Exceptions
{
public class VersioningErrorResponseProvider : DefaultErrorResponseProvider
{
public override IActionResult CreateResponse(ErrorResponseContext context)
{
string message = string.Empty;
switch (context.ErrorCode)
{
case “ApiVersionUnspecified”:
message = “An API version is required, but was not specified.”;

                break;case "UnsupportedApiVersion":message = "The specified API version is not supported.";break;case "InvalidApiVersion":message = "An API version was specified, but it is invalid.";break;case "AmbiguousApiVersion":message = "An API version was specified multiple times with different values.";break;default:message = context.ErrorCode;break;}throw new HttpException(System.Net.HttpStatusCode.BadRequest, message, context.MessageDetail);}
}

}

这个类继承了DefaultErrorResponseProvider。它只是根据ErrorCode(代码列表)格式化一个友好的消息,并抛出HttpException BadRequest异常。然后异常由我们的ExceptionHandlerMiddleware通过日志记录、统一的错误消息格式等处理。

最后一步是注册VersioningErrorResponseProvider类作为版本管理HTTP错误响应生成器。在Startup类中,在ConfigureServices方法中添加API版本化服务注册选项:

隐藏,复制Code…
services.AddMvc();
services.AddApiVersioning(options =>
{
options.ErrorResponses = new VersioningErrorResponseProvider();
});

因此,我们已经将标准错误响应行为更改为我们想要的。

内部HTTP调用的版本控制

我们还必须在SelfHttpClient类中应用版本控制。在类中,我们设置HttpClient的BaseAddress属性来调用API。在构建基址时,我们应该考虑版本控制。

为了避免对将要调用的API版本进行硬编码,我们创建了一个用于API版本控制的settings类。appsettings。json文件创建一个API节:

隐藏,复制Code…
,
“Api”: {
“Version”: “1.0”
}

然后在设置文件夹中创建apiset .cs文件:

隐藏,复制Codenamespace SpeedUpCoreAPIExample.Settings
{
public class ApiSettings
{
public string Version { get; set; }
}
}

在启动的ConfigureServices方法中声明类:

隐藏,复制Code…
public void ConfigureServices(IServiceCollection services)

//Settings
services.Configure(Configuration.GetSection(“Api”));

最后,更改SelfHttpClient的构造函数:

隐藏,复制Codepublic SelfHttpClient(HttpClient httpClient, IHttpContextAccessor httpContextAccessor, IOptions settings)
{
string baseAddress = string.Format("{0}

加快ASP。NET Core WEB API应用程序。第3部分相关推荐

  1. 加速ASP.NET Core WEB API应用程序——第2部分

    目录 应用程序生产力 异步设计模式 数据规范化与SQL查询效率 NCHAR与NVARCHAR 使用MSSQL服务器的全文引擎 存储过程 优化存储过程 预编译和重用存储过程执行计划 使用Entity F ...

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

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

  3. 在docker中运行ASP.NET Core Web API应用程序

    本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Cor ...

  4. 加速ASP.NET Core WEB API应用程序。 第三部分

    深度重构和完善ASP.NET Core WEB API应用程序代码 (Deep refactoring and refinement of ASP.NET Core WEB API applicati ...

  5. 使用Entity Developer构建ASP.NET Core Web API应用程序

    目录 介绍 先决条件 在Visual Studio 2019中创建新的ASP.NET Core Web API项目 使用Entity Developer创建实体数据模型 创建API控制器 通过实体数据 ...

  6. 加速ASP.NET Core WEB API应用程序——第1部分

    目录 介绍 创建测试RESTful WEB API服务 应用程序架构 数据库 创建ASP.NET核心WEB API应用程序 使用实体框架核心进行数据库访问 异步设计模式 存储库 存储库实现 服务 服务 ...

  7. 支持多个版本的ASP.NET Core Web API

    基本配置及说明 版本控制有助于及时推出功能,而不会破坏现有系统. 它还可以帮助为选定的客户提供额外的功能. API版本可以通过不同的方式完成,例如在URL中添加版本或通过自定义标头和通过Accept- ...

  8. [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了

    园子里关于ASP.NET Core Web API的教程很多,但大多都是使用EF+Mysql或者EF+MSSQL的文章.甚至关于ASP.NET Core Web API中使用Dapper+Mysql组 ...

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

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

最新文章

  1. ***CI中的数据库操作(insert_id新增后返回记录ID)
  2. 处理SSL certificate problem self signed certificate
  3. LeetCode-46. Permutations
  4. bootstrap 垂直居中 布局_CSS3 flex 布局必须要掌握的知识点
  5. java 二叉树的各种遍历
  6. 如何在 iPhone、iPad、Mac 和 PC 上设置 iCloud 照片共享?
  7. 《第一堂棒球课》:王牌左外野·棒球7号位
  8. 方差、标准差、均方根误差、平均绝对误差的总结
  9. matlab 离散点求导_如何用matlab求离散型数值的导数
  10. 网络协议 18 - CDN:家门口的小卖铺 1
  11. IoT当前最重要的机遇,全面解读专为边缘计算而生的EdgeX Foundry
  12. Java生成词云!你喜欢得书都在图里!
  13. 超棒的Mac动画设计软件,提升你的动画制作效果
  14. 通用 Mapper UUID 简单示例
  15. workflow 添加html,为alfred编写workflow
  16. 现代控制工程-状态空间(正在更新)
  17. leaflet 使用 wmts
  18. iOS-plist: iOS Keys
  19. 置换贴图 Displacement Mapping
  20. android 4.4 自定义广播,Android 4.4.2 系统 自定义 鼠标 光标 替换 接口实现

热门文章

  1. 微擎视频小程序4.3.0 视频音频图文发布平台
  2. 日积月累Day6(为什么家庭会生病)
  3. Latex 双引号和连字符问题
  4. 计算机软件登记权证书,计算机软件著作权登记申请表(计算机软件登记文书).doc...
  5. mlogic S905x 开机logo 开机视频 默认的luancher的修改
  6. rel=stylesheet
  7. 架构技术实践系列文章
  8. 序列化和反序列化总结
  9. 前百度员工,现 Google 员工爆料:百度对比Google有量级差距
  10. Vscode MacOS版本下载及html配置