3

我遇到了 API 版本控制 (ASP.NET Core API 2.1) 的问题。我试图取代现有控制器的一种方法,而不必复制以前版本中的所有方法。我认为这会起作用,但它给我带来了路由冲突的问题。例子:

namespace MyApi.Controllers
{
    [Produces("application/json")]
    [Route("api/v{version:apiVersion}")]
    public class BaseController : Controller
    {
        public string VersionNumber => GetRouteValue<string>(ControllerContext, "version");

        protected static TValue GetRouteValue<TValue>(ControllerContext context, string name)
        {
            return (TValue)Convert.ChangeType(context.RouteData.Values[name], typeof(TValue));
        }
    }
}

namespace MyApi.Controllers
{
    [ApiVersion("1.0")]
    [ApiVersion("2.0")]
    public class ValuesController : BaseController
    {
        [HttpGet("values", Name = "GetValuesV1.0")]
        public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2" });

        [HttpGet("values/{value}", Name = "GetValueV1.0")]
        public IActionResult GetValue(string value) => Ok( value });
    }
}

namespace MyApi.Controllers.V2_0
{
    [ApiVersion("2.0")]
    public class ValuesController : BaseController
    {
        [HttpGet("values", Name = "GetValuesV2.0")]
        public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2", "value 3" });
    }
}

然后我得到错误:

路径 'xxx' 上的方法 'Get' 被多次注册。

我想在两个版本中都支持 GetValue(string value) 方法,但我不想在每次版本时都复制新控制器中的代码。我只想替换一种方法。这是可能的,还是我必须复制整个以前的控制器和其中的每个方法?这有效,但感觉很糟糕:

namespace MyApi.Controllers
{
    [Produces("application/json")]
    [Route("api/v{version:apiVersion}")]
    public class BaseController : Controller
    {
        public string VersionNumber => GetRouteValue<string>(ControllerContext, "version");

        protected static TValue GetRouteValue<TValue>(ControllerContext context, string name)
        {
            return (TValue)Convert.ChangeType(context.RouteData.Values[name], typeof(TValue));
        }
    }
}

namespace MyApi.Controllers
{
    [ApiVersion("1.0")]
    public class ValuesController : BaseController
    {
        [HttpGet("values", Name = "GetValuesV1.0")]
        public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2" });

        [HttpGet("values/{value}", Name = "GetValueV1.0")]
        public IActionResult GetValue(string value) => Ok( value });
    }
}

namespace MyApi.Controllers.V2_0
{
    [ApiVersion("2.0")]
    public class ValuesController : BaseController
    {
        [HttpGet("values", Name = "GetValuesV2.0")]
        public IActionResult GetValues() => Ok(new string[] { "value 1", "value 2", "value 3" });

        [HttpGet("values/{value}", Name = "GetValueV2.0")]
        public IActionResult GetValue(string value) => Ok( value });
    }
}

这现在有效,但我只是无缘无故地复制了代码。在有大量代码的控制器中,这感觉就像代码异味。有解决方法吗?

4

1 回答 1

2

继承是一件棘手的事情,可以说与 REST 或没有继承概念的 Web API 不一致。然而,斗争是真实的,你真正想要实现的是保持实施DRY - 完全公平。您没有尝试在示例中继承API 版本,但我已经看到很多次了。我不推荐它。

要了解为什么您的第一次尝试无效而您的第二次尝试无效,您需要考虑 URL(例如identifiers)。问题不在于 REST,而在于像 ASP.NET Core 这样的框架如何将 HTTP 请求映射到代码。失败是有道理的,因为 2 个不同的实现报告它们处理相同的请求。从调度的角度来看,这是模棱两可的。

在我看来,控制器(以及一般的 Web 服务)不应该包含业务逻辑。API 充当 HTTP 外观,通过网络表示您的业务逻辑。控制器中的任何逻辑都应限制在 HTTP 的上下文中。这可以通过提供通用功能(但不是 API)的扩展方法或基类进一步概括。如果您的业务逻辑被抽象为一个 - 因为缺乏更好的术语 -业务层,那么重复是最小的。每个新版本都是旧版本的复制和粘贴,只需与 API 本身相关的少量更改。我已经看到创建新版本的复制/粘贴/替换方法很有效。

另一个选择是在控制器中交错版本。这可能很糟糕,但如果您在版本之间几乎没有变化,这是一个可行的折衷方案。根据您对更改旧代码的关注程度,如果您发现事情变得站不住脚,您可以随时将事情分开。一些服务作者坚持不更改旧版本的代码,这是可以理解的,但我觉得通过良好的测试覆盖率可以缓解。使用客户端驱动的合同是另一种很好的验证方法。

最后要考虑的,也是我认为很大程度上被忽视的,是你的版本控制策略。即使没有正式定义它,大多数服务作者在他们的脑海中也会有N-1N-2的策略。如果您有一个明确定义的策略,那将是创建新版本时有多少行李重复代码的指标。例如,如果您的策略是N-2,那么某些控制器级别的位被复制多达 3 次真的很糟糕吗?虽然我们理想情况下不希望重复,但在我们概括了所有其他我们可以做的事情之后尝试重构最后一点可能是不值得的。

我希望你觉得这很有见地。我很乐意进一步详细说明。

于 2020-04-28T23:50:03.117 回答