Microsoft.AspNetCore.Mvc.Versioning //引入程序集
.net core 下面api的版本控制做用不須要多說,能夠查閱http://www.javashuo.com/article/p-kcbbyayk-bo.htmlhtml
普通的版本控制通常是經過連接、header此類方法進行控制,對ApiVersionReader進行設置,例如git
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這種方式github
這兩種方式都須要傳遞api的版本信息,若是不傳遞將會報錯json
{"error":{"code":"ApiVersionUnspecified","message":"An API version is required, but was not specified.","innerError":null}}
若是咱們不想傳遞api的版本信息時,能夠將api
o.AssumeDefaultVersionWhenUnspecified = true; //此選項將用於在沒有版本的狀況下提供請求
o.DefaultApiVersion = new ApiVersion(1, 0); //設置默認Api版本是1.0
打開,這個咱們每次請求若是不傳遞版本信息也不會報錯了,但咱們的請求將會指向1.0版本,那麼我想讓默認版本指向我寫的api裏面的最高版本怎麼作?ide
咱們將默認版本修改成最高版本能夠嗎?ui
這裏將會出現一個問題,個人api版本可能因爲各類各樣緣由形成最高版本不一致的問題this
因此咱們不能採用指定默認版本是最高版本的方法來解決,這個最高版本還必需要是動態的,經過翻閱https://github.com/microsoft/aspnet-api-versioning/wiki/API-Version-Selector#current-implementation-api-selector能夠得知google
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.AddApiVersioningspa
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; public HomeController (ILogger<HomeController> logger) { _logger = logger; } public JsonResult GetJson() { return Json("Home 1.0"); } }
namespace Default.v2.Controllers { [ApiVersion("2.0")] [Route("[controller]/[action]")] [ApiController] public class HomeController : Controller, IBaseController { private readonly ILogger<HomeController> _logger; public HomeController (ILogger<HomeController> logger) { _logger = logger; } public JsonResult GetJson() { return Json("Home 2.0"); } }
namespace Default.v1.Controllers { [ApiVersion("1.0")] [Route("[controller]/[action]")] [ApiController] public class TestController : Controller, IBaseController { private readonly ILogger<HomeController> _logger; public TestController (ILogger<HomeController> logger) { _logger = logger; } public JsonResult GetJson() { return Json("Test 1.0"); } }
咱們在
請求/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; /// <summary> /// Represents an object that collates <see cref="ApiVersion">API versions</see> per <see cref="ActionDescriptor">action</see>. /// </summary> [CLSCompliant( false )] public class ApiVersionCollator : IActionDescriptorProvider { readonly IOptions<ApiVersioningOptions> options; /// <summary> /// Initializes a new instance of the <see cref="ApiVersionCollator"/> class. /// </summary> /// <param name="options">The current <see cref="ApiVersioningOptions">API versioning options</see>.</param> public ApiVersionCollator( IOptions<ApiVersioningOptions> options ) => this.options = options; /// <summary> /// Gets the API versioning options associated with the collator. /// </summary> /// <value>The current <see cref="ApiVersioningOptions">API versioning options</see>.</value> protected ApiVersioningOptions Options => options.Value; /// <inheritdoc /> public int Order { get; protected set; } /// <inheritdoc /> public virtual void OnProvidersExecuted( ActionDescriptorProviderContext context ) { if ( context == null ) { throw new ArgumentNullException( nameof( context ) ); } foreach ( var actions in GroupActionsByController( context.Results ) ) { var collatedModel = CollateModel( actions ); foreach ( var action in actions ) { var model = action.GetProperty<ApiVersionModel>(); if ( model != null && !model.IsApiVersionNeutral ) { action.SetProperty( model.Aggregate( collatedModel ) ); } } } } /// <inheritdoc /> public virtual void OnProvidersExecuting( ActionDescriptorProviderContext context ) { } /// <summary> /// Resolves and returns the logical controller name for the specified action. /// </summary> /// <param name="action">The <see cref="ActionDescriptor">action</see> to get the controller name from.</param> /// <returns>The logical name of the associated controller.</returns> /// <remarks> /// <para> /// The logical controller name is used to collate actions together and aggregate API versions. The /// default implementation uses the "controller" route parameter and falls back to the /// <see cref="ControllerActionDescriptor.ControllerName"/> property when available. /// </para> /// <para> /// The default implementation will also trim trailing numbers in the controller name by convention. For example, /// the type "Values2Controller" will have the controller name "Values2", which will be trimmed to just "Values". /// This behavior can be changed by using the <see cref="ControllerNameAttribute"/> or overriding the default /// implementation. /// </para> /// </remarks> protected virtual string GetControllerName( ActionDescriptor action ) { if ( action == null ) { throw new ArgumentNullException( nameof( action ) ); } if ( !action.RouteValues.TryGetValue( "controller", out var key ) ) { if ( action is ControllerActionDescriptor controllerAction ) { key = controllerAction.ControllerName; } } return TrimTrailingNumbers( key ); } IEnumerable<IEnumerable<ActionDescriptor>> GroupActionsByController( IEnumerable<ActionDescriptor> actions ) { var groups = new Dictionary<string, List<ActionDescriptor>>( StringComparer.OrdinalIgnoreCase ); foreach ( var action in actions ) { var key = GetControllerName( action ); if ( string.IsNullOrEmpty( key ) ) { continue; } if ( !groups.TryGetValue( key, out var values ) ) { groups.Add( key, values = new List<ActionDescriptor>() ); } values.Add( action ); } foreach ( var value in groups.Values ) { yield return value; } } static string TrimTrailingNumbers( string? name ) { if ( string.IsNullOrEmpty( name ) ) { return string.Empty; } var last = name!.Length - 1; for ( var i = last; i >= 0; i-- ) { if ( !char.IsNumber( name[i] ) ) { if ( i < last ) { return name.Substring( 0, i + 1 ); } return name; } } return name; } static ApiVersionModel CollateModel( IEnumerable<ActionDescriptor> actions ) => actions.Select( a => a.GetApiVersionModel() ).Aggregate(); } }
其中GroupActionsByController將Controller按照Controller的名字進行分組,再看看內部,分組的時候將GetControllerName( action )做爲key,那麼GetControllerName是幹嗎的,
protected virtual string GetControllerName( ActionDescriptor action ) { if ( action == null ) { throw new ArgumentNullException( nameof( action ) ); } if ( !action.RouteValues.TryGetValue( "controller", out var key ) ) { if ( action is ControllerActionDescriptor controllerAction ) { key = controllerAction.ControllerName; } } return TrimTrailingNumbers( key ); }
這個方法本來是沒有問題的,可是牽扯到Area的時候就會出問題了。。它將根目錄下的HomeController和User.HomeController視爲同一類的Controller而後去作版本的屬性注入,形成CurrentImplementationApiVersionSelector選擇器選不到正確的版本,因此返回了上面的錯誤,咱們將GetControllerName內部修改成
protected virtual string GetControllerName( ActionDescriptor action ) { if ( action == null ) { throw new ArgumentNullException( nameof( action ) ); } if ( !action.RouteValues.TryGetValue( "controller", out var key ) ) { if ( action is ControllerActionDescriptor controllerAction ) { key = controllerAction.ControllerName; } } if ( !action.RouteValues.TryGetValue( "area", out var area ) ) { } return TrimTrailingNumbers( area + key ); }
這樣就能夠走通了
咱們有兩種解決辦法,一個是把源碼拉取下來,方法修改掉,項目的依賴項替換爲本身修改的Microsoft.AspNetCore.Mvc.Versioning,另外一種辦法是將services.AddApiVersioning重寫。。。請相信我,拉取修改替換依賴比重寫services.AddApiVersioning快且簡便。。。
issue:https://github.com/microsoft/aspnet-api-versioning/issues/630