【问题标题】:Conflicting routes in ASP.NET Core when using Attribute based routing使用基于属性的路由时 ASP.NET Core 中的路由冲突
【发布时间】:2017-01-16 22:26:58
【问题描述】:

上下文

我正在尝试构建一个 ASP.NET Core Web API 控制器,它将公开以下具有特定语义的方法:

/api/experimental/cars — 获取整个集合

[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> AllCars()

/api/experimental/cars/123 — 通过 id "123" 获取汽车

[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)

/api/experimental/cars?nameFilter=Maz — 获取与 nameFilter = "Maz" 匹配的汽车

[HttpGet("/api/experimental/cars/{nameFilter?}")]
public Task<List<Car>> CarsByNameFilter([FromQuery] string nameFilter = "")

/api/experimental/cars?nameFilter=Maz&rating=2 — 获取匹配 nameFilter = "Maz" 且评分大于或等于 2 的汽车

[HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
public Task<List<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)

注意:我真的很想保持控制器类干净,并且每个 Web API 路由都有一个方法——这可能吗?

问题

如您所料,这些 API 定义存在问题。基本上,AllCars 拦截了几乎所有的请求。 (当我至少能够让/api/experimental/cars/{carId} 工作时,基于查询字符串的 API 仍然无法工作并被另一种方法拦截......

我尝试了许多可能的路由语法来表达我想要的,但没有运气。是否甚至可以使用默认路由机制,或者我需要实现自己的路由器类或中间件或其他东西?

更新 1:问题定义

我知道我可以将至少三个方法及其路由加入到单个 WebAPI 方法中,该方法对接收到的参数很智能。请注意,这正是我想要避免的。

为什么?

原因 1:我看到在非 .NET 路由器中,它运行良好,并且没有技术上不可能实现基于语义的路由解析。

原因 2:我认为上面提到的所有四种 URL 模式都是四种不同的路由。可能有人不同意我的观点,但没关系,但就我的目的而言,方法和路线不同,必须保持不同。

原因 3.1:这使控制器代码保持干净。每种方法只处理一种特定情况。参数名称足以正确解析路线(至少在人类头脑中,因此机器也可以做到 - 将算法形式化很容易)。如果客户端使用不受支持的查询参数发出请求,它应该会导致 HTTP 404 Not Found 或 HTTP 400 Bad Request -- 完全没问题(客户端宁愿构造正确的 URL)。

原因 3.2:相反,如果我加入方法并使用更通用的路由,我的实现需要在参数组合方面“聪明”。这实际上是将路由抽象泄漏到它不属于我的体系结构的层中。复杂的验证是我不想在 Controller 中看到的另一件事——代码越少越好。

更新 2:Nancy — 另一个 .NET 示例(其他 .NET Core WebApi)

有 Nancy(一个 .NET 框架)完美地处理了路由的这一方面:https://github.com/NancyFx/Nancy/wiki/Defining-routes#pattern 问题是在我的项目中我们没有使用它...... Nancy 是一个完美的工具示例将路由语义的准确定义留给客户端,而不是对 什么是路由 vs 什么不是执行过于严格的规则。

【问题讨论】:

  • 可选的路由参数是你的问题的原因
  • 车牌号是字母数字还是只有数字?
  • @nkosi 我知道问题出在哪里。 :) 汽车 ID 是字符串,而不是数字。
  • 它是否遵循一种模式,您可以在路由时应用约束以唯一标识它。
  • 如果您在谈论 /api/experimental/cars/123,那么是的。这个定位到一辆 ID 为“123”(可能存在也可能不存在)的汽车。

标签: c# asp.net-core asp.net-core-webapi


【解决方案1】:

您只需两条路线即可实现这一目标:

[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> SearchCars([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)

和

[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)

即一条路线将整个集合带回,但可以相应地进行过滤,其中一条路线通过 Id 带回单个 Car 对象。

您会注意到 SearchCars 方法不包含路由中的参数,FromQuery 无论如何都会捕获这些参数。

编辑: 如果您的请求变得复杂,最好定义一个自定义请求对象类型来将所有过滤器包装在一起:

public class MyRequestObject
{
    public string NameFilter {get;set;}
    public int Rating {get;set;}
}

然后:

   [HttpGet("/api/experimental/cars/")]
   public Task<List<Car>> SearchCars([FromQuery] MyRequestObject requestParams)

【讨论】:

  • 我知道我可以使用两个不同的路由而不是四个,但这会使实现更加繁重。我真的不希望这样——我希望 API 实现精简且切中要害。我的问题明确地将解决方案限制为四个独立的路线和方法。你知道如何实现吗?
  • 我认为在此处将路由值作为查询参数进行区分很重要。您定义的四个路线存在歧义。路线:...api/cars 和 ...api/cars?nameFilter=something 是相同的路线“api/cars”,具有不同的路线,网址看起来像:api/cars/someFilter。这是一条新路线,因为“​​/”表示一个新参数 - 但我假设您希望 url 更具可读性,这就是我建议我所做的原因 - 它符合您对拥有正确 Url 的要求,我相信它是最干净,如果不是唯一的方法。
  • 进一步...还要考虑您使用可选参数尝试的两条路线。如果未提供这些,则存在路线不明确的问题。即 /api/experimental/cars/{nameFilter?} 没有提供的参数是 /api/experimental/cars/ 你已经定义了
  • @IgorSoloydenko 您正在尝试将一般概念应用于非常特定于 .Net 的实现。如果不对如何唯一区分路线进行更改,您想要的内容将不适用于当前的框架迭代。从框架的角度来看,您的路由模板会相互冲突,因为它们是相同的。
  • @IgorSoloydenko 可以通过对路线模板进行一些更改来实现您想要的,但看起来您在特定模式上已经死了。如果这些是您的要求,则不能因此而责怪您。这就是为什么我在询问是否可以将路由约束应用于模板。
【解决方案2】:

查看以下建议的路线,这些路线在测试时不会相互冲突,并且仍然允许隔离所有操作。

[Route("api/experimental/cars")]
public class CarsController : Controller {

    //GET api/experimental/cars
    [HttpGet("")]
    public IActionResult AllCars() { ... }

    //GET api/experimental/cars/123
    [HttpGet("{carId}")]
    public IActionResult CarById(string carId) { ... }

    //GET api/experimental/cars/named/Maz 
    //GET api/experimental/cars/named?filter=Maz 
    [HttpGet("named/{filter?}")]
    public IActionResult CarsByNameFilter(string filter = "") { ... }

    //GET api/experimental/cars/filtered?rating=2&name=Maz 
    //GET api/experimental/cars/filtered?rating=2 
    //GET api/experimental/cars/filtered?name=Maz 
    [HttpGet("filtered")]
    public IActionResult CarsByNameAndRatingFilter(string name = "", int rating = 1) { ... }

}

【讨论】:

  • 感谢您提供此变体。我知道这是一个可行的解决方案,但cars/named?filer=... 的语法和/cars/filtered 的语法是我试图避免的事情。直接应用于资源名称 (cars) 的查询参数看起来更加自然和富有表现力。我将cars/named?filter=... 读作命名对象集合的地址。问题是实际上没有这样的资源。有资源cars,但没有cars/named。后者是一个虚拟实体,并不是真正的业务领域的一部分。
  • 据我所知,我的问题定义没有基于 .NET WebAPI 的解决方案来满足每一个需求点。因此,我们的团队可能会坚持您描述的方法,即使我发现它的表现力不如预期的方法。请更新您的答案,并明确说明这无法以请求的形式实现。我将等待几天并将您的答案标记为已接受。谢谢!
  • @IgorSoloydenko 已解决此问题。跟进。
【解决方案3】:

我对这个主题的经验告诉我,实现我想要的 API 的最佳方法是有两种方法:

class CarsController {

    // [HttpGet("/api/experimental/cars/")]
    [HttpGet("/api/experimental/cars/{carId}")]
    public Task<IEnumerable<Car>> CarById([FromRoute] string carId)
    {
        if (carId == null)
            return GetAllCars();
        else
            return GetCarWithId(carId);
    }

    // [HttpGet("/api/experimental/cars/{nameFilter?}")]
    [HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
    public Task<IEnumerable<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
    {
        // TODO Validate the combination of query string parameters for your specific API/business rules.
        var filter = new Filter {
            NameFilter = nameFilter,
            Rating = rating
        };
        return GetCarsMatchingFilter(filter);
    }

}

第一个 API 几乎是微不足道的。尽管在包装集合对象中返回单个项目可能看起来不太好,但它最大限度地减少了 API 方法的数量(我个人对此很好)。

第二个 API 比较复杂:在某种程度上,它就像façade pattern。 IE。该 API 将响应几乎所有可能的基于 /api/experimental/cars? 的路由。因此,在进行实际工作之前,我们需要非常仔细地验证收到的参数的组合。

【讨论】:

    猜你喜欢
    • 2021-09-04
    • 1970-01-01
    • 1970-01-01
    • 2020-12-23
    • 2016-04-17
    • 2017-07-16
    • 1970-01-01
    • 2018-08-28
    • 2020-03-16
    相关资源
    最近更新 更多