I'm quite familiar with MVC, but fairly new to WebAPI and I've run into a confusing issue.
I have a controller (Which inherits from ApiController) called "DummyController" and it's got the 5 default scaffolded methods for get,post,put and delete (2 for get), and I've addd my own method at the bottom called "FindDummyObjects()" which I've decorated with the [HttpGet] attribute.
when I navigate to "api/dummy" or "api/dummy/get", I get the default result fo the 2 string objects ("value1" and "value2").
However, when I navigate to "api/dummy/FindDummyObjects", it complains that "The parameters dictionary contains a null entry for parameter 'id'".
This means that it's not pointing to my Action at all (As it is parameterless), so it's probably pointing to the default "Get(int id)" action.
When I comment out all actions except my own, I get the results I expect.
So my question is this, with WebAPI, is it only possible to have 1 action per http verb with a certain set of parameters, regardless of whether the action's names differ?
For example, it seems as though it will not be possible for me to 10 different http GET actions in a single controller, unless they all have different parameters and use the action name "Get" (Unless I do some custom routing I suppose).
Is that correct?
Code:
// GET api/dummy
public IEnumerable Get()
{
return new string[] { "value1", "value2" };
}
// GET api/dummy/5
public string Get(int id)
{
return "value";
}
// POST api/dummy
public void Post([FromBody]string value)
{
}
// PUT api/dummy/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/dummy/5
public void Delete(int id)
{
}
[HttpGet]
public IEnumerable<Models.DummyObject> FindDummyObjects()
{
IList<Models.DummyObject> myDummyList = new List<Models.DummyObject>();
for (int i = 0; i < 10; i++)
{
Models.DummyObject dumObj = new Models.DummyObject();
dumObj.ObjectId = i;
dumObj.ObjectName = string.Empty;
myDummyList.Add(dumObj);
}
return myDummyList;
}
Web API routing has quite a few holes (I'm being polite), you happened to hit on one. This is one of the reasons that they introduced Attribute Routing in Web API2. You might want to try that as it is quite a bit more flexible.
Related
We have an IRouteConstraint that is getting checked much more than it should. Upon further testing, it looks like that Order on [Route] gets ignored by route constraints.
For example, if I have the following constraint:
public class TestConstraint : IRouteConstraint {
public bool Match(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
) {
Debug.WriteLine("TestConstraint");
return true;
}
}
And wire it up:
constraintResolver.ConstraintMap.Add("testConstraint", typeof(TestConstraint));
And have the following routes:
public partial class HomeController {
[Route("test/0", Order = 1)]
public ActionResult Test0() {
return Content("Test0");
}
[Route("{someParam}/{test:testConstraint}", Order = 10)]
public ActionResult Test1() {
return Content("Test1");
}
}
And then make a request for http://localhost/test/0, it will return the proper content (Test0), but TestContraint.Match() is still executed.
I would think that route constraints are only executed once the route is encountered in the RouteTable, but it seems to run it on every request that can match the [Route] pattern.
If it makes a difference, we are on ASP.NET MVC v5.2.4.
In ASP.NET MVC pipeline, the stage of routing and the stage of selection of invoked controller action are separated. On routing stage you can't just select the first matching action and stop further lookup. Found action (strictly speaking method) could be filtered on later stage. For example it may not satisfy applied action selectors (e.g. NonAction attribute).
That's why basic action selection algorithm is the following:
Pass Request URL through configured routes and select all matching actions.
Pass all matching actions through action selectors, filter out non matching.
Order candidate actions by routing order.
Now there are following options:
No matching actions found. Request result to 404 error.
Multiple matching actions share the same highest order. Exception is thrown ("The current request is ambiguous between the following action methods ...").
Exactly one matching action have highest order (or one action matches at all). The action is selected and executed.
If you are interested in correspondig ASP.NET MVC source code, here are some references:
IRouteConstraint.Match() is invoked by ProcessConstraint() method in System.Web.Routing.Route. The nearest method in call stack, which operates on route collection level, is GetRouteData() method in System.Web.Mvc.Routing.RouteCollectionRoute class:
Here is its source code:
public override RouteData GetRouteData(HttpContextBase httpContext)
{
List<RouteData> matches = new List<RouteData>();
foreach (RouteBase route in _subRoutes)
{
var match = route.GetRouteData(httpContext);
if (match != null)
{
matches.Add(match);
}
}
return CreateDirectRouteMatch(this, matches);
}
As you see, the loop does not break when matching route is found.
The code that applies action selectors, performs ordering and chooses action candidate resides in DirectRouteCandidate.SelectBestCandidate() (source code):
public static DirectRouteCandidate SelectBestCandidate(List<DirectRouteCandidate> candidates, ControllerContext controllerContext)
{
Debug.Assert(controllerContext != null);
Debug.Assert(candidates != null);
// These filters will allow actions to opt-out of execution via the provided public extensibility points.
List<DirectRouteCandidate> filteredByActionName = ApplyActionNameFilters(candidates, controllerContext);
List<DirectRouteCandidate> applicableCandidates = ApplyActionSelectors(filteredByActionName, controllerContext);
// At this point all of the remaining actions are applicable - now we're just trying to find the
// most specific match.
//
// Order is first, because it's the 'override' to our algorithm
List<DirectRouteCandidate> filteredByOrder = FilterByOrder(applicableCandidates);
List<DirectRouteCandidate> filteredByPrecedence = FilterByPrecedence(filteredByOrder);
if (filteredByPrecedence.Count == 0)
{
return null;
}
else if (filteredByPrecedence.Count == 1)
{
return filteredByPrecedence[0];
}
else
{
throw CreateAmbiguiousMatchException(candidates);
}
}
I'm currently developing a Web API and I'm figuring out about how to add a new method inside my controller FilmsController which has to execute a LINQ query simply returning the related JSON to the user. Everything seems correct but when I try to call that API an error 404 appears. The API I'm trying to call is api/NewFilms, which should be correct.
Here is the method GetNewFilms inside FilmsController:
public IQueryable<Film> GetNewFilms()
{
var query = from f in db.Films
orderby f.ReleaseYear descending
select f;
return query;
}
// GET: api/Films
public IQueryable<Film> GetFilms()
{
return db.Films;
}
With the default routing configuration, web api controller allows to have only one GET action (without any parameters). If you have more than one GET actions, you will get a 500 error with message like
Multiple actions were found that match the request
If you need to have more than one GET actions, you may explicitly define a route pattern for those using Attribute routing.
public class FilmsController : ApiController
{
[Route("api/Films/NewFilms")]
public IEnumerable<string> GetNewFilms()
{
return new List<string> { "New Film 1","New Film 1"};
}
// GET: api/Films
public IEnumerable<string> GetFilms()
{
return new List<string> { "Film 1","Film 2"};
}
public string GetFilm(int id)
{
return "A single film";
}
}
Also, you may consider changing your return type from IQueryable to IEnumerable of your Dto ( instead of the entity class created by your ORM)
So I have three different Get actions:
public IHttpActionResult Get(){
//get all entries
}
public IHttpActionResult Get(int id){
//get an entry based on an ID
}
public IHttpActionResult Get(int page =0, int pageSize = 2){
//Pagination:get entries by pages of 2 entries
}
At first I was working with the two first methods it was fine using this routing:
config.Routes.MapHttpRoute("Default",
"api/{controller}/{id}",
defaults: new { controller = "project", id = RouteParameter.Optional }
);
After adding the third Get action that has two parameters, it started returning an exception: Multiple actions were found that match the request. By the way, I was sending the page parameter as a query string like this: api/project/?page=0.
I do understand that the two last actions are the source of the problem and the router can't decide which one to match but I haven't been able to come up with the right routing function.
Although, I have used attribute routing which kind of solves the issue: Route["api/project/{page:int}/{pageSize:int}"] I am more interested in using the old routing way to solve this issue.
I apologize for the lengthy message and thank you in advance for your time.
Try changing this
public IHttpActionResult Get(int page =0, int pageSize = 2){
//Pagination:get entries by pages of 2 entries
}
for
public IHttpActionResult Get(int? page, int pageSize = 2){
//Pagination:get entries by pages of 2 entries
}
We're using ASP.Net Web API to generate a feed and it includes the ability to do paging.
myfeed.com/afeed?page=2
My boss says "let's also allow users to use 'paged', because that's what WP uses." In addition, we're also using pageIndex in some of our older feeds. So what I'd like to do is accept all three.
myfeed.com/afeed?page=2
myfeed.com/afeed?paged=2
myfeed.com/afeed?pageIndex=2
I'd like to do is be able to write a clean Web API method, such as
public Foo Get(int page = 1)
{
//do some stuff
return foo;
}
without cluttering the method with page 'plumbing'. So I tried creating an ActionFilter
public override void OnActionExecuting(HttpActionContext actionContext)
{
object pageParam = new object(); //query["page"]
if (pageParam == null)
{
var altPageParam = GetPageParamUsingAlternateParams(actionContext);
if (altPageParam != null){}
//SetPageParam here
}
base.OnActionExecuting(actionContext);
}
private object GetPageParamUsingAlternateParams(HttpActionContext actionContext)
{
object result = new object();
object pageIndexParam = new object(); //Query["pageIndex"]
object pagedParam = new object(); ////Query["paged"]
if (pagedParam != null)
result = pagedParam;
else if (pageIndexParam != null)
result = pageIndexParam;
return result;
}
I didn't finish. As I was looking for the best way to get the query params, I stumbled into a big mistake!
OnActionExecuting is executed after int page = 1. Sure, I could override it in an ActionFilter, but that would lead to confusion down the road. I really want to be able to do a simple flow through the URI query parameters that goes from
page -> paged -> pageIndex -> default value in method
I have found a lot of articles on custom binding to a an object. Also, I found articles about "parameter binding", however those dealt with FromUri and FromBody. I didn't find anything that I felt had a direct parallel to what I'm facing.
You could achieve what you want by defining 3 different GET method with parameters matched with the query segment of the Url like the code snippet below:
public class ProductsController : ApiController
{
//Matched api/products?page=1
public IHttpActionResult Get(int page)
{
return GetPagedData(page);
}
//Matched api/products?paged=1
public IHttpActionResult GetPaged(int paged)
{
return GetPagedData(paged);
}
//Matched api/products?pagIndex=1
public IHttpActionResult GetPageIndex(int pageIndex)
{
return GetPagedData(pageIndex);
}
//Do the real paging here
private IHttpActionResult GetPagedData(int page =1)
{
return Ok("Data Pages");
}
}
I have an ASP.NET MVC application where certain resources are addressed like this:
/controller/action/id?revision=123
The revision parameter is optional:
if it is missing I do a 302 redirect to the latest revision. I want this redirection response to be cached only for a short while, or not at all.
if it is present, I want to cache the response for a long time because any given revision of the resource is immutable.
My first attempt was to do something like this:
[OutputCache(Duration=10,Location=OutputCacheLocation.Server)]
public Action(string id)
{
long lastRevision = GetLastRevision(id);
return RedirectToAction("Action",
new { Id = id, revision = lastRevision });
}
[OutputCache(Duration=int.MaxValue,Location=OutputCacheLocation.Server)]
public Action(string id, long revision)
{
// ...
}
Unfortunately, the ASP.NET MVC routing doesn't seem to like method overloads. It expects to have a single Action method with an optional parameter instead (i.e. long? revision), but then I can't specify different caching policies for both cases.
How can I chose a different caching policy based on the presence of the query string here?
You could write a custom method selector:
public class RevisionMethodSelectorAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var revision = controllerContext.Controller.ValueProvider.GetValue("revision");
var hasRevisionParam = methodInfo.GetParameters().Any(p => string.Equals("revision", p.Name, StringComparison.OrdinalIgnoreCase));
if (revision != null && !string.IsNullOrEmpty(revision.AttemptedValue) && hasRevisionParam)
{
return true;
}
if ((revision == null || string.IsNullOrEmpty(revision.AttemptedValue)) && !hasRevisionParam)
{
return true;
}
return false;
}
}
and then decorate the 2 actions with it:
[RevisionMethodSelector]
public ActionResult MyAction(string id)
{
long lastRevision = GetLastRevision(id);
return RedirectToAction("MyAction", new { id = id, revision = lastRevision });
}
[RevisionMethodSelector]
[OutputCache(Duration = int.MaxValue, Location = OutputCacheLocation.Server, VaryByParam = "revision")]
public ActionResult MyAction(string id, long revision)
{
...
}
The first action is not cached. It will be picked up if there's no revision parameter in the request and it will simply redirect to the second action. The second action is cached for a very long time, this cache is made to vary according to the revision parameter value (which you didn't have) and will be picked by the custom method selector if a revision parameter is present in the request.
It turns out that I had already solved this problem without realizing it by making use of 302 redirects: apparently 302 responses are not cached even if you have an OutputCache attribute on your controller method!
Therefore both cases can be handled by a single controller method with the [OutputCache(...)] attribute specifying what to do for 200 responses.
Though this now begs the question of what to do if you do want to cache a 302...