第 6 章 高级查询和日志
6.3 排序
RESTful API 在实现排序时应支持对集合资源的一个或多个属性进行排序
示例对 authors 资源按照其属性 Age 升序排序,再按 BirthPlace 属性降序排序:
https://localhost:5000/api/authors? orderby=age,birthplace desc
在 ASP.NET Core 中实现排序,与过滤和查询一样,通过对查询字符串中的排序项进行解析,然后在分页操作之前,将它们指定的排序方式进行排序,并最终返回结果
首先在 AuthorResourceParameters 中添加属性
public string SortBy { get; set; } = "Name";
接下来,在 AuthorRepository 的 GetAllAsync 方法中,使用 OrderBy 子句来实现查询
if (parameters.SortBy == "Name") | |
{ | |
queryableAuthors = queryableAuthors.OrderBy(author => author.Name); | |
} |
由于 LINQ 的 OrderBy 扩展方法不支持直接使用字符串,当资源支持多个排序字段时,一一判断比较繁琐,而且在进行后续排序时,还应该使用 ThenBy 子句,使得判断更加复杂,幸运的是可以借助第三方库 System.Linq.Dynamic.Core 实现动态 LINQ 查询
System.Linq.Dynamic.Core 除了支持直接使用属性名排序之外,还支持多属性排序,多个属性之间使用逗号隔开,每个属性默认以升序排序,若要使用降序排序,则应在属性名后添加 desc 或 descending,并以空格隔开
nuget 安装该库
Install-Package Microsoft.EntityFrameworkCore.DynamicLinq
安装成功后修改 AuthorRepository 的 GetAllAsync 方法
var orderedAuthors = queryableAuthors.OrderBy(parameters.SortBy); | |
return PagedList<Author>.CreateAsync(orderedAuthors, parameters.PageNumber, parameters.PageSize); |
排序选项 SortBy 同样作为分页数据的一部分,应返回给客户端,在 AuthorController 的 GetAuthorsAsync 方法生成分页数据时,添加代码
previousePageLink = pagedList.HasPrevious | |
? Url.Link(nameof(GetAuthorsAsync), new | |
{ | |
pageNumber = pagedList.CurrentPage - 1, | |
pageSize = pagedList.PageSize, | |
birthPlace = parameters.BirthPlace, | |
searchQuery = parameters.SearchQuery, | |
sortBy = parameters.SortBy | |
}) | |
: null, | |
nextPageLink = pagedList.HasNext | |
? Url.Link(nameof(GetAuthorsAsync), new | |
{ | |
pageNumber = pagedList.CurrentPage + 1, | |
pageSize = pagedList.PageSize, | |
birthPlace = parameters.BirthPlace, | |
searchQuery = parameters.SearchQuery, | |
sortBy = parameters.SortBy | |
}) | |
: null |
为了解决 DTO 与实体属性名不同时的映射问题,可以在程序中添加一个字典,来存储需要进行映射的属性及其对应的属性名
然而对于 AuthorDto 中的 Age 属性和 Author 中的 BirthDate 属性,其排序规则正好相反,即年龄越小,出生日期越靠后,这种情况下,除了要考虑映射外,还应考虑方向
namespace Library.API.Helpers | |
{ | |
public class PropertyMapping | |
{ | |
public bool IsRevert { get; private set; } | |
public string TargetProperty { get; private set; } | |
public PropertyMapping(string targetProperty, bool isRevert = false) | |
{ | |
IsRevert = isRevert; | |
TargetProperty = targetProperty; | |
} | |
} | |
} |
接着,可以在 AuthorRepository 中定义一个字典
private Dictionary<string, PropertyMapping> mappingDict = null; | |
public AuthorRepository(DbContext dbContext) : base(dbContext) | |
{ | |
mappingDict = new Dictionary<string, PropertyMapping>(StringComparer.OrdinalIgnoreCase); | |
mappingDict.Add("Name", new PropertyMapping("Name")); | |
mappingDict.Add("Age", new PropertyMapping("BirthDate", true)); | |
mappingDict.Add("BirthPlace", new PropertyMapping("BirthPlace")); | |
} |
为了使这一分析逻辑的通用性和 GetAllAsync 的方法简洁,可以将它放到一个扩展方法中
namespace Library.API.Extentions | |
{ | |
public static class IQueryableExtention | |
{ | |
private const string OrderSequence_Asc = "asc"; | |
private const string OrderSequence_Desc = "desc"; | |
public static IQueryable<T> Sort<T>(this IQueryable<T> source, string orderBy, | |
Dictionary<string, PropertyMapping> mapping) where T : class | |
{ | |
var allQueryParts = orderBy.Split(','); | |
List<string> sortParts = new List<string>(); | |
foreach (var item in allQueryParts) | |
{ | |
string property = string.Empty; | |
bool isDescending = false; | |
if (item.ToLower().EndsWith(OrderSequence_Desc)) | |
{ | |
property = item.Substring(0, item.Length - OrderSequence_Desc.Length).Trim(); | |
isDescending = true; | |
} | |
else | |
{ | |
property = item.Trim(); | |
} | |
if (mapping.ContainsKey(property)) | |
{ | |
if (mapping[property].IsRevert) | |
{ | |
isDescending = !isDescending; | |
} | |
if (isDescending) | |
{ | |
sortParts.Add($"{mapping[property].TargetProperty} {OrderSequence_Desc}"); | |
} | |
else | |
{ | |
sortParts.Add($"{mapping[property].TargetProperty} {OrderSequence_Asc}"); | |
} | |
} | |
} | |
string finalExpression = string.Join(',', sortParts); | |
source = source.OrderBy(finalExpression); | |
return source; | |
} | |
} | |
} |
在 Sort 逻辑内部中,通过解析得到最终的排序表达式,并使用 System.Linq.Dynamic.Core 库中的 OrderBy 对 IQueryable 对象排序,并返回排序后的结果
接着,修改 AuthorRepository 的 GetAuthorsAsync 方法中的返回结果语句
//var orderedAuthors = queryableAuthors.OrderBy(parameters.SortBy); | |
var orderedAuthors = queryableAuthors.Sort(parameters.SortBy, mappingDict); |
运行程序,请求 URL:
https://localhost:5001/api/authors?pageSize=3&sortby=birthplace,age
6.4 日志与异常
ASP.NET Core 内部集成了日志的功能,但是并不支持向文件输出日志,因此我们通过 NLog 实现
安装nuget
Install-Package NLog.Extensions.Logging
NLog 通过 XML 形式的文件来配置它的使用方式,添加一个 nlog.config
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |
<targets><target name="asyncFile" xsi:type="AsyncWrapper"><target name="log_file" xsi:type="File"fileName="${basedir}/Logs/${shortdate}/${shortdate}.txt"layout="${longdate} | ${message} ${onexception:${exception:format=message} ${newline} ${stacktrace} ${newline}"archiveFileName="${basedir}/archives/${shortdate}-{#####}.txt"archiveAboveSize="102400"archiveNumbering="Sequence"concurrentWrites="true"keepFileOpen="false" /></target><target name="console" xsi:type="ColoredConsole" layout="[${date:format=HH\:mm\:ss}]:${message} ${exception:format=message}" /></targets> | |
<rules><logger name="*" minlevel="Error" writeTo="asyncFile" /><logger name="*" minlevel="Debug" writeTo="console" /></rules> | |
</nlog> |
在 Configure 添加如下代码
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IMapper mapper, ILoggerFactory loggerFactory){ | |
loggerFactory.AddNLog(); | |
loggerFactory.ConfigureNLog("nlog.config"); | |
。。。 |
在 Controller 注入使用
public IMapper Mapper { get; set; } | |
public IRepositoryWrapper RepositoryWrapper { get; set; } | |
public ILogger<AuthorController> Logger { get; set; } | |
public AuthorController(IRepositoryWrapper repositoryWrapper, IMapper mapper, ILogger<AuthorController> logger) | |
{ | |
RepositoryWrapper = repositoryWrapper; | |
Mapper = mapper; | |
Logger = logger; | |
} |
在 MVC 应用程序中,可以通过异常过滤器 IExceptionFilter 处理异常
首先定义 ApiError
namespace Library.API.Helpers | |
{ | |
public class ApiError | |
{ | |
public string Message { get; set; } | |
public string Detail { get; set; } | |
} | |
} |
接着,添加一个过滤器
namespace Library.API.Filters | |
{ | |
public class JsonExceptionFilter : IExceptionFilter | |
{ | |
public IHostEnvironment Environment { get; } | |
public ILogger Logger { get; } | |
public JsonExceptionFilter(IHostEnvironment environment, ILogger<Program> logger) | |
{ | |
Environment = environment; | |
Logger = logger; | |
} | |
public void OnException(ExceptionContext context) | |
{ | |
var error = new ApiError(); | |
if (Environment.IsDevelopment()) | |
{ | |
error.Message = context.Exception.Message; | |
error.Detail = context.Exception.ToString(); | |
} | |
else | |
{ | |
error.Message = "服务器出错"; | |
error.Detail = context.Exception.Message; | |
} | |
context.Result = new ObjectResult(error) | |
{ | |
StatusCode = StatusCodes.Status500InternalServerError | |
}; | |
StringBuilder sb = new StringBuilder(); | |
sb.AppendLine($"服务器发生异常:{context.Exception.Message}"); | |
sb.AppendLine(context.Exception.ToString()); | |
Logger.LogCritical(sb.ToString()); | |
} | |
} | |
} |
最后将它添加到 MVC 配置中,即可生效
services.AddMvc(configure => | |
{ | |
configure.Filters.Add<JsonExceptionFilter>(); | |
。。。 | |
} |
原文地址:
https://www.cnblogs.com/MingsonZheng/p/13296917.html