hcd
hcd

Reputation: 1388

Adding a new method to WebAPI gives "multiple actions" error

I have an existing WebAPI 2 project that has the current routing :

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

The controllers consist of a generic basecontroller and derived controllers per "entity type" that implement the routes :

[GET] api/{entity}/  <- returns an overview list of entities
[GET] api/{entity}/{id} <-  returns the full entity + details
[POST] api/{entity}/{id} <- saves the entity
[DEL] api/{entity}/{id} <- deletes the entity
[POST] api/{entity}/ <- creates a new entity
[POST] api/{entity}/{id}/{function} <- performs a function on an entity (eg. recalculate, send orders,..) 

Now i want to add a new method to my basecontroller to be able to get the "count" for an overviewlist. So basically

[GET] api/{entity}/count

I've added the following route to the webapi config :

config.Routes.MapHttpRoute
(
    name: "count",
    routeTemplate: "api/{controller}/", defaults: new { action = "count" }
);

and added a method to my controller:

[HttpGet]
public async Task<int> Count()
{
    return 5;//just a fixed testvalue
}

If i now browse to /api/{entity}/count , i get the value "5" returned. But the problem is that the overviewlist /api/{entity}/ is no longer working. It says :

ExceptionMessage: "Multiple actions were found that match the request"

I've tried paying around with the "Route" attribute and and the order of the routes, but I cannot get it as I want (which is: everything working like before + the addition of the "count" in the API). I've also looked around on SO and found threads like How to add custom methods to ASP.NET WebAPI controller? but I still can't get working :(

Any idea ?

Thnx.

Upvotes: 2

Views: 861

Answers (3)

hcd
hcd

Reputation: 1388

After a great deal of searching around I've cooked up the following solution that doesn't need any new routes and is more "RESTfull" according to my searches.

I've opted for the /api{entity}?count query parameter and passing the count in a response header "X-Total-Count".

But the problem is, my exisitng GET method already returns a Generic IList which I cannot just change without breaking the API. I can also not just return an "object" which is a list or and int, depending on whether the "count" queryparameter is supplied, because that breaks the Swagger documentation (it no longer sees the returntype)

I've made a new class CountList :

public interface IHasCount { int TotalCount { get; } }

public class CountList<T> : List<T>, IHasCount
{
    public int TotalCount { get; set; }

    public CountList(int count)
    {
        TotalCount = count;
    } 

    public CountList(IList<T> list )
    {
        this.AddRange(list);
        this.TotalCount = list.Count;
    }
}

A nice side-effect of inheriting fromù the List is that the extra information is stripped off during json-serialization so I can safely return a Countlist instead of a List, and existing clients won't now the difference !

Then for extracting the count out of the Countlist and put it into a response header, I made a small action filter:

public class GetCountFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        base.OnActionExecuted(actionExecutedContext);
        var counter = (actionExecutedContext.ActionContext.Response.Content as ObjectContent)?.Value as IHasCount;
        if (counter != null)
            actionExecutedContext.ActionContext.Response.Headers.Add("X-Total-Count", counter.TotalCount.ToString());
    }
}

And then I've decorated my Get method :

    [HttpGet]
    [GetCountFilter]
    public async Task<CountList<T>> GetOverview()
    {
        //special case, we only need the count !
        if (ServerContext.QueryFilter.CountOnly) //custom object that parses the queryParameters
        {
            //todo, but out of scope here,, make a real Count method in the manager that actually executes a count query instead of fetching the whole list
            var count = (await _entityManager.GetOverview()).Count; 
            var result = new CountList<T>(count);
            return result;
        }
        //return the full list
        return new CountList<T>( await _entityManager.GetOverview());
    }

So , as an endresult, when a client invokes /api/{entity} he gets the overview as usual, with the addidtion of the X-Total-Count header filled in. And when he invokes /api/{entity}?count, he gets an empty list, but the total count is still in the header !

This does the trick for me ! If I'm completely not seeing the elephant in the room, please do tell me !

Upvotes: 0

bviale
bviale

Reputation: 5375

The new route you configured for your count action enter in conflict with [GET] api/{entity}/ when /count is not specified.

You can remove your new route and use the default /api/{controller}/{action} which will call your count method when you call the url /api/{entity}/count

Upvotes: 0

Mihail Stancescu
Mihail Stancescu

Reputation: 4138

This is because /{id}/{function} are defined as optional parameters to the API route and both routes matches the url provided.

There is no need to define another route for that just define the method in the controller and decorate it with [HttpGet] attribute and you should be fine.

Upvotes: 1

Related Questions