3

I have read a lot of questions about routing and controllers, but I simply can't find what I'm looking for. I have this controller which has this structure:

Update: Included full class source.

public class LocationsController : ApiController
{
    private readonly IUnitOfWork _unitOfWork;

    public LocationsController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    // GET /api/locations/id
    public Location Get(Guid id)
    {
        return this.QueryById<Location>(id, _unitOfWork);
    }

    // GET /api/locations
    public IQueryable<Location> Get()
    {
        return this.Query<Location>(_unitOfWork);
    }

    // POST /api/locations
    public HttpResponseMessage Post(Location location)
    {
        var id = _unitOfWork.CurrentSession.Save(location);
        _unitOfWork.Commit();

        var response = Request.CreateResponse<Location>(HttpStatusCode.Created, location);
        response.Headers.Location = new Uri(Request.RequestUri, Url.Route(null, new { id }));

        return response;
    }

    // PUT /api/locations
    public Location Put(Location location)
    {
        var existingLocation = _unitOfWork.CurrentSession.Query<Location>().SingleOrDefault(x => x.Id == location.Id);

        //check to ensure update can occur
        if (existingLocation == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        //merge detached entity into session
        _unitOfWork.CurrentSession.Merge(location);
        _unitOfWork.Commit();

        return location;
    }

    // DELETE /api/locations/5
    public HttpResponseMessage Delete(Guid id)
    {
        var existingLocation = _unitOfWork.CurrentSession.Query<Location>().SingleOrDefault(x => x.Id == id);

        //check to ensure delete can occur
        if (existingLocation != null)
        {
            _unitOfWork.CurrentSession.Delete(existingLocation);
            _unitOfWork.Commit();
        }

        return new HttpResponseMessage(HttpStatusCode.NoContent);
    }

    // rpc/locations
    public HttpResponseMessage Dummy()
    {
        // I use it to generate some random data to fill the database in a easy fashion
        Location location = new Location();
        location.Latitude = RandomData.Number.GetRandomDouble(-90, 90);
        location.Longitude = RandomData.Number.GetRandomDouble(-180, 180);
        location.Name = RandomData.LoremIpsum.GetSentence(4, false);

        var id = _unitOfWork.CurrentSession.Save(location);
        _unitOfWork.Commit();

        var response = Request.CreateResponse<Location>(HttpStatusCode.Created, location);
        response.Headers.Location = new Uri(Request.RequestUri, Url.Route(null, new { id }));

        return response;
    }
}

And my routes definition (Global.asax):

public static void RegisterRoutes(RouteCollection routes)
{
    // Default route
    routes.MapHttpRoute(
        name: "Default",
        routeTemplate: "{controller}/{id}",
        defaults: new { id =  RouteParameter.Optional }
    );

    // A route that enables RPC requests
    routes.MapHttpRoute(
        name: "RpcApi",
        routeTemplate: "rpc/{controller}/{action}",
        defaults: new { action = "Get" }
    );
}

So far if I hit the browser with:

  • [baseaddress]/locations/s0m3-gu1d-g0e5-hee5eeeee // It works
  • [baseaddress]/locations/ // Multiple results found
  • [baseaddress]/rpc/locations/dummy // It works

The strangest thing is that this used to work, until I messed up with my NuGet while performing some updates. What am I missing here?

Verbs starting with GET, POST, PUT or delete would be automapped to the first route and my dummy test method would be called via rpc, which would fall into the second route.

The error that is thrown is InvalidOperationException with message

Multiple actions were found that match the request: System.Linq.IQueryable`1[Myproject.Domain.Location] Get() on type Myproject.Webservices.Controllers.LocationsController System.Net.Http.HttpResponseMessage Dummy() on type Myproject.Webservices.Controllers.LocationsController

Any ideas?

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Joel
  • 7,401
  • 4
  • 52
  • 58
  • Please can you post the full code for LocationsController, including the trimmed RPC methods? – Luke Bennett Feb 27 '13 at 15:18
  • _"that this used to work, until I messed up with my NuGet while performing some updates. What am I missing here?"_ - source control. Right click -> Undo / Revert. – CodeCaster Feb 27 '13 at 15:23
  • The project builds and runs fine. I'd rather try to fix and understand what is wrong instead of just reverting. (And I confess that such revert would set me back several hours). – Joel Feb 27 '13 at 15:28

2 Answers2

5

The problem is in the order that the routes are loaded. If they are like this:

// A route that enables RPC requests
routes.MapHttpRoute(
    name: "RpcApi",
    routeTemplate: "rpc/{controller}/{action}",
    defaults: new { action = "Get" }
);

// Default route
routes.MapHttpRoute(
    name: "Default",
    routeTemplate: "{controller}/{id}",
   defaults: new { id =  RouteParameter.Optional }
);

It will work fine. First the request will be mapped against the RPC, then against the controller (thaty may have or not an Id).

cuongle
  • 74,024
  • 28
  • 151
  • 206
  • "It will work fine" It sure did! Thank you for setting me up in the right direction! On a side note, is the new route necessary? (I'm asking because with only the first 2 it worked, as soon as I reordered them) – Joel Feb 27 '13 at 15:40
  • @Joel: I have just test, seems it does not need. Adding one more route will make you easy to break if number of methods increasing for the time being – cuongle Feb 27 '13 at 15:52
  • My original idea is to let REST handle the 4 Http verbs and use RPC calls only when really needed. I suggest that you edit your answer and remove the unnecessary route :) – Joel Feb 27 '13 at 16:27
  • @Joel: will remove my answer since it is not correct anymore in your case – cuongle Feb 27 '13 at 16:29
  • @Joel: Oops, cannot delete, you can edit my answer by yourself – cuongle Feb 27 '13 at 16:30
0

We can also create custom action selector for Api Controllers as follows, so that that can freely work with complex types with traditional "GET,POST,PUT,DELETE":

 class ApiActionSelector : IHttpActionSelector
    {
        private readonly IHttpActionSelector defaultSelector;

        public ApiActionSelector(IHttpActionSelector defaultSelector)
        {
            this.defaultSelector = defaultSelector;
        }
        public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
        {
            return defaultSelector.GetActionMapping(controllerDescriptor);
        }
        public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
        {
            // Get HttpMethod from current context
            HttpMethod httpMethod = controllerContext.Request.Method;

            // Set route values for API
            controllerContext.RouteData.Values.Add("action", httpMethod.Method);

            // Invoke Action
            return defaultSelector.SelectAction(controllerContext);
        }
    }

And we can register the same in WebApiConfig as :

 config.Services.Replace(typeof(IHttpActionSelector), new
 ApiActionSelector(config.Services.GetActionSelector()));

May help users who are running this kind of issue.

Anupam Singh
  • 1,158
  • 13
  • 25