惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
O
OpenAI News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
Webroot Blog
Webroot Blog
GbyAI
GbyAI
S
SegmentFault 最新的问题
Cyberwarzone
Cyberwarzone
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
J
Java Code Geeks
Google DeepMind News
Google DeepMind News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 【当耐特】
S
Secure Thoughts
酷 壳 – CoolShell
酷 壳 – CoolShell
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta
S
Security Affairs
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
Spread Privacy
Spread Privacy
T
Threatpost
Forbes - Security
Forbes - Security
C
Cisco Blogs
Scott Helme
Scott Helme
Attack and Defense Labs
Attack and Defense Labs
Simon Willison's Weblog
Simon Willison's Weblog
腾讯CDC
The Last Watchdog
The Last Watchdog
Cloudbric
Cloudbric
Last Week in AI
Last Week in AI
Recorded Future
Recorded Future
小众软件
小众软件
V
Vulnerabilities – Threatpost
美团技术团队
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
Hacker News - Newest:
Hacker News - Newest: "LLM"
I
Intezer
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 司徒正美
C
Cybersecurity and Infrastructure Security Agency CISA
Martin Fowler
Martin Fowler
博客园 - 聂微东

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
通过 ApiExplorer 为 Swagger 提供 MVC 扩展 - 元视角
飞鸿踏雪 · 2019-08-07 · via 元视角

我一直想吐槽下运维同事提供的 Jekins 项目模板,因为它居然不支持含有多个项目的解决方案。作为一个有追求的程序员,每个解决方案下最少应该含有两个项目,即项目本身和项目对应的单元测试。可惜在这样一种情形下,我没法再去坚持这样的原则,而这真正让我感到难过的是,为了在编译环节向 Jekins 妥协,大家在一个项目里极尽所能,在这一个项目里居然混合了MVCWebApiWebService三种技术,甚至到最后连传统三层的界限都越来越模糊了。这让我意识到一件事情,工程上的妥协和技术选型一样,在某种意义上它们都不能被称之为科学,因为确实没什么道理,完全是运维为了方便而制造出的问题。在我们意识到文档的重要性以后,写文档就变成了日常工作。我一直坚持的原则是,文档能通过工具生成就坚决不要手写,所以,看到项目目录里充斥着各种各样的文档格式,譬如 Word、Excel、Pdf、Viso 等等的时候,我毅然决然地选择了 Swagger。而今天这篇文章的原由,恰恰来自于这个"混搭"的项目。说到这里,你可能已经想到我想做什么了。不错,我们有部分 WebApi 是写在 MVC 的控制器里的,我希望使用者可以通过 Swagger 看到这部分接口的文档,这样我就有时间在这里写博客了。😄

故事缘起

常规的 Swagger 的使用就不再说啦,因为基本上你通过 Nuget 安装完Swashbuckle以后,再配置下项目生成的 XML 注释文档就可以使用啦!不过,博主在这里遇到的第一个问题就是,按照常规套路配置好了以后,Swagger 页面打开完全就是空白的啊,反复尝试直至怀疑人生后,我突然意识到,莫非是因为我这是一个 MVC 的项目?立马跑到官方的 Issues 下面逐个扫视,果不其然,大佬们一致给出了答案:Swagger 是不支持 MVC 类型的项目的。这里补充说明,这里的 MVC 是指ASP.NET MVC。目前官方主推的ASP.NET Core是没有这种困惑的啦,因为微软在这个新版本中统一了 MVC 和 WebApi。对于这种历史“遗留问题”,既然 Swagger 官方都不愿意提供支持,那么,博主只好勉为其难的提供一个实现,我不得不承认,带着历史包袱的 ASP.NET 在扩展性上的确不如全新的“Core”系列,因为单单是System.Web系列的动态链接库版本就令人痛苦不堪,因此,博主在写这个扩展的时候,全部升到了最新的 5.2.7.0。

实现 MvcApiExplorer

