加载中...
api.versioning 版本控制 自动识别最高版本和多Area但同名Contoller问题解决办法
发表于:2020-06-03 |
字数统计: 5.4k | 阅读时长: 31分钟 |
Microsoft.AspNetCore.Mvc.Versioning //引入程序集

.net core 下面api的版本控制作用不需要多说,可以查阅https://www.cnblogs.com/dc20181010/p/11313738.html

普通的版本控制一般是通过链接、header此类方法进行控制,对ApiVersionReader进行设置,例如

services.AddApiVersioning(o => {
                //o.ReportApiVersions = true;//返回版本可使用的版本
                o.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version"));//通过Header或QueryString进行传值来判断api的版本
//o.DefaultApiVersion
= new ApiVersion(1, 0);//默认版本号
});

或者使用https://www.cnblogs.com/tdfblog/p/asp-net-core-api-versioning.html这种方式

这两种方式都需要传递api的版本信息,如果不传递将会报错

{"error":{"code":"ApiVersionUnspecified","message":"An API version is required, but was not specified.","innerError":null}}

如果我们不想传递api的版本信息时,可以将

o.AssumeDefaultVersionWhenUnspecified = true; //此选项将用于在没有版本的情况下提供请求
o.DefaultApiVersion = new ApiVersion(1, 0); //设置默认Api版本是1.0

打开,这个我们每次请求如果不传递版本信息也不会报错了,但我们的请求将会指向1.0版本,那么我想让默认版本指向我写的api里面的最高版本怎么做?

我们将默认版本修改为最高版本可以吗?

这里将会出现一个问题,我的api版本可能由于各种各样原因造成最高版本不一致的问题

所以我们不能采用指定默认版本是最高版本的方法来解决,这个最高版本还必须要是动态的,通过翻阅https://github.com/microsoft/aspnet-api-versioning/wiki/API-Version-Selector#current-implementation-api-selector可以得知

The CurrentImplementationApiVersionSelector selects the maximum API version available which does not have a version status. 
If no match is found, it falls back to the configured DefaultApiVersion. For example, if the versions "1.0", "2.0", and "3.0-Alpha" are available,
then "2.0" will be selected because it's the highest, implemented or released API version. CurrentImplementationApiVersionSelector选择不具有版本状态的最大可用API版本。 如果找不到匹配项,它将回退到配置的DefaultApiVersion。
例如,如果提供版本“
1.0”,“ 2.0”和“ 3.0-Alpha”,则将选择“ 2.0”,因为它是最高,已实施或已发布的API版本。

services.AddApiVersioning(
    options => options.ApiVersionSelector =
        new CurrentImplementationApiVersionSelector( options ) );

通过这个版本选择器,我们可以将最大版本得出,修改上面services.AddApiVersioning

services.AddApiVersioning(o => {
                o.ReportApiVersions = true;//返回版本可使用的版本
                //o.ApiVersionReader = new UrlSegmentApiVersionReader();
                //o.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version"));
                //o.ApiVersionReader = ApiVersionReader.Combine(new QueryStringApiVersionReader("api-version"));
                o.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"));//版本号以什么形式,什么字段传递
                o.AssumeDefaultVersionWhenUnspecified = true;//此选项将用于在没有版本的情况下提供请求
                o.DefaultApiVersion = new ApiVersion(1, 0);//默认版本号
                o.ApiVersionSelector = new CurrentImplementationApiVersionSelector(o);//默认以当前最高版本进行访问
            });

举个栗子

namespace Default.v1.Controllers
{
    [ApiVersion("1.0")]
    [Route("[controller]/[action]")]
    [ApiController]
    public class HomeController : Controller, IBaseController
    {
        private readonly ILogger<HomeController> _logger;

    </span><span style="color: #0000ff;">public</span> HomeController (ILogger&lt;HomeController&gt;<span style="color: #000000;"> logger)
    {
        _logger </span>=<span style="color: #000000;"> logger;
    }

