4

经过多次尝试和阅读文章,我决定将我的问题放在这里。我想要的是以下内容:我正在研究应用程序的 api 版本。.NET Core(包)支持的版本格式Microsoft.AspNetCore.Mvc.Versioning是 Major.Minor,这就是我想在我从事的项目中使用的格式。我想要的是一个备用版本,以防客户端未指定次要版本。我正在使用 .NET core 2.2,并api-version在标题中指定。相应的 API 版本控制配置如下所示:

    services.AddApiVersioning(options => { 
        options.ReportApiVersions = true;
        options.ApiVersionReader = new HeaderApiVersionReader("api-version");
        options.ErrorResponses = new ApiVersioningErrorResponseProvider();
    });

每个版本我都有以下两个控制器:(为了这个 SO 问题,控制器被简化了):

[ApiVersion("1.0")]  
[Route("api/[controller]")]  
public class ValueControllerV10 : Controller  
{  
    [HttpGet(Name = "collect")]  
    public String Collect()  
    {  
        return "Version 1.0";  
    }  
} 


[ApiVersion("1.1")]  
[Route("api/[controller]")]  
public class ValueControllerV11 : Controller  
{  
    [HttpGet(Name = "collect")]  
    public String Collect()  
    {  
        return "Version 1.1";  
    }  
}  

如果客户端指定api-version=1.0,则使用 ValueControllerV10。当然,如果客户端指定api-version=1.1,则按预期使用 ValueControllerV11。

现在我的问题来了。如果客户端指定api-version=1(因此只有主要版本没有次要版本),则使用 ValueControllerV10。这是因为ApiVersion.Parse("1")等于ApiVersion.Parse("1.0"),如果我没记错的话。但在这种情况下,我想要的是调用给定主要版本的最新版本,在我的示例中为 1.1。

我的尝试:

第一:指定[ApiVersion("1")]ValueControllerV11

    [ApiVersion("1")]  
    [ApiVersion("1.1")]  
    [Route("api/[controller]")]  
    public class ValueControllerV11 : Controller  
    {  
        [HttpGet(Name = "collect")]  
        public String Collect()  
        {  
            return "Version 1.1";  
        }  
    }  

它不起作用,它导致

AmbiguousMatchException: The request matched multiple endpoints

为了解决这个问题,我想出了第二种方法:

第二:使用自定义IActionConstraint。为此,我关注了这些文章:

然后我创建了以下类:

[AttributeUsage(AttributeTargets.Method)]
public class HttpRequestPriority : Attribute, IActionConstraint
{
    public int Order
    {
        get
        {
            return 0;
        }
    }

    public bool Accept(ActionConstraintContext context)
    {
        var requestedApiVersion = context.RouteContext.HttpContext.GetRequestedApiVersion();

        if (requestedApiVersion.MajorVersion.Equals(1) && !requestedApiVersion.MinorVersion.HasValue)
        {
            return true;
        }

        return false;
    }
}

并用于ValueControllerV11

[ApiVersion("1")]  
[ApiVersion("1.1")]  
[Route("api/[controller]")]  
public class ValueControllerV11 : Controller  
{  
    [HttpGet(Name = "collect")]
    [HttpRequestPriority]  
    public String Collect()  
    {  
        return "Version 1.1";  
    }  
}

好吧,它解决了AmbiguousMatchException,但是覆盖了Microsoft.AspNetCore.Mvc.Versioningpackage 的默认行为,所以如果客户端使用api-version 1.1,那么她会得到一个 404 Not Found ,根据实现是可以理解的HttpRequestPriority

第三:有条件地使用MapSpaFallbackRoutein Startup.cs

        app.MapWhen(x => x.GetRequestedApiVersion().Equals("1") && x.GetRequestedApiVersion().MinorVersion == null, builder =>
        {
            builder.UseMvc(routes =>
            {
                routes.MapSpaFallbackRoute(
                    name: "spa-fallback",
                    defaults: new {controller = nameof(ValueControllerV11), action = "Collect"});
            });
        });

        app.UseMvc();

它也不起作用,没有任何影响。这个名字MapSpaFallbackRoute也给我一种感觉,它不是我需要使用的......

