I have a very simple endpoint as an example
/users/
to obtain all users, wich returns an array of N elements (maybe 0 elements if no users)
this is implemented on one specific method
[HttpGet]
[Route("api/users")]
public void getUsers() { ... }
But then, i need to get a user by id, so i would need another api enpoint. The question is, should i implement this on the same method, or a separate one ?
[HttpGet]
[Route("api/users")]
[Route("api/users/{id:int}")]
public void getUsers(int? id) { ... }
In this case, my return is different, a single user or a NotFound instead of an array.
So i was wondering if this should be implemented as a single method (and reuse code) or 2 different methods.
2 different methods in case if response is going to be different. It looks like one method is to get all users details and another is to get specific user details based on user id.
Related
Current situation
[HttpGet]
public HttpResponseMessage CheckProfileStatus(int id)
{
//check profile status from third party
return //profile status and HttpStatus;
}
[HttpGet]
public HttpResponseMessage ProcessTheProfile(int profileId)
{
//check profile status from third party again, if status is "Good", do something
return //someMessage and HttpStatus;
}
Url to call CheckProfileStatus method
myUrl/api/Profile/123
Url to call ProcessTheProfile method
myUrl/api/Profile/?profileId=123
I hope the comments in the code makes the situation clear.
I don't really like the fact that I have two HttpGet methods with same signature in one controller (even though it works).
I don't know if it is best practice.
Question:
Should I extract these two methods in two seperate controllers, is it completely fine to have two HttpGet methods with same signature in one controller or is there better way to handle this situation?
First, this is confusing to any client of the API.
You have 2 GET methods which are virtually identical. In fact most people prefer to add a route covering your first option which basically sets the profileId to the value after the forward slash.
Second, the expectation is that when you issue a GET against an entity with an ID, you get the representation of that entity. This is not what's happening here.
You need to decide which kind of API do you want. A generic one where stuff like this is fine :
myUrl/api/profile/process/123 or process/profile, or whatever else makes sense to the API.
If your API is supposed to be RESTful ( which basically means you will have one method per HTTP verb and nothing more ) then you need to add a new controller for ProcessProfile and then your url can look like this :
myUrl/api/ProcessProfile/123
None of these options is set in stone, they are just that, options.
The comments to the OP thread give some good options as well, which should be considered.
I have a controller marked with [Route("api/entities")]. There is a method for getting all entities:
[Audit(ActionType.EntityList)] // custom annotation for audit
[Authorize]
[HttpGet]
public IActionResult GetEntities()
{
// ...
}
As you can see, by using annotations I save the request to some audit and authorize the request only to allowed users.
Now, I want to enhance this endpoint so it can return the top N entities. Example request: /api/entities?top=5. I have found that I should use an optional parameter for the method and use if to detect the case.
However, I need to save such call in audit as differnt type (e.g. [Audit(ActionType.EntityTop)]) and I do not need an authorization there (everyone can fetch the top entities).
How can I map the /api/entities request to one method and /api/entities?top=N to another? In Spring from Java I would use a params field of #RequestMapping.
I do not want to change the URL of this endpoint, because the top parameter modifies only the list that is being returned so one should use GET parameters for that. By having the same URL I also do not change the semantic meaning of the response (it is still list of the same entities). It is important when using RESTful frontend framework like Restangular.
Are you sure you need same action? It looks like you want to solve different tasks here, so I'd suggest you to create another action
[Audit(ActionType.EntityTop)]
[HttpGet("top/{top:int}")] //you can specify route via Annotations
public IActionResult GetEntities(int top)
{
// ...
}
Request with top entities will match /api/entities/top/5
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.
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.
What I'm trying to do with allow someone to post either:
[{...}, {...}] or {...} to a route and for it to be correctly bound to:
... Post(IEnumerable<MyModel> models)
I've got it working fine if I post a collection ([{...}, {...}] but I would like it to create a collection with one object when I also post valid json as single object ({...})
The reason I'm trying to do this is because our API works dynamically against a model that is defined by the user at runtime and so I don't know if the uri represents a list or a single resource.
I could achieve this with a customer IModelBinder but I was wondering if there is a way of getting the jsonserializer to deal with this use case without any custom code?
One solution would be to have two API methods call a private method that performs the same logic. For example:
public HttpResponseMessage Post(IEnumerable<MyModel> models)
{
return DoSomething(models);
}
public HttpResponseMessage Post(MyModel model)
{
return DoSomething(new List<MyModel> { model });
}
private HttpResponseMessage DoSomething(IEnumerable<MyModel> models)
{
// Do something
}
Web API would figure out which one gets called based on the parameters that get passed in, but both would share the same code under the covers.
Update: If your parameters are coming from the body, there are some solutions described here: How to work with ASP.Net WebApi overloaded methods?