ASP.NET Web Api 2 + Swagger 接口文档多版本控制

ASP.NET Web Api 2 + Swagger 接口文档多版本控制

  • WebApi+Swagger多版本控制
    • github
    • 新建一个WebApi项目
    • 引入Swagger并多版本管理
        • NuGet安装Swashbuckle和Swagger.Net.UI
        • 配置XML文档
        • 新建Swagger辅助类
        • 取消SwaggerConfig.cs中四个代码块的注释
        • WebApiConfig中添加代码
        • 多版本路由注册
        • 注释SwaggerNet.cs中的两行代码
    • 添加接口和访问
        • 添加接口TestController.cs
        • 访问
    • 参考文档

WebApi+Swagger多版本控制

效果展示
ASP.NET Web Api 2 + Swagger 接口文档多版本控制_第1张图片

github

afresh/ASP.NET-Web-Api-Swagger

新建一个WebApi项目

ASP.NET Web Api 2 + Swagger 接口文档多版本控制_第2张图片
ASP.NET Web Api 2 + Swagger 接口文档多版本控制_第3张图片

引入Swagger并多版本管理

NuGet安装Swashbuckle和Swagger.Net.UI

ASP.NET Web Api 2 + Swagger 接口文档多版本控制_第4张图片

配置XML文档

右键点击项目-属性-生成,勾选XML文档文件。
ASP.NET Web Api 2 + Swagger 接口文档多版本控制_第5张图片

新建Swagger辅助类

根目录下新建Swaggers文件夹,并创建5个辅助类。
新建Swagger文件夹

  • ApplyDocumentVendorExtensions.cs
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web.Http.Description;
using Swashbuckle.Swagger;

namespace WebYKT.Web.Swaggers
{
    internal class ApplyDocumentVendorExtensions : IDocumentFilter
    {
        /// 
        /// //swagger版本控制过滤
        /// 
        /// 文档
        /// schema注册
        /// api概览
        public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
        {
            //缓存目标路由api
            IDictionary<string, PathItem> match = new Dictionary<string, PathItem>();
            //取版本
            var version = swaggerDoc.info.version;
            foreach (var path in swaggerDoc.paths)
            {
                //过滤命名空间 按名称空间区分版本
                if (path.Key.Contains(string.Format("/{0}/", version)))
                {
                    //匹配controller descript中的版本信息
                    Regex r = new Regex("/\\w+" + version, RegexOptions.IgnoreCase);
                    string newKey = path.Key;
                    if (r.IsMatch(path.Key))
                    {
                        var routeinfo = r.Match(path.Key).Value;
                        //修正controller别名路由符合RoutePrefix配置的路由 如api/v2/ValuesV2 修正为 api/v2/Values
                        newKey = path.Key.Replace(routeinfo, routeinfo.Replace(version.ToLower(), "")).Replace(
                            routeinfo, routeinfo.Replace(version.ToUpper(), ""));
                    }
                    //保存修正的path
                    match.Add(newKey, path.Value);
                }
            }
            //当前版本的swagger document
            swaggerDoc.paths = match;
        }
    }
}

  • CachingSwaggerProvider.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Swashbuckle.Swagger;

namespace WebYKT.Web.Swaggers
{
    public class CachingSwaggerProvider : ISwaggerProvider
    {
        private static ConcurrentDictionary<string, SwaggerDocument> _cache =
            new ConcurrentDictionary<string, SwaggerDocument>();

        private readonly ISwaggerProvider _swaggerProvider;

        public CachingSwaggerProvider(ISwaggerProvider swaggerProvider)
        {
            _swaggerProvider = swaggerProvider;
        }