所以我的问题是:如果没有指定次要版本,我该如何引入后备“使用最新”行为api-version?提前致谢!

4

3 回答 3

4

这本质上是不支持开箱即用的。浮动版本、范围等与 API 版本控制原则背道而驰。API 版本没有也不能暗示任何向后兼容性。除非您在一个封闭系统中控制双方,否则假设客户可以处理任何合同变更,即使您只添加一个新成员,也是一种谬误。最终,如果客户端要求 1/1.0,那么这就是他们应该得到的,或者服务器应该说它不受支持。

除了我的意见,有些人仍然想要这种行为。这不是特别直接,但您应该能够使用自定义IApiVersionRoutePolicy或自定义端点匹配器来实现您的目标 - 这取决于您使用的路由样式。

如果您仍在使用路由,这可能是最简单的,因为您只需创建一个新策略或通过覆盖OnSingleMatch扩展现有的DefaultApiVersionRoutePolicy并将其注册到您的服务配置中。您会知道这是您正在寻找的场景,因为传入的 API 版本没有次要版本。您是正确的,并且将等同于相同,但是次要版本未合并;因此,将在这种情况下。11.0ApiVersion.MinorVersionnull

如果您使用的是Endpoint Routing,则需要替换ApiVersionMatcherPolicy。以下应该接近您想要实现的目标:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionMapping;

public sealed class MinorApiVersionMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
    public MinorApiVersionMatcherPolicy(
        IOptions<ApiVersioningOptions> options,
        IReportApiVersions reportApiVersions,
        ILoggerFactory loggerFactory )
    {
        DefaultMatcherPolicy = new ApiVersionMatcherPolicy(
            options, 
            reportApiVersions, 
            loggerFactory );
        Order = DefaultMatcherPolicy.Order;
    }

    private ApiVersionMatcherPolicy DefaultMatcherPolicy { get; }

    public override int Order { get; }

    public bool AppliesToEndpoints( IReadOnlyList<Endpoint> endpoints ) =>
        DefaultMatcherPolicy.AppliesToEndpoints( endpoints );

    public async Task ApplyAsync(
        HttpContext httpContext,
        EndpointSelectorContext context,
        CandidateSet candidates )
    {
        var requestedApiVersion = httpContext.GetRequestedApiVersion();
        var highestApiVersion = default( ApiVersion );
        var explicitIndex = -1;
        var implicitIndex = -1;

        // evaluate the default policy
        await DefaultMatcherPolicy.ApplyAsync( httpContext, context, candidates );

        if ( requestedApiVersion.MinorVersion.HasValue )
        {
            // we're done because a minor version was specified
            return;
        }

        var majorVersion = requestedApiVersion.MajorVersion;

        for ( var i = 0; i < candidates.Count; i++ )
        {
            // make all candidates invalid by default
            candidates.SetValidity( i, false );

            var candidate = candidates[i];
            var action = candidate.Endpoint.Metadata?.GetMetadata<ActionDescriptor>();

            if ( action == null )
            {
                continue;
            }

            var model = action.GetApiVersionModel( Explicit | Implicit );
            var maxApiVersion = model.DeclaredApiVersions
                                        .Where( v => v.MajorVersion == majorVersion )
                                        .Max();

            // remember the candidate with the next highest api version
            if ( highestApiVersion == null || maxApiVersion >= highestApiVersion )
            {
                highestApiVersion = maxApiVersion;

                switch ( action.MappingTo( maxApiVersion ) )
                {
                    case Explicit:
                        explicitIndex = i;
                        break;
                    case Implicit:
                        implicitIndex = i;
                        break;
                }
            }
        }

        if ( explicitIndex < 0 && ( explicitIndex = implicitIndex ) < 0 )
        {
            return;
        }

        var feature = httpContext.Features.Get<IApiVersioningFeature>();

        // if there's a match:
        //
        // 1. make the candidate valid
        // 2. clear any existing endpoint (ex: 400 response)
        // 3. set the requested api version to the resolved value
        candidates.SetValidity( explicitIndex, true );
        context.Endpoint = null;
        feature.RequestedApiVersion = highestApiVersion;
    }
}

然后你需要像这样更新你的服务配置:

