首页 > Windows开发 > 详细

记录一次.net core web Api 项目中在全局对返回值进行类包装的解决过程

时间:2021-07-13 15:01:16      阅读:19      评论:0      收藏:0      [点我收藏+]

 // 第一次写,写的比较乱

一. 先看需求:

要求绝大部分的api接口的返回值都用此类型包装过后进行返回,将原返回值放到result中。

    /// <summary>
    /// 结果返回模型
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class ApiResult<T>
    {
        public int code { get; set; }
        public string message { get; set; }
        public T result { get; set; }
    }


二、 分析:

如果只是在返回值中进行类包装,那应该是很简单的一个需求了,直接在OnActionExecuted中进行一下包装即可,但是如果只是如此的话,swagger中肯定只是有原模型的信息,而不是包装好的。那么我将这个需求分为两部分。

第一部分就是将返回值先进行类包装,第二部分再看一下如何将swagger中的信息进行类包装。

三、第一部分(将返回值先进行类包装):

这个直接用filter拦截器就可以了

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace OnePiece.Tools.Filters
{
    public class ApiResult : IActionResult
    {
        public int code { get; set; }
        public string message { get; set; }
        public object result { get; set; }

        public Task ExecuteResultAsync(ActionContext context)
        {
            HttpResponse response = context.HttpContext.Response;
            string json = string.Empty;
            if (this != null)
            {
                json = JsonConvert.SerializeObject(this);
            }
            response.Headers["content-type"] = "application/json; charset=utf-8";
            return Task.FromResult(response.WriteAsync(json));
        }
    }
    public class ApiResourceFilter : IActionFilter
    {
        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
            if (((ControllerActionDescriptor)filterContext.ActionDescriptor).MethodInfo.CustomAttributes.Any(e => e.AttributeType.Name == nameof(NotApiResultAttribute)))
            {
                return;
            }
            filterContext.Result = new ApiResult
            {
                code = 200,
                message = "",
                result = ((ObjectResult)filterContext.Result).Value
            };
        }
        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
        }
    }
}

这是拦截器的代码,

1. 对类进行包装的拦截器实现了IActionFilter接口,OnActionExecuted方法使得action结束时会经过该方法,那么我们在此进行类包装即可。

2. 由于filterContext.Result的值需要实现IActionResult,我只好重新写了一个ApiResult类,实现了一下IActionResult接口。

3. 其中用到了一个特性NotApiResultAttribute,这个特性中没有什么实际的内容,只是为了标识一下它不需要被类包装。

4. 最后再在全局中添加上此filter即可。

技术分享图片

四、第二部分(将swagger中的信息进行类包装)

1.先看看swagger返回的json是什么含义

要想对swagger进行类包装就先需要知道swagger是如何呈现的,通过把源码下载到本地并调试,我知道了实际上swagger会返回如下图的json,“--”后面的内容是我自己写的(其实通过F12看api的返回内容也是可以看到的)

技术分享图片

 

 

 那么再详细看看paths中,以及components中的内容,诶?我发现paths中的每个路由下都有一个responses,而且在responses中还有一段

"OnePiece.Models.Models.ApiResult`1[[OnePiece.Entities.Partner, OnePiece.Models, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",前面还有一个“$ref”,em...ref?引用?

如果比较了解反射,应该可以看出来这是ApiResult<Partner>此类型的Fullname,如果看不出来也没关系,我发现components中有一个完全相同的key,那是不是上面的那个ref就是指引用这个模型呢?

经过我尝试了几次之后,发现确实是。那么下一步,我就想去看看有没有什么办法可以改变paths和components,如果可以改变,那我就可以通过手动往components中添加我自己的字典,然后改变paths中的responses中的“$ref”的值来实现对swagger进行类包装了

技术分享图片技术分享图片

 

那么顺着这个思路继续往下走,我先是大概溜了一遍,我相信swagger一定会有暴露给我们的,供我们可以进行类包装的方法。

