4

There seems to be a thousand people asking the same question on stack overflow, but there doesn't seem to be a single solution to this problem. I am going to ask it again...

I have an API controller that has the following actions:

    // 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;
    }

In my APIConfig I have defined the following routes:

        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+" }
        );

I am finding that I can access the following actions no problem: /api/exploitation /api/exploitation/getbysongwriterid/1 /api/exploitation/getbysongid/1

When I try to access /api/exploitation/1 I get this exception

"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.

Can anyone see what is wrong with my routes? Or wrong with something else?

3 Answers 3

6

I have found an elegant solution to the problem.

I modified my ApiRouteConfig to have the following routes:

        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 }
        );

Now I can access:

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

I did not need to modify my controller actions to work with this new routing config at all.

If you had multiple PUT or POST actions you could the create new routes that looked as follows:

    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) }
    );

I hope that this answer helps everyone as this seems to be a common issue that people are having.

Sign up to request clarification or add additional context in comments.

1 Comment

This is almost perfect. What happens when the ID needs to be a GUID?
0

The problem you have is that /api/exploitation/1 falls under:

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

All of your GET methods satisfy that routing as well particularly because {id} is optional and their controller is the same.

So you have one HTTP GET request from the client and multiple methods that accept GET requests. It doesn't know which one to go to.

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

I hope that answers your question.

1 Comment

thank you for the response, but it does not really answer my question, it just tells me why this doesn't work. Do you have any ideas as to how to make this work? I assume that this is possible no?
0

Try the following in your route definition. Keep only the following route:

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

Make the first Get method private, modify the second one so that id has a default value:

// 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
}

This way (I haven't tested it myself) I expect that:

  • api/Exploitation/ will map to api/Exploitation/Get(as default action) with id = 0 as default param
  • api/Exploitation/1 will map to api/Exploitation/Get/1 so it will call Get(1)
  • api/Exploitation/someOtherAction/345 will call the correct action method

This might work. A tighter route definition could actually be like this:

  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+" }
    );

But something along these lines... give it a try I hope it solves your problem.

2 Comments

Thank you for your response, however, I am really not happy with how inelegant that having to call another get action from another is. I have a solution that is quite elegant and will be posting it now.
No problem at all, I'm curious to see it. Only to clarify something: the solution I posted doesn't call an action from an action. Once the first method turns private, it's not an action anymore.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.