        public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
        {
            var cacheKey = String.Format("{0}_{1}", rootUrl, apiVersion);
            SwaggerDocument srcDoc;
            //只读取一次
            if (!_cache.TryGetValue(cacheKey, out srcDoc))
            {
                srcDoc = _swaggerProvider.GetSwagger(rootUrl, apiVersion);
                var patht = new Dictionary<string, PathItem>();
                foreach (var item in srcDoc.paths)
                {
                    var arr = item.Key.Split('/');
                    var i = arr[3].LastIndexOf('.') + 1;
                    if (i != -1)
                    {
                        arr[3] = arr[3].Substring(i);
                    }
                    patht.Add(string.Join("/", arr), item.Value);
                }
                srcDoc.paths = patht;
                HashSet<string> moduleList = new HashSet<string>();
                srcDoc.vendorExtensions = new Dictionary<string, object>
                {
                    {"ControllerDesc", GetControllerDesc(moduleList)},
                    {"AreaDescription", moduleList}
                };
                _cache.TryAdd(cacheKey, srcDoc);
            }
            return srcDoc;
        }

        /// 
        /// 从API文档中读取控制器描述
        /// 
        /// 所有控制器描述
        public static ConcurrentDictionary<string, string> GetControllerDesc(HashSet<string> moduleList)
        {
            string xmlpath = String.Format("{0}/bin/swagger.XML", AppDomain.CurrentDomain.BaseDirectory);
            ConcurrentDictionary<string, string> controllerDescDict = new ConcurrentDictionary<string, string>();
            if (File.Exists(xmlpath))
            {
                XmlDocument xmldoc = new XmlDocument();
                xmldoc.Load(xmlpath);
                int cCount = "Controller".Length;
                foreach (XmlNode node in xmldoc.SelectNodes("//member"))
                {
                    var type = node.Attributes["name"].Value;
                    if (type.StartsWith("T:"))
                    {
                        //控制器
                        var arrPath = type.Split('.');
                        var length = arrPath.Length;
                        var controllerName = arrPath[length - 1];
                        if (controllerName.EndsWith("Controller"))
                        {
                            //模块信息
                            var moduleName = arrPath[length - 2];
                            moduleList.Add(moduleName);
                            //获取控制器注释
                            var summaryNode = node.SelectSingleNode("summary");
                            string key = controllerName.Remove(controllerName.Length - cCount, cCount);
                            if (summaryNode != null && !String.IsNullOrEmpty(summaryNode.InnerText) && !controllerDescDict.ContainsKey(key))
                            {

                                controllerDescDict.TryAdd(key, summaryNode.InnerText.Trim());
                            }
                        }
                    }
                }
            }
            return controllerDescDict;
        }
    }
}

  • SwaggerVersionHelper.cs
using System;
using System.Linq;
using System.Web.Http.Description;

namespace WebApiSwagger.Swaggers
{
    public class SwaggerVersionHelper
    {
        public static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
        {
            var attr = apiDesc.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<VersionedRoute>().FirstOrDefault();
            return attr != null && attr.Version == Convert.ToInt32(targetApiVersion.TrimStart('v'));
        }
    }
}
  • VersionControllerSelector.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Web.Http.Routing;

namespace WebYKT.Web.Swaggers
{
    public class VersionControllerSelector : IHttpControllerSelector
    {
        private const string VersionKey = "version";
        private const string ControllerKey = "controller";
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
        private readonly HashSet<string> _duplicates;

