【问题标题】:MVC API Routing When Multiple Get Actions Are Present存在多个获取操作时的 MVC API 路由
【发布时间】:2013-02-24 19:25:58
【问题描述】:

似乎有一千人在堆栈溢出问题上提出相同的问题,但似乎没有一个单一的解决方案来解决这个问题。我又要问了……

我有一个具有以下操作的 API 控制器:

    // GET api/Exploitation
    public HttpResponseMessage Get() {
        var items = _exploitationRepository.FindAll();

        var mappedItems = Mapper.Map<IEnumerable<Exploitation>, IEnumerable<ExploitationView>>(items);

        var response = Request.CreateResponse<IEnumerable<ExploitationView>>(HttpStatusCode.OK, mappedItems);
        response.Headers.Location = new Uri(Url.Link("DefaultApi", new { }));
        return response;
    }

    // GET api/Exploitation/5        
    [HttpGet, ActionName("Get")]
    public HttpResponseMessage Get(int id) {
        var item = _exploitationRepository.FindById(id);
        var mappedItem = Mapper.Map<Exploitation, ExploitationView>(item);

        var response = Request.CreateResponse<ExploitationView>(HttpStatusCode.OK, mappedItem);
        response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = id }));
        return response;
    }

    // GET api/Exploitation/GetBySongwriterId/5
    [HttpGet, ActionName("GetBySongwriterId")]
    public HttpResponseMessage GetBySongwriterId(int id) {
        var item = _exploitationRepository.Find(e => e.Song.SongWriterSongs.Any(s => s.SongWriterId == id))
                                          .OrderByDescending(e => e.ReleaseDate);
        var mappedItem = Mapper.Map<IEnumerable<Exploitation>, IEnumerable<ExploitationView>>(item);

        var response = Request.CreateResponse<IEnumerable<ExploitationView>>(HttpStatusCode.OK, mappedItem);
        response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = id }));
        return response;
    }

    // GET api/Exploitation/GetBySongwriterId/5
    [HttpGet, ActionName("GetBySongId")]
    public HttpResponseMessage GetBySongId(int id) {
        var item = _exploitationRepository.Find(e => e.SongId == id)
                                          .OrderByDescending(e => e.ReleaseDate);
        var mappedItem = Mapper.Map<IEnumerable<Exploitation>, IEnumerable<ExploitationView>>(item);

        var response = Request.CreateResponse<IEnumerable<ExploitationView>>(HttpStatusCode.OK, mappedItem);
        response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = id }));
        return response;
    }

在我的 APIConfig 中,我定义了以下路线:

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "ActionApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional, action = RouteParameter.Optional },
            constraints: new { id = @"\d+" }
        );

我发现我可以毫无问题地访问以下操作: /api/exploitation /api/exploitation/getbysongwriterid/1 /api/exploitation/getbysongid/1

当我尝试访问 /api/exploitation/1 时出现此异常

"Multiple actions were found that match the request: System.Net.Http.HttpResponseMessage Get(Int32) on type Songistry.API.ExploitationController System.Net.Http.HttpResponseMessage GetBySongwriterId(Int32)" exception.

谁能看到我的路线有什么问题?还是有其他问题?

【问题讨论】:

    标签: asp.net-mvc asp.net-mvc-routing asp.net-web-api-routing


    【解决方案1】:

    我找到了一个优雅的解决方案。

    我修改了我的 ApiRouteConfig 以具有以下路由:

            config.Routes.MapHttpRoute(
                name: "DefaultGetApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional, action = "Get" },
                constraints: new { id = @"\d+", httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
            );
    
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional },
                constraints: new { id = @"\d+" }
            );            
    
            config.Routes.MapHttpRoute(
                name: "ActionApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional, action = RouteParameter.Optional }
            );
    

    现在我可以访问了:

    /api/exploitation
    /api/exploitation/1
    /api/exploitation/getbysongid/1
    /api/exploitation/getbysongwriterid/1
    

    我根本不需要修改我的控制器操作来使用这个新的路由配置。

    如果您有多个 PUT 或 POST 操作,您可以创建如下所示的新路由:

        config.Routes.MapHttpRoute(
            name: "DefaultGetApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional, action = "Put" },
            constraints: new { id = @"\d+", httpMethod = new HttpMethodConstraint(HttpMethod.Put) }
        );
    
        config.Routes.MapHttpRoute(
            name: "DefaultGetApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional, action = "Delete" },
            constraints: new { id = @"\d+", httpMethod = new HttpMethodConstraint(HttpMethod.Delete) }
        );
    

    我希望这个答案对每个人都有帮助,因为这似乎是人们经常遇到的问题。

    【讨论】:

    • 这几乎是完美的。当 ID 需要是 GUID 时会发生什么?
    【解决方案2】:

    您遇到的问题是 /api/exploitation/1 属于:

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    

    您的所有 GET 方法也都满足该路由,特别是因为 {id} 是可选的并且它们的控制器是相同的。

    所以你有一个来自客户端的 HTTP GET 请求和多个接受 GET 请求的方法。它不知道该去哪一个。

    api/{controller}/{action}/{id} 
    //This works fine because you specified which action explicitly
    

    我希望这能回答你的问题。

    【讨论】:

    • 感谢您的回复,但它并没有真正回答我的问题,它只是告诉我为什么这不起作用。您对如何完成这项工作有任何想法吗?我认为这是可能的,不是吗?
    【解决方案3】:

    在您的路线定义中尝试以下操作。只保留以下路线:

      config.Routes.MapHttpRoute(
            name: "ActionApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional, action = "Get" },
            constraints: new { id = @"\d+" }
        );
    

    将第一个 Get 方法设为私有,修改第二个使 id 具有默认值:

    // GET api/Exploitation
    private HttpResponseMessage Get() {
        // implementation stays the same but now it's private
    }
    
    // GET api/Exploitation/5        
    [HttpGet, ActionName("Get")]
    public HttpResponseMessage Get(int id = 0) {
        if (id == 0) {
            return Get();
        }
    
        // continue standard implementation
    }
    

    这种方式(我自己没有测试过)我希望:

    • api/Exploitation/ 将映射到 api/Exploitation/Get(作为默认操作),其中 id = 0 作为默认参数
    • api/Exploitation/1 将映射到 api/Exploitation/Get/1,因此它将调用 Get(1)
    • api/Exploitation/someOtherAction/345 将调用正确的操作方法

    这可能有效。更严格的路由定义实际上可能是这样的:

      config.Routes.MapHttpRoute(
            name: "ApiWithRequiredId",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: null /* make sure we have explicit action and id */,
            constraints: new { id = @"\d+" }
        );
    
      config.Routes.MapHttpRoute(
            name: "ApiWithOptionalId",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional, action = "Get" },
            constraints: new { action = "Get" /* only allow Get method to work with an optional id */, id = @"\d+" }
        );
    

    但是按照这些思路...尝试一下,我希望它可以解决您的问题。

    【讨论】:

    • 感谢您的回复,但是,我真的不满意不得不从另一个人那里调用另一个 get 操作是多么的不雅。我有一个非常优雅的解决方案,现在将发布它。
    • 完全没问题,我很好奇看看。只是为了澄清一些事情:我发布的解决方案不会从动作中调用动作。一旦第一个方法变为私有,它就不再是一个动作了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-17
    • 2017-01-03
    • 1970-01-01
    • 2018-07-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多