web api 通过过滤器统一输出格式输出json格式以便对接及反馈响应内容,统一输出格式可以通过继承TextOutputFormatter进行重写或通过过滤器,本文通过过滤器实现格式输出,输出格式如下
//code及内容就结合实际业务做统一规定(响应体没有创建实体使用的是匿名对象)
{
"code":200,
"message":null,
"result":""
}
一、统一接口正常输出
1.添加过滤器
/// <summary>
/// 不采用统一输出特性
/// </summary>
public class ApiOutputDefaultAttribute : Attribute
{
}
/// <summary>
/// 统一输出过滤器
/// </summary>
public class ApiOutputFormatterFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (!context.ActionDescriptor.EndpointMetadata.Any(p => p is ApiOutputDefaultAttribute))
{
ObjectResult? objectResult = context.Result as ObjectResult;
int statusCode = objectResult?.StatusCode ?? context.HttpContext.Response.StatusCode;
context.Result = new JsonResult(new
{
Code = statusCode,
Message = statusCode == 400 ? objectResult?.Value : string.Empty,
Result = objectResult == null ? "操作成功" : statusCode != 400 ? objectResult.Value : null,
});
}
await next();
}
}
2.注入过滤器
...
builder.Services.AddControllers(options =>
{
options.Filters.Add(typeof(ApiOutputFormatterFilter));
});
...
3.测试响应结果
{
"code": 200,
"message": "",
"result": [
{
"date": "2022-11-26",
"temperatureC": 51,
"temperatureF": 123,
"summary": "Chilly"
},
{
"date": "2022-11-27",
"temperatureC": 20,
"temperatureF": 67,
"summary": "Cool"
}
]
}
二、统一接口异常输出
1.添加异常过滤器
public class ApiExceptionFilter : IAsyncExceptionFilter
{
private readonly ILogger<ApiExceptionFilter> _logger;
public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger)
{
_logger = logger;
}
public async Task OnExceptionAsync(ExceptionContext context)
{
await Task.CompletedTask;
context.Result = new JsonResult(new
{
Code = 500,
Message = string.Empty,
Result = "服务异常,请稍后重试"
});
_logger.LogError(context.Exception.ToString());
context.ExceptionHandled = true;
}
}
2.注入异常过滤器
...
builder.Services.AddControllers(options =>
{
options.Filters.Add(typeof(ApiOutputFormatterFilter));
options.Filters.Add(typeof(ApiExceptionFilter));
});
...
3.测试异常响应结果
{
"code": 500,
"message": "",
"result": "服务异常,请稍后重试"
}
三、统一模型校验输出
在接口正常输出的基础上,使用模型校验,校验未通过返回的信息对接并不友好,结构如下
{
"code": 400,
"message": "",
"result": {
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-654b6ec917b00598fd0eead6dee76508-34ac81e93990404d-00",
"errors": {
"Name": [
"The field Name must be a string or array type with a maximum length of '10'."
]
}
}
}
1.设置模型规则时
添加响应错误友好信息
public class TestRequest
{
[MaxLength(10,ErrorMessage ="名称过长,请调整后重试")]
public string Name { get; set; } = string.Empty;
}
2.配置模型状态响应
...
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errText = context.ModelState.Values.SelectMany(p => p.Errors).Select(p => p.ErrorMessage);
return new ObjectResult(string.Join("|", errText))
{
StatusCode = 400,
};
};
});
...
3.测试模型校验输出
{
"code": 400,
"message": "名称过长,请调整后重试",
"result": null
}
四、统一接口路由前缀
添加统一前缀是因为网关下好配置转发,在统一封装的接口基类中可以通过添加route特性的方式实现,我呢比较懒不想通过新建接口基类继承的方式调整路由,便通过继承IControllerModelConvention动态调整路由。
1.添加路由重写
/// <summary>
/// 统一路由调整
/// </summary>
public class ApiRoutingConvention : Attribute, IControllerModelConvention
{
private readonly AttributeRouteModel _prefix;
public ApiRoutingConvention(string prefix)
{
_prefix = new AttributeRouteModel(new RouteAttribute(prefix));
}
public void Apply(ControllerModel controller)
{
var attributeRouteSelectors = controller.Selectors.Where(p => p.AttributeRouteModel != null).ToList();
foreach (var matchedSelector in attributeRouteSelectors)
matchedSelector.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_prefix, matchedSelector.AttributeRouteModel);
var notAttributeRouteSelectors = controller.Selectors.Where(p => p.AttributeRouteModel == null).ToList();
foreach (var matchedSelector in notAttributeRouteSelectors)
matchedSelector.AttributeRouteModel = _prefix;
}
}
2.配置路由调整
...
builder.Services.AddControllers(options =>
{
options.Conventions.Add(new ApiRoutingConvention("api"));
});
...
五、统一流程中断返回
开发接口中通过throw自定义异常及内容来中断流程,着实不要太爽(开发爽了性能就不爽了),记录一下返回ActionResult<>方式(不影响swagger响应展示,后面可以尝试按此隐式转换的方式扩展重写)
[HttpGet(Name = "GetWeatherForecast")]
public ActionResult<IEnumerable<WeatherForecast>> Get()
{
if (DateTime.Now.Millisecond%2!=0)
return BadRequest("暂无信息");
return Enumerable.Range(1, 2).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
Comments NOTHING