MVC Web Api routing Multiple Gets that have different parameters - asp.net-mvc

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
}

Related

MVC routing a repeatable pattern?

A design goal for a website I'm working on is to keep the URL in the browser in a state where the user can copy it, and the link can be used from another browser/user/machine to return to the spot that the url was copied. (The actual changes will happen via AJAX, but the URL will change to reflect where they are.)
Example: If you were on the customer page looking at customer 123, and had details pulled up on their order #456, and full details on line 6 of this order, your url could simply be /customer/123/456/6
The challenge comes with a second feature: Users can add UI columns (analogous to adding a new tab in a tab view, or a new document in an MDI app) Each column can easily generate a routable url, but I need the url to reflect one or more columns. (E.G. User has both /customer/123/456/6 and /customer/333/55/2 in two side by side columns)
In a perfect world, I'd like the url to be /customer/123/456/6/customer/333/55/2 for the above scenario, but I don't know if MVC routing can handle repetitive patterns, or, if so, how it is done.
Can this be done via routing? If not is there a way to get this type of one-or-more functionality from Url?
You could create a custom route handler (see my previous answer) or derive from a RouteBase like NightOwl888 suggested. Another approach would be to simply use a model binder and a model binder attribute.
public class CustomerInvoiceLineAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new CustomerInvoiceLineModelBinder();
}
}
public class CustomerInvoiceLineModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var path = (string)bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
var data = path.Split(new[] { "/customer/" }, StringSplitOptions.RemoveEmptyEntries);
return data.Select(d =>
{
var rawInfo = d.Split('/');
return new CustomerInvoiceLine
{
CustomerId = int.Parse(rawInfo[0]),
InvoiceId = int.Parse(rawInfo[1]),
Line = int.Parse(rawInfo[2])
};
});
}
}
You define your route by specifying a star route data. This mean that the route parameter will contains everything following the action
routes.MapRoute(
name: "CustomerViewer",
url: "customer/{*customerInfo}",
defaults: new { controller = "Customer", action = "Index" });
Then in your controller, you bind your parameter with the same name as the star route parameter using the custom model binder defined above:
public ActionResult Index([CustomerInvoiceLine] IEnumerable<CustomerInvoiceLine> customerInfo)
{
return View();
}
You will need to add validation during the parsing and probably security too, so that a customer cannot read the invoice of other customers.
Also know that URL have a maximum length of 2000 characters.
You can do this with the built-in routing as long as you don't anticipate that any of your patterns will repeat or have optional parameters that don't appear in the same segment of the URL as other optional parameters.
It is possible to use routing with optional parameters by factoring out all of the permutations, but if you ask me it is much simpler to use the query string for this purpose.
NOTE: By definition, a URL must be unique. So you must manually ensure your URLs don't have any collisions. The simplest way to do this is by matching the page with the path (route) and adding this extra information as query string values. That way you don't have to concern yourself with accidentally making routes that are exactly the same.
However, if you insist on using a route for this purpose, you should probably put your URLs in a database in a field with a unique constraint to ensure they are unique.
For the most advanced customization of routing, subclass RouteBase or Route. This allows you to map any URL to a set of route values and map the route values back to the same URL, which lets you use it in an ActionLink or RouteLink to build the URLs for your views and controllers.
public class CustomPageRoute : RouteBase
{
// This matches the incoming URL and translates it into RouteData
// (typically a set of key value pairs in the RouteData.Values dictionary)
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Trim the leading slash
var path = httpContext.Request.Path.Substring(1);
if (/* the path matches your route logic */)
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = "MyController";
result.Values["action"] = "MyAction";
// Any other route values to match your action...
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
// This builds the URL for ActionLink and RouteLink
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
if (/* all of the expected route values match the request (the values parameter) */)
{
result = new VirtualPathData(this, page.VirtualPath);
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
}
Usage
routes.Add(
name: "CustomPage",
item: new CustomPageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Manipulate the url using routing

In my website I have the following route defined:
routes.MapRoute(
name: "Specific Product",
url: "product/{id}",
defaults: new { controller = "", action = "Index", id = UrlParameter.Optional }
);
In that way I want customers to be able to add the ID of the product and go to the product page.
SEO advisors have said that it would be better if we could add a description of the product on the URL, like product-name or something. So the URL should look something like:
/product/my-cool-product-name/123
or
/product/my-cool-product-name-123
Of course the description is stored in the db and I cannot do that with a url rewrite (or can I?)
Should I add a redirection on my controller (this would seem to do the job, but it just doesn't feel right)
On a few sites I checked they do respond with a 301 Moved Permanently. Is that really the best approach?
UPDATE
As per Stephen Muecke's comment I checked on what is happening on SO.
The suggested url was my own Manipulate the url using routing and i opened the console to see any redirections. Here is a screenshot:
So, first of all very special thanks to #StephenMuecke for giving the hint for slugs and also the url he suggested.
I would like to post my approach which is a mix of that url and several other articles.
My goal was to be able to have the user enter a url like:
/product/123
and when the page loads to show in the address bar something like:
/product/my-awsome-product-name-123
I checked several web sites that have this behaviour and it seems that a 301 Moved Permanently response is used in all i checked. Even SO as shown in my question uses 301 to add the title of the question. I thought that there would be a different approach that would not need the second round trip....
So the total solution i used in this case was:
I created a SlugRouteHandler class which looks like:
public class SlugRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var url = requestContext.HttpContext.Request.Path.TrimStart('/');
if (!string.IsNullOrEmpty(url))
{
var slug = (string)requestContext.RouteData.Values["slug"];
int id;
//i care to transform only the urls that have a plain product id. If anything else is in the url i do not mind, it looks ok....
if (Int32.TryParse(slug, out id))
{
//get the product from the db to get the description
var product = dc.Products.Where(x => x.ID == id).FirstOrDefault();
//if the product exists then proceed with the transformation.
//if it does not exist then we could addd proper handling for 404 response here.
if (product != null)
{
//get the description of the product
//SEOFriendly is an extension i have to remove special characters, replace spaces with dashes, turn capital case to lower and a whole bunch of transformations the SEO audit has requested
var description = String.Concat(product.name, "-", id).SEOFriendly();
//transform the url
var newUrl = String.Concat("/product/",description);
return new RedirectHandler(newUrl);
}
}
}
return base.GetHttpHandler(requestContext);
}
}
From the above i need to also create a RedirectHandler class to handle the redirections. This is actually a direct copy from here
public class RedirectHandler : IHttpHandler
{
private string newUrl;
public RedirectHandler(string newUrl)
{
this.newUrl = newUrl;
}
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext httpContext)
{
httpContext.Response.Status = "301 Moved Permanently";
httpContext.Response.StatusCode = 301;
httpContext.Response.AppendHeader("Location", newUrl);
return;
}
}
With this 2 classes i can transform product ids to SEO friendly urls.
In order to use these i need to modify my route to use the SlugRouteHandler class, which leads to :
Call SlugRouteHandler class from the route
routes.MapRoute(
name: "Specific Product",
url: "product/{slug}",
defaults: new { controller = "Product", action = "Index" }
).RouteHandler = new SlugRouteHandler();
Here comes the use of the link #StephenMuecke mentioned in his comment.
We need to find a way to map the new SEO friendly url to our actual controller. My controller accepts an integer id but the url will provide a string.
We need to create an Action filter to handle the new param passed before calling the controller
public class SlugToIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var slug = filterContext.RouteData.Values["slug"] as string;
if (slug != null)
{
//my transformed url will always end in '-1234' so i split the param on '-' and get the last portion of it. That is my id.
//if an id is not supplied, meaning the param is not ending in a number i will just continue and let something else handle the error
int id;
Int32.TryParse(slug.Split('-').Last(), out id);
if (id != 0)
{
//the controller expects an id and here we will provide it
filterContext.ActionParameters["id"] = id;
}
}
base.OnActionExecuting(filterContext);
}
}
Now what happens is that the controller will be able to accept a non numeric id which ends in a number and provide its view without modifying the content of the controller. We will only need to add the filter attribute on the controller as shown in the next step.
I really do not care if the product name is actually the product name. You could try fetching the following urls:
\product\123
\product\product-name-123
\product\another-product-123
\product\john-doe-123
and you would still get the product with id 123, though the urls are different.
Next step is to let the controller know that it has to use a special filer
[SlugToId]
public ActionResult Index(int id)
{
}

