asp.net core 中优雅的进行响应包装的实现方法

目录
  • 摘要
  • 正常响应包装
  • 实现按需禁用包装
  • 总结

摘要

在 asp.net core 中提供了 Filter 机制,可以在 Action 执行前后进行一些特定的处理,例如模型验证,响应包装等功能就可以在此基础上实现,同时也提供了 ApplicationModel API, 我们可以在此基础上实现选择性的添加 Filter,满足部分接口需要响应特定的结构, 我们常见的 [AllowAnonymous] 正是基于这种机制。同时也将介绍如何让 Swagger 展示正确的包装响应体,以满足第三方对接或前端的代码生成

效果图

正常响应包装

首先我们定义包装体的接口, 这里主要分为正常响应和模型验证失败的响应,其中正常响应分为有数据返回和没有数据返回两种情况,使用接口的目的是为了方便自定义包装体。

public interface IResponseWrapper
{
    IResponseWrapper Ok();
    IResponseWrapper ClientError(string message);
}
public interface IResponseWrapper<in TResponse> : IResponseWrapper
{
    IResponseWrapper<TResponse> Ok(TResponse response);
}

然后根据接口实现我们具体的包装类

没有数据返回的包装体:

/// <summary>
/// Default wrapper for <see cref="EmptyResult"/> or error occured
/// </summary>
public class ResponseWrapper : IResponseWrapper
{
    public int Code { get; }
    public string? Message { get; }
    ...
    public IResponseWrapper Ok()
    {
        return new ResponseWrapper(ResponseWrapperDefaults.OkCode, null);
    }
    public IResponseWrapper BusinessError(string message)
    {
        return new ResponseWrapper(ResponseWrapperDefaults.BusinessErrorCode, message);
    }
    public IResponseWrapper ClientError(string message)
    {
        return new ResponseWrapper(ResponseWrapperDefaults.ClientErrorCode, message);
    }
}

有数据返回的包装体:

/// <summary>
/// Default wrapper for <see cref="ObjectResult"/>
/// </summary>
/// <typeparam name="TResponse"></typeparam>
public class ResponseWrapper<TResponse> : ResponseWrapper, IResponseWrapper<TResponse>
{
    public TResponse? Data { get; }
    public ResponseWrapper()
    {
    }
    private ResponseWrapper(int code, string? message, TResponse? data) : base(code, message)
    {
        Data = data;
    }
    public IResponseWrapper<TResponse> Ok(TResponse response)
    {
        return new ResponseWrapper<TResponse>(ResponseWrapperDefaults.OkCode, null, response);
    }
}

然后实现我们的响应包装 Filter,这里分为正常响应包装,和模型验证错误包装两类 Filter,在原本的响应结果 context.Result 的基础上加上我们的包装体

正常响应包装 Filter, 注意处理一下 EmptyResult 的情况,就是常见的返回 Void 或 Task 的场景:

public class ResultWrapperFilter : IResultWrapperFilter
{
    private readonly IResponseWrapper _responseWrapper;
    private readonly IResponseWrapper<object?> _responseWithDataWrapper;
    ...
    public void OnActionExecuted(ActionExecutedContext context)
    {
        switch (context.Result)
        {
            case EmptyResult:
                context.Result = new OkObjectResult(_responseWrapper.Ok());
                return;
            case ObjectResult objectResult:
                context.Result = new OkObjectResult(_responseWithDataWrapper.Ok(objectResult.Value));
                return;
        }
    }
}

模型验证错误的 Filter,这里我们将 ErrorMessage 提取出来放在包装体中, 并返回 400 客户端错误的状态码

public class ModelInvalidWrapperFilter : IActionFilter
{
    private readonly IResponseWrapper _responseWrapper;
    private readonly ILogger<ModelInvalidWrapperFilter> _logger;
    ...
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.Result == null && !context.ModelState.IsValid)
        {
            ModelStateInvalidFilterExecuting(_logger, null);
            context.Result = new ObjectResult(_responseWrapper.ClientError(string.Join(",",
                context.ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage))))
            {
                StatusCode = StatusCodes.Status400BadRequest
            };
        }
    }
    ...
}

这里基本的包装结构和 Filter 已经定义完成,但如何实现按需添加 Filter,以满足特定情况下需要返回特定的结构呢?

实现按需禁用包装

回想 asp.net core 中的 权限验证,只有添加了 [AllowAnonymous] 的 Controller/Action 才允许匿名访问,其它接口即使不添加 [Authorize] 同样也会有基础的登录验证,我们这里同样可以使用这种方法实现,那么这一功能是如何实现的呢?

