具有相同 URL 路由但不同 HTTP 方法的多个控制器
Multiple controllers with same URL routes but different HTTP methods
我有以下两个控制器:
[RoutePrefix("/some-resources")
class CreationController : ApiController
{
[HttpPost, Route]
public ... CreateResource(CreateData input)
{
// ...
}
}
[RoutePrefix("/some-resources")
class DisplayController : ApiController
{
[HttpGet, Route]
public ... ListAllResources()
{
// ...
}
[HttpGet, Route("{publicKey:guid}"]
public ... ShowSingleResource(Guid publicKey)
{
// ...
}
}
所有三个动作实际上都有三个不同的路线:
GET /some-resources
POST /some-resources
GET /some-resources/aaaaa-bbb-ccc-dddd
如果我将它们放入单个控制器中,一切正常,但是如果我将它们分开(如上所示),WebApi 会抛出以下异常:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.
这条消息很明显。在为 controller/action.
寻找合适的候选人时,WebApi 似乎没有考虑 HTTP 方法
我怎样才能实现预期的行为?
更新:我深入研究了 Web API 的内部结构,我知道这就是默认情况下的工作方式。我的目标是分离代码和逻辑——在现实世界的情况下,这些控制器具有不同的依赖关系并且有点复杂。为了维护、可测试性、项目组织等。它们应该是不同的对象(SOLID 和东西)。
我想我可以覆盖一些 WebAPI 服务(IControllerSelector
等)但是对于这种简单且 - 正如我假设的 - 常见的方法,这似乎有点冒险和非标准方法案例.
更新
根据您的评论、更新的问题和此处提供的答案
Multiple Controller Types with same Route prefix ASP.NET Web Api
可以通过为应用于控制器操作的 HTTP 方法自定义路由约束来实现所需的结果。
检查默认的 Http{Verb} 属性,即 [HttpGet]
, [HttpPost]
and the RouteAttribute
,顺便说一句,它们是密封的,我意识到它们的功能可以合并为一个 class,类似于它们的实现方式在 Asp.Net-Core.
以下是针对 GET 和 POST 的,但为要应用于控制器的其他 HTTP 方法 PUT, DELETE...etc
创建约束应该不难。
class HttpGetAttribute : MethodConstraintedRouteAttribute {
public HttpGetAttribute(string template) : base(template, HttpMethod.Get) { }
}
class HttpPostAttribute : MethodConstraintedRouteAttribute {
public HttpPostAttribute(string template) : base(template, HttpMethod.Post) { }
}
重要的class是路由工厂和约束本身。该框架已经有了基础 classes 来处理大部分路由工厂工作,还有一个 HttpMethodConstraint 所以这只是应用所需路由功能的问题。
class MethodConstraintedRouteAttribute
: RouteFactoryAttribute, IActionHttpMethodProvider, IHttpRouteInfoProvider {
public MethodConstraintedRouteAttribute(string template, HttpMethod method)
: base(template) {
HttpMethods = new Collection<HttpMethod>(){
method
};
}
public Collection<HttpMethod> HttpMethods { get; private set; }
public override IDictionary<string, object> Constraints {
get {
var constraints = new HttpRouteValueDictionary();
constraints.Add("method", new HttpMethodConstraint(HttpMethods.ToArray()));
return constraints;
}
}
}
因此给定以下应用了自定义路由约束的控制器...
[RoutePrefix("api/some-resources")]
public class CreationController : ApiController {
[HttpPost("")]
public IHttpActionResult CreateResource(CreateData input) {
return Ok();
}
}
[RoutePrefix("api/some-resources")]
public class DisplayController : ApiController {
[HttpGet("")]
public IHttpActionResult ListAllResources() {
return Ok();
}
[HttpGet("{publicKey:guid}")]
public IHttpActionResult ShowSingleResource(Guid publicKey) {
return Ok();
}
}
进行了内存单元测试以确认功能并且有效。
[TestClass]
public class WebApiRouteTests {
[TestMethod]
public async Task Multiple_controllers_with_same_URL_routes_but_different_HTTP_methods() {
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
var errorHandler = config.Services.GetExceptionHandler();
var handlerMock = new Mock<IExceptionHandler>();
handlerMock
.Setup(m => m.HandleAsync(It.IsAny<ExceptionHandlerContext>(), It.IsAny<System.Threading.CancellationToken>()))
.Callback<ExceptionHandlerContext, CancellationToken>((context, token) => {
var innerException = context.ExceptionContext.Exception;
Assert.Fail(innerException.Message);
});
config.Services.Replace(typeof(IExceptionHandler), handlerMock.Object);
using (var server = new HttpTestServer(config)) {
string url = "http://localhost/api/some-resources/";
var client = server.CreateClient();
client.BaseAddress = new Uri(url);
using (var response = await client.GetAsync("")) {
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
using (var response = await client.GetAsync("3D6BDC0A-B539-4EBF-83AD-2FF5E958AFC3")) {
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
using (var response = await client.PostAsJsonAsync("", new CreateData())) {
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}
}
public class CreateData { }
}
原始答案
引用:Routing and Action Selection in ASP.NET Web API
那是因为它使用路由 table 中的路由首先找到控制器,然后检查 Http{Verb} 到 select 一个动作。这就是为什么当它们都在同一个控制器中时它起作用的原因。如果它找到到两个不同控制器的相同路由,它不知道何时到达 select,因此会出现错误。
如果目标是简单的代码组织,那么利用部分 classes
ResourcesController.cs
[RoutePrefix("/some-resources")]
partial class ResourcesController : ApiController { }
ResourcesController_Creation.cs
partial class ResourcesController {
[HttpPost, Route]
public ... CreateResource(CreateData input) {
// ...
}
}
ResourcesController_Display.cs
partial class ResourcesController {
[HttpGet, Route]
public ... ListAllResources() {
// ...
}
[HttpGet, Route("{publicKey:guid}"]
public ... ShowSingleResource(Guid publicKey) {
// ...
}
}
我有以下两个控制器:
[RoutePrefix("/some-resources")
class CreationController : ApiController
{
[HttpPost, Route]
public ... CreateResource(CreateData input)
{
// ...
}
}
[RoutePrefix("/some-resources")
class DisplayController : ApiController
{
[HttpGet, Route]
public ... ListAllResources()
{
// ...
}
[HttpGet, Route("{publicKey:guid}"]
public ... ShowSingleResource(Guid publicKey)
{
// ...
}
}
所有三个动作实际上都有三个不同的路线:
GET /some-resources
POST /some-resources
GET /some-resources/aaaaa-bbb-ccc-dddd
如果我将它们放入单个控制器中,一切正常,但是如果我将它们分开(如上所示),WebApi 会抛出以下异常:
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.
这条消息很明显。在为 controller/action.
寻找合适的候选人时,WebApi 似乎没有考虑 HTTP 方法我怎样才能实现预期的行为?
更新:我深入研究了 Web API 的内部结构,我知道这就是默认情况下的工作方式。我的目标是分离代码和逻辑——在现实世界的情况下,这些控制器具有不同的依赖关系并且有点复杂。为了维护、可测试性、项目组织等。它们应该是不同的对象(SOLID 和东西)。
我想我可以覆盖一些 WebAPI 服务(IControllerSelector
等)但是对于这种简单且 - 正如我假设的 - 常见的方法,这似乎有点冒险和非标准方法案例.
更新
根据您的评论、更新的问题和此处提供的答案
Multiple Controller Types with same Route prefix ASP.NET Web Api
可以通过为应用于控制器操作的 HTTP 方法自定义路由约束来实现所需的结果。
检查默认的 Http{Verb} 属性,即 [HttpGet]
, [HttpPost]
and the RouteAttribute
,顺便说一句,它们是密封的,我意识到它们的功能可以合并为一个 class,类似于它们的实现方式在 Asp.Net-Core.
以下是针对 GET 和 POST 的,但为要应用于控制器的其他 HTTP 方法 PUT, DELETE...etc
创建约束应该不难。
class HttpGetAttribute : MethodConstraintedRouteAttribute {
public HttpGetAttribute(string template) : base(template, HttpMethod.Get) { }
}
class HttpPostAttribute : MethodConstraintedRouteAttribute {
public HttpPostAttribute(string template) : base(template, HttpMethod.Post) { }
}
重要的class是路由工厂和约束本身。该框架已经有了基础 classes 来处理大部分路由工厂工作,还有一个 HttpMethodConstraint 所以这只是应用所需路由功能的问题。
class MethodConstraintedRouteAttribute
: RouteFactoryAttribute, IActionHttpMethodProvider, IHttpRouteInfoProvider {
public MethodConstraintedRouteAttribute(string template, HttpMethod method)
: base(template) {
HttpMethods = new Collection<HttpMethod>(){
method
};
}
public Collection<HttpMethod> HttpMethods { get; private set; }
public override IDictionary<string, object> Constraints {
get {
var constraints = new HttpRouteValueDictionary();
constraints.Add("method", new HttpMethodConstraint(HttpMethods.ToArray()));
return constraints;
}
}
}
因此给定以下应用了自定义路由约束的控制器...
[RoutePrefix("api/some-resources")]
public class CreationController : ApiController {
[HttpPost("")]
public IHttpActionResult CreateResource(CreateData input) {
return Ok();
}
}
[RoutePrefix("api/some-resources")]
public class DisplayController : ApiController {
[HttpGet("")]
public IHttpActionResult ListAllResources() {
return Ok();
}
[HttpGet("{publicKey:guid}")]
public IHttpActionResult ShowSingleResource(Guid publicKey) {
return Ok();
}
}
进行了内存单元测试以确认功能并且有效。
[TestClass]
public class WebApiRouteTests {
[TestMethod]
public async Task Multiple_controllers_with_same_URL_routes_but_different_HTTP_methods() {
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
var errorHandler = config.Services.GetExceptionHandler();
var handlerMock = new Mock<IExceptionHandler>();
handlerMock
.Setup(m => m.HandleAsync(It.IsAny<ExceptionHandlerContext>(), It.IsAny<System.Threading.CancellationToken>()))
.Callback<ExceptionHandlerContext, CancellationToken>((context, token) => {
var innerException = context.ExceptionContext.Exception;
Assert.Fail(innerException.Message);
});
config.Services.Replace(typeof(IExceptionHandler), handlerMock.Object);
using (var server = new HttpTestServer(config)) {
string url = "http://localhost/api/some-resources/";
var client = server.CreateClient();
client.BaseAddress = new Uri(url);
using (var response = await client.GetAsync("")) {
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
using (var response = await client.GetAsync("3D6BDC0A-B539-4EBF-83AD-2FF5E958AFC3")) {
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
using (var response = await client.PostAsJsonAsync("", new CreateData())) {
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}
}
public class CreateData { }
}
原始答案
引用:Routing and Action Selection in ASP.NET Web API
那是因为它使用路由 table 中的路由首先找到控制器,然后检查 Http{Verb} 到 select 一个动作。这就是为什么当它们都在同一个控制器中时它起作用的原因。如果它找到到两个不同控制器的相同路由,它不知道何时到达 select,因此会出现错误。
如果目标是简单的代码组织,那么利用部分 classes
ResourcesController.cs
[RoutePrefix("/some-resources")]
partial class ResourcesController : ApiController { }
ResourcesController_Creation.cs
partial class ResourcesController {
[HttpPost, Route]
public ... CreateResource(CreateData input) {
// ...
}
}
ResourcesController_Display.cs
partial class ResourcesController {
[HttpGet, Route]
public ... ListAllResources() {
// ...
}
[HttpGet, Route("{publicKey:guid}"]
public ... ShowSingleResource(Guid publicKey) {
// ...
}
}