Web API - Only 1 Action Per HTTP Verb/Parameter Combination?

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.

MVC query string with ICollection or array

My MVC 4 web service has a new use case. I need to pass a list of arguments on the query string to a Web API, e.g.,
http://host/SomeWebApi?arg=x&arg=y
I previously did this simply and easily in my Web Site controller using ICollection<string> arg as a parameter in the controller. That is working now, but it is a Web page, as opposed to an API.
Now I am knocking my head against the wall trying to get a Web API version of the same thing to work. I've made a simple test interface, below, and the collection arg is always null. I've tried List<string> and string[] as types as well. What am I overlooking?
Route register:
config.Routes.MapHttpRoute(
name: "Experiment",
routeTemplate: "Args",
defaults: new
{
controller = "Storage",
action = "GetArgTest",
suite = UrlParameter.Optional,
},
constraints: new
{
httpMethod = new HttpMethodConstraint(new HttpMethod[] { new HttpMethod("GET") })
}
);
Web API controller code:
public string GetArgTest(ICollection<string> suite)
{
if (suite == null)
{
return "suite is NULL";
}
else
{
return "suite is NON-NULL";
}
}
Test query string that results in "suite is NULL":
http://localhost:5101/Args?suite=1&suite=2
I came across this answer ApiController Action Failing to parse array from querystring and discovered that to resolve this you need to put in the FromUriAttribute. So in your example:
public string GetArgTest([FromUri]ICollection<string> suite)
{
}
This makes sense I guess, typically with an API controller you'd expect to be doing more POST and PUT requests for which you'd typically include post data rather than a QueryString.