Asp.net core 提供了 ApplicationModel 的 API,会在程序启动时扫描所有的 Controller 类,添加到了 ApplicationModelProviderContext 中,并公开了 IApplicationModelProvider 接口,可以选择性的在 Controller/Action 上添加 Filter,上述功能正是基于该接口实现的,详细代码见 AuthorizationApplicationModelProvider 类,我们可以参照实现自定义的响应包装 Provider 实现在特定的 Controller/Action 禁用包装,并默认给其它接口加上包装 Filter 的功能。

定义禁止包装的接口及 Attribute:

public interface IDisableWrapperMetadata
{
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableWrapperAttribute : Attribute, IDisableWrapperMetadata
{
}

自定义 Provider 实现,这里实现了选择性的添加 Filter,以及后文提到的如何让 Swagger 正确的识别响应包装(详细代码见 Github)

public class ResponseWrapperApplicationModelProvider : IApplicationModelProvider
{
    ...
    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        foreach (var controllerModel in context.Result.Controllers)
        {
            if (_onlyAvailableInApiController && IsApiController(controllerModel))
            {
                continue;
            }

            if (controllerModel.Attributes.OfType<IDisableWrapperMetadata>().Any())
            {
                if (!_suppressModelInvalidWrapper)
                {
                    foreach (var actionModel in controllerModel.Actions)
                    {
                        actionModel.Filters.Add(new ModelInvalidWrapperFilter(_responseWrapper, _loggerFactory));
                    }
                }
                continue;
            }
            foreach (var actionModel in controllerModel.Actions)
            {
                if (!_suppressModelInvalidWrapper)
                {
                    actionModel.Filters.Add(new ModelInvalidWrapperFilter(_responseWrapper, _loggerFactory));
                }
                if (actionModel.Attributes.OfType<IDisableWrapperMetadata>().Any()) continue;
                actionModel.Filters.Add(new ResultWrapperFilter(_responseWrapper, _genericResponseWrapper));
                // support swagger
                AddResponseWrapperFilter(actionModel);
            }
        }
    }
    ...
}

如何让 Swagger 识别正确的响应包装

通过查阅文档可以发现,Swagger 支持在 Action 上添加 [ProducesResponseType] Filter 来显示地指定响应体类型。 我们可以通过上边的自定义 Provider 动态的添加该 Filter 来实现 Swagger 响应包装的识别。

需要注意这里我们通过 ActionModel 的 ReturnType 来取得原响应类型,并在此基础上添加到我们的包装体泛型中,因此我们需要关于 ReturnType 足够多的元数据 (metadata),因此这里推荐返回具体的结构,而不是 IActionResult,当然 Task 这种在这里是支持的。

关键代码如下:

actionModel.Filters.Add(new ProducesResponseTypeAttribute(_genericWrapperType.MakeGenericType(type), statusCode));

禁用默认的模型验证错误包装

默认的模型验证错误是如何添加的呢,答案和 [AllowAnonymous] 类似,都是通过 ApplicationModelProvider 添加上去的,详细代码可以查看 ApiBehaviorApplicationModelProvider 类,关键代码如下:

if (!options.SuppressModelStateInvalidFilter)
{
    ActionModelConventions.Add(new InvalidModelStateFilterConvention());
}

可以看见提供了选项可以阻止默认的模型验证错误惯例,关闭后我们自定义的模型验证错误 Filter 就能生效

public static IMvcBuilder AddResponseWrapper(this IMvcBuilder mvcBuilder, Action<ResponseWrapperOptions> action)
{
    mvcBuilder.Services.Configure(action);
    mvcBuilder.ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });
    mvcBuilder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IApplicationModelProvider, ResponseWrapperApplicationModelProvider>());
    return mvcBuilder;
}

使用方法以及自定义返回结构体

安装 Nuget 包

dotnet add package AspNetCore.ResponseWrapper --version 1.0.1

使用方法:

// .Net5
services.AddApiControllers().AddResponseWrapper();
// .Net6
builder.Services.AddControllers().AddResponseWrapper();

如何实现自定义响应体呢,首先自定义响应包装类,并实现上面提到的响应包装接口,并且需要提供无参的构造函数

示例代码: https://github.com/huiyuanai709/AspNetCore.ResponseWrapper/tree/main/samples/CustomResponseWrapper/ResponseWrapper

自定义响应体:

