• 统一NET Core WebApi返回结果


    ASP.NET Core WebApi返回结果统一包装实践

    
        private static readonly string[] Summaries = new[]
        {
           "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };
    
        [HttpGet("GetWeatherForecast")]
        public ResponseResult<IEnumerable<WeatherForecast>> GetAll()
        {
            var datas = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            });
            return SuccessResult(datas);
        }
    }
    

     

    这个时候确实变得很美好了,但是还是没有逃脱一点,那就是我还是得通过特定的方法来得到一个ResponseResult<T>类型的返回结果,包括我们给ResponseResult<T>类封装静态方法,或者甚至是定义ApiControllerBase基类,都是为了进一步简化这个操作。现在呢我想告别这个限制,我能不能把返回的结果直接就默认的转化成ResponseResult<T>类型的结果呢?当然可以,这也是通过ASP.NET Core的封装思路中得到的启发,借助implicit自动完成隐式转换,这个在ASP.NET Core的ActionResult<T>类中也有体现

     

    public static implicit operator ActionResult<TValue>(TValue value)
    {
        return new ActionResult<TValue>(value);
    }
    

     

    通过这个思路我们可以进一步完善ResponseResult<T>类的实现方式,给它添加一个隐式转换的操作,仅仅定义一个方法即可,在ResponseResult<T>类中继续完善

     

    /// <summary>
    /// 隐式将T转化为ResponseResult<T>
    /// </summary>
    /// <param name="value"></param>
    public static implicit operator ResponseResult<T>(T value)
    {
        return new ResponseResult<T> { Data = value };
    }
    

     

    这种对于绝大部分返回成功结果的时候提供了非常简化的操作,这个时候如果你再去使用action的时候就可以进一步来简化返回值的操作了

     

    [HttpGet("GetWeatherForecast")]
    public ResponseResult<IEnumerable<WeatherForecast>> GetAll()
    {
        var datas = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        });
        return datas.ToList();
    }
    

     

    因为我们定义了TResponseResult<T>的隐式转换,所以这个时候我们就可以直接返回结果了,而不需要手动对结果返回值进行包装。

     

    漏网之鱼处理

     

    在上面我们为了尽量简化action返回ResponseResult<T>的统一返回结构的封装,已经对ResponseResult<T>类进行了许多的封装,并且还通过封装ApiControllerBase基类进一步简化这一操作,但是终究还是避免不了一点,那就是很多时候可能想不起来对action的返回值去加ResponseResult<T>类型的返回值,但是我们之前的所有封装都得建立在必须要声明ResponseResult<T>类型的返回值的基础上才行,否则就不存在统一返回格式这一说法了。所以针对这些漏网之鱼,我们必须要有统一的拦截机制,这样才能更完整的针对返回结果进行处理,针对这种对action返回值的操作,我们首先想到的就是定义过滤器进行处理,因此笔者针对这一现象封装了一个统一包装结果的过滤器,实现如下

     

    public class ResultWrapperFilter : ActionFilterAttribute
    {
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
            var actionWrapper = controllerActionDescriptor?.MethodInfo.GetCustomAttributes(typeof(NoWrapperAttribute), false).FirstOrDefault();
            var controllerWrapper = controllerActionDescriptor?.ControllerTypeInfo.GetCustomAttributes(typeof(NoWrapperAttribute), false).FirstOrDefault();
            //如果包含NoWrapperAttribute则说明不需要对返回结果进行包装,直接返回原始值
            if (actionWrapper != null || controllerWrapper != null)
            {
                return;
            }
    
            //根据实际需求进行具体实现
            var rspResult = new ResponseResult<object>();
            if (context.Result is ObjectResult)
            {
                var objectResult = context.Result as ObjectResult;
                if (objectResult?.Value == null)
                {
                    rspResult.Status = ResultStatus.Fail;
                    rspResult.Message = "未找到资源";
                    context.Result = new ObjectResult(rspResult);
                }
                else
                {
                    //如果返回结果已经是ResponseResult<T>类型的则不需要进行再次包装了
                    if (objectResult.DeclaredType.IsGenericType && objectResult.DeclaredType?.GetGenericTypeDefinition() == typeof(ResponseResult<>))
                    {
                        return;
                    }
                    rspResult.Data = objectResult.Value;
                    context.Result = new ObjectResult(rspResult);
                }
                return;
            }
        }
    }
    

     

    折叠

     

    在使用WebAPI的过程中,我们的action绝大部分是直接返回ViewModelDto而并没有返回ActionResult类型相关,但是无妨,这个时候MVC的底层操作会为我们将这些自定义的类型包装成ObjectResult类型的,因此我们的ResultWrapperFilter过滤器也是通过这一机制进行操作的。这里有两点需要考虑的

     

    • 首先是,我们必须要允许并非所有的返回结果都要进行ResponseResult<T>的包装,为了满足这一需求我们还定义了NoWrapperAttribute来实现这一效果,只要Controller或Action有NoWrapperAttribute的修饰则不对返回结果进行任何处理。
    • 其次是,如果我们的Action上的返回类型已经是ResponseResult<T>类型的,则也不需要对返回结果进行再次的包装。

     

    关于ResultWrapperFilter的定义其实很简单,因为在这里它只是起到了一个标记的作用

     

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    public class NoWrapperAttribute:Attribute
    {
    }
    

     

    到了这里,还有一种特殊的情况需要注意,那就是当程序发生异常的时候,我们上面的这些机制也是没有办法生效的,因此我们还需要定义一个针对全局异常处理的拦截机制,同样是可以使用统一异常处理过滤器进行操作,实现如下

     

    public class GlobalExceptionFilter : IExceptionFilter
    {
        private readonly ILogger<GlobalExceptionFilter> _logger;
        public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
        {
            _logger = logger;
        }
    
        public void OnException(ExceptionContext context)
        {
            //异常返回结果包装
            var rspResult = ResponseResult<object>.ErrorResult(context.Exception.Message);
            //日志记录
            _logger.LogError(context.Exception, context.Exception.Message);
            context.ExceptionHandled = true;
            context.Result = new InternalServerErrorObjectResult(rspResult);
        }
    
        public class InternalServerErrorObjectResult : ObjectResult
        {
            public InternalServerErrorObjectResult(object value) : base(value)
            {
                StatusCode = StatusCodes.Status500InternalServerError;
            }
        }
    }
    

     

    写完过滤器了,千万不能忘了全局注册一下,否则它也就只能看看了,不会起到任何效果

     

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<ResultWrapperFilter>();
        options.Filters.Add<GlobalExceptionFilter>();
    });
    

     

    漏网之鱼另一种处理

     

    当然针对上面两种针对漏网之鱼的处理,在ASP.NET Core上还可以通过中间件的方式进行处理,至于过滤器和中间件有何不同,相信大家已经非常清楚了,核心不同总结起来就一句话二者的处理阶段不同,即针对管道的生命周期处理是不一样的,中间件可以处理任何生命周期在它之后的场景,但是过滤器只管理Controller这一块的一亩三分地但是针对结果包装这一场景,笔者觉得使用过滤器的方式更容易处理一点,因为毕竟我们是要操作Action的返回结果,通过过滤器中我们可以直接拿到返回结果的值。但是这个操作如果在中间件里进行操作的话,只能通过读取Response.Body进行操作了,笔者这里也封装了一个操作,如下所示

     

    public static IApplicationBuilder UseResultWrapper(this IApplicationBuilder app)
    {
            var serializerOptions = app.ApplicationServices.GetRequiredService<IOptions<JsonOptions>>().Value.JsonSerializerOptions;
            serializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
            return app.Use(async (context, next) =>
            {
                var originalResponseBody = context.Response.Body;
                try
                {
                    //因为Response.Body没办法进行直接读取,所以需要特殊操作一下
                    using var swapStream = new MemoryStream();
                    context.Response.Body = swapStream;
                    await next();
                    //判断是否出现了异常状态码,需要特殊处理
                    if (context.Response.StatusCode == StatusCodes.Status500InternalServerError)
                    {
                        context.Response.Body.Seek(0, SeekOrigin.Begin);
                        await swapStream.CopyToAsync(originalResponseBody);
                        return;
                    }
                    var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;
                    if (endpoint != null)
                    {
                        //只针对application/json结果进行处理
                        if (context.Response.ContentType.ToLower().Contains("application/json"))
                        {
                            //判断终结点是否包含NoWrapperAttribute
                            NoWrapperAttribute noWrapper = endpoint.Metadata.GetMetadata<NoWrapperAttribute>();
                            if (noWrapper != null)
                            {
                                context.Response.Body.Seek(0, SeekOrigin.Begin);
                                await swapStream.CopyToAsync(originalResponseBody);
                                return;
                            }
                            //获取Action的返回类型
                            var controllerActionDescriptor = context.GetEndpoint()?.Metadata.GetMetadata<ControllerActionDescriptor>();
                            if (controllerActionDescriptor != null)
                            {
                                //泛型的特殊处理
                                var returnType = controllerActionDescriptor.MethodInfo.ReturnType;
                                if (returnType.IsGenericType && (returnType.GetGenericTypeDefinition() == typeof(Task<>) || returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)))
                                {
                                    returnType = returnType.GetGenericArguments()[0];
                                }
                                //如果终结点已经是ResponseResult<T>则不进行包装处理
                                if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ResponseResult<>))
                                {
                                    context.Response.Body.Seek(0, SeekOrigin.Begin);
                                    await swapStream.CopyToAsync(originalResponseBody);
                                    return;
                                }
                                context.Response.Body.Seek(0, SeekOrigin.Begin);
                                //反序列化得到原始结果
                                var result = await JsonSerializer.DeserializeAsync(context.Response.Body, returnType, serializerOptions);
                                //对原始结果进行包装
                                var bytes = JsonSerializer.SerializeToUtf8Bytes(ResponseResult<object>.SuccessResult(result), serializerOptions);
                                new MemoryStream(bytes).CopyTo(originalResponseBody);
                                return;
                            }
                        }
                    }
                    context.Response.Body.Seek(0, SeekOrigin.Begin);
                    await swapStream.CopyToAsync(originalResponseBody);
                }
                finally
                {
                    //将原始的Body归还回来
                    context.Response.Body = originalResponseBody;
                }
            });
        }
    }
    

     

    折叠

     

    相信通过上面的处理,我们就可以更容易的看出来,谁更容易的对统一结果进行包装处理了,毕竟我们是针对Action的返回结果进行处理,而过滤器显然就是为针对Controller和Action的处理而生的。但是通过中间件的方式能更完整的针对结果进行处理,因为许多时候我们可能是在自定义的中间件里直接拦截请求并返回,但是根据二八原则这种情况相对于Action的返回值毕竟是少数,有这种情况我们可以通过直接ResponseResult<T>封装的方法进行返回操作,也很方便。但是这个时候呢,关于异常处理我们通过全局异常处理中间件,则能更多的处理更多的场景,且没有副作用,看一下它的定义

     

    public static IApplicationBuilder UseException(this IApplicationBuilder app)
    {
        return app.UseExceptionHandler(configure =>
        {
            configure.Run(async context =>
            {
                var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
                var ex = exceptionHandlerPathFeature?.Error;
                if (ex != null)
                {
                    var _logger = context.RequestServices.GetService<ILogger<IExceptionHandlerPathFeature>>();
                    var rspResult = ResponseResult<object>.ErrorResult(ex.Message);
                    _logger?.LogError(ex, message: ex.Message);
                    context.Response.StatusCode = StatusCodes.Status500InternalServerError;
                    context.Response.ContentType = "application/json;charset=utf-8";
                    await context.Response.WriteAsync(rspResult.SerializeObject());
                }
            });
        });
    }
    

     

    使用全局异常梳理中间件是没有副作用的,主要因为在异常处理的时候我们不需要读取Response.Body进行读取操作的,所以至于是选择异常处理中间件还是过滤器,大家可以针对自己的实际场景进行选择,两种方式都是可以的。

     

    总结

     

        本文主要是展示了针对ASP.NET Core WeApi结果统一返回格式的相关操作,通过示例我们一步一步的展示了完成这一目标的不断升级的实现,虽然整体看起来比较简单,但是却承载着笔者一次又一次的思考升级。每次实现完一个阶段,都会去想有没有更好的方式去完善它。这其中还有一些思路来自微软源码为我们提供的思路,所以很多时候还是建议大家去看一看源码的,可以在很多时候为我们提供一种解决问题的思路。正如我看到的一句话,读源码也是一种围城,外面的人不想进去,里面的人不想出来。如果大家有更好的实现方式,欢迎一起讨论。曾经的时候我会为自己学到了一个新的技能而感到高兴,到了后来我会对有一个好的思路,或者好的解决问题的方法而感到高兴。读万卷书很重要,行万里路同样重要,读书是沉淀,行路是实践,结合到一起才能更好的促进,而不是只选择一种。

  • 相关阅读:
    [转]Asp.Net页面输出到WORD、EXCEL、TXT、HTM等类型的文档
    人工智能AI基础 四
    关于设计的一点小结 四
    Visual Studio 11 将强化对2D/3D游戏开发的支持 四
    如何正确的对待设计模式 四
    C++设计模式原型模式 四
    C++游戏编程8步云 四
    Qt编译 四
    软件架构师应该知道的97件事 四
    给年轻程序员的几句话 四
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/16472550.html
Copyright © 2020-2023  润新知