具有相同URL路由但不同HTTP方法的多个控制器

13

我有以下两个控制器:

[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.

这个消息很明显。看起来 WebApi 在查找控制器/操作的正确候选项时没有考虑 HTTP 方法。

我该如何实现期望的行为?


更新:我深入研究了 Web API 的内部机制,并且我理解这是默认的工作方式。我的目标是分离代码和逻辑 - 在实际情况下,这些控制器具有不同的依赖关系并且更加复杂。为了维护、可测试性、项目组织等方面,它们应该是不同的对象(遵循 SOLID 原则等)。

我原以为可以覆盖一些 WebAPI 服务 (IControllerSelector 等),但这似乎是一个有点冒险和非标准的方法,对于这个简单而且 - 如我所想 - 常见的情况。


这是因为它使用路由表中的路由先找到控制器,然后检查 Http{Verb} 以选择一个操作。这就是为什么当它们都在同一个控制器中时它能够正常工作。如果它找到相同的路由到两个不同的控制器,它就不知道该选择哪一个,因此会出现错误。 - Nkosi
3
你的路由逻辑与默认的路由逻辑不同,因此你必须接受这个事实(例如,将所有逻辑从控制器移动到两个单独的非控制器类中,并从单个控制器使用这些类),或者提供自定义的 IHttpControllerSelector 实现来使用你自己的路由逻辑。 - Evk
可能可以在单个操作路由中添加"/some-resources",而不是路由前缀。 - Developer
正如开发人员所提到的,您可以使用“Route”属性装饰方法,并在路由前缀中添加“/some-resources”,以绕过URL的控制器部分。如果您有大量的方法,这并不是理想的解决方案,这就是为什么我没有将其发布为答案的原因。 - ColinM
1个回答

14

更新

根据您的评论,更新了问题并提供了答案

ASP.NET Web Api中具有相同路由前缀的多个控制器类型

可以通过自定义路由约束来实现所需结果,用于应用于控制器操作的HTTP方法。

检查默认的Http{Verb}属性,即 [HttpGet][HttpPost]RouteAttribute,它们是密封的。我意识到它们的功能可以合并成一个类,类似于它们在Asp.Net-Core中的实现方式。

以下是GET和POST的示例,但为应用于控制器的其他HTTP方法 PUT、DELETE...等 创建约束不应该很困难。

class HttpGetAttribute : MethodConstraintedRouteAttribute {
    public HttpGetAttribute(string template) : base(template, HttpMethod.Get) { }
}

class HttpPostAttribute : MethodConstraintedRouteAttribute {
    public HttpPostAttribute(string template) : base(template, HttpMethod.Post) { }
}

重要的类是路由工厂和约束本身。框架已经有了负责大部分路由工厂工作的基类,还有一个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 { }
}

原始回答

参考: 在ASP.NET Web API中的路由和操作选择

这是因为它使用路由表中的路由首先找到控制器,然后检查Http{Verb}以选择一个操作。这就是为什么当它们都在同一个控制器中时它能够工作的原因。如果它发现相同的路由到两个不同的控制器,它就不知道选择哪一个,因此会出现错误。

如果目标是简单的代码组织,则可以利用部分类。

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) {
        // ...
    }
}

抱歉耽搁了。一切似乎都运行得很完美。谢谢! - Crozin
我认为您忘记在“使用应用自定义路由约束的以下控制器”部分实际使用[MethodConstraintedRoute(..)]了。 - quetzalcoatl
1
@quetzalcoatl 没有。如果您查看上面的代码片段,您会发现MethodConstraintedRouteHttpGetAttributeHttpPostAttribute使用的基类。 - Nkosi
哦,那是真的。我忽略了那部分,并认为HttpGet / HttpPost是标准框架代码。谢谢! - quetzalcoatl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接