        public VersionControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _duplicates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
        }

        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
            IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (Type t in controllerTypes)
            {
                var segments = t.Namespace?.Split(Type.Delimiter);
                var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
                string version = segments?[segments.Length - 1];
                var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName);
                if (version == "Controllers")
                {
                    key = String.Format(CultureInfo.InvariantCulture, "{0}", controllerName);
                }
                if (dictionary.Keys.Contains(key))
                {
                    _duplicates.Add(key);
                }
                else
                {
                    dictionary[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
                }
            }
            foreach (string s in _duplicates)
            {
                dictionary.Remove(s);
            }
            return dictionary;
        }

        private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T) result;
            }
            return default(T);
        }

        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            IHttpRouteData routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            string version = GetRouteVariable<string>(routeData, VersionKey);
            if (string.IsNullOrEmpty(version))
            {
                version = GetVersionFromHTTPHeaderAndAcceptHeader(request);
            }
            string controllerName = GetRouteVariable<string>(routeData, ControllerKey);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            string key = String.Format(CultureInfo.InvariantCulture, "{0}", controllerName);
            if (!string.IsNullOrEmpty(version))
            {
                key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName);
            }
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(key, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            else if (_duplicates.Contains(key))
            {
                throw new HttpResponseException(
                    request.CreateErrorResponse(HttpStatusCode.InternalServerError,
                        "Multiple controllers were found that match this request."));
            }
            else
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }

        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }

        private string GetVersionFromHTTPHeaderAndAcceptHeader(HttpRequestMessage request)
        {
            if (request.Headers.Contains(VersionKey))
            {
                var versionHeader = request.Headers.GetValues(VersionKey).FirstOrDefault();
                if (versionHeader != null)
                {
                    return versionHeader;
                }
            }
            var acceptHeader = request.Headers.Accept;
            foreach (var mime in acceptHeader)
            {
                if (mime.MediaType == "application/json" || mime.MediaType == "text/html")
                {
                    var version = mime.Parameters
                        .Where(v => v.Name.Equals(VersionKey, StringComparison.OrdinalIgnoreCase))
                        .FirstOrDefault();
                    if (version != null)
                    {
                        return version.Value;
                    }
                    return string.Empty;
                }
            }
            return string.Empty;
        }
    }
}
  • VersionedRoute.cs
using System;

namespace WebApiSwagger.Swaggers
{
    [AttributeUsage(AttributeTargets.All)]
    public class VersionedRoute : Attribute
    {
        public VersionedRoute(string name, int version)
        {
            Name = name;
            Version = version;
        }
        public string Name { get; set; }
        public int Version { get; set; }
    }
}

取消SwaggerConfig.cs中四个代码块的注释

  • c.MultipleApiVersions
                        c.MultipleApiVersions(
                            SwaggerVersionHelper.ResolveVersionSupportByRouteConstraint,
                            (vc) =>
                            {
                                vc.Version("v2", "Swashbuckle Dummy API V2");
                                vc.Version("v1", "Swashbuckle Dummy API V1");
                            });
  • c.IncludeXmlComments
                        c.IncludeXmlComments($"{AppDomain.CurrentDomain.BaseDirectory}/bin/WebApiSwagger.XML");
  • c.DocumentFilter
                        c.DocumentFilter<ApplyDocumentVendorExtensions>();
  • c.CustomProvider
                        c.CustomProvider((defaultProvider) => new CachingSwaggerProvider(defaultProvider));

WebApiConfig中添加代码

            config.Services.Replace(typeof(IHttpControllerSelector), new VersionControllerSelector(config));

多版本路由注册

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/v2/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
            config.Routes.MapHttpRoute(
                name: "DefaultApiV1",
                routeTemplate: "api/v1/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

注释SwaggerNet.cs中的两行代码

//[assembly: WebActivator.PreApplicationStartMethod(typeof(WebApiSwagger.App_Start.SwaggerNet), "PreStart")]
//[assembly: WebActivator.PostApplicationStartMethod(typeof(WebApiSwagger.App_Start.SwaggerNet), "PostStart")]

添加接口和访问

添加接口TestController.cs

添加接口

using System.Web.Http;
using WebApiSwagger.Swagger;

namespace WebApiSwagger.Controllers.v2
{
    /// 
    /// 测试
    /// 
    [VersionedRoute("api/version", 2)]
    public class TestController : ApiController
    {
        /// 
        /// 
        /// 
        /// 
        [HttpGet]
        public string Test()
        {
            return "测试";
        }
    }
}

访问

http://localhost:11124/swagger/ui/index

参考文档

你可能感兴趣的:(C#)