web-dev-qa-db-ja.com

URLルートは同じでHTTPメソッドが異なる複数のコントローラー

私は次の2つのコントローラーを持っています:

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

3つのアクションはすべて、実際には3つの異なるルートを取得しました。

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

それらを単一のコントローラーに入れると、すべてが正常に機能しますが、(上記のように)それらを分離すると、WebApiは次の例外をスローします。

URLに一致する複数のコントローラータイプが見つかりました。これは、複数のコントローラーの属性ルートが要求されたURLと一致する場合に発生する可能性があります。

このメッセージは非常に明白です。 WebApiは、コントローラー/アクションの適切な候補を探すときにHTTPメソッドを考慮していないようです。

どうすれば期待される動作を実現できますか?


[〜#〜] update [〜#〜]:Web APIの内部を少し掘り下げましたが、これがデフォルトで機能する方法であることがわかりました。私の目標は、コードとロジックを分離することです。実際の場合、これらのコントローラーの依存関係は異なり、少し複雑です。メンテナンス、テスト容易性、プロジェクト編成などのために、それら異なるオブジェクト(SOLIDなど)である必要があります。

一部のWebAPIサービス(IControllerSelectorなど)をオーバーライドできると思いましたが、これは、この単純で(私が想定したように)一般的なケースでは、少し危険で非標準的なアプローチのようです。

8
Crozin

[〜#〜]更新[〜#〜]

あなたのコメント、更新された質問とここに提供された回答に基づいて

同じルートプレフィックスASP.NET Web APIを持つ複数のコントローラータイプ

望ましい結果は、コントローラーアクションに適用されるHTTPメソッドのカスタムルート制約を介して達成できます。

デフォルトのHttp {Verb}属性の検査時すなわち [HttpGet][HttpPost]RouteAttribute は封印されていますが、Asp.Net-での実装と同様に、それらの機能を1つのクラスに組み合わせることができることに気付きました。芯。

以下は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) { }
}

重要なクラスは、ルートファクトリと制約自体です。フレームワークには、ルートファクトリの作業のほとんどを処理する基本クラスと 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}をチェックしてアクションを選択するためです。これが、それらがすべて同じコントローラー内にある場合に機能する理由です。 2つの異なるコントローラーへの同じルートが見つかった場合、どちらを選択するかわからないため、エラーが発生します。

目標が単純なコード編成である場合は、部分的なクラスを利用します

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