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

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

一. 先看需求:

要求绝大部分的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即可。

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

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

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

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

记录一次.net core web 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进行类包装了

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

 

那么顺着这个思路继续往下走,我先是大概溜了一遍,我相信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:

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

 

 

   components中对应的字典:

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

 

 

   被包装过的responses:

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

 

 

  components中对应的字典:

 

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

 

 

   通过对比,我基本知道自己该怎么做了,第一步,将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的原因在于下图:

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

 

 

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

 

 

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

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

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

 

 

   他们的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 项目中在全局对返回值进行类包装的解决过程

 

 

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

 

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

 

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

 

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

 

 (完)

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

上一篇:[hyddd安全性测试笔记1]URL Encode and URL Decode


下一篇:C# WinForm 获取带有焦点的控件