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<HomeController><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;">); }
}
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<HomeController><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;">); }
}
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<HomeController><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;">); }
}
我们在
请求/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.csnamespace 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;"><summary></span> <span style="color: #808080;">///</span><span style="color: #008000;"> Represents an object that collates </span><span style="color: #808080;"><see cref="ApiVersion"></span><span style="color: #008000;">API versions</span><span style="color: #808080;"></see></span><span style="color: #008000;"> per </span><span style="color: #808080;"><see cref="ActionDescriptor"></span><span style="color: #008000;">action</span><span style="color: #808080;"></see></span><span style="color: #008000;">. </span><span style="color: #808080;">///</span> <span style="color: #808080;"></summary></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<ApiVersioningOptions><span style="color: #000000;"> options; </span><span style="color: #808080;">///</span> <span style="color: #808080;"><summary></span> <span style="color: #808080;">///</span><span style="color: #008000;"> Initializes a new instance of the </span><span style="color: #808080;"><see cref="ApiVersionCollator"/></span><span style="color: #008000;"> class. </span><span style="color: #808080;">///</span> <span style="color: #808080;"></summary></span> <span style="color: #808080;">///</span> <span style="color: #808080;"><param name="options"></span><span style="color: #008000;">The current </span><span style="color: #808080;"><see cref="ApiVersioningOptions"></span><span style="color: #008000;">API versioning options</span><span style="color: #808080;"></see></span><span style="color: #008000;">.</span><span style="color: #808080;"></param></span> <span style="color: #0000ff;">public</span> ApiVersionCollator( IOptions<ApiVersioningOptions> options ) => <span style="color: #0000ff;">this</span>.options =<span style="color: #000000;"> options; </span><span style="color: #808080;">///</span> <span style="color: #808080;"><summary></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;"></summary></span> <span style="color: #808080;">///</span> <span style="color: #808080;"><value></span><span style="color: #008000;">The current </span><span style="color: #808080;"><see cref="ApiVersioningOptions"></span><span style="color: #008000;">API versioning options</span><span style="color: #808080;"></see></span><span style="color: #008000;">.</span><span style="color: #808080;"></value></span> <span style="color: #0000ff;">protected</span> ApiVersioningOptions Options =><span style="color: #000000;"> options.Value; </span><span style="color: #808080;">///</span> <span style="color: #808080;"><inheritdoc /></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;"><inheritdoc /></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<ApiVersionModel><span style="color: #000000;">(); </span><span style="color: #0000ff;">if</span> ( model != <span style="color: #0000ff;">null</span> && !<span style="color: #000000;">model.IsApiVersionNeutral ) { action.SetProperty( model.Aggregate( collatedModel ) ); } } } } </span><span style="color: #808080;">///</span> <span style="color: #808080;"><inheritdoc /></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;"><summary></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;"></summary></span> <span style="color: #808080;">///</span> <span style="color: #808080;"><param name="action"></span><span style="color: #008000;">The </span><span style="color: #808080;"><see cref="ActionDescriptor"></span><span style="color: #008000;">action</span><span style="color: #808080;"></see></span><span style="color: #008000;"> to get the controller name from.</span><span style="color: #808080;"></param></span> <span style="color: #808080;">///</span> <span style="color: #808080;"><returns></span><span style="color: #008000;">The logical name of the associated controller.</span><span style="color: #808080;"></returns></span> <span style="color: #808080;">///</span> <span style="color: #808080;"><remarks></span> <span style="color: #808080;">///</span> <span style="color: #808080;"><para></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;"><see cref="ControllerActionDescriptor.ControllerName"/></span><span style="color: #008000;"> property when available. </span><span style="color: #808080;">///</span> <span style="color: #808080;"></para></span> <span style="color: #808080;">///</span> <span style="color: #808080;"><para></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;"><see cref="ControllerNameAttribute"/></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;"></para></span> <span style="color: #808080;">///</span> <span style="color: #808080;"></remarks></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><IEnumerable<ActionDescriptor>> GroupActionsByController( IEnumerable<ActionDescriptor><span style="color: #000000;"> actions ) { </span><span style="color: #0000ff;">var</span> groups = <span style="color: #0000ff;">new</span> Dictionary<<span style="color: #0000ff;">string</span>, List<ActionDescriptor>><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<ActionDescriptor><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 >= <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 <<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<ActionDescriptor> actions ) => actions.Select( a =><span style="color: #000000;"> a.GetApiVersionModel() ).Aggregate(); }
}
其中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