美文网首页.Netcore
Asp.Net Core WebApi使用Swagger结合Ve

Asp.Net Core WebApi使用Swagger结合Ve

作者: Weidaicheng | 来源:发表于2020-06-07 16:50 被阅读0次

    最近刚开一个新项目,以asp.net core webapi为app提供接口后台,当然就需要添加版本控制。同时,为了更好的生成文档,也要添加swagger,但是在结合这两者的过程中遇到了一些问题,在此记录下来以便后期查找,也希望能帮助到遇到同类问题的同学。
    废话不多说,让我们开始。

    1、新建webapi项目

    此步跳过,我是用的是3.1的SDK。

    2、添加版本控制

    添加版本控制包

    dotnet add package Microsoft.AspNetCore.Mvc.Versioning
    

    修改 StartupConfigureServices 添加如下代码:

    services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = false;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    });
    

    该文使用的版本控制参数为url链接的方式,具体关于Versioning包的使用方法不多赘述,可以 查看这篇文章,或者官网
    然后在项目中生成的 WeatherForecastController 修改控制器路由为 [Route("api/v{version:apiVersion}/[controller]/[action]")],并添加两个如下方法:

    // route: /api/v1.0/WeatherForecast/Hello
    [HttpGet]
    [ApiVersion("1.0")]
    public string Hello()
    {
        return "Hello world from Hello!";
    }
    
    // route: /api/v1.1/WeatherForecast/Hello2
    [HttpGet]
    [ApiVersion("1.1")]
    public string Hello2()
    {
        return "Hello world from Hello2!";
    }
    

    3、添加Swagger

    dotnet add package Swashbuckle.AspNetCore
    

    使用官网提供的quick start方式添加如下代码:

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
    });
    
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });
    

    具体添加位置可以查看上方链接,因为不是最终版本,故不做赘述。
    运行程序后浏览器访问https://localhost:5001/swagger返回如图:

    swagger 1.png
    输入版本号后可以正常访问。

    4、自动替换版本号

    但是每次调试都需要输入版本号就很麻烦,有没有什么方法不输入呢?
    答案是肯定的。
    首先添加如下两个类:

    public class RemoveVersionFromParameter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            var versionParameter = operation.Parameters.Single(p => p.Name == "version");
            operation.Parameters.Remove(versionParameter);
        }
    }
    
    public class ReplaceVersionWithExactValueInPath : IDocumentFilter
    {
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            var newPaths = new OpenApiPaths();
            foreach(var item in swaggerDoc.Paths)
            {
                var arr = item.Key.Split('/');
                var controller = arr[arr.Length - 2];
                var action = arr[arr.Length - 1];
                var version = typeof(Program).Assembly
                        .GetTypes()
                        .FirstOrDefault(x => typeof(ControllerBase).IsAssignableFrom(x) &&
                            x.Name == $"{controller}Controller")
                        .GetMethods()
                        .FirstOrDefault(x => x.IsPublic && x.Name == action)
                        .GetCustomAttribute<ApiVersionAttribute>().Versions.First().ToString();
                        
                newPaths.Add(item.Key.Replace("v{version}", $"v{version}"), item.Value);
            }
            swaggerDoc.Paths = newPaths;
        }
    }
    

    然后修改 StartupAddSwaggerGen 添加如下代码:

    c.OperationFilter<RemoveVersionFromParameter>();
    c.DocumentFilter<ReplaceVersionWithExactValueInPath>();
    

    运行程序后浏览器访问https://localhost:5001/swagger返回如图:

    swagger 2.png
    已经不需要在手动输入版本号。

    5、按版本分类

    让我们把话题再深入一些,上述两个api分别为1.0和1.1版本,但是出现在了一起,有没有办法分开显示呢?
    首先添加如下Model类来缓存反射的数据:

    public class ReflectionCache
    {
        public IEnumerable<Type> AllControllers { get; set; }
    
        public IEnumerable<string> AllApiVersions { get; set; }
    }
    

    并在 Startup 类中添加如下属性:

    public ReflectionCache ReflectionCache { get; set; }
    

    并向构造方法中添加如下代码:

    ReflectionCache = new ReflectionCache();
    ReflectionCache.AllControllers = typeof(Program).Assembly
                    .GetTypes()
                    .Where(x => typeof(ControllerBase).IsAssignableFrom(x));
    ReflectionCache.AllApiVersions = ReflectionCache.AllControllers.SelectMany(x => x.GetMethods()
                    .Where(x => x.IsPublic && x.GetCustomAttribute<ApiVersionAttribute>() != null)
                    .SelectMany(x => x.GetCustomAttribute<ApiVersionAttribute>().Versions))
                    .GroupBy(x => x.ToString())
                    .Select(x => x.Key);
    

    并将其以 Singleton 方式注入:

    services.AddSingleton<ReflectionCache>(provider => ReflectionCache);
    

    然后修改调用 AddSwaggerGen 方法如下:

    c.OperationFilter<RemoveVersionFromParameter>();
    c.DocumentFilter<ReplaceVersionWithExactValueInPath>();
    
    foreach (var version in ReflectionCache.AllApiVersions)
    {
        c.SwaggerDoc($"v{version}", new OpenApiInfo() { Title = "My API", Version = $"v{version}" });
    }
    

    以及 UseSwaggerUI 修改如下:

    foreach (var version in ReflectionCache.AllApiVersions)
    {
        c.SwaggerEndpoint($"/swagger/v{version}/swagger.json", $"My API V{version}");
    }
    

    最后一步,修改 ReplaceVersionWithExactValueInPath 类中的 Apply 方法中 newPaths.Add 部分如下:

    if (swaggerDoc.Info.Version == $"v{version}")
    {
        newPaths.Add(item.Key.Replace("v{version}", $"v{version}"), item.Value);
    }
    

    运行程序后浏览器访问https://localhost:5001/swagger返回如图:

    swagger by version.png
    选择对应版本,将只展示对应版本的api。
    swagger v1.0.png
    swagger v1.1.png

    6、出现问题了

    上述结果并不是我们想要的结果,我们想要的路由应该是如下方式:

    /api/v1.0/WeatherForecast/Hello
    /api/v1.1/WeatherForecast/Hello
    

    Action应是一样大,不同的是版本号。
    我们修改 Hello2 方法如下:

    // route: /api/v1.1/WeatherForecast/Hello
    [HttpGet]
    [ApiVersion("1.1")]
    [ActionName("Hello")]
    public string Hello2()
    {
        return "Hello world from Hello2!";
    }
    

    运行程序后使用postman或其他工具GET请求如下两个链接:
    http://localhost:5000/api/v1.0/WeatherForecast/Hello

    response from Hello v1.0.png

    http://localhost:5000/api/v1.1/WeatherForecast/Hello

    response from Hello v1.1.png
    返回结果正如期待。
    然后让我们再次浏览器访问https://localhost:5001/swagger返回如图:
    swagger 2.png
    很明显,报错了,让我们看一下调试控制台的输出
    error.png
    错误提示说有两个方法的action是一样的,导致生成swagger.json的时候冲突了,细心的同学应该发现此时错误的原因是因为我们给 Hello2 方法加了一个显式ActionName:Hello。

    7、解决方法

    首先来理一下解决思路:
    1)添加一个自定义的ActionName特性,对于需要使用其他Action名称的方法打上这个特性并提供Action名称。
    2)生成swagger.json时,以版本号+自定义的ActionName(如果有,否在就用方法名)作为key值。
    3)真正进行Http访问时,添加管道方法将请求转到真正的Action上。
    开工!
    首先添加一个新类:

    public class ActionNameAttribute : Attribute
    {
        public string Name { get; set; }
    
        public ActionNameAttribute(string name)
        {
            Name = name;
        }
    }
    

    并将控制器中的ActionName特性显式强制改为使用上述添加的类,而不是 Microsoft.AspNetCore.Mvc 中的。
    修改 ReplaceVersionWithExactValueInPath.Apply 方法如下:

    var newPaths = new OpenApiPaths();
    foreach (var item in swaggerDoc.Paths)
    {
        var arr = item.Key.Split('/');
        // route as /api/[controller]/[action] mode
        if (_reflectionCache.AllControllers.Any(x => x.Name == $"{arr[arr.Length - 2]}Controller"))
        {
            var methods = _reflectionCache.AllControllers.FirstOrDefault(x => x.Name == $"{arr[arr.Length - 2]}Controller")
                            .GetMethods();
            var action = arr[arr.Length - 1];
    
            var version = "v" + methods
                .FirstOrDefault(x => x.Name == action &&
                    x.IsPublic &&
                    x.GetCustomAttribute<ApiVersionAttribute>() != null)
                .GetCustomAttribute<ApiVersionAttribute>()?.Versions
                .FirstOrDefault()
                .ToString();
            var settedAction = methods
                .FirstOrDefault(x => x.Name == action &&
                    x.IsPublic &&
                    x.GetCustomAttribute<ApiVersionAttribute>() != null)
                .GetCustomAttribute<ActionNameAttribute>()?.Name;
            action = settedAction ?? action;
    
            if (swaggerDoc.Info.Version == version)
            {
                newPaths.Add($"/api/{version}/{arr[arr.Length - 2]}/{action}", item.Value);
            }
        }
    }
    
    swaggerDoc.Paths = newPaths;
    

    上述代码逻辑如下:
    首先通过生成的path中的控制器名称以及action名字通过反射找到对应的action,然后检查有没有手动设置Action名称,如果有使用设置的action替换掉原来的。
    然后修改 Configure 方法签名,添加注入

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ReflectionCache reflectionCache)
    

    并添加一个中间件

    app.Use(async (context, next) =>
    {
            // Do work that doesn't write to the Response.
            if (context.Request.Path.HasValue &&
                context.Request.Path.Value.StartsWith("/api/"))
            {
                // arr as this:
                // api, version, controller, action
                var arr = context.Request.Path.Value.Split("/")
                    .Where(x => !string.IsNullOrEmpty(x))
                        .ToArray();
                var version = arr[1];
                var controller = arr[2];
                var action = arr[3];
    
                // trying to get all actions with this name
                var realAction = reflectionCache.AllControllers.FirstOrDefault(x => x.Name == $"{controller}Controller")
                    .GetMethods()
                    .Where(x => x.IsPublic &&
                        x.GetCustomAttribute<ApiVersionAttribute>() != null &&
                        Convert.ToDouble(x.GetCustomAttribute<ApiVersionAttribute>().Versions.FirstOrDefault()?.ToString()) <= Convert.ToDouble(version.TrimStart('v')) &&
                        (x.Name == action || x.GetCustomAttribute<ActionNameAttribute>()?.Name == action))
                    .OrderByDescending(x => x.GetCustomAttribute<ApiVersionAttribute>().Versions.FirstOrDefault()?.ToString())
                    .First();
                var realVersion = $"{realAction.GetCustomAttribute<ApiVersionAttribute>().Versions.FirstOrDefault()?.ToString()}";
    
                if (realAction != null)
                {
                    context.Request.Path = new Microsoft.AspNetCore.Http.PathString($"/api/v{realVersion}/{controller}/{realAction.Name}");
                }
            }
    
            // Do logging or other work that doesn't write to the Response.
            await next.Invoke();
    });
    

    其中,在 <= 判断版本号的地方实现了如果访问的方法没有该版本,将使用现在有的最大的版本,比如访问 /api/v1.2/WeatherForecast/Hello 将会访问到 /api/v1.1/WeatherForecast/Hello
    当然,如果需要判断当前使用的版本号系统中是否存在,如果不存在进行错误提示等,可以添加如下代码:

    if(!reflectionCache.AllApiVersions.Contains(version.TrimStart('v')))
    {
        // 自定义错误处理
    }
    

    好了, 现在让我们启动程序在浏览器访问https://localhost:5001/swagger返回如图:

    swagger v1.0 try it out.png
    swagger v1.1 try it out.png
    结果正如预期。

    相关文章

      网友评论

        本文标题:Asp.Net Core WebApi使用Swagger结合Ve

        本文链接:https://www.haomeiwen.com/subject/kjrctktx.html