Web API 2 Attribute Routing Controller Selection

2019-09-01 06:41发布

I use Web API 2 Attribute Routing in my project to provide JSON interface over my data. I am facing weird behaviour of controller selection, not decided yet whether it's a bug or a feature :) Let me describe my approach.

I would like to simulate OData syntax with help of attribute routing (direct OData usage has been refused due to design principles). For example, to get entity with id=5 I use HTTP GET request to URI http://mydomain.com/api/Entity(5) . I expect to use the same URI with HTTP PUT verb to update the entity. This is where the journey begins...

I would like to have separate controller for getting entities (FirstController in the example provided below) and another one for modifying entities (SecondController). Both controllers handles the same URI (e.g. http://mydomain.com/api/Entity(5)) the only difference is HTTP verb used with the URI - GET should be handled by FirstController, PUT should be handled by SecondController. But the URI is handled by none of them; instead HTTP 404 error is returned. When I "merge" GET and PUT actions to only one controller (commented out in FirstController), both verbs are handled correctly. I am using IIS Express and all conventional routes are disabled, only attribute routing is in charge.

It looks like the controller selection process does not work with HTTP verb. In another words, HttpGet and HttpPut attributes just limit action usage but they do not serve as criteria during controller selection. I am not so familiar with MVC / Web API fundamentals, so let me ask you my big question:

Is the behaviour, described herein before, a feature intentionally implemented by MVC / Web API 2 or a bug to be fixed?

If it is considered as a feature, it prevents me to follow design principles. I can live with "merged" controllers but still considering it as a bad practice... Or am I missing something in my train of thought?

My environment setup:

  • Windows 7 (virtual machine using Oracle VirtualBox)
  • Visual Studio 2013
  • .NET 4.5.1
  • Web API 2

The following is implementation of FirstController class:

public class FirstController : ApiController
{
  [HttpGet]
  [Route("api/Entity({id:int})")]
  public Output GetEntity(int id)
  {
    Output output = new Output() { Id = id, Name = "foo" };

    return output;
  }

  //[HttpPut]
  //[Route("api/Entity({id:int})")]
  //public Output UpdateEntity(int id, UpdateEntity command)
  //{
  //  Output output = new Output() { Id = id, Name = command.Name };

  //  return output;
  //}
}

The following is implementation of SecondController class:

public class SecondController : ApiController
{
  [HttpPut]
  [Route("api/Entity({id:int})")]
  public Output UpdateEntity(int id, UpdateEntity command)
  {
    Output output = new Output() { Id = id, Name = command.Name };

    return output;
  }
}

The following is implementation of a console application to test the described behaviour:

class Program
{
  static void Main(string[] args)
  {
    // HTTP client initialization
    HttpClient httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri("http://localhost:1567");
    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    // HTTP GET - FirstController.GetEntity
    HttpResponseMessage getEntityResponse = httpClient.GetAsync("/api/Entity(5)").Result;
    Output getOutput = getEntityResponse.Content.ReadAsAsync<Output>().Result;

    // HTTP PUT - SecondController.UpdateEntity
    UpdateEntity updateCommand = new UpdateEntity() { Name = "newEntityname" };
    HttpResponseMessage updateEntityResponse = httpClient.PutAsJsonAsync("/api/Entity(10)", updateCommand).Result;
    Output updateOutput = updateEntityResponse.Content.ReadAsAsync<Output>().Result;
  }
}

For completion, the following are used DTOs:

public class UpdateEntity
{
  public string Name { get; set; }
}


public class Output
{
  public int Id { get; set; }
  public string Name { get; set; }
}

Thanks in advance for your responses,

Jan Kacina

1条回答
Explosion°爆炸
2楼-- · 2019-09-01 07:29

This design was intentional as we thought it to be an error case where a user would be having same route template on different controllers which can cause ambiguity in the selection process.

Also if we keep aside attribute routing, how would this work with regular routing? Let's imagine we have 2 regular routes where first one is targeted for FirstController and the second to SecondController. Now if a request url is like api/Entity(5), then Web API would always match the 1st route in the route table which would always hit the FirstController and would never reach SecondController. Remember that once Web API matches a route it tries to go till the action selection process and if the action selection process doesn't result in an action being selected, then an error response is sent to the client. You probably are assuming that if an action is not selected in one controller then Web API would route it to the next one in the route configuration. This is incorrect.

Route probing occurs only once and if it results in a match, then the next steps take place...that is controller and action selection. Hope this helps.

查看更多
登录 后发表回答