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

Cro*_*zin 7 c# asp.net-web-api attributerouting asp.net-web-api-routing asp.net-web-api2

我有以下两个控制器:

[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)
    {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

所有这三个行动实际上都有三条不同的路线:

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

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

找到了与URL匹配的多种控制器类型.如果多个控制器上的属性路由与请求的URL匹配,则会发生这种情

这个消息很明显.在寻找适合控制器/操作的候选者时,WebApi似乎没有考虑HTTP方法.

我怎样才能实现预期的行为?


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

我认为我可以覆盖一些WebAPI服务(IControllerSelector等),但这似乎是一个有点风险和非标准的方法,这个简单的 - 和我假设 - 常见的情况.

Nko*_*osi 11

UPDATE

根据您的评论,更新的问题和这里提供的答案

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

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

在默认的HTTP检查{}动词属性即[HttpGet],[HttpPost]RouteAttribute,它的方式是密封的,我意识到,它们的功能可以被组合成类似于它们是如何在Asp.Net核实现的一类.

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

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

class HttpPostAttribute : MethodConstraintedRouteAttribute {
    public HttpPostAttribute(string template) : base(template, HttpMethod.Post) { }
}
Run Code Online (Sandbox Code Playgroud)

重要的类是路由工厂和约束本身.该框架已经有基类来处理大部分路由工厂工作以及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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,给定以下控制器应用自定义路径约束...

[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();
    }
}
Run Code Online (Sandbox Code Playgroud)

内存单元测试是否确认功能并且有效.

[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 { }
}
Run Code Online (Sandbox Code Playgroud)

原始答案

引用:ASP.NET Web API中的路由和操作选择

那是因为它首先使用路由表中的路由来查找控制器,然后检查Http {Verb}以选择一个动作.这就是为什么它们都在同一个控制器中的原因.如果它找到到两个不同控制器的相同路由,则它不知道何时选择,因此错误.

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

ResourcesController.cs

[RoutePrefix("/some-resources")]
partial class ResourcesController : ApiController { }
Run Code Online (Sandbox Code Playgroud)

ResourcesController_Creation.cs

partial class ResourcesController {
    [HttpPost, Route]
    public ... CreateResource(CreateData input) {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

ResourcesController_Display.cs

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

    [HttpGet, Route("{publicKey:guid}"]
    public ... ShowSingleResource(Guid publicKey) {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)