I've just switched from AttributeRouting to WebApi 2.0 AttributeRouting, and have got a controller and action defined like so:
public class InvitesController : ApiController
{
[Route("~/api/invites/{email}")]
[HttpGet]
[ResponseType(typeof(string))]
public IHttpActionResult InviteByEmail(string email)
{
return this.Ok(string.Empty);
}
}
Example query:
GET: http://localhost/api/invites/test#foo.com
The response I receive is a 200, with empty content (due to string.Empty).
This all works fine -- but I want to change the email property to be a query parameter instead . So I update the controller to:
public class InvitesController : ApiController
{
[Route("~/api/invites")]
[HttpGet]
[ResponseType(typeof(string))]
public IHttpActionResult InviteByEmail(string email)
{
return this.Ok(string.Empty);
}
}
But now when querying the endpoint with:
GET: http://localhost/api/invites?email=test#foo.com
The response I receive is a 404:
{
"message": "No HTTP resource was found that matches the request URI 'http://localhost/api/invites?email=test#foo.com'.",
"messageDetail": "No route providing a controller name was found to match request URI 'http://localhost/api/invites?email=test#foo.com'"
}
Does anyone know why it doesn't match the route when the parameter is swapped to a query parameter, rather than inline of the url ?
As requested, WebApiConfig is defined like so:
public static void Register(HttpConfiguration config)
{
var jsonFormatter = config.Formatters.JsonFormatter;
jsonFormatter.Indent = true;
jsonFormatter.SerializerSettings.ContractResolver = new RemoveExternalContractResolver();
config.MapHttpAttributeRoutes();
}
Thanks !
I think you need to include the query parameter (along with its type) in the Route as follows:
[Route("api/invites/{email:string}")]
using it will be
POST: http://localhost/api/invites/test#foo.com
Alternatively if you want to name the query parameter:
[Route("api/invites")]
using it will be (as long as you have an email parameter in your method)
POST: http://localhost/api/invites?email=test#foo.com
As you comment in edhedges answer: the route template cannot start with a '/' or a '~', so you can remove that from the route as above
Issue is a clash of route definitions, which went unnoticed due to being cross-controller (and also some of the routes being 'absolute' (~/)). Below is an example that reproduces the result.
public class ValuesController : ApiController
{
[Route("~/api/values")]
[HttpGet]
public IHttpActionResult First(string email)
{
return this.Ok("first");
}
}
[RoutePrefix("api/values")]
public class ValuesTwoController : ApiController
{
[Route("")]
[HttpGet]
public IHttpActionResult Second(string email)
{
return this.Ok("second");
}
}
Issuing a request:
GET: http://localhost/api/values?email=foo
Will return a 404 with a response of:
{
"message": "No HTTP resource was found that matches the request URI 'http://localhost/api/values?email=foo'.",
"messageDetail": "No route providing a controller name was found to match request URI 'http://localhost/api/values?email=foo'"
}
What is misleading is the response message.
I think you need to change your Route declaration to this: [Route("~/api/invites?{email}")]
Here is a relevant link: http://attributerouting.net/#route-constraints
Related
I don't know why I get 406 error when I write my route like that "teste/teste":
[HttpGet("teste/teste", Name = nameof(GetAsync))]
[ApiConventionMethod(typeof(CommonApiConventions),
nameof(CommonApiConventions.Get))]
[Authorize(nameof(DefaultScopePolicy.read))]
while its work when I name my route only "teste/":
[HttpGet("teste/", Name = nameof(GetAsync))]
[ApiConventionMethod(typeof(CommonApiConventions),
nameof(CommonApiConventions.Get))]
[Authorize(nameof(DefaultScopePolicy.read))]
Why are you setting your route value by repeating the same name? Is teste the name of your controller? You should set your base route at the top of your controller declaration like that:
[Route("api/[controller]")]
[ApiController]
public class ControllerName
{
}
and then your method within the class
[HttpGet]
public function Get()
{
}
My FBMessageController has nothing but the following methods:
public string Get() { ... }
[ChildActionOnly]
public SendResponse Send(ComplexType msg) { ... }
[ChildActionOnly]
public SendResponse SendImage(string x, string y) { ... }
[HttpPost]
public SendResponse Post([FromBody]AnotherComplexType yyy) { ... }
public void Put(..) { ... }
public void Delete(..) { ... }
However, when I tried to send a request using POST to .../api/fbMessage,
I get the following exception:
"Mutliple actions were found that match the request"
WebApiConfig.Register contains the default code:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
What is causing the error?
If HTTP method attribute is not defined specifically, default is POST. So Send() is also considered as post method and the exception occurs because more than one action were found.
You can try installing Debug Router tool to get visual understanding of how the controller and action were selected. Here's a link
Though you seem to have already isolated the issue, I did the following integration test to recreate your problem and help explain what is causing the error.
[TestClass]
public class FBMessageControllerTests {
[TestMethod]
public async Task HttpClient_Should_Get_OKStatus_From_Post_To_FBMessage() {
using (var server = new TestServer()) {
var config = server.Configuration;
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var handlerMock = new Mock<IExceptionHandler>();
handlerMock
.Setup(m => m.HandleAsync(It.IsAny<ExceptionHandlerContext>(), It.IsAny<System.Threading.CancellationToken>()))
.Callback<ExceptionHandlerContext, CancellationToken>((context, token) => {
var innerException = context.ExceptionContext.Exception;
Assert.Fail(innerException.Message);
});
config.Services.Replace(typeof(IExceptionHandler), handlerMock.Object);
var client = server.CreateClient();
string url = "http://localhost/api/fbMessage";
var body = new { body = "Hello World" };
using (var response = await client.PostAsJsonAsync(url, body)) {
var message = await response.Content.ReadAsStringAsync();
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, message);
}
}
}
[Authorize]
public class FBMessageController : ApiController {
public string Get() {
return null;
}
[System.Web.Mvc.ChildActionOnly]
public SendResponse Send(ComplexType test) {
return null;
}
[System.Web.Mvc.ChildActionOnly]
public SendResponse SendImage(string x, string y) {
return null;
}
[HttpPost]
public SendResponse Post([FromBody]AnotherComplexType body) {
return null;
}
public void Put(string gbody) {
return;
}
public void Delete(string body) {
return;
}
}
public class SendResponse { }
public class ComplexType { }
public class AnotherComplexType { }
}
Which produced the following result message:
Multiple actions were found that match the request:
Send on type MiscUnitTests+FBMessageControllerTests+FBMessageController
Post on type MiscUnitTests+FBMessageControllerTests+FBMessageController
The above message tells you exactly what caused the problem. The framework usually tells you why you got certain errors. Sometimes you have to know where to look.
Referencing the following Routing and Action Selection in ASP.NET Web API, here is why you got the problem.
here is the action selection algorithm.
Create a list of all actions on the controller that match the HTTP request method.
If the route dictionary has an "action" entry, remove actions whose name does not match this value.
Try to match action parameters to the URI, as follows:
For each action, get a list of the parameters that are a simple type, where the binding gets the parameter from the URI.
Exclude optional parameters.
From this list, try to find a match for each parameter name, either in the route dictionary or in the URI query string. Matches are
case insensitive and do not depend on the parameter order.
Select an action where every parameter in the list has a match in the URI.
If more that one action meets these criteria, pick the one with the most parameter matches.
Ignore actions with the [NonAction] attribute.
Step #3 is probably the most confusing. The basic idea is that a
parameter can get its value either from the URI, from the request
body, or from a custom binding. For parameters that come from the URI,
we want to ensure that the URI actually contains a value for that
parameter, either in the path (via the route dictionary) or in the
query string.
For example, consider the following action:
public void Get(int id)
The id parameter binds to the URI. Therefore, this action can only
match a URI that contains a value for "id", either in the route
dictionary or in the query string.
Optional parameters are an exception, because they are optional. For
an optional parameter, it's OK if the binding can't get the value from
the URI.
Complex types are an exception for a different reason. A complex type
can only bind to the URI through a custom binding. But in that case,
the framework cannot know in advance whether the parameter would bind
to a particular URI. To find out, it would need to invoke the binding.
The goal of the selection algorithm is to select an action from the
static description, before invoking any bindings. Therefore, complex
types are excluded from the matching algorithm.
After the action is selected, all parameter bindings are invoked.
Summary:
The action must match the HTTP method of the request.
The action name must match the "action" entry in the route dictionary, if present.
For every parameter of the action, if the parameter is taken from the URI, then the parameter name must be found either in the route
dictionary or in the URI query string. (Optional parameters and
parameters with complex types are excluded.)
Try to match the most number of parameters. The best match might be a method with no parameters.
Hope this helps you understand Why you got the “Multiple actions were found …” message.
Happy coding!!!
Can you try to decorate all your action with attribute like this to see if anything change
[HttpGet]
public string Get() { ... }
[ChildActionOnly]
public SendResponse Send(..) { ... }
[ChildActionOnly]
public SendResponse SendImage(..) { ... }
[HttpPost]
public SendResponse Post([FromBody]xxx yyy) { ... }
[HttpPut]
public void Put(..) { ... }
[HttpDelete]
public void Delete(..) { ... }
May be your Put and Post are conflicting somehow.
I have a controller in my web api. Let's call it TimeController.
I have a GET action and a PUT action. They look like this:
public class TimeController : ApiController
{
[HttpGet]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK, DateTime.UtcNow);
}
[HttpPut]
public HttpResponseMessage Put(int id)
{
_service.Update(id);
return Request.CreateResponse(HttpStatusCode.OK);
}
}
I also have a route config as follows:
routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional });
so I can access it in a restful manner.
Now I also want to version the GET action using a custom Route attribute. I'm using code very similar to what Richard Tasker talks about in this blog post.
(the difference being that I use a regular expression to get the version from the accept header. Everything else is pretty much the same)
So my controller now looks like this:
public class TimeController : ApiController
{
private IService _service;
public TimeController(IService service)
{
_service = service;
}
[HttpGet, RouteVersion("Time", 1)]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.Ok, DateTime.UtcNow);
}
[HttpGet, RouteVersion("Time", 2)]
public HttpResponseMessage GetV2()
{
return Request.CreateResponse(HttpStatusCode.Ok, DateTime.UtcNow.AddDays(1));
}
[HttpPut]
public HttpResponseMessage Put(int id)
{
_service.Update(id);
return Request.CreateResponse(HttpStatusCode.OK);
}
}
However, now when I try to access the PUT endpoint I'm getting a 404 response from the server. If I step through the code in debug mode, I can see that the RouteVersion attribute is being fired, even though I haven't decorated the action with it.
If I add the attribute to the PUT action with a version of 1, or I add the built in Route attribute like this: Route("Time") then it works.
So my question is: why is the attribute firing even though I haven't decorated the action with it?
Edit: Here is the code for the attribute:
public class RouteVersion : RouteFactoryAttribute
{
private readonly int _allowedVersion;
public RouteVersion(string template, int allowedVersion) : base(template)
{
_allowedVersion = allowedVersion;
}
public override IDictionary<string, object> Constraints
{
get
{
return new HttpRouteValueDictionary
{
{"version", new VersionConstraint(_allowedVersion)}
};
}
}
}
public class VersionConstraint : IHttpRouteConstraint
{
private const int DefaultVersion = 1;
private readonly int _allowedVersion;
public VersionConstraint(int allowedVersion)
{
_allowedVersion = allowedVersion;
}
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
if (routeDirection != HttpRouteDirection.UriResolution)
{
return true;
}
int version = GetVersionFromHeader(request) ?? DefaultVersion;
return (version == _allowedVersion);
}
private int? GetVersionFromHeader(HttpRequestMessage request)
{
System.Net.Http.Headers.HttpHeaderValueCollection<System.Net.Http.Headers.MediaTypeWithQualityHeaderValue> acceptHeader = request.Headers.Accept;
var regularExpression = new Regex(#"application\/vnd\.\.v([0-9]+)",
RegexOptions.IgnoreCase);
foreach (var mime in acceptHeader)
{
Match match = regularExpression.Match(mime.MediaType);
if (match.Success)
{
return Convert.ToInt32(match.Groups[1].Value);
}
}
return null;
}
}
Edit2: I think there is some confusion so I've updated the Put action to match the route config
So my question is: why is the attribute firing even though I haven't decorated the action with it?
It is clear from both the way your question is phrased "when I try to access the PUT endpoint" and the fact that it matches the GET action (and then subsequently runs its constraint) that you have not issued a PUT request to the server. Most browsers are not capable of issuing a PUT request, you need a piece of code or script to do that.
Example
using (var client = new System.Net.WebClient())
{
// The byte array is the data you are posting to the server
client.UploadData(#"http://example.com/time/123", "PUT", new byte[0]);
}
Reference: How to make a HTTP PUT request?
I think its because of your action signature in combination with the default route
In your default route you specify the Id attribute as optional, however in your action you use the parameter days, in this case the framework can't resolve it. you either have to add it as a query string parameter eg:
?days={days}
Or change the signature to accept id as input.
Since it can't resove the action with days in the url it will return a 404
Personally i don't use the default routes and always use Attribute routing to prevent this kinda behavior
So my question is: why is the attribute firing even though I haven't decorated the action with it?
Any controller methods that do not have a route attribute use convention-based routing. That way, you can combine both types of routing in the same project.
Please see this link :
attribute-routing-in-web-api-2
Also as method is not decorated with route attribute, When the Web API framework receives an HTTP request, it tries to match the URI against one of the route templates in the routing table. If no route matches, the client receives a 404 error. That is why you are getting 404
Please see this one as well : Routing in ASP.NET Web API
I have been scratching my head for a whole day since I cannot figure out what is wrong in my code.
First, I have a working controller as this one:
namespace MyProject.Controllers {
[RoutePrefix("api/Account")]
public class AccountController : ApiController {
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(AccountModel model) {
...
return Ok();
}
}
}
However, when I add a new controller and introduce a new route, problem arises.
The new controller is as follows:
namespace MyProject.Controllers {
[RoutePrefix("api/Admin")]
public class AdminController : ApiController {
[AllowAnonymous]
[Route("Encrypt")]
public IHttpActionResult Encrypt(string clientSecret) {
...
return Ok();
}
}
}
Then I make the request via Postman like this:
Unfortunately, an error is returned:
{
"message": "No HTTP resource was found that matches the request URI 'http://localhost/api/admin/encrypt'."
}
I am using the default route mapping configuration defined in WebApiConfig.cs:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Can anyone shed some light on it?
UPDATE
If I send the POST request in this way, I can get the Ok() response successfully.
I have a question now. Why can I only pass the parameter via the URL but not with a form object?
To call with the parameter, include parameter in route attribute and add POST verb as follows:
[Route("Encrypt/{clientSecret}")]
[HttpPost]
public IHttpActionResult Encrypt(string clientSecret) {
...
return Ok();
}
Then call as follows:
http://localhost/api/admin/encrypt/123456
However convention for post methods is without the parameter as follows:
[Route("Encrypt")]
[HttpPost]
public IHttpActionResult Encrypt() {
...
return Ok();
}
}
Then call as follows:
http://localhost/api/admin/encrypt
If you do not want to add the POST verb, you can prefix the methods with the POST verb as follows:
public IHttpActionResult PostEncrypt(string clientSecret)
public IHttpActionResult PostEncrypt()
I suggest the slight change in your route definition:
api/{controller}/{action}/{id}
The reason, imo, is that Web api actions default to the standard accept verbs.
Then you can name your api action methods anyhow, only decorate them with the relevant accept attribute.
I have two API endpoints defined as such:
[Route("create")]
[HttpPost]
[ResponseType(typeof(User))]
public async Task<IHttpActionResult> CreateUser(User user)
[Route("login")]
[HttpPost]
[ResponseType(typeof (User))]
public async Task<IHttpActionResult> Login(string email, string password)
And the controller defined as
[RoutePrefix("api/users")]
public class UserController : ApiController
When I call it with this information (both in plain chrome, my app and the Postman application)
POST /api/users/login HTTP/1.1
Host: mysite.azurewebsites.net
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
email=somemail&password=somepw
I receive a 404:
{
"Message": "No HTTP resource was found that matches the request URI 'http://mysite.azurewebsites.net/api/users/login'.",
"MessageDetail": "No action was found on the controller 'User' that matches the request."
}
It does work for another route which I can call with /api/users/1:
[Route("{id:int}")]
[HttpGet]
[ResponseType(typeof(User))]
public async Task<IHttpActionResult> GetUser(int? id)
Can't I explicitly define such endpoints? I tried creating a custom route but this made no difference (I placed this before the default route and after calling config.MapHttpAttributeRoutes()).
config.Routes.MapHttpRoute(
name: "Login",
routeTemplate: "api/users/login",
defaults: new { controller = "User", action = "Login" }
);
Note that explicitly defining the route as [Route("~api/users/login")] didn't work either.
I have also noticed that routes in my other controller don't seem to work anymore. More specifically I have these definitions:
[RoutePrefix("api/movies")]
public class MovieController : BaseController
[Route("~api/genres")]
[HttpGet]
[ResponseType(typeof(IEnumerable<Genre>))]
public IHttpActionResult GetGenres()
[Route("~api/genres/{id:int}")]
[HttpGet]
[ResponseType(typeof(IEnumerable<MovieResult>))]
public IHttpActionResult GetMoviesForGenre(int id)
[Route("{id:int}")]
[HttpGet]
[ResponseType(typeof(Movie))]
public IHttpActionResult GetMovieDetails(int id)
Of these options, only a call to /api/movies/16 succeeds, the others return
No type was found that matches the controller named 'genres'.
Is there something elementary I'm overlooking?
I have made the genre routes available again by changing them to genres and genres/{id:int} and adding this route
config.Routes.MapHttpRoute(
name: "test",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
but I would assume that this shouldn't be necessary. For some reason the request to /api/movies/genres works while /api/users/login doesn't. I did notice that creating a GET method with URI /api/users/genres DOES work so I believe it must have something to do with that. Why won't it find my POST-methods?
It looks like there are lots of moving pieces here so it's hard to figure out exactly what will fix all the problems. But here are a couple of issues to address...
Web API (unlike MVC) can only have one parameter read from the request body. So to make your Login action work, try to create a LoginInfo class...
public class LoginInfo
{
public string email { get; set; }
public string password { get; set; }
}
And change the Login method to...
public async Task<IHttpActionResult> Login([FromBody]LoginInfo loginInfo)
The issues with the genres appear to be the incorrect usage of ~ in the attribute routing (should be ~/). Try...
[Route("~/api/genres")]
and
[Route("~/api/genres/{id:int}")]