上一篇博客介绍了使用Nancy框架内部的方法来建立了一个简单到不能再简单的Document。可是还有许许多多的不足。html
为了能稍微完善一下这个Document,这篇引用了当前流行的Swagger,以及另外一个开源的Nancy.Swagger项目来完成今天的任务!git
注:Swagger是已经相对成熟的了,但Nancy(2.0.0-clinteastwood)和Nancy.Swagger(2.2.6-alpha)是基于目前的最新版本,但目前的都是没有发布正式版,因此后续API可能会有些许变化。github
下面先来简单看看什么是Swaggerjson
The World's Most Popular Framework for APIs.
这是Swagger官方的描述。能说出是世界上最流行的,也是要有必定资本的!c#
光看这个描述就知道Swagger不会差!毕竟人家敢这样说。固然我的也认为Swagger确实很不错。windows
经过官方文档,咱们都知道要想生成Swagger文档,可使用YAML或JSON两种方式来书写,因为咱们日常写程序用的比较多的是JSON!api
因此本文主要是使用了JSON,顺带说一下YAML的语法也是属于易懂易学的。数组
既然是用JSON书写,那么要怎么写呢?这个实际上是有一套规定、约束,咱们只要遵照这些来写就能够了。详细内容能够参见OpenAPI Specification安全
本文后面的内容将默认园友们对Swagger有过了解。服务器
Swagger主要有下面几个东西,要引用基本的样式和脚本就不在多说了。
固然,引用样式和脚本只是最基本的前提,下面这段js(来自swagger-ui项目)才是最为主要的!
<script> window.onload = function() { // Build a system const ui = SwaggerUIBundle({ url: "your url",//返回json数据的url地址 dom_id: '#swagger-ui',//在这个div展现内容 presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }) window.ui = ui } </script>
就是在上面加上注释的两个属性:url指定了咱们要展现数据(JSON格式)的来源,dom_id指定了在id为swagger-ui
的容器中展现咱们的文档。
在加载的时候建立了Swagger相关的内容,主要的有下面的两个,其他的用默认的就能够了。
简单来讲,咱们请求了这个url拿到了这些json数据,再根据这些数据在dom_id中构造出咱们所看到的页面。有那么点数据驱动的意思。
固然这些JSON数据是有格式要求的。能够看看下面的简单示例
{ "swagger": "2.0", "info": { "title": "Simple API overview", "version": "v2" }, "paths": { "/": { "get": { "operationId": "listVersionsv2", "summary": "List API versions", "produces": [ "application/json" ], "responses": { "200": { "description": "200 300 response", "examples": { "application/json": "一串json" } } } } } }, "consumes": [ "application/json" ] }
这也就意味着咱们只须要严格按照Swagger的定义,就能够生成一个即美观,又可执行的API文档了。
更多相关JSON示例可参见
https://github.com/OAI/OpenAPI-Specification/tree/master/examples/v2.0/json
Nancy.Swagger
是咱们今天的主角,是一个基于MIT协议的开源项目。Github地址:Nancy.Swagger
固然经过上面关于Swagger的说明,也已经大概明白了这个项目主要为咱们作了什么。就是构造Swgger所须要的JSON格式的数据!
它并无像Swashbuckle.AspNetCore同样集成了SwaggerUI的内容到项目中去,只是一个提供数据的项目。
其官方的示例Demo是用跳转到petstore.swagger.io方式来完成的。可是常常性是要等待很长时间的,应该是网络的问题。
为了不这一状况,能够经过下面的操做避免:
添加一个用于显示的页面,示例为doc.html,内容能够照搬swagger-ui目录下面的index.html
在Bootstrapper中添加静态资源的引用
protected override void ConfigureConventions(NancyConventions nancyConventions) { base.ConfigureConventions(nancyConventions); nancyConventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("swagger-ui")); }
public class HomeModule : NancyModule { public HomeModule() { Get("/", _ => { return Response.AsRedirect("/swagger-ui"); }); Get("/swagger-ui",_=> { var url = $"{Request.Url.BasePath}/api-docs"; return View["doc", url]; }); } }
@Model
window.onload = function() { // Build a system const ui = SwaggerUIBundle({ url: "@Model", dom_id: '#swagger-ui', presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }) window.ui = ui }
完成上面的内容后,就开始构造咱们的文档了。
这里主要是设置这个API文档的概要信息,好比文档的标题,此api的版本等
须要经过SwaggerMetadataProvider的SetInfo方法来设置这些信息
下面是具体的示例代码,写在Bootstrapper中:
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) { SwaggerMetadataProvider.SetInfo("Nancy Swagger Example", "v1.0", "Some open api", new Contact() { EmailAddress = "catcher_hwq@outlook.com", Name = "Catcher Wong", Url = "http://www.cnblogs.com/catcher1994" }, "http://www.cnblogs.com/catcher1994"); base.ApplicationStartup(container, pipelines); }
此时对应的大体效果(这个时候是不能正常运行的,只是显示了这部分的效果)以下:
上面代码生成的JSON数据是符合规范的,以下所示:
下面要作的就是构造路由相关的信息
先在Module中定义一个简单的路由,这个路由不带任何参数。
Get("/", _ => { var list = new List<Product> { new Product{ Name="p1", Price=199 , IsActive = true }, new Product{ Name="p2", Price=299 , IsActive= true } }; return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), list); }, null, "GetProductList");
而后在MetadataModule中添加相应的描述,这里的MetadataModule与上一篇是类似的,这也是为何我会在上一篇先介绍不使用
第三方组件的来构造的缘由,由于这种写法下面,二者没有本质的区别!
Describe["GetProductList"] = desc => desc.AsSwagger( with => with.Operation( op => op.OperationId("GetProductList") .Tag("Products") .Summary("Get all products") .Response(r=>r.Schema<IEnumerable<Product>>().Description("OK")) .Description("This returns a list of products") ));
下面是部分Nancy.Swagger
里面的核心内容,也是上一篇所没有的特殊之处。
AsSwagger是RouteDescription一个扩展方法,这个方法是返回咱们须要的PathItem。
OperationId是这个路由的一个友好名称,源码里面的字段定义代表它要惟一。对更加详尽的描述可能去看Swagger中对这些参数的说明!
Tag能够理解为这个路由属于那个分组,起分隔符的做用,举个例子,如今有A,B两个模块的API,咱们确定不能把它们交叉排列下去
而是A的放到一个地方,B的一个地方,便于咱们的的区分。
Summary是当前路由的精简描述,要小于120个字符。
Description是当前路由的详细描述。
Response是指望的运行结果的相关内容,能够有多个,这里没有标明状态码,而是直接写处理的内容,此时说明这里用的是默认的状态码。
Response里面又是一个委托,里面又有部分定义:
Schema
Description是这个响应对应的描述信息
这个时候是会出错的,由于咱们在Respoonse的时候指定了Schema,可是咱们并无指定它的定义。
咱们须要先在MetadataModule中引用ISwaggerModelCatalog这个接口并调用它的AddModel方法把相关的类型添加进去,这样才能正常运行!
public ProductsMetadataModule(ISwaggerModelCatalog modelCatalog) { //添加相应的类型 modelCatalog.AddModels(typeof(Product), typeof(IEnumerable<Product>)); Describe["GetProductList"] = desc => desc.AsSwagger( with => with.Operation( op => op.OperationId("GetProductList") .Tag("Products") .Summary("Get all products") //在Schema中使用modelCatalog .Response(r => r.Schema<IEnumerable<Product>>(modelCatalog).Description("OK")) .Description("This returns a list of products") )); }
示例结果以下:
先来看看上面设置对应的内容:
点击Try it out运行的结果
能够看到使用curl 去访问咱们的实际接口拿到服务器的响应信息(结果和头部)
在终端执行一下这个命令,也是这个结果。
一样的,先在Module中定义一个路由,这个路由包含了一个Path参数和一个Query参数
Get("/{productid}", _ => { var productId = _.productid; if (string.IsNullOrWhiteSpace(productId)) { return HttpStatusCode.NotFound; } var isActive = Request.Query.isActive ?? true; var product = new Product { Name = "apple", Price = 100, IsActive = isActive }; return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product); }, null, "GetProductByProductId");
这里做了多一点操做,为的是演示尽量多的用法。若是传递的产品id为空,则直接返回404。若是没有输入isActive这个Query参数
返回Productr的IsActive就为false。
而后在MetadataModule中添加相应的描述
Describe["GetProductByProductId"] = desc => desc.AsSwagger( with => with.Operation( op => op.OperationId("GetProductByProductId") .Tag("Products2") .Summary("Get a product by product's id") .Description("This returns a product's infomation by the special id") .Parameter(new Parameter { Name = "productid", In = ParameterIn.Path,//指明该参数是对应路由上面的同名参数 Required = true,//必填 Description = "id of a product" }) .Parameter(new Parameter { Name = "isactive", In = ParameterIn.Query,//指明该参数是对应QueryString上面的参数 Description = "get the actived product", Required = false//非必填 }) .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product")) .Response(404, r => r.Description("Can't find the product")) ));
这里多了一个Parameter是上面没有提到的,这个就是咱们的请求参数,这里的请求参数包含下面五种:
下面是运行的效果图,分别演示了下面几种状况
固然如今在MetadataModule的参数还有其余的写法
Describe["GetProductByProductId"] = desc => desc.AsSwagger( with => with.Operation( op => op.OperationId("GetProductByProductId") .Tag("Products2") .Summary("Get a product by product's id") .Description("This returns a product's infomation by the special id") .Parameters(new List<Parameter> { new Parameter{Name = "productid",In = ParameterIn.Path,Required = true,Description = "id of a product"}, new Parameter{Name = "isactive",In = ParameterIn.Query,Description = "get the actived product",Required = false} }) .ProduceMimeType("application/json") .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product")) .Response(404, r => r.Description("Can't find the product")) ));
能够用Parameters直接将全部的参数,组合成一个集合来进行处理。
此时的效果和上面是同样的。
在Module中添加一个新增商品的方法,这个方法包含两种请求参数,一种是正常POST的json格式的数据,一种是请求头,对于请求头,只是判断了一下客户端发起的请求有没有包含相应的请求头就是了,并无作严格的判断。同时为了演示多种MIME类型的返回结果,这里兼容了json和xml格式的返回结果。
Post("/", _ => { var product = this.Bind<Product>(); if(!Request.Headers.Any(x=>x.Key=="test")) { return HttpStatusCode.BadRequest; } return Negotiate .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product) .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/xml"), product) ; }, null, "AddProduct");
一样的,MetadataModule中添加以下的描述:
Describe["AddProduct"] = desc => desc.AsSwagger( with => with.Operation( op => op.OperationId("AddProduct") .Tag("Products") .Summary("Add a new product to database") .Description("This returns the added product's infomation") .BodyParameter(para=>para.Name("para").Schema<Product>().Description("the infomation of the adding product").Build())//Request body .Parameter(new Parameter() { Name = "test", In = ParameterIn.Header,//http请求头 Description = "must be not null", Required = true, }) .ConsumeMimeType("application/json") //post的参数只容许是json格式 .ProduceMimeTypes(new List<string>{ "application/json","application/xml" })//结果支持json和xml .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the added product")) .Response(400, r => r.Description("Some errors occur during the processing")) ));
BodyParameter是咱们在POST等操做时用的,它须要指定咱们POST的数据格式(Schema那里的类型),为了演示添加请求头信息,因此这里也加了一个必填的请求头信息。
ConsumeMimeType表示咱们发起请求的数据格式必须是json格式的,固然也能够支持多种不一样的数据格式。
ProduceMimeTypes表示服务端响应时支持的数据格式,这里指定了json和Xml也是为了和咱们Module中的内容相对应。
演示效果:
有时候,API的界限分的不是很清晰或者有交集的时候,可能会出现这样的状况:一个api会属于多个分组。
前面咱们都是直接指定了一个tag,也就表示上面的只是对应一个tag。
先来定义一个方法,用于演示多分组和过期、废弃的API
Head("/",_=> { return HttpStatusCode.OK; },null,"HeadOfProduct");
Metadata内容
Describe["HeadOfProduct"] = desc => desc.AsSwagger( with => with.Operation( op => op.OperationId("HeadOfProduct") .Tags(new List<string>() { "Products", "Products2" })//同时属于两个分组 .Summary("Something is deprecated") .Description("This returns only http header") .IsDeprecated()//过期的,至关于经常使用的Obsolete,可是还能够用 .Response(r => r.Description("Nothing will return but http headers")) ));
效果以下:
虽然说已经标记为过期了,可是本质这个方法仍是存在,因此也是能正常调用的。
Swagger支持3种安全认证折方式:APIKEY、Basic、OAuth2.0,一样的Nancy.Swagger
也支持,不过有点坑就是了。
使用的话有两个步骤(这里用最简单的APIKEY演示):
Step 1: 引用定义,在Bootstrapper中添加验证相关的内容
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) { SwaggerMetadataProvider.SetInfo("Nancy Swagger Example", "v1.0", "Some open api", new Contact() { EmailAddress = "catcher_hwq@outlook.com", Name = "Catcher Wong", Url = "http://www.cnblogs.com/catcher1994", }, "http://www.cnblogs.com/catcher1994"); var securitySchemeBuilder = new ApiKeySecuritySchemeBuilder(); securitySchemeBuilder.Description("Authentication with apikey"); securitySchemeBuilder.IsInQuery(); securitySchemeBuilder.Name("Item1"); SwaggerMetadataProvider.AddSecuritySchemeBuilder(securitySchemeBuilder, "Item1"); base.ApplicationStartup(container, pipelines); }
Step 2 : 在MetadataModule中添加描述
Describe["Head"] = description => description.AsSwagger( with => with.Operation( op => op.OperationId("Head") .Tag("Head method") .SecurityRequirement(SecuritySchemes.ApiKey) .Summary("an example head method") .Response(r => r.Description("OK"))));
固然,目前是没有办法正常运行的!此时运行效果以下:
单独打开/api-docs这个路径时提示以下错误:
这个十有八九是Nancy.Swagger
的安全验证存在bug的,这个项目没有足够多的单元测试可能也是致使问题的一部分缘由。
发现的主要bug是在MetadataModule中使用SecurityRequirement(SecuritySchemes.ApiKey)
时一直在报错,报错内容以下:
Nancy.RequestExecutionException: Oh noes! ---< System.InvalidCastException: Unable to cast object of type 'Swagger.ObjectModel.SecuritySchemes' to type 'System.String'.
at Swagger.ObjectModel.SwaggerModel.SwaggerSerializerStrategy.ToObject(IDictionary source)
因而调试源码,发如今Swagger.ObjectModel项目下的ToObject方法有问题
private static dynamic ToObject(IDictionary source) { var expando = new ExpandoObject(); var expandoCollection = (ICollection<KeyValuePair<string, object>>)expando; foreach (string key in source.Keys) { expandoCollection.Add(new KeyValuePair<string, object>(key, source[key])); } return expando; }
从上面的出错内容也能清楚的看到,SecuritySchemes不能转成string的,其中SecuritySchemes是一个枚举类型。
为了能正常运行,确定要修改验证一下!!因而修改为以下 :
private static dynamic ToObject(IDictionary source) { var expando = new ExpandoObject(); var expandoCollection = (ICollection<KeyValuePair<string, object>>)expando; //用了var,在使用的时候强制ToString一下将其转成string foreach (var key in source.Keys) { expandoCollection.Add(new KeyValuePair<string, object>(key.ToString(), source[key])); } return expando; }
因为在Mac上没法打开这个项目,因此上面的修改是切换回windows完成的。
进行上面的修改后,项目是已经能正常运行了!可是却少了一个很重要的东西!
在这个方法里面加了APIKEY验证的,可是小锁的标记却没有出来!
以后对比了Swagger的官方示例http://petstore.swagger.io/
竟然有这么坑爹的事情!security是一个数组啊,不是一个对象啊~~
后面就修改了Nancy.Swagger里面的许多代码(瞎改的,只为了能正常运行),涉及了好几个类文件,就不一一说明了。
第一个问题已经提了PR到这个项目了,第二个问题还没找到比较满意的方案,暂时没提。
直接上最后的效果图,分别演示了,没有验证,验证成功和验证失败这三种状况!
注:本文只演示了其中Nancy.Swagger的其中一种用法,并且还有部份内容是没有涉及到的。还有两种其余用法有时间会拿出来和你们分享。
在过程当中还有一个须要十分注意的地方(原本这个应该是在上一篇说起的):就是XXModule和XXMetadataModule相对应的位置关系。
Nancy在这里限制的比较死,强制了下面三种状况:
Module所在的位置 | MetadtaModule应该在的位置 |
---|---|
./BlahModule | ./BlahMetadataModule |
./BlahModule | ./Metadata/BlahMetadataModule |
./Modules/BlahModule | ../Metadata/BlahMetadataModule |
这是文件分布所要注意的问题。
还有一个命名应该注意的问题:当咱们对一个Module起名为ProductsModule时,它对应的MetadataModule必定要是ProductsMetadataModule。
而不能是其它,有一次因为粗心,忘记把s字母带上,花了很多时间去找缘由~~
上述两个问题的答案在Nancy.Metadata.Modules
项目的DefaultMetadataModuleConventions类中。
Nancy.Swagger
给咱们API文档化的道路上带来了很多的便利之处,除了安全验证这一块的问题有点坑,其余的算是比较正常,用起来也还算简单。
对于Swagger来讲,通用性很好,只要提供的指定格式的数据就能很好的渲染出让人温馨的界面,或许这就是它这么流行的一个关键点吧。
下面是一张脑图简单的归纳相关的内容 :
本文已同步到Catcher写的Nancy汇总博客:Nancy之大杂烩