ASP.Net MVC: Request variables through Routes and View method

Basically if I wanted to make something a search page with paging I would need a url like:
/Topics/Index?search=hi&page=1
What I can't seem to figure out is how to:
A) Set a default route with no search and page 1
/Topics/Index?page=1 or even /Topics/Index?search=&page=1
B) use the View method to do the same
I do see that if I have a method on the control:
Index(String search, Int32? page)
And use the url:
/Topics/Index?search=hi&page=1 or /Topics/Index?search=hi
It gives me what I want in the method. I just need a way to get a default route for the Topic controller to create a default url with said request variables. I just don't think that
/Topics/Index/hi/1
Is conducive to a search url, mostly because there's no guarantee I'll have search terms or a page so it could end up like:
/Topics/Index/1
Anything you pass in the RouteValueDictionary that doesn't map to a part of your Url will get added as a querystring parameter. So you can do:
Url.GenerateUrl("Route", "Index", "Topics",
new RouteValueDictionary(new
{
page = this.Model.PageNumber,
search = this.Model.Search
});
So basically I resorted to handling the non values by setting up defaults on the controller. Not sure this is the best idea though.
In GLobal.asax:
routes.MapRoute
(
"TopicDefault",
"Topic/{action}",
new { controller = "Topic", action = "Index"}
);
On the Controller:
public ActionResult Index(Int32? parentForumId, Int32? pageNumber, Int32? amountToShow)
{
Int32 revisedPageNumber = pageNumber.HasValue ? pageNumber.Value : 0;
Int32 revisedAmountToShow = amountToShow.HasValue ? amountToShow.Value : 10;
Int32 revisedParentForumId = parentForumId.HasValue ? parentForumId.Value : 1;
IList<TopicCreateViewModel> modelValues =
Topic.GetListForGrid(revisedParentForumId, revisedPageNumber,
revisedAmountToShow, out realPage)
return View("Index", modelValues);
}

Resources