WebAPI controller same action different parameters - c#

I have a base controller as with 2 actions:
[ActionName("Find")]
[HttpGet]
public virtual IHttpActionResult Find(string name)
{
return null;
}
[ActionName("Find")]
[HttpGet]
public virtual IHttpActionResult Find(int number)
{
return null;
}
Some of my controllers use the different Find method, for example:
public override IHttpActionResult Find(string number)
{
return OK;
}
However, I get an error when calling this action from the client:
Multiple actions were found that match the request: \r\nFind on type API.Controllers.CustomerController
How can I solve this problem?

The only way to solve this is by changing the ActionName attribute for one of the actions.
ASP.NET MVC /Web API doesn't support two actions with the same name and same HTTP verb in the same controller.
Also take a look at this question (ASP.NET MVC ambiguous action methods) if you want to go for the 'hack solution' (my opinion).

Why don't you just pass both parameters to the same action method? And inside your method simply check if they are null and do something about it.
Use string and int? (nullable int) to allow both parameters to contain nulls.
This way you get to use one view without any attribute jiggery pockery.

I think, you should reconsider your endpoint structure:
An action that selects one element from a resource collection should do that along the key of the resource (i.e. the unique database key). This key can be either of type int or alphanum, but not both.
What you probably want to realize with one or both of your finds, is to establish a filter function. Filter parameters should be passed to REST endpoints as query string parameters.
Examples:
/api/employees → returns resource set with all employees
/api/employees/5 → returns single resource (one employee)
/api/employees?name=john → returns resource set with all employees named "john"
Example 3 is a filter, and I guess at least one of your finds is just that.

Related

Resolve ambiguous route issue