    </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> JsonResult GetJson()
    {
        </span><span style="color: #0000ff;">return</span> Json(<span style="color: #800000;">"</span><span style="color: #800000;">Home 1.0</span><span style="color: #800000;">"</span><span style="color: #000000;">);
    }

}

Default.v1.Controllers.Home
namespace Default.v2.Controllers
{
    [ApiVersion("2.0")]
    [Route("[controller]/[action]")]
    [ApiController]
    public class HomeController : Controller, IBaseController
    {
        private readonly ILogger<HomeController> _logger;

    </span><span style="color: #0000ff;">public</span> HomeController (ILogger&lt;HomeController&gt;<span style="color: #000000;"> logger)
    {
        _logger </span>=<span style="color: #000000;"> logger;
    }

    </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> JsonResult GetJson()
    {
        </span><span style="color: #0000ff;">return</span> Json(<span style="color: #800000;">"</span><span style="color: #800000;">Home 2.0</span><span style="color: #800000;">"</span><span style="color: #000000;">);
    }

}

Default.v2.Controllers.Home
namespace Default.v1.Controllers
{
    [ApiVersion("1.0")]
    [Route("[controller]/[action]")]
    [ApiController]
    public class TestController : Controller, IBaseController
    {
        private readonly ILogger<HomeController> _logger;

    </span><span style="color: #0000ff;">public</span> TestController (ILogger&lt;HomeController&gt;<span style="color: #000000;"> logger)
    {
        _logger </span>=<span style="color: #000000;"> logger;
    }

    </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> JsonResult GetJson()
    {
        </span><span style="color: #0000ff;">return</span> Json(<span style="color: #800000;">"</span><span style="color: #800000;">Test 1.0</span><span style="color: #800000;">"</span><span style="color: #000000;">);
    }

}

Default.v1.Controllers.Test

 

 

 

