具有相同 URL 路由但不同 HTTP 方法的多个控制器 [英] Multiple controllers with same URL routes but different HTTP methods

查看:29
本文介绍了具有相同 URL 路由但不同 HTTP 方法的多个控制器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有以下两个控制器:

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

所有三个动作实际上都得到了三个不同的路由:

All three actions got in fact three different routes:

  • GET/some-resources
  • POST/some-resources
  • GET/some-resources/aaaaa-bbb-ccc-dddd

如果我将它们放入单个控制器中,一切正常,但是如果我将它们分开(如上所示)WebApi 会引发以下异常:

If I put them into single controller everything works just fine, however if I separate them (as shown above) WebApi throws following exception:

找到多个与 URL 匹配的控制器类型.这个可以如果多个控制器上的属性路由与请求的匹配,就会发生网址.

Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.

这条信息很明显.WebApi 在为控制器/动作寻找合适的候选者时似乎没有考虑 HTTP 方法.

This message is quite obvious. It seems WebApi does not take HTTP method into account when looking for a right candidate for controller/action.

我怎样才能达到预期的行为?

How could I achieve the expected behavior?

更新:我已经深入研究了 Web API 的内部结构,我知道这是默认情况下的工作方式.我的目标是将代码和逻辑分开——在现实世界中,这些控制器具有不同的依赖关系,并且更复杂一些.出于维护、可测试性、项目组织等的考虑,它们应该是不同的对象(SOLID 和东西).

UPDATE: I've digged a little into Web API internals and I understand that's the way it works by default. My goal is to separate the code and logic - in real world case those controllers have different dependencies and are a bit more complex. For the sake of maintenance, testability, project organization etc. they should be different objects (SOLID and stuff).

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

I thought I could override some WebAPI services (IControllerSelector etc) however this seems to be a little bit risky and non-standard approach for this simple and - as I assumed - common case.

推荐答案

UPDATE

根据您的评论、更新的问题和此处提供的答案

Based on your comments, updated question and the answer provided here

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

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

Desired result can be achieved via custom route constraints for the HTTP method applied to controller actions.

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

On inspection of the default Http{Verb} attributes ie [HttpGet], [HttpPost] and the RouteAttribute, which by the way are sealed, I realized that their functionality can be combine into one class similar to how they are implemented in Asp.Net-Core.

以下是针对 GET 和 POST 的,但为其他 HTTP 方法 PUT、DELETE...etc 创建约束应该不难,以应用于控制器.

The following is for GET and POST, but it shouldn't be difficult to create constraints for the other HTTP methods PUT, DELETE...etc to be applied to the controllers.

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

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

重要的类是路由工厂和约束本身.该框架已经具有处理大部分路由工厂工作的基类以及一个 HttpMethodConstraint 所以这只是应用所需的路由功能的问题.

The important class is the route factory and the constraint itself. The framework already has base classes that take care of most of the route factory work and also a HttpMethodConstraint so it is just a matter of applying the desired routing functionality.

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;
        }
    }
}

因此,鉴于以下控制器应用了自定义路由约束...

So given the following controller with the custom route constraints applied...

[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();
    }
}

进行了内存单元测试以确认功能并且它有效.

Did an in-memory unit test to confirm functionality and it worked.

[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} 以选择一个动作.这就是为什么当它们都在同一个控制器中时它起作用的原因.如果它发现到两个不同控制器的相同路由,它不知道何时选择一个,因此会出现错误.

That's because it uses the routes in the route table to find the controller first and then checks for Http{Verb} to select an action. which is why it works when they are all in the same controller. if it finds the same route to two different controllers it doesn't know when one to select, hence the error.

如果目标是简单的代码组织,那么利用部分类

If the goal is simple code organization then take advantage of partial classes

ResourcesController.cs

ResourcesController.cs

[RoutePrefix("/some-resources")]
partial class ResourcesController : ApiController { }

ResourcesController_Creation.cs

ResourcesController_Creation.cs

partial class ResourcesController {
    [HttpPost, Route]
    public ... CreateResource(CreateData input) {
        // ...
    }
}

ResourcesController_Display.cs

ResourcesController_Display.cs

partial class ResourcesController {
    [HttpGet, Route]
    public ... ListAllResources() {
        // ...
    }

    [HttpGet, Route("{publicKey:guid}"]
    public ... ShowSingleResource(Guid publicKey) {
        // ...
    }
}

这篇关于具有相同 URL 路由但不同 HTTP 方法的多个控制器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