使用基于属性的路由时 ASP.NET 核心中的路由冲突
Conflicting routes in ASP.NET Core when using Attribute based routing
上下文
我正在尝试构建一个 ASP.NET 核心网络 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"[=19= 的汽车]
[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)
注意:我真的很想保持控制器 class 干净,并且每个 Web API 路由只有一个方法——这可能吗?
问题
如您所料,这些 API 定义存在问题。基本上,AllCars 几乎拦截了所有请求。 (当我至少能够让 /api/experimental/cars/{carId}
工作时,基于查询字符串的 API 仍然无法工作并被另一种方法拦截...
我尝试了很多可能的路由语法来表达我想要的东西,但没有成功。甚至可以使用默认路由机制,还是我需要实现自己的路由器 class 或中间件或其他东西?
更新 1:问题定义
我知道我可以将至少三个方法及其路由加入一个 WebAPI 方法中,该方法对接收到的参数很智能。请注意,这正是我要避免的。
为什么?
原因 1:我看到它在非 .NET 路由器中运行良好,并且在技术上不存在实现基于语义的路由解析的可能性。
原因 2:我将上述所有四种 URL 模式视为四种不同的路线。有人可能不同意我的看法,没关系,但就我的目的而言,方法和路线不同,必须保持不同。
原因 3.1:这使控制器代码保持 干净。每种方法只处理一种特定情况。参数名称足以正确解析路由(至少在人脑中是这样,因此机器也可以这样做——很容易将算法形式化)。如果客户端使用不受支持的查询参数发出请求,它应该导致 HTTP 404 Not Found
或 HTTP 400 Bad Request
——完全没问题(客户端宁愿构造正确的 URLs)。
原因3.2:相反,如果我加入方法并使用更通用的路由,我的实现需要'smart'关于参数的组合。这实际上是将路由抽象泄漏到它不属于我的体系结构的层中。复杂的验证是我不想在控制器中看到的另一件事——代码越少越好。
更新 2:Nancy — 另一个 .NET 示例(除了 .NET Core WebApi)
Nancy(一个 .NET 框架)完美地处理了路由的这一方面:https://github.com/NancyFx/Nancy/wiki/Defining-routes#pattern问题是在我的项目中我们没有使用它......Nancy 是一个完美的例子一种将路由语义的确切定义留给客户端的工具,而不是在什么是路由 vs 什么不是 上执行过于严格的规则。
您可以通过两条路线实现这一目标:
[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。
您会注意到 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)
查看以下建议的路线,这些路线在测试时不会相互冲突,并且仍然允许隔离所有操作。
[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) { ... }
}
我在这个主题上的经验告诉我,实现我想要的 APIs 的最好方法是有两种方法:
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?
的路由。因此,在进行实际工作之前,我们需要非常仔细地验证接收到的参数的组合。
上下文
我正在尝试构建一个 ASP.NET 核心网络 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"[=19= 的汽车]
[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)
注意:我真的很想保持控制器 class 干净,并且每个 Web API 路由只有一个方法——这可能吗?
问题
如您所料,这些 API 定义存在问题。基本上,AllCars 几乎拦截了所有请求。 (当我至少能够让 /api/experimental/cars/{carId}
工作时,基于查询字符串的 API 仍然无法工作并被另一种方法拦截...
我尝试了很多可能的路由语法来表达我想要的东西,但没有成功。甚至可以使用默认路由机制,还是我需要实现自己的路由器 class 或中间件或其他东西?
更新 1:问题定义
我知道我可以将至少三个方法及其路由加入一个 WebAPI 方法中,该方法对接收到的参数很智能。请注意,这正是我要避免的。
为什么?
原因 1:我看到它在非 .NET 路由器中运行良好,并且在技术上不存在实现基于语义的路由解析的可能性。
原因 2:我将上述所有四种 URL 模式视为四种不同的路线。有人可能不同意我的看法,没关系,但就我的目的而言,方法和路线不同,必须保持不同。
原因 3.1:这使控制器代码保持 干净。每种方法只处理一种特定情况。参数名称足以正确解析路由(至少在人脑中是这样,因此机器也可以这样做——很容易将算法形式化)。如果客户端使用不受支持的查询参数发出请求,它应该导致 HTTP 404 Not Found
或 HTTP 400 Bad Request
——完全没问题(客户端宁愿构造正确的 URLs)。
原因3.2:相反,如果我加入方法并使用更通用的路由,我的实现需要'smart'关于参数的组合。这实际上是将路由抽象泄漏到它不属于我的体系结构的层中。复杂的验证是我不想在控制器中看到的另一件事——代码越少越好。
更新 2:Nancy — 另一个 .NET 示例(除了 .NET Core WebApi)
Nancy(一个 .NET 框架)完美地处理了路由的这一方面:https://github.com/NancyFx/Nancy/wiki/Defining-routes#pattern问题是在我的项目中我们没有使用它......Nancy 是一个完美的例子一种将路由语义的确切定义留给客户端的工具,而不是在什么是路由 vs 什么不是 上执行过于严格的规则。
您可以通过两条路线实现这一目标:
[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。
您会注意到 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)
查看以下建议的路线,这些路线在测试时不会相互冲突,并且仍然允许隔离所有操作。
[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) { ... }
}
我在这个主题上的经验告诉我,实现我想要的 APIs 的最好方法是有两种方法:
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?
的路由。因此,在进行实际工作之前,我们需要非常仔细地验证接收到的参数的组合。