// IMPORTANT: must be configured after AddApiVersioning
services.Remove( services.Single( s => s.ImplementationType == typeof( ApiVersionMatcherPolicy ) ) );
services.TryAddEnumerable( ServiceDescriptor.Singleton<MatcherPolicy, MinorApiVersionMatcherPolicy>() );

如果我们考虑这样的控制器:

[ApiController]
[ApiVersion( "2.0" )]
[ApiVersion( "2.1" )]
[ApiVersion( "2.2" )]
[Route( "api/values" )]
public class Values2Controller : ControllerBase
{
    [HttpGet]
    public string Get( ApiVersion apiVersion ) =>
        $"Controller = {GetType().Name}\nVersion = {apiVersion}";

    [HttpGet]
    [MapToApiVersion( "2.1" )]
    public string Get2_1( ApiVersion apiVersion ) =>
        $"Controller = {GetType().Name}\nVersion = {apiVersion}";

    [HttpGet]
    [MapToApiVersion( "2.2" )]
    public string Get2_2( ApiVersion apiVersion ) =>
        $"Controller = {GetType().Name}\nVersion = {apiVersion}";
}

当你请求时api/values?api-version=2,你会匹配2.2

我要重申,这通常不是一个好主意,因为客户应该能够依赖稳定版本。如果您想要预发布API(例如:) ,则使用版本中的状态可能更合适。2.0-beta1

我希望这会有所帮助。

于 2019-06-29T00:20:02.190 回答
0

好吧,回答这个问题的功劳归于@Chris Martinez,另一方面,我可以想出另一种方法来解决我的问题:我已经创建了一个扩展RouteAttribute,实现IActionConstraintFactory

public class RouteWithVersionAttribute : RouteAttribute, IActionConstraintFactory
{
    private readonly IActionConstraint _constraint;

    public bool IsReusable => true;

    public RouteWithVersionAttribute(string template, params string[] apiVersions) : base(template)
    {
        Order = -10; //Minus value means that the api-version specific route to be processed before other routes
        _constraint = new ApiVersionHeaderConstraint(apiVersions);
    }

    public IActionConstraint CreateInstance(IServiceProvider services)
    {
        return _constraint;
    }
}

如下IActionContraint所示:

    public class ApiVersionHeaderConstraint : IActionConstraint
{
    private const bool AllowRouteToBeHit = true;
    private const bool NotAllowRouteToBeHit = false;

    private readonly string[] _allowedApiVersions;

    public ApiVersionHeaderConstraint(params string[] allowedApiVersions)
    {
        _allowedApiVersions = allowedApiVersions;
    }

    public int Order => 0;

    public bool Accept(ActionConstraintContext context)
    {
        var requestApiVersion = GetApiVersionFromRequest(context);

        if (_allowedApiVersions.Contains(requestApiVersion))
        {
            return AllowRouteToBeHit;
        }

        return NotAllowRouteToBeHit;
    }

    private static string GetApiVersionFromRequest(ActionConstraintContext context)
    {
        return context.RouteContext.HttpContext.Request.GetTypedHeaders().Headers[CollectApiVersion.HeaderKey];
    }
}

然后我可以ApiVersionAttribute和我的自定义RouteWithVersionAttribute一起使用,如下:

[ApiVersion("1")]
[ApiVersion("1.1")]
[Route("collect", "1", "1.1")]
public class ValueControllerV11 : Controller
{
    [HttpRequestPriority]
    public String Collect()
    {
        return "Version 1.1";
    }
}

干杯!

于 2019-07-03T12:25:15.910 回答
0

CurrentImplementationApiVersionSelector注册服务时的选项呢?见这里:https ://github.com/microsoft/aspnet-api-versioning/wiki/API-Version-Selector

CurrentImplementationApiVersionSelector 选择没有版本状态的最大可用 API 版本。如果未找到匹配项,则回退到配置的 DefaultApiVersion。例如,如果版本“1.0”、“2.0”和“3.0-Alpha”可用,则将选择“2.0”,因为它是最高、已实施或已发布的 API 版本。

services.AddApiVersioning(
    options => options.ApiVersionSelector =
        new CurrentImplementationApiVersionSelector( options ) );
于 2019-10-02T20:07:10.250 回答