好了,Swagger 之所以能够生成友好、可交互的 API 文档,其核心依赖于 IApiExplorer 接口。这一点,我们可以通过 Swashbuckle 项目中的源代码来得到验证。其中,生成符合 Swagger 规范的 JSON 文档,是通过 SwaggerGenerator 这个类来实现的。而进一步研究这个类,我们就会发现它依赖IApiExplorer接口。这个接口位于System.Web.Http.Description命名空间下,而显然这是 WebApi 相关的命名空间,所以,对于一般的 WebApi 项目,因为微软已经帮我们实现了默认的 ApiExplorer,所以,Swagger 可以识别出项目中的 Controller 及其 Action,并通过 XML 注释文档进一步填充接口相关的描述信息。一旦想到这一层,我们就会明白,为什么 Swagger 不支持 MVC 项目,因为 MVC 里压根就没有实现 IApiExplorer 接口啊!那么,怎么办呢?我们的想法是通过反射取出所有的 MVC 控制器及其 Action,然后组织出这些接口的描述信息,再将它们添加到默认的 IApiExplorer 实现中去,这样 MVC 和 WebApi 都可以被 Swagger 识别,为此,我们继承默认的 ApiExplorer,并实现我们自定义的MvcApiExplorer

public class MvcApiExplorer : ApiExplorer 
{
    /// <summary>
    /// HttpConfiguration
    /// </summary>
    private HttpConfiguration _configuration;

    public MvcApiExplorer (Assembly assembly, HttpConfiguration configuration) : base (configuration) 
    {
        _configuration = configuration;
        assembly.GetTypes ()
            .Where (type => typeof (IController).IsAssignableFrom (type) && type.Name != "ErrorController" && type.BaseType != typeof (ApiController))
            .ToList ().ForEach (controller => 
            {
                base.ApiDescriptions.AddRange (BuildControllerApiDescription (controller));
            });
    }

    /// <summary>
    /// ApiExolorer for Action is visible
    /// </summary>
    /// <param name="actionVariableValue"></param>
    /// <param name="actionDescriptor"></param>
    /// <param name="route"></param>
    /// <returns></returns>
    public override bool ShouldExploreAction (string actionVariableValue, HttpActionDescriptor actionDescriptor, IHttpRoute route) => true;

    /// <summary>
    /// ApiExolorer for Controller is visible
    /// </summary>
    /// <param name="controllerVariableValue"></param>
    /// <param name="controllerDescriptor"></param>
    /// <param name="route"></param>
    /// <returns></returns>
    public override bool ShouldExploreController (string controllerVariableValue, HttpControllerDescriptor controllerDescriptor, IHttpRoute route) => true;

    private List<ApiDescription> BuildControllerApiDescription (Type type) 
    {
        var controllerName = type.Name.Replace ("Controller", "");
        var methods = type.GetMethods (System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | BindingFlags.DeclaredOnly)
            .Where (m => typeof (ActionResult).IsAssignableFrom (m.ReturnType));

        var list = new List<ApiDescription> ();
        foreach (var method in methods)
        {
            var apiDescription = new ApiDescription ();
            apiDescription.ActionDescriptor = new MvcHttpActionDescriptor (method);
            apiDescription.ActionDescriptor.ControllerDescriptor = new HttpControllerDescriptor (_configuration, controllerName, type);
            apiDescription.HttpMethod = HttpMethod.Post;
            apiDescription.Route = new HttpRoute (string.Format ("{0}/{1}", controllerName, method.Name));
            apiDescription.RelativePath = string.Format ("{0}/{1}", controllerName, method.Name);
            apiDescription.Documentation = string.Empty;
            typeof (ApiDescription).GetProperty ("ParameterDescriptions").SetValue (apiDescription, BuildApiParameters (method));
            typeof (ApiDescription).GetProperty ("ResponseDescription").SetValue (apiDescription, new ResponseDescription () 
            {
                ResponseType = method.ReturnType,
                DeclaredType = method.DeclaringType,
                Documentation = string.Empty
            });
            list.Add (apiDescription);
        }

        return list;
    }

    private Collection<ApiParameterDescription> BuildApiParameters (MethodInfo methodInfo) 
    {
        return new Collection<ApiParameterDescription> (
            methodInfo.GetParameters ().Select (p => new ApiParameterDescription () 
            {
                Name = p.Name,
                Documentation = string.Empty,
                Source = ApiParameterSource.Unknown,
                ParameterDescriptor = new MvcHttpActionParameterDescriptor (p, new MvcHttpActionDescriptor (methodInfo)),
            }).ToList ());
    }
}

