.net web api 统一输出格式

烂柯 发布于 2022-11-25 196 次阅读


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

最后更新于 2022-12-19