public class CustomResponseWrapper : IResponseWrapper
{
    public bool Success => Code == 0;
    public int Code { get; set; }
    public string? Message { get; set; }
    public CustomResponseWrapper()
    {
    }
    public CustomResponseWrapper(int code, string? message)
    {
        Code = code;
        Message = message;
    }
    public IResponseWrapper Ok()
    {
        return new CustomResponseWrapper(0, null);
    }
    public IResponseWrapper BusinessError(string message)
    {
        return new CustomResponseWrapper(1, message);
    }
    public IResponseWrapper ClientError(string message)
    {
        return new CustomResponseWrapper(400, message);
    }
}
public class CustomResponseWrapper<TResponse> : CustomResponseWrapper, IResponseWrapper<TResponse>
{
    public TResponse? Result { get; set; }
    public CustomResponseWrapper()
    {
    }

    public CustomResponseWrapper(int code, string? message, TResponse? result) : base(code, message)
    {
        Result = result;
    }
    public IResponseWrapper<TResponse> Ok(TResponse response)
    {
        return new CustomResponseWrapper<TResponse>(0, null, response);
    }
}

使用方法, 这里以 .Net 6 为例, .Net5 也是类似的

// .Net6
builder.Services.AddControllers().AddResponseWrapper(options =>
{
    options.ResponseWrapper = new CustomResponseWrapper.ResponseWrapper.CustomResponseWrapper();
    options.GenericResponseWrapper = new CustomResponseWrapper<object?>();
});

SourceCode && Nuget package

SourceCode: https://github.com/huiyuanai709/AspNetCore.ResponseWrapper

Nuget Package: https://www.nuget.org/packages/AspNetCore.ResponseWrapper

总结

本文介绍了 Asp.Net Core 中的通用响应包装的实现,以及如何让 Swagger 识别响应包装,由于异常处理难以做到通用和一致,本文不处理异常情况下的响应包装,读者可以自定义实现 ExceptionFilter。