通过代码可以看出,实现 MvcApiExplorer 的过程,其实就是向 ApiDescriptions 集合中添加元素的过程。为此,我们通过程序集去反射所有实现了IController接口,同时其父类不是ApiController,并且方法返回值类型为ActionResult的所有类型,通过这个类型信息,我们进一步反射每个方法以及方法的参数,并把这些参数转换为 ApiDescription 类型需要的参数类型。在组织信息的过程中,有一部分属性被微软设计为只读,故而不得不通过反射的方式来解决。我们知道,MVC 里默认的路由模板是:{controller}/{action},这是 WebApi 里的特性路由流行以前默认的、最基础的路由。我们这里基于沿用这个规则,所谓“约定大于配置”,这可以为我们节省不少时间。MVC 里的 HTTP 动词我全部使用了 POST,这是因为 MVC 里真正控制一个方法是 GET 还是 POST 请求,其实是JsonRequestBehavior这个参数,当它设置为 AllowGet 时,该方法可以同时支持这两种 HTTP 动词。同样,在模模型绑定阶段,我全部使用了 Unknown,因为 MVC 会尝试通过 Body 或者 Form 的形式来接受一个参数,这两个地方完全是来自 MVC 本身机制的限制,如果大家有更好的思路,欢迎大家在博客里留言。

一旦实现了自定义的 MvcApiExplorer,我们就可以尝试用它来替换微软默认的实现。在 ASP.NET 中,我们通过GlobalConfiguration.Configuration.Services.Replace()方法来实现服务的替换。其实,这种思路在 ASP.NET Core 里依然存在,比如我们在实现动态 WebApi 时采用的ControllerFeatureProvider都属于服务替换,所不同的时,ASP.NET 时代是通过一个内置的 IoC 容器来实现服务替换,而 ASP.NET Core 时代,我们显然有了更多的选择,甚至依赖注入渗透到了整个.NET Core 的方方面面,这的确是一种相当大的进步。曾几何时,Javaer 嘲笑我们只会拖控件,可今天的我们,Java 里有的概念我们都有对应的实现,反倒是 Java 开始从 C#身上学习那些有点“甜”的语法糖啦!的确,我们写了这么多代码,其实最关键的就只有下面这一句,住口!这明明是三行:

var assembly = typeof(DefaultMvcProject.MvcApplication).Assembly;
var apiExplorer = new MvcApiExplorer(assembly, GlobalConfiguration.Configuration);         GlobalConfiguration.Configuration.Services.Replace(typeof(IApiExplorer), apiExplorer);

OK,接下来我们简单写几个 MVC 的控制器,来验证下我们为 Swagger 编写的 MVC 控制。在此之前,请确保完成了 Swagger 的两步常规配置,即为 Swagger 引入 XML 注释文档、在项目属性中勾选 XML 注释文档。这是使用 Swagger 的最小配置,相信大家一定都知道啦!

 GlobalConfiguration.Configuration
     .EnableSwagger (c => {
         c.SingleApiVersion ("v1", "DefaultMvcProject");
         c.IncludeXmlComments ($"{System.AppDomain.CurrentDomain.BaseDirectory}/bin/DefaultMvcProject.XML");
     })
     .EnableSwaggerUi (c => {
         c.DocumentTitle ("My Swagger UI");
     });

两个非常简单的 Controller,这里就不再贴代码啦!

非常简单的Controller 非常简单的Controller

可以注意到,一切都工作的很好,我们在 Swagger 里可以看到我们编写的 Api 接口,并且可以直接对接口进行调试。因为 MVC 本身的原因,这些 MVC 控制器的注释都不会生成到 XML 注释文档里。所以,稍微有一点遗憾的地方就是,这些接口都没有对应注释。不过,这已经达到了本文最初的目的,至少我不用再去写文档,告诉使用者这个接口里有哪些参数,以及这个接口的地址是什么啦,说到底啊,懒惰是人类进步的阶梯。这篇博客里实现的扩展,我已经发布到 Github 上,并附带了一个简单的示例(不要想太多哦,就是这篇文章里的示例),感兴趣的朋友可以自助研究,仓库地址为:https://github.com/qinyuanpei/Swashbuckle.Extension.Mvc

差一点就完美了 差一点就完美了

本文小结

本文实现了一个针对 MVC 项目的 Swagger 扩展,它可以让你编写在 MVC 控制器里的 API 接口,像普通 WebApi 项目一样展示在 Swagger 里。其原理是继承并重写了ApiExplorer类,这是 Swagger 生成 API 文档的核心接口。好了,以上就是这篇文章的全部内容啦,写这种短小的文章没有那么累,希望大家读起来一样不会累吧,晚安,世界!