I wish to implement 2 get routes with definition as:
GET /products - gets all products.
GET /products?name={name} - finds all products matching the specified name.
The code that I am using is:
[HttpGet] public IActionResult GetProducts()
and
[HttpGet] public IActionResult GetProductByName([FromQuery(Name = "name")] string name)
Can I achieve it?
Why not turn these into two separate endpoints one that is something like [HttpGet("Products")] and another endpoint that is [HttpGet("Products/{name}]"?
That way you can call your API like
.../Products and get all your Products back or .../Products/Name and get specific product information. You could also combine these into one endpoint [HttpGet("Products/{name}] and then return all your Products if name is null or empty.

How to create multiple WebApi enpoints in ASP.NET Core

So i need to create three Endpoints that are -
/api/cities
/api/cities/:id
/api/cities?country=
The first endpoint should be when I type in -
www.mywebsite/api/cities
The second endpoint should be when I type in -
www.mywebsite/api/cities/1
The third endpoint should be when I type in -
www.mywebsite/api/cities?country=canada
I tried the following:
[HttpGet("")]
public async Task<IActionResult> GetCities() {/*...*/} // Should Get All Cities from DB
[HttpGet("{id:int}", Name="GetCityById")]
public async Task<IActionResult> GetCityById([FromRoute]int id) {/*...*/} // Should Get just one city from DB
[HttpGet("{country:alpha}")]
public async Task<IActionResult> GetCitiesByCountry([FromQuery]string country) {/*...*/} // Should get all cities from country
First two endpoints work well, but when I try to get all the cities from a country that I pass with query parameter, I seem to trigger the first endpoint that is /api/cities instead of /api/cities?country=
Before I get to the solution, you have one problem in your code. Defining [HttpGet("{country:alpha}")] on your last action is specifying a route template in which you expect a country route parameter to exist in the path portion of the URL (e.g. https://www.example.org/i/am/a/path). However, as you're marking string country with the FromQuery attribute, it will only bind country from the query string, and not the path.
Specifying the route template also means that when you send a request to /api/cities?country=blah it will never match your GetCitiesByCountry action, because it's expecting it to be in the form of /api/cities/country, as that's what you specified by using the route template. Therefore, the first thing you need to do is to change your last action to:
[HttpGet]
public async Task<IActionResult> GetCitiesByCountry([FromQuery] string country)
Now that's out of the way, it still won't work. The problem comes down to this: only the path portion of a request's URL is considered when selecting an action. That means, now that we've removed the route template for GetCitiesByCountry, a request of /api/cities, or /api/cities?country=blah, will return a server 500 error, because both GetCities and GetCitiesByCountry are a match for those requests, and the framework doesn't know which one to execute.
To allow this to work, we need to use what's called an action constraint. In this case, we want to specify that GetCities should only be considered a match if there is no query string present in the request. To do that, we can inherit from ActionMethodSelectorAttribute:
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Routing;
public class IgnoreIfRequestHasQueryStringAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
return !routeContext.HttpContext.Request.QueryString.HasValue;
}
}
And then decorate GetCities with it:
[HttpGet]
[IgnoreIfRequestHasQueryString]
public async Task<IActionResult> GetCities()
If we now send a request to /api/cities?country=blah, GetCities will be excluded from selection because the request has a query string, and so GetCitiesByCountry will execute just as we want. If we send a request to /api/cities, however, we might expect that we still get the ambiguous action selection error, because the request does not contain a query string and so it would seem both GetCities and GetCitiesByCountry are still a match for the request. But the request does actually succeed, and runs GetCities as we want.
The reason for this can be summed up by the following remark on the IActionConstraint type, which ActionMethodSelectorAttribute itself implements:
Action constraints have the secondary effect of making an action with a constraint applied a better match than one without. Consider two actions, 'A' and 'B' with the same action and controller name. Action 'A' only allows the HTTP POST method (via a constraint) and action 'B' has no constraints. If an incoming request is a POST, then 'A' is considered the best match because it both matches and has a constraint. If an incoming request uses any other verb, 'A' will not be valid for selection due to it's constraint, so 'B' is the best match.
That means because we defined a custom constraint of IgnoreIfRequestHasQueryString, which succeeds in matching /api/cities, the GetCities method is considered to be the preferred choice, and so there is no ambiguity.

Conflicting routes in ASP.NET Core when using Attribute based routing

Context
I am trying to build an ASP.NET Core web API controller that will expose the following methods with specific semantics:
/api/experimental/cars — get entire collection
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> AllCars()
/api/experimental/cars/123 — get car by id "123"
[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)
/api/experimental/cars?nameFilter=Maz — get cars that match nameFilter = "Maz"
[HttpGet("/api/experimental/cars/{nameFilter?}")]
public Task<List<Car>> CarsByNameFilter([FromQuery] string nameFilter = "")
/api/experimental/cars?nameFilter=Maz&rating=2 — get cars that match nameFilter = "Maz" and with rating greater or equal to 2
[HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
public Task<List<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
Note: I really want to keep the controller class clean and have a single method per Web API route — is it possible?
Problem
As you could guess, there's an issue with these API definitions. Basically, AllCars is intercepting pretty much all the requests. (When I was able to get at least the /api/experimental/cars/{carId} working, the query-string based APIs were still not working and intercepted by another method...
I tried many possible route syntaxes to express what I want with no luck. Is it even possible to use the default routing mechanism or I need to implement my own Router class or Middleware or something else?
Update 1: Problem definition
I know I can join at least three methods and their routes into a single WebAPI method that is being smart about the received parameters. Notice that this is exactly what I am trying to avoid.
Why?
Reason 1: I saw that in non-.NET routers, it worked well and there's no technical impossibility to implement semantic based route resolution.
Reason 2: I perceive all four URL patterns mentioned above as four different routes. One may not agree with me and it's okay, but for my purposes the methods and the routes are different and have to stay different.
Reason 3.1: This keeps controller code clean. Every method only handles one specific case. Parameter names are sufficient to properly resolve the routes (at least in humans head, therefore machine can do it too -- it's easy to formalize the algorithm). If client make a request with an unsupported query parameter, it should result in HTTP 404 Not Found or HTTP 400 Bad Request -- totally fine (client rather construct correct URLs).
Reason 3.2: On contrary, if I join the methods and use a more generic route, my implementation needs to be 'smart' about the combination of parameters. This is effectively, a leak of routing abstractions into a layer where it does not belong in my architecture. Complex validation is another thing I don't want to see in the Controller -- less code is better.
Update 2: Nancy — Another .NET example (other that .NET Core WebApi)
There is Nancy (a .NET framework) which perfectly deals with this aspect of routing: https://github.com/NancyFx/Nancy/wiki/Defining-routes#pattern The issue is that in my project we're not using it... Nancy works as a perfect example of a tool that leaves exact definition of routing semantics to the client, instead of enforcing too tight rules on what is the route vs what is not.
You could Achieve this with just two routes:
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> SearchCars([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
and
[HttpGet("/api/experimental/cars/{carId}")]
public Task<Car> CarById([FromRoute] string carId)
I.e one route which brings the entire set back but can be filtered accordingly and one the brings back a single Car object by Id.
You will notice that the SearchCars method doesn't include the parameters in the route, FromQuery will catch these anyway.
EDIT:
if your request becomes complex it can be nice to define a custom request object type to wrap all your filters together:
public class MyRequestObject
{
public string NameFilter {get;set;}
public int Rating {get;set;}
}
then:
[HttpGet("/api/experimental/cars/")]
public Task<List<Car>> SearchCars([FromQuery] MyRequestObject requestParams)
Take a look at the following suggested routes that when tested do not conflict with each other and still allow for all the actions to be segregated.
[Route("api/experimental/cars")]
public class CarsController : Controller {
//GET api/experimental/cars
[HttpGet("")]
public IActionResult AllCars() { ... }
//GET api/experimental/cars/123
[HttpGet("{carId}")]
public IActionResult CarById(string carId) { ... }
//GET api/experimental/cars/named/Maz
//GET api/experimental/cars/named?filter=Maz
[HttpGet("named/{filter?}")]
public IActionResult CarsByNameFilter(string filter = "") { ... }
//GET api/experimental/cars/filtered?rating=2&name=Maz
//GET api/experimental/cars/filtered?rating=2
//GET api/experimental/cars/filtered?name=Maz
[HttpGet("filtered")]
public IActionResult CarsByNameAndRatingFilter(string name = "", int rating = 1) { ... }
}
My experience with this topic tells me that the best way to implement the APIs I wanted is to have two methods:
class CarsController {
// [HttpGet("/api/experimental/cars/")]
[HttpGet("/api/experimental/cars/{carId}")]
public Task<IEnumerable<Car>> CarById([FromRoute] string carId)
{
if (carId == null)
return GetAllCars();
else
return GetCarWithId(carId);
}
// [HttpGet("/api/experimental/cars/{nameFilter?}")]
[HttpGet("/api/experimental/cars/{nameFilter?}/{rating?}")]
public Task<IEnumerable<Car>> CarsByNameAndRatingFilter([FromQuery] string nameFilter = "", [FromQuery] int rating = 1)
{
// TODO Validate the combination of query string parameters for your specific API/business rules.
var filter = new Filter {
NameFilter = nameFilter,
Rating = rating
};
return GetCarsMatchingFilter(filter);
}
}
The first API is almost trivial. Even though returning a single item within a wrapping collection object may not look nice, it minimizes the number of API methods (which I personally am fine with).
The second API is trickier: in a way, it works as the façade pattern. I.e. that API will respond to pretty much all the possible /api/experimental/cars? based routes. Therefore, we need to very carefully validate the combination of received arguments before doing the actual work.

Asp net Web Api: Overload Api Controller Parameter with List of Object?

I have an Api controller with the following method:
[Route("{app}")]
public IHttpActionResult Put(string app, Property setting){ //do stuff }
I want to over load it with:
[Route("{app}")]
public IHttpActionResult Put(string app, Property[] settings){
foreach (Property property in settings)
{
Put(app, property);
}
return Ok("Settings Updated");
}
This overload causes a routing conflict. I don't want to change the route. How can I achieve what I want?
You're using attribute routing here with two put requests. Firstly I'd suggest to use a [FromBody] atrribute before the Property[] settings parameter here.
Based on the way you used Route Attributes here points to the same action where you have multiple assigned already. So I can suggest two things here.
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection.
Just use the second action that you mentioned that takes Property[] settings and just check whether the array is empty or not and you really don't have to worry about one setting change or more as essentially it's the same underneath. :)
And as you're using Asp.net Web Api 2 this could contain important information.

Same Name For Action Methods With Different Return Types

I've two action methods in my controller, both are called on POST request but have different return type:
public JsonResult AJAXCreate()
public string AJAXCreateNSave([Bind(Exclude = "Id, OrderItems")]Order order)
When I rename the second one to AJAXCreate, it is not called at all. I want to use same name for both action methods.
Much like operations in WCF services, no two actions can have the same name, unless they target different HTTP verbs, e.g.:
public ActionResult MyAction() { }
[HttpPost]
public ActionResult MyAction(MyModel model) { }
If you try and use two actions with the same name, MVC doesn't know which action to select.
Overloaded methods are not allowed in ASP.NET MVC without an attribute to specify a different action name. Check out this similar question and answer: Can you overload controller methods in ASP.NET MVC?

Categories

Resources