Problem with Web API controller that supports multiple gets with actions. API won't recognize optional ID, appears to think it's an action - webapi

I have seen this brought up on StackOverflow several times and have tried all the accepted answers with none of them working. I am trying to do a simple API that supports multiple GETs with actions. My problem is I can do a GET with a specific action, and I can do a GET with no action specified, but if I try to do a GET (with no action) but an OPTIONAL ID specified, I get an error which I will show below after I show you my code.
I started with the following code in my Global.asax:
RouteTable.Routes.MapHttpRoute(
name: "Api with action",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
So, I want to support multiple GETs to my SuppliersController, which has these methods:
// GET api/<controller>/<action>
[HttpGet]
public string GetCompanyNames()
// GET api/<controller>
[HttpGet]
[NonAction]
public List<string> Get()
and, finally:
// GET api/<controller>/5
[HttpGet]
[NonAction]
public string Get(int id)
It is this last GET, where I don't specify an action and just try to pass an ID, that I get this error:
No HTTP resource was found that matches the request URI 'https://localhost:44381/api/Suppliers/5'.
No action was found on the controller 'Suppliers' that matches the name '5'.
What I have tried:
The error seems to be indicating to me that even though I have a route mapping set up to match the pattern api/{controller}/{id}, the ID of "5" that I am specifying is being interpreted as an ACTION, not an ID. Research suggested that I add the [NonAction] attribute to the method, and I have done this, but it has not changed anything.
For the record, in my testing, I have found that the following URLs DO work and are correctly matching the patterns I have set up in my route mapping:
https://localhost:44381/api/Suppliers is correctly mapping to my default GET on the Suppliers API. It looks to be matching the pattern api/{controller}.
https://localhost:44381/api/Suppliers/GetCompanyNames is also working, it looks to be matching the pattern api/{controller}/{action}
I cannot for the life of me match the pattern api/{controller}/{id} The only time this ever worked was when I completely removed the routetemplate/mapping for api/{controller}/{action}/{id} from my config, but this means I can't use Actions on GETs.
Any idea what I am missing here?

Related

ASP.NET WebApi POST response URL or location

I wanted the result URL/Location to be: api/orders/15
But the code below give me back api/orders?id=15
What should I change to make this happen?
[Route("api/orders", Name = "CreateOrder")]
public IHttpActionResult Post([FromBody] Order order)
{
//...
return CreatedAtRoute("DefaultApi", new { newOrder.Id }, newOrder);
}
Here is routes defined under App_Start:
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new {id = RouteParameter.Optional});
It turns out that the attribute routing
[Route("api/orders", Name = "CreateOrder")] is the culprit. It was there because another action (HttpGet) has it "by accident." That overrides the convention based routing which would give the correct URL in response location.
After I delete the attribute routing on both actions, the response location is the correct format based on the convention based routing. This solves this problem. However, it would be nice to know in the case that attribute based routing is required, what should be the syntax.

Why are these routes being matched?

