api.versioning 版本控制 自动识别最高版本和多Area但同名Contoller问题解决办法

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;        public HomeController (ILogger<HomeController> logger)        {            _logger = logger;        }        public JsonResult GetJson()        {            return Json("Home 1.0");        }}

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;        public HomeController (ILogger<HomeController> logger)        {            _logger = logger;        }        public JsonResult GetJson()        {            return Json("Home 2.0");        }}

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;        public TestController (ILogger<HomeController> logger)        {            _logger = logger;        }        public JsonResult GetJson()        {            return Json("Test 1.0");        }}

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.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;    /// <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();    }}

View Code

其中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

(0)

相关推荐