Why can't Web API Core 2 tell these apart?
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values?name=dave
[HttpGet]
public string Get(string name)
{
return $"name is {name}";
}
Here's what happens -
Both http://localhost:65528/api/values and http://localhost:65528/api/values?name=dave cause the first Get() method to execute.
This exact code works fine in Web Api 2.
I know multiple ways of getting around this, but I don't know why it happens.
Can someone explain why this has changed?
I don't think you can even compile your code in ASP.NET Core Mvc 2.0 since you have 2 actions mapped to same route [HttGet] api/values:
AmbiguousActionException: Multiple actions matched.
Remember, ASP.NET Web API uses the HTTP verb as part of the request to figure which action to call. Although it uses conventional routing (you name your actions Get, Post, Put and Delete, etc) if you don't have route attribute specify, I would highly recommend to always use routing attribute to annotate your controllers and actions.
Api Design time
Now it's up to you to design the route, as a developer. Remember the route is supposed to be a Uri that can identify a resource / resources.
Use the name as identifier along with the route
[Route("api/[controller]")]
public class CustomersController : Controller
{
// api/customers
[HttpGet]
public IActionResult Get()
{
...
}
// api/customers/dave
[HttpGet("{name:alpha}")] // constraint as a string
public IActionResult GetByName(string name)
{
...
}
}
Use the name as filter, against the resource collection
[Route("api/[controller]")]
public class CustomersController : Controller
{
// api/customers
// api/customers?name=dave
[HttpGet]
public IActionResult Get(string name)
{
...
}
}
To confuse you more
api/customers/dave will still execute GetById first!
[Route("api/[controller]")]
public class CustomersController : Controller
{
[HttpGet]
public IActionResult Get()
{
...
}
[HttpGet("{name}")]
public IActionResult GetByName(string name)
{
...
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
...
}
}
Both methods GetByName and GetById are potential candidates but MVC picks GetById method first because MVC compares the method/template name {name} and {id} through case-insensitive string comparison, and i comes before n.
That's when you want to put constraints.
[Route("api/[controller]")]
public class CustomersController : Controller
{
[HttpGet]
public IActionResult Get()
{
...
}
// api/customers/dave
[HttpGet("{name:alpha}")]
public IActionResult GetByName(string name)
{
...
}
// api/customers/3
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
...
}
}
You can also specify the Ordering too!
[Route("api/[controller]")]
public class CustomersController : Controller
{
[HttpGet]
public IActionResult Get()
{
...
}
// api/customers/portland
[HttpGet("{city:alpha}", Order = 2)]
public IActionResult GetByCity(string city)
{
...
}
// api/customers/dave
[HttpGet("{name:alpha}", Order = 1)]
public IActionResult GetByName(string name)
{
...
}
// api/customers/3
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
...
}
}
Without the Order, the method GetByCity will be in favor than GetByName because character c of {city} comes before the character n of {name}. But if you specify the order, MVC will pick the action based on the Order.
Sigh the post is too long....
Because in your case the best match in the route pipeline is the default httpget attribute (the one that get all). The query is a regular string so if you don't ask what you want from the query the best match is still the one that get all.
[HttpGet]
public string Get([FromQuery]string name)
{
return $"name is {name}";
}
The [FromQuery] is pointing to the key "name" in the query string to get the value.
Your should read Routing in asp.net core
Related
I tried to do some simple thing. I wanted to have different action name and different method name:
public class SuperController: Controller
{
[HttpGet("dosth")]
public IActionResult DoSomethingWithThoseParameters(int id, string token)
{
}
}
Look that there is no Route attribute on the controller.
In such case tag helper asp-action works perfectly. But I thought that my action dosth would be placed in: localhost/Super/dosth
But it was not. So I figured it out that I probably should set the route for the whole controller, like this:
[Route("[controller]")]
public class SuperController: Controller
{
[HttpGet("dosth")]
public IActionResult DoSomethingWithThoseParameters(int id, string token)
{
}
public IActionResult Register()
{
return View();
}
}
But now asp-action stopped working. For example:
<a asp-controller="Super" asp-action="Register">
creates anchor to: localhost/Super and not to: localhost/Super/Register
When I remove Route tag from controller it works again.
My mappings are configured as standard says:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
So, how come asp-action does not work when there is a Route attribute on the whole controller
Applying [Route] attribute on a controller enables attribute routing for all controller methods. So, by doing so you force yourself to provide the route for every method (in one way or another).
With [Route("[controller]")] the base route template for your controller actions is just the controller name, so if you have multiple actions without [Route] or [HttpGet] (and other HTTP verbs) attributes applied on them:
[Route("[controller]")]
public class SuperController: Controller
{
public IActionResult DoSomethingWithThoseParameters(int id, string token)
{
}
public IActionResult Register()
{
return View();
}
}
...you get yourself a AmbiguousMatchException because multiple controller actions will be matching same route:
/Super
You can either explicitly specify the route for every action:
[Route("[controller]")]
public class SuperController: Controller
{
[HttpGet("dosth")]
public IActionResult DoSomethingWithThoseParameters(int id, string token)
{
}
[HttpGet("Register")]
public IActionResult Register()
{
return View();
}
}
or specify action name as an part of expected route already on controller level:
[Route("[controller]/[action]")]
public class SuperController : Controller
{
public IActionResult DoSomethingWithThoseParameters(int id, string token)
{
}
public IActionResult Register()
{
return View();
}
}
then, you don't have to specify the routes for actions, because you applied a route template on a controller level. Your actions will inherit that route template.
But, be aware that in order to overwrite it you'll have to overwrite it all, otherwise you'll append to the route template.
[HttpGet("/[controller]/dosth")]
public IActionResult DoSomethingWithThoseParameters(int id, string token)
{
}
Read more about routing in official documentation.
I'm working on app with recipes.
I'm wrote delete recipe method and this worked fine but then had to add method which delete photo from recipe.
Working on Core 3.1.1 with Api's
I'm getting this error when I try to have 2 "Delete" methods
My code:
namespace RecipesApp.API.Controllers
{
[Authorize]
[Route("api/users/{userId}/recipes/{recipeId}/photos")]
[Route("api/users/{userId}/recipes")]
[Route("api/[controller]")]
[ApiController]
public class RecipesController : ControllerBase
{
// http://localhost:5000/api/users/{userId}/recipes/{id}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteRecipe(int userId, int id)
{
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeletePhoto(int recipeId, int id)
{
}
The Route annotation must be unique on the class definition, and is mainly used to prefix the controller path. The Http* annotations go on the controller public methods, their value represents the unique path assigned to each one.
Combining both (Route and Http*) you will get the full template path assigned to your method. According to this, your code must look like this:
namespace RecipesApp.API.Controllers
{
[Authorize]
[Route("/api/users/{userId}/[controller]")]
[ApiController]
public class RecipesController : ControllerBase
{
// /api/users/{userId}/recipes/{id}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteRecipe(int userId, int id)
{
//
}
// /api/users/{userId}/recipes/{id}/photos
[HttpDelete("{id}/photos")]
public async Task<IActionResult> DeletePhoto(int recipeId, int id)
{
//
}
}
}
I am playing around with the idea of having a base controller that uses a generic repository to provide the basic CRUD methods for my API controllers so that I don't have to duplicate the same basic code in each new controller. But am running into problems with the routing attribute being recognized when it's in the base controller. To show exactly what the problem I'm having I've created a really simple WebAPI controller.
When I have a Get method in the main Controller and it inherits from the ApiController directly I don't have any problems and this works as expected.
[RoutePrefix("admin/test")]
public class TestController : ApiController
{
[Route("{id:int:min(1)}")]
public string Get(int id)
{
return "Success";
}
}
When I move the Get method into a base controller it is returning the contents of the 404 page.
[RoutePrefix("admin/test")]
public class TestController : TestBaseController
{
}
public class TestBaseController : ApiController
{
[Route("{id:int:min(1)}")]
public string Get(int id)
{
return "Success";
}
}
Some more interesting notes:
I can access the action at GET /Test/1. So it is finding it based on the default route still.
When I try to access POST /admin/test, it returns the following JSON
{
"Message":"No HTTP resource was found that matches the request URI 'http://test.com/admin/test'.",
"MessageDetail":"No type was found that matches the controller named 'admin'."
}
Does anyone know of a way to get the routing to work with attributes from a base controller?
Attribute routes cannot be inherited. This was a deliberate design decision. We didn't feel right and didn't see valid scenarios where it would make sense to inherit them.
Could you give a more realistic scenario as to where you would want to use this?
[Update(3/24/2014)]
In the upcoming 5.2 release of MVC Web API, there is going to be an extensibility point called System.Web.Http.Routing.IDirectRouteProvider through which you can enable the inheritance scenario that you are looking for here. You could try this yourself using the latest night builds(documentation on how to use night builds is here)
[Update(7/31/2014)]
Example of how this can be done in Web API 2.2 release:
config.MapHttpAttributeRoutes(new CustomDirectRouteProvider());
//---------
public class CustomDirectRouteProvider : DefaultDirectRouteProvider
{
protected override IReadOnlyList<IDirectRouteFactory>
GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
{
// inherit route attributes decorated on base class controller's actions
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>
(inherit: true);
}
}
Using Web API 2.2, you can:
public class BaseController : ApiController
{
[Route("{id:int}")]
public string Get(int id)
{
return "Success:" + id;
}
}
[RoutePrefix("api/values")]
public class ValuesController : BaseController
{
}
config.MapHttpAttributeRoutes(new CustomDirectRouteProvider());
public class CustomDirectRouteProvider : DefaultDirectRouteProvider
{
protected override IReadOnlyList<IDirectRouteFactory>
GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
{
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>
(inherit: true);
}
}
as outlined here: http://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-22
Got it.
[Route("api/baseuploader/{action}")]
public abstract class BaseUploaderController : ApiController
{
[HttpGet]
public string UploadFile()
{
return "UploadFile";
}
}
[Route("api/values/{action}")]
public class ValuesController : BaseUploaderController
{
[HttpGet]
public string Get(int id)
{
return "value";
}
}
One caveat here is that the route action paramter must be the same as the action name. I could not find a way to get around that. (You cannot rename the route with a RouteAttribute)
I have a base request type..
class RequestBase
{
public string inputId;
public string derivedid;
}
and types that inherit ..
class RequestA : RequestBase
{
public string name;
}
and
class RequestB : RequestBase
{
public string color;
}
I have a webapi service, some actions take an input parameter of RequestA, some take RequestB
[HttpPost]
[MyFilter]
[ActionName("Process1")]
public HttpResponseMessage Process1(RequestA request)
{
//do something with request.derivedId
}
[HttpPost]
[MyFilter]
[ActionName("Process2")]
public HttpResponseMessage Process2(RequestB request)
{
//do something with request.derivedId
}
I have an actionfilter that takes the inputId from the request and generates a derivedId
public override void OnActionExecuting(HttpActionContext actionContext)
{
RequestBase request = (RequestBase)actionContext.ActionArguments["request"];
string inputId = request.inputId;
string derivedId = inputId + "123";
// ?? somehow inject derivedId back into the actionContext so that my controller methods can access?
}
As my comment states above, I'd like to populate the derivedId field and have it accessible to my controller methods.
Thanks in advance
There's a few solutions to this problem already described in this thread - one of them should suit you:
ASP.NET MVC Pass object from Custom Action Filter to Action
Is it possible to pass an ViewModel object to WebApi controller action instead of separate params?
Instead of using:
public class ContactsController : ApiController
{
public IEnumerable<Contact> GetContacts(string p1, string p2)
{
// some logic
}
}
I would like to use:
public class ContactsController : ApiController
{
public IEnumerable<Contact> GetContacts(TestVM testVM)
{
// some logic
}
}
public class TestVM
{
public string P1 { get; set; }
public string P2 { get; set; }
}
This doesn't seem to work for me. When I call /api/contacts/?P1=aaa&P2=bbb the testVM object doesn't get populated (null).
Also, I would like the TestVM to have valdiation attribtues defined and use ModelState.IsValid in my API controller.
Unless told otherwise WebApi will deserialise complex models using the content/body of the request. To tell WebApi to use the Url to construct the model you need to specify the [FromUri] attribute:
public IEnumerable<Contact> GetContacts([FromUri]TestVM testVM)
{
// some logic
}
I know it's kind of late to post another answer but I thought it could be useful for anyone who uses .net core as a web API service
public IEnumerable<Contact> GetContacts([FromQuery]TestVM testVM)