到此这篇关于asp.net core 中优雅的进行响应包装的文章就介绍到这了,更多相关asp.net core 响应包装内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ASP.NET Core 文件响应压缩的常见使用误区

    误区1:未使用 Brotil 压缩 几乎不需要任何额外的代价,Brotil 压缩算法可以帮助你的网站提升约 20% 静态资源加载性能. 同时启用 Gzip / Brotil 压缩 Gzip 有更好的 user-agent 兼容性,而 Brotli 有更好的性能. 所以我们通常需要在 ASP.NET Core 网站中同时启用这两种压缩. 如何区分 Gzip 压缩和 Brotli 压缩 网站启用 Brotli 压缩时,服务器请求返回头 Content-Encoding 中会包含 br 字样,否则是

  • ASP.NET Core中的响应压缩的实现

    介绍# 响应压缩技术是目前Web开发领域中比较常用的技术,在带宽资源受限的情况下,使用压缩技术是提升带宽负载的首选方案.我们熟悉的Web服务器,比如IIS.Tomcat.Nginx.Apache等都可以使用压缩技术,常用的压缩类型包括Brotli.Gzip.Deflate,它们对CSS.JavaScript.HTML.XML 和 JSON等类型的效果还是比较明显的,但是也存在一定的限制对于图片效果可能没那么好,因为图片本身就是压缩格式.其次,对于小于大约150-1000 字节的文件(具体取决于文

  • asp.net core 中优雅的进行响应包装的实现方法

    目录 摘要 正常响应包装 实现按需禁用包装 总结 摘要 在 asp.net core 中提供了 Filter 机制,可以在 Action 执行前后进行一些特定的处理,例如模型验证,响应包装等功能就可以在此基础上实现,同时也提供了 ApplicationModel API, 我们可以在此基础上实现选择性的添加 Filter,满足部分接口需要响应特定的结构, 我们常见的 [AllowAnonymous] 正是基于这种机制.同时也将介绍如何让 Swagger 展示正确的包装响应体,以满足第三方对接或前

  • Asp.net core中实现自动更新的Option的方法示例

    Asp.net core可以监视json.xml等配置文件的变化, 自动刷新内存中的配置内容, 但如果想每隔1秒从zookeeper.consul获取最新的配置信息, 需要自己实现. 阅读了 Asp.net core Document的Custom configuration provider, 得知只需要实现自己的IConfigurationSource和对应ConfigurationProvider即可 在这个示例中, 我建立了一个简单的option, 只包含一个不断变化的计数器变量. pu

  • 在ASP.NET Core 中发送邮件的实现方法(必看篇)

    前言 我们知道目前 .NET Core 还不支持 SMTP 协议,当我么在使用到发送邮件功能的时候,需要借助于一些第三方组件来达到目的,今天给大家介绍两款开源的邮件发送组件,它们分别是 MailKit 和 FluentEmail , 下面我对它们分别进行介绍. MailKit 在 ASP.NET Core 中,可以使用 MailKit 来发送邮件,它支持跨平台,并且支持 IMAP, POP3, SMTP 等协议. 你可以使用下面的方式安装: Install-Package MailKit 下面是

  • 在ASP.NET Core中显示自定义的错误页面

    前言 相信每位程序员们应该都知道在 ASP.NET Core 中,默认情况下当发生500或404错误时,只返回http状态码,不返回任何内容,页面一片空白. 如果在 Startup.cs 的 Configure() 中加上 app.UseStatusCodePages(); ,500错误时依然是一片空白(不知为何对500错误不起作用),404错误时有所改观,页面会显示下面的文字: Status Code: 404; Not Found 如果我们想实现不管500还是404错误都显示自己定制的友好错

  • ASP.NET Core中的Action的返回值类型实现

    在Asp.net Core之前所有的Action返回值都是ActionResult,Json(),File()等方法返回的都是ActionResult的子类.并且Core把MVC跟WebApi合并之后Action的返回值体系也有了很大的变化. ActionResult类 ActionResult类是最常用的返回值类型.基本沿用了之前Asp.net MVC的那套东西,使用它大部分情况都没问题.比如用它来返回视图,返回json,返回文件等等.如果是异步则使用Task. public class Te

  • Asp.Net Core 中的“虚拟目录”实现

    写在前面 现在部署Asp.Net Core应用已经不再限制于Windows的IIS上,更多的是Docker容器.各种反向代理来部署.也有少部分用IIS部署的,IIS部署确实是又快又简单,图形化操作三下五除二就可以发布好一个系统了.在过去Asp.Net MVC 项目部署的时候,还常常使用IIS一个功能--虚拟目录. 虚拟目录可以直接定位到非项目的其他路径,将路径作为网站的一部分,可实现上传文件保存到其他盘符或间接的使用项目以外的静态文件.在Asp.Net MVC中从虚拟路径中存取文件也很简单,如S

  • 详解如何在ASP.NET Core中使用Route特性

    ASP.NET Core 中的 Route 中间件的职责在于将 request 匹配到各自 Route 处理程序上,Route 分两种:基于约定 和 基本特性 模式. 基于约定 模式的Route采用集中化的方式,而 基于特性 的方式允许你在 Action 或者 Controller 上单独定义,到底采用哪一种可以基于你自己的应用场景,本篇就来讨论如何使用 基于特性 模式. 创建 Controller 类 创建一个 DefaultController 类,新增如下代码. public class

  • 详解如何在ASP.NET Core中使用IHttpClientFactory

    利用IHttpClientFactory可以无缝创建HttpClient实例,避免手动管理它们的生命周期. 当使用ASP.Net Core开发应用程序时,可能经常需要通过HttpClient调用WebAPI的方法以检查终结点是否正常工作.要实现这一点,通常需要实例化HttpClient并使用该实例来调用你的方法.但是直接使用HttpClient也有一些缺点,主要与手动管理实例的生命周期有关. 你可以使用IHttpClientFactory创建HttpClient来避免这些问题.IHttpClie

  • ASP.NET Core中实现全局异常拦截的完整步骤

    前言 异常是一种运行时错误,当异常没有得到适当的处理,很可能会导致你的程序意外终止,这篇就来讨论一下如何在 ASP.Net Core MVC 中实现全局异常处理,我会用一些 样例代码 和 截图 来说明这些概念. 全局异常处理 其实在 ASP.Net Core MVC 框架中已经有了全局异常处理的机制,你可以在一个中心化的地方使用 全局异常处理中间件 来进行异常拦截,如果不用这种中心化方式的话,你就只能在 Controller 或者 Action 作用域上单独处理,这会导致异常处理代码零散在项目各

  • 详解如何在ASP.Net Core中实现健康检查

    健康检查 常用于判断一个应用程序能否对 request 请求进行响应,ASP.Net Core 2.2 中引入了 健康检查 中间件用于报告应用程序的健康状态. ASP.Net Core 中的 健康检查 落地做法是暴露一个可配置的 Http 端口,你可以使用 健康检查 去做一个最简单的活性检测,比如说:检查网络和系统的资源可用性,数据库资源是否可用,应用程序依赖的消息中间件或者 Azure cloud service 的可用性 等等,这篇文章我们就来讨论如何使用这个 健康检查中间件. 注册健康检查

随机推荐