 我们在

请求/home/getjson 时返回“Home 2.0”

请求/test/getjson 时返回“Test 1.0”

这样就可以动态的请求最高版本了

 

但是还是会有问题的,比如,在我添加了Area和User区域下的HomeController,且User区域下的HomeController增加了1.0和3.0版本之后,神奇的一幕出现了

我的HomeController进不去了。。。

{"error":{"code":"UnsupportedApiVersion","message":"The HTTP resource that matches the request URI 'https://localhost:44311/home/getjson' is not supported.","innerError":null}}

这个时候去google都查不到原因。。。

查看api-supported-versions,返回的是1.0,2.0,3.0。。。我的api版本控制被污染了3.0版本从哪里来的哪?第一反应是从User区域来的

我现在在User区域下添加一个除了Home和Test以外Name的Controller就可以请求成功,这个让我怀疑到是不是api.versioning本身的问题,首先怀疑的是Controller的Name问题,源码拉取下来,从添加版本控制的地方(services.AddApiVersioning)开始找

 

 

 

最后终于在ApiVersionCollator中找到了蛛丝马迹

///https://github.com/microsoft/aspnet-api-versioning/blob/master/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionCollator.cs

namespace Microsoft.AspNetCore.Mvc.Versioning
{
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;

</span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;summary&gt;</span>
<span style="color: #808080;">///</span><span style="color: #008000;"> Represents an object that collates </span><span style="color: #808080;">&lt;see cref="ApiVersion"&gt;</span><span style="color: #008000;">API versions</span><span style="color: #808080;">&lt;/see&gt;</span><span style="color: #008000;"> per </span><span style="color: #808080;">&lt;see cref="ActionDescriptor"&gt;</span><span style="color: #008000;">action</span><span style="color: #808080;">&lt;/see&gt;</span><span style="color: #008000;">.
</span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/summary&gt;</span>
[CLSCompliant( <span style="color: #0000ff;">false</span><span style="color: #000000;"> )]
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">class</span><span style="color: #000000;"> ApiVersionCollator : IActionDescriptorProvider
{
    </span><span style="color: #0000ff;">readonly</span> IOptions&lt;ApiVersioningOptions&gt;<span style="color: #000000;"> options;

    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;summary&gt;</span>
    <span style="color: #808080;">///</span><span style="color: #008000;"> Initializes a new instance of the </span><span style="color: #808080;">&lt;see cref="ApiVersionCollator"/&gt;</span><span style="color: #008000;"> class.
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/summary&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;param name="options"&gt;</span><span style="color: #008000;">The current </span><span style="color: #808080;">&lt;see cref="ApiVersioningOptions"&gt;</span><span style="color: #008000;">API versioning options</span><span style="color: #808080;">&lt;/see&gt;</span><span style="color: #008000;">.</span><span style="color: #808080;">&lt;/param&gt;</span>
    <span style="color: #0000ff;">public</span> ApiVersionCollator( IOptions&lt;ApiVersioningOptions&gt; options ) =&gt; <span style="color: #0000ff;">this</span>.options =<span style="color: #000000;"> options;

    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;summary&gt;</span>
    <span style="color: #808080;">///</span><span style="color: #008000;"> Gets the API versioning options associated with the collator.
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/summary&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;value&gt;</span><span style="color: #008000;">The current </span><span style="color: #808080;">&lt;see cref="ApiVersioningOptions"&gt;</span><span style="color: #008000;">API versioning options</span><span style="color: #808080;">&lt;/see&gt;</span><span style="color: #008000;">.</span><span style="color: #808080;">&lt;/value&gt;</span>
    <span style="color: #0000ff;">protected</span> ApiVersioningOptions Options =&gt;<span style="color: #000000;"> options.Value;

    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;inheritdoc /&gt;</span>
    <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">int</span> Order { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">protected</span> <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;inheritdoc /&gt;</span>
    <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> OnProvidersExecuted( ActionDescriptorProviderContext context )
    {
        </span><span style="color: #0000ff;">if</span> ( context == <span style="color: #0000ff;">null</span><span style="color: #000000;"> )
        {
            </span><span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> ArgumentNullException( nameof( context ) );
        }

        </span><span style="color: #0000ff;">foreach</span> ( <span style="color: #0000ff;">var</span> actions <span style="color: #0000ff;">in</span><span style="color: #000000;"> GroupActionsByController( context.Results ) )
        {
            </span><span style="color: #0000ff;">var</span> collatedModel =<span style="color: #000000;"> CollateModel( actions );

            </span><span style="color: #0000ff;">foreach</span> ( <span style="color: #0000ff;">var</span> action <span style="color: #0000ff;">in</span><span style="color: #000000;"> actions )
            {
                </span><span style="color: #0000ff;">var</span> model = action.GetProperty&lt;ApiVersionModel&gt;<span style="color: #000000;">();

                </span><span style="color: #0000ff;">if</span> ( model != <span style="color: #0000ff;">null</span> &amp;&amp; !<span style="color: #000000;">model.IsApiVersionNeutral )
                {
                    action.SetProperty( model.Aggregate( collatedModel ) );
                }
            }
        }
    }

    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;inheritdoc /&gt;</span>
    <span style="color: #0000ff;">public</span> <span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> OnProvidersExecuting( ActionDescriptorProviderContext context ) { }

    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;summary&gt;</span>
    <span style="color: #808080;">///</span><span style="color: #008000;"> Resolves and returns the logical controller name for the specified action.
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/summary&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;param name="action"&gt;</span><span style="color: #008000;">The </span><span style="color: #808080;">&lt;see cref="ActionDescriptor"&gt;</span><span style="color: #008000;">action</span><span style="color: #808080;">&lt;/see&gt;</span><span style="color: #008000;"> to get the controller name from.</span><span style="color: #808080;">&lt;/param&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;returns&gt;</span><span style="color: #008000;">The logical name of the associated controller.</span><span style="color: #808080;">&lt;/returns&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;remarks&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;para&gt;</span>
    <span style="color: #808080;">///</span><span style="color: #008000;"> The logical controller name is used to collate actions together and aggregate API versions. The
    </span><span style="color: #808080;">///</span><span style="color: #008000;"> default implementation uses the "controller" route parameter and falls back to the
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;see cref="ControllerActionDescriptor.ControllerName"/&gt;</span><span style="color: #008000;"> property when available.
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/para&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;para&gt;</span>
    <span style="color: #808080;">///</span><span style="color: #008000;"> The default implementation will also trim trailing numbers in the controller name by convention. For example,
    </span><span style="color: #808080;">///</span><span style="color: #008000;"> the type "Values2Controller" will have the controller name "Values2", which will be trimmed to just "Values".
    </span><span style="color: #808080;">///</span><span style="color: #008000;"> This behavior can be changed by using the </span><span style="color: #808080;">&lt;see cref="ControllerNameAttribute"/&gt;</span><span style="color: #008000;"> or overriding the default
    </span><span style="color: #808080;">///</span><span style="color: #008000;"> implementation.
    </span><span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/para&gt;</span>
    <span style="color: #808080;">///</span> <span style="color: #808080;">&lt;/remarks&gt;</span>
    <span style="color: #0000ff;">protected</span> <span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">string</span><span style="color: #000000;"> GetControllerName( ActionDescriptor action )
    {
        </span><span style="color: #0000ff;">if</span> ( action == <span style="color: #0000ff;">null</span><span style="color: #000000;"> )
        {
            </span><span style="color: #0000ff;">throw</span> <span style="color: #0000ff;">new</span><span style="color: #000000;"> ArgumentNullException( nameof( action ) );
        }

        </span><span style="color: #0000ff;">if</span> ( !action.RouteValues.TryGetValue( <span style="color: #800000;">"</span><span style="color: #800000;">controller</span><span style="color: #800000;">"</span>, <span style="color: #0000ff;">out</span> <span style="color: #0000ff;">var</span><span style="color: #000000;"> key ) )
        {
            </span><span style="color: #0000ff;">if</span> ( action <span style="color: #0000ff;">is</span><span style="color: #000000;"> ControllerActionDescriptor controllerAction )
            {
                key </span>=<span style="color: #000000;"> controllerAction.ControllerName;
            }
        }

        </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> TrimTrailingNumbers( key );
    }

    IEnumerable</span>&lt;IEnumerable&lt;ActionDescriptor&gt;&gt; GroupActionsByController( IEnumerable&lt;ActionDescriptor&gt;<span style="color: #000000;"> actions )
    {
        </span><span style="color: #0000ff;">var</span> groups = <span style="color: #0000ff;">new</span> Dictionary&lt;<span style="color: #0000ff;">string</span>, List&lt;ActionDescriptor&gt;&gt;<span style="color: #000000;">( StringComparer.OrdinalIgnoreCase );

        </span><span style="color: #0000ff;">foreach</span> ( <span style="color: #0000ff;">var</span> action <span style="color: #0000ff;">in</span><span style="color: #000000;"> actions )
        {
            </span><span style="color: #0000ff;">var</span> key =<span style="color: #000000;"> GetControllerName( action );

            </span><span style="color: #0000ff;">if</span> ( <span style="color: #0000ff;">string</span><span style="color: #000000;">.IsNullOrEmpty( key ) )
            {
                </span><span style="color: #0000ff;">continue</span><span style="color: #000000;">;
            }

            </span><span style="color: #0000ff;">if</span> ( !groups.TryGetValue( key, <span style="color: #0000ff;">out</span> <span style="color: #0000ff;">var</span><span style="color: #000000;"> values ) )
            {
                groups.Add( key, values </span>= <span style="color: #0000ff;">new</span> List&lt;ActionDescriptor&gt;<span style="color: #000000;">() );
            }

            values.Add( action );
        }

        </span><span style="color: #0000ff;">foreach</span> ( <span style="color: #0000ff;">var</span> value <span style="color: #0000ff;">in</span><span style="color: #000000;"> groups.Values )
        {
            </span><span style="color: #0000ff;">yield</span> <span style="color: #0000ff;">return</span><span style="color: #000000;"> value;
        }
    }

    </span><span style="color: #0000ff;">static</span> <span style="color: #0000ff;">string</span> TrimTrailingNumbers( <span style="color: #0000ff;">string</span>?<span style="color: #000000;"> name )
    {
        </span><span style="color: #0000ff;">if</span> ( <span style="color: #0000ff;">string</span><span style="color: #000000;">.IsNullOrEmpty( name ) )
        {
            </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">string</span><span style="color: #000000;">.Empty;
        }

        </span><span style="color: #0000ff;">var</span> last = name!.Length - <span style="color: #800080;">1</span><span style="color: #000000;">;

        </span><span style="color: #0000ff;">for</span> ( <span style="color: #0000ff;">var</span> i = last; i &gt;= <span style="color: #800080;">0</span>; i--<span style="color: #000000;"> )
        {
            </span><span style="color: #0000ff;">if</span> ( !<span style="color: #0000ff;">char</span><span style="color: #000000;">.IsNumber( name[i] ) )
            {
                </span><span style="color: #0000ff;">if</span> ( i &lt;<span style="color: #000000;"> last )
                {
                    </span><span style="color: #0000ff;">return</span> name.Substring( <span style="color: #800080;">0</span>, i + <span style="color: #800080;">1</span><span style="color: #000000;"> );
                }

                </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> name;
            }
        }

        </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> name;
    }

    </span><span style="color: #0000ff;">static</span> ApiVersionModel CollateModel( IEnumerable&lt;ActionDescriptor&gt; actions ) =&gt; actions.Select( a =&gt;<span style="color: #000000;"> a.GetApiVersionModel() ).Aggregate();
}

}

View Code

 

其中GroupActionsByController将Controller按照Controller的名字进行分组,再看看内部,分组的时候将GetControllerName( action )作为key,那么GetControllerName是干嘛的,

protected virtual string GetControllerName( ActionDescriptor action )
        {
            if ( action == null )
            {
                throw new ArgumentNullException( nameof( action ) );
            }

        </span><span style="color: #0000ff;">if</span> ( !action.RouteValues.TryGetValue( <span style="color: #800000;">"</span><span style="color: #800000;">controller</span><span style="color: #800000;">"</span>, <span style="color: #0000ff;">out</span> <span style="color: #0000ff;">var</span><span style="color: #000000;"> key ) )
        {
            </span><span style="color: #0000ff;">if</span> ( action <span style="color: #0000ff;">is</span><span style="color: #000000;"> ControllerActionDescriptor controllerAction )
            {
                key </span>=<span style="color: #000000;"> controllerAction.ControllerName;
            }
        }

        </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> TrimTrailingNumbers( key );
    }</span></pre>

这个方法原本是没有问题的,但是牵扯到Area的时候就会出问题了。。它将根目录下的HomeController和User.HomeController视为同一类的Controller然后去做版本的属性注入,造成CurrentImplementationApiVersionSelector选择器选不到正确的版本,所以返回了上面的错误,我们将GetControllerName内部修改为

protected virtual string GetControllerName( ActionDescriptor action )
        {
            if ( action == null )
            {
                throw new ArgumentNullException( nameof( action ) );
            }

        </span><span style="color: #0000ff;">if</span> ( !action.RouteValues.TryGetValue( <span style="color: #800000;">"</span><span style="color: #800000;">controller</span><span style="color: #800000;">"</span>, <span style="color: #0000ff;">out</span> <span style="color: #0000ff;">var</span><span style="color: #000000;"> key ) )
        {
            </span><span style="color: #0000ff;">if</span> ( action <span style="color: #0000ff;">is</span><span style="color: #000000;"> ControllerActionDescriptor controllerAction )
            {
                key </span>=<span style="color: #000000;"> controllerAction.ControllerName;
            }
        }

        </span><span style="color: #0000ff;">if</span> ( !action.RouteValues.TryGetValue( <span style="color: #800000;">"</span><span style="color: #800000;">area</span><span style="color: #800000;">"</span>, <span style="color: #0000ff;">out</span> <span style="color: #0000ff;">var</span><span style="color: #000000;"> area ) )
        {
        }

        </span><span style="color: #0000ff;">return</span> TrimTrailingNumbers( area +<span style="color: #000000;"> key );
    }</span></pre>

这样就可以走通了

 

我们有两种解决办法,一个是把源码拉取下来,方法修改掉,项目的依赖项替换为自己修改的Microsoft.AspNetCore.Mvc.Versioning,另一种办法是将services.AddApiVersioning重写。。。请相信我,拉取修改替换依赖比重写services.AddApiVersioning快且简便。。。

issue:https://github.com/microsoft/aspnet-api-versioning/issues/630

上一篇:
.net core3.1 abp学习开始(一)
下一篇:
.netcore 定制化项目开发的思考和实现
本文目录
本文目录