下图我贴出swaggermiddleware的核心invoke方法,以及其中的getswagger方法

        public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
        {
            if (!RequestingSwaggerDocument(httpContext.Request, out string documentName))
            {
                await _next(httpContext);
                return;
            }

            try
            {
                var basePath = httpContext.Request.PathBase.HasValue
                    ? httpContext.Request.PathBase.Value
                    : null;

                var swagger = swaggerProvider.GetSwagger(
                    documentName: documentName,
                    host: null,
                    basePath: basePath);

                // One last opportunity to modify the Swagger Document - this time with request context
                foreach (var filter in _options.PreSerializeFilters)
                {
                    filter(swagger, httpContext.Request);
                }
                if (Path.GetExtension(httpContext.Request.Path.Value) == ".yaml")
                {
                    await RespondWithSwaggerYaml(httpContext.Response, swagger);
                }
                else
                {
                    await RespondWithSwaggerJson(httpContext.Response, swagger);
                }
            }
            catch (UnknownSwaggerDocument)
            {
                RespondWithNotFound(httpContext.Response);
            }
        }

  我发现其中有一个filters是PreSerializeFilters,可以看到它的入参是有一个swagger和一个rquest,根据上下文可知,它应该是一个已经完整的模型,还没被json序列化而已。

  既然如此,那我就开始往components里面加我自己的keyvalue了,那么我先看看同一个类型,被包装过和没有被包装过的区别好了,还是看上面那个partner返回类型的例子:

  未被包装的responses:

  技术分享图片

 

 

   components中对应的字典:

  技术分享图片

 

 

   被包装过的responses:

  技术分享图片

 

 

  components中对应的字典:

 

   技术分享图片

 

 

   通过对比,我基本知道自己该怎么做了,第一步,将responses中的$ref  改成被包装过得类型的fullname。第二步,往components中加入一个以该fullname为key的keyvalue,其中除了result中的$ref,其它全部照搬上图即可。(其实也不需要照搬,看两眼这个图基本也能知道各个kv所代表的含义,自己写也完全没问题。)

  

  直接贴出我的代码了 

    // 这是startup里的Configure中的内容

    app.UseSwagger(
      c => c.PreSerializeFilters.Add(ApiRuslt)
    );


  // 这是上面filter的具体实现
     public void ApiRuslt(OpenApiDocument a, HttpRequest b) { var codeSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "相应代码", Format = "int32", Type = "integer" }; var messageSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "信息", Nullable = true, Type = "string" }; var keys = a.Components.Schemas.Keys.ToList(); for (int i = 0; i < keys.Count; i++) { var type = typeof(ApiResult<>); Type s = StaticMethods.GetTypeByName(keys[i]); type = type.MakeGenericType(s); var schema = a.Components.Schemas[keys[i]]; OpenApiSchema aa = new OpenApiSchema(); aa.Type = "object"; aa.Description = "结果返回模型"; aa.Properties = new Dictionary<string, OpenApiSchema> { { "code", codeSchema } }; aa.Properties.Add("message", messageSchema); aa.Properties.Add("result", new OpenApiSchema { Properties = new Dictionary<string, OpenApiSchema> { }, Reference = new OpenApiReference { Id = keys[i], Type = ReferenceType.Schema }, Description = "信息", Type = "object" }); if (!a.Components.Schemas.ContainsKey(type.FullName)) { a.Components.Schemas.Add(type.FullName, aa); } } }

  代码写的比较具体,其实可以写成可以泛用的,我这边没太大兴趣写了。。。

  但是有个东西我还没有实现,那就是如何判断这个action是否有notapiresult这个特性呢,想判断这个的话,就需要去别的地方再找找了,但我知道我需要找一个可以单独看action相关内容的地方。

  后来在GetSwagger方法中的GeneratePaths方法中的GenerateOperations方法中的GenerateOperation终于找到了我想要的OperationFilters

  直接贴代码了

  

         services.AddSwaggerGen(options =>
         {
             .........    
              options.OperationFilter<AddSwaggerBizParametersFilters>();
             .........
        }); 

  

  

    public class AddSwaggerBizParametersFilters : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            if (context.MethodInfo.CustomAttributes.Any(e => e.AttributeType.Name == nameof(NotApiResultAttribute)))
            {
                return;
            }
            operation.Responses.ForEach(e => e.Value.Content.ForEach(a =>
            {
                var type = typeof(ApiResult<>);
                
                Type s = StaticMethods.GetTypeByName(a.Value.Schema.Reference.Id);
                type = type.MakeGenericType(s);

                a.Value.Schema.Reference.Id = type.FullName;
            }));
            
        }
    }

  我这边把修改responses中的$ref的工作也放到这里了。

  至于修改Id就可以修改$ref的原因在于下图:

  技术分享图片

 

 

   技术分享图片

 

 

   至此,swagger里的object类型的对象都可以进行包装了。

   但是,当我用bool或者int或者string或者guid等等这种基本类型的时候,问题出现了,因为他们根本就不需要引用其他类型

  技术分享图片

 

 

   他们的schema只有type和format,之后我尝试了通过type和format去创建类似ApiResult<Int>,ApiResult<decimal>之类的类型的fullname,但是失败了,其实在失败之后我有想过是不是基本类型不能使用我想的这种方法,

   但是第二天我突然想到会不会只要$ref的值和components中key的对应即可,并不需要一定是反射出来的fullname,经过测试,发现果真如此。

   除了跟swagger有关的两个filter进行了一些改动,其他没有任何变动,我只贴出两个filter的代码了:

  

    public class AddSwaggerBizParametersFilters : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            if (context.MethodInfo.CustomAttributes.Any(e => e.AttributeType.Name == nameof(NotApiResultAttribute)))
            {
                return;
            }
            operation.Responses.ForEach(e => e.Value.Content.ForEach(a =>
            {
                var type = typeof(ApiResult<>);
                if (a.Value.Schema.Reference == null)
                {
                    a.Value.Schema = new OpenApiSchema
                    {
                        AdditionalPropertiesAllowed = false,
                        Reference = new OpenApiReference
                        {
                            Id = a.Value.Schema.Format + "tyc" + a.Value.Schema.Type,
                            Type = ReferenceType.Schema
                        },
                        Items = a.Value.Schema.Items
                    };
                    return;
                }
                
                Type s = StaticMethods.GetTypeByName(a.Value.Schema.Reference.Id);
                type = type.MakeGenericType(s);

                a.Value.Schema.Reference.Id = type.FullName;
            }));
            
        }
    }

  

        public void ApiRuslt(OpenApiDocument a, HttpRequest b)
        {
            var codeSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "相应代码", Format = "int32", Type = "integer" };
            var messageSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "信息", Nullable = true, Type = "string" };
            var needAdds = a.Paths.SelectMany(e => e.Value.Operations).SelectMany(e => e.Value.Responses)
                .SelectMany(e => e.Value.Content).Select(e => e.Value.Schema).Where(e => e.Reference != null && e.Reference.Id.Contains("tyc")).ToList();
            var needButy = a.Components.Schemas.SelectMany(e => e.Value.Properties).Where(e => e.Value.Description == null && e.Value.Reference != null).Select(e => e.Value).ToList();
            needButy.ForEach(e =>
            {
                var oo = a.Components.Schemas.Where(s => e.Reference.Id.Contains(s.Key)).FirstOrDefault();
                e.Description = oo.Value.Description;
            });
            var keys = a.Components.Schemas.Keys.ToList();
            for (int i = 0; i < keys.Count; i++)
            {
                var type = typeof(ApiResult<>);
                Type s = StaticMethods.GetTypeByName(keys[i]);
                type = type.MakeGenericType(s);
                var schema = a.Components.Schemas[keys[i]];
                OpenApiSchema aa = new OpenApiSchema();
                aa.Type = "object";
                aa.Description = "结果返回模型";
                aa.Properties = new Dictionary<string, OpenApiSchema> { { "code", codeSchema } };
                aa.Properties.Add("message", messageSchema);
                aa.Properties.Add("result", new OpenApiSchema
                {
                    Properties = new Dictionary<string, OpenApiSchema> { },
                    Reference = new OpenApiReference { Id = keys[i], Type = ReferenceType.Schema },
                    Description = "信息",
                    Type = "object"
                });
                if (!a.Components.Schemas.ContainsKey(type.FullName))
                {
                    a.Components.Schemas.Add(type.FullName, aa);
                }
            }
            needAdds.ForEach(e =>
            {
                if (!a.Components.Schemas.ContainsKey(e.Reference.Id))
                {
                    OpenApiSchema xx = new OpenApiSchema();
                    xx.Type = "object";
                    xx.Description = "结果返回模型";
                    xx.AdditionalPropertiesAllowed = false;
                    xx.Properties = new Dictionary<string, OpenApiSchema> { { "code", codeSchema } };
                    xx.Properties.Add("message", messageSchema);
                    var list = e.Reference.Id.Split("tyc");
                    var schema = new OpenApiSchema
                    {
                        Properties = new Dictionary<string, OpenApiSchema> { },
                        Items = e.Items
                    };
                    schema.AdditionalPropertiesAllowed = true;
                    if (list.Length == 2)
                    {
                        if (list[0] != "")
                        {
                            schema.Format = list[0];
                        }
                        schema.Type = list[1];
                        if (schema.Items != null)
                        {
                            schema.Format = schema.Items.Format;
                        }
                    }
                    xx.Properties.Add("result", schema);
                    a.Components.Schemas.Add(e.Reference.Id, xx);
                }
            });
        }

  

五、看看实际效果

技术分享图片

 

 

 技术分享图片

 

 技术分享图片

 

 技术分享图片

 

 技术分享图片

 

 (完)

记录一次.net core web Api 项目中在全局对返回值进行类包装的解决过程

原文:https://www.cnblogs.com/ttyycc/p/14982788.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!