The following code:
<li>#Html.ActionLink(metaTapp.Nav_About, "Mayla", "About")</li>
<li>#Html.ActionLink(metaTapp.Nav_Support, "Support", "About")</li>
<li>#Html.ActionLink(metaTapp.Nav_Exchange, "Index", "Exchange")</li>
<li>#Html.ActionLink("Post Rfq", "Create", "Rfq")</li>
is producing the following links:
<li>About</li>
<li>Support</li>
<li>Exchange</li>
<li>Post Rfq</li>
My Global Application Start looks like this:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
UploadRouteConfig.RegisterRoutes(RouteTable.Routes);
LocalizationConfig.RegisterRoutes(RouteTable.Routes);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
DatabaseFactory.SetDatabaseProviderFactory(new DatabaseProviderFactory());
}
}
UploadRouteConfig.RegisterRoutes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Upload", "Upload/Image", null).RouteHandler = new UploadMvcRouteHandler();
}
LocalizationConfig.RegisterRoutes
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Account", // Route name
"Account/{action}", // URL with parameters
new { controller = "Account", action = "Index" } // Parameter defaults
);
routes.MapRoute(
"RfqCategory",
string.Format("{{{0}}}/Rfq/CategoryFilter/{{category}}", Constants.ROUTE_PARAMNAME_LANG),
new { controller = "Rfq", action = "CategoryFilter", category = Guid.Empty.ToString() }
);
routes.MapRoute(
Constants.ROUTE_NAME,
string.Format("{{{0}}}/{{controller}}/{{action}}/{{rfqid}}", Constants.ROUTE_PARAMNAME_LANG),
new { controller = "About", action = "Home", rfqid = "00000000-0000-0000-0000-000000000000" }
);
}
RouteConfig.RegisterRoutes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("TappDefault", "{controller}/{action}/{id}", new { controller = "About", action = "Home", id = UrlParameter.Optional }
);
I cant understand how upload is being matched to everything. If the route doesn't start with Upload/Image it should fall through to the localizationconfig routes?
Ok so the short answer to my problem is that I am rendering ActionLinks when I should be using RouteLinks. ActionLink will perform a match based on route table entries which seems like what I want but because I'm using a static Url:
"Upload/Image"
every url is matched. Why? Because routes are not filters. Routes work by matching supplied route values to the parameters of segments of the Url, but since:
"Upload/Image"
has no parameters i.e. {controller} then technically EVERYTHING is a match. RouteLink on tyhe pother hand allows me to specify which route to use when rendering the link:
#Html.RouteLink(
linkText: "route: Home",
routeName: "TappDefault",
routeValues: new {controller="About", action="Home"}
)
..
From Professional ASP.NET.MVC4 (Jon Galloway, Phil Haack, Brad Wilson, K. Scott Allen)
Page 232 Chapter 9 Routing:
Let’s suppose you add the following page route at the beginning of
your list of routes so that the URL /static/url is handled by the
page /aspx/SomePage.aspx:
routes.MapPageRoute("new", "static/url", "~/aspx/SomePage.aspx");
Note that you can’t put this route at the end of the list of routes
within the RegisterRoutes method because it would never match
incoming requests. Why wouldn’t it? Well, a request for /static/url
would be matched by the default route and never make it through
the list of routes to get to the new route. Therefore, you need to
add this route to the beginning of the list of routes before the
default route.
NOTE This problem isn’t specific to Routing with Web Forms. There are many cases where you might route to a non-ASP.NET MVC route
handler.
Moving this route to the beginning of the defined list of routes seems
like an innocent enough change, right? For incoming requests, this
route will match only requests that exactly match /static/url but will
not match any other requests. This is exactly what you want. But
what about generated URLs? If you go back and look at the result of
the two calls to Url.RouteLink, you’ll find that both URLs are broken:
/url?controller=section&action=Index&id=123
and
/static/url?controller=Home&action=Index&id=123
This goes into a subtle behavior of Routing, which is admittedly
somewhat of an edge case, but is something that people run into from
time to time.
Typically, when you generate a URL using Routing, the route values
you supply are used to “fill in” the URL parameters as discussed
earlier in this chapter.
When you have a route with the URL {controller}/{action}/{id}, you’re
expected to supply values for controller, action, and id when
generating a URL. In this case, because the new route doesn’t have
any URL parameters, it matches every URL generation attempt because
technically, “a route value is supplied for each URL parameter.” It
just so happens that there aren’t any URL parameters. That’s why all
the existing URLs are broken, because every attempt to generate a URL
now matches this new route.
This might seem like a big problem, but the fix is very simple. Use
names for all your routes and always use the route name when
generating URLs. Most of the time, letting Routing sort out which
route you want to use to generate a URL is really leaving it to
chance, which is not something that sits well with the
obsessive-compulsive control freak developer. When generating a
URL, you gener- ally know exactly which route you want to link to, so
you might as well specify it by name. If you have a need to use
non-named routes and are leaving the URL generation entirely up to
Routing, we recommend writing unit tests that verify the expected
behavior of the routes and URL generation within your application.
Specifying the name of the route not only avoids ambiguities, but it
may even eke out a bit of a per- formance improvement because the
Routing engine can go directly to the named route and attempt to use
it for URL generation.
In the previous example, where you generated two links, the following
change fixes the issue. We changed the code to use named parameters to
make it clear what the change was. #Html.RouteLink( linkText: "route:
Test", routeName: "test", routeValues: new {controller="section",
action="Index", id=123})

Handle collections inside collections in Web API using ASP .Net MVC4

I installed the new ASP .Net MVC4 beta on my machine and have been trying to understand how the Web API works. I built a demo for a single collection (ex. books). I followed the example on the asp.net website. I implemented my own methods for posting to a collections i.e. adding a new book, getting all books, getting a particular book, updating a book and deleting a book record. All this works fine.
Ex:
POST /books - adds a new book
GET /books - gets all books
GET /books/1 - get a particular book
PUT /books/1 - update a particular book
DELETE /books/1 - delete a particular book
Now I want to add another collection inside the books collection, say authors and want to implement the same POST, PUT, GET and DELETE calls for the new collection
I want the new calls to be something like this:
POST /books/1/authors - add a new author to a book
GET /books/1/authors - gets all authors of a book
GET /books/1/authors/a#a.com - get a particular author for a book
PUT /books/1/authors/a#a.com - update a particular author for a book
DLETE /books/1/authors/a#a.com - delete a particular author for a book
I am confused how to add a route to make this call work. By default I get this route with the project.
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
What is the right way to handle routes in this pattern for collections and associations between them?
Managing routes in Global can be confusing and error prone. Personally, I found the Attribute Routing package helps simplify routing configuration greatly. This article explains how to acquire and use it.
http://www.strathweb.com/2012/05/attribute-based-routing-in-asp-net-web-api/
I think Ken's method of using Attribute Routing is better, I just found about it from this post, and I myself probably will use that as well. But here is what I came up with before I knew about the AR.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "JustController",
routeTemplate: "api/Books/{bookId}/{category}/{categoryId}/{subCategory}",
defaults: new
{
controller = "books",
bookId= RouteParameter.Optional,
category = RouteParameter.Optional,
categoryId = RouteParameter.Optional,
subCategory = RouteParameter.Optional
},
constraints: new
{
bookId= #"\d*",
category= #"(|authors|pictures|videos)",
categoryId = #"\d*",
subCategory = #"(|comments)"
}
);
Then I was thinking of using the URL from the Request property in the Get, Post, Delete, Put functions, get the parameters I needed.
For example:
public class BooksController : ApiController
{
// GET api/books
public Book Get(int bookId)
{
var url = this.Request.RequestUri.ToString() // decide how to handle!

ActionLink 404 Error

Building my first ASP.NET MVC 3 application and trying to implement the ability to disassociate a given ice cream from a menu. Each has an integer identifier associated with it and on a page I display these mappings and provide a link by them to remove the ice cream from the menu.
I've got an ActionLink that looks like this:
#Html.ActionLink("Remove", "RemoveMenuIceCreamMapping", "IceCream", new { iceCreamId=item.IceCreamId, menuId=item.MenuId}, null)
In my IceCreamController I've got an Action that looks like this:
[HttpPost]
public PartialViewResult RemoveMenuIceCreamMapping(int iceCreamId, int menuId)
{
...
}
Did a little searching and believe I may need to modify the routes in the Global.asax.cs file's RegisterRoutes to handle these two parameters. So I tried this like so:
public static void RegisterRoutes(RoutesCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// I added this route in an attempt to handle the two parameters:
routes.MapRoute(
"RemoveMenuIceCreamMapping", // Route name
"IceCream/RemoveMenuIceCreamMapping/{iceCreamId}/{menuId}", // URLwith parameters
new
{
controller = "IceCream",
action = "RemoveMenuIceCreamMapping",
iceCreamId = UrlParameter.Optional,
menuId = UrlParameter.Optional
}
);
// this was there by default
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = UrlParameter.Optional }
};
}
But this doesn't work - I get a "The resource cannot be found." error, 404. Requested URL: /IceCream/RemoveMenuIceCreamMapping/1/10
1 is the Id of the IceCream and 10 is the menu's Id.
What I was expecting to happen was that the action RemoveMenuIceCreamMapping would get called, passing those two parameters, but I'm obviously not doing something right here and may just misunderstand how to accomplish what I want and be going about this the wrong way. Any guidance would be most appreciated.
Update
So, one more thing I've learned, after reading this SO question, my ActionLink isn't triggering a POST so removing the [HttpPost] from the action seemed like the right thing to do. And, in fact, as soon as I did that, the route was found and the action executed.
I think you problem is that the ActionLink uses an HTTP GET and you are only accepting HTTP POST.
You will probably need to change your view to issue an HTTP POST (e.g. with a regular HTML button inside a form) so that the verb that the browser sends matches with what you accept on the controller.

where to use route-name of routing in aspnet mvc

i'm new to routing in aspnet mvc.. i have following code:
Action Controller
public ActionResult SchoolIndex()
{
return View(SchoolRepository.GetAllSchools());
}
here is the routing
routes.MapRoute(
"School", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "School", action = "SchoolIndex", id = "" } ); // Parameter defaults
when i enter "localhost/school" in addressbar, it is giving 404 error instead it should route to my "schoolIndex" action
i have given route-name as "School" where it is used ?
You can't specify a route name in a URI, ever.
Route names are for, e.g., Html.RouteLink(). RouteLink allows you to specify exactly the route you want, even if it's not the first matching route for the arguments you pass.
URIs are matched in order. The first matching route wins.
Read my post on routing for much more detail.

Resources