Routing with Multiple Parameters using ASP.NET MVC - c#

Our company is developing an API for our products and we are thinking about using ASP.NET MVC. While designing our API, we decided to use calls like the one below for the user to request information from the API in XML format:
http://ws.audioscrobbler.com/2.0/?method=artist.getimages&artist=cher&api_key=b25b959554ed76058ac220b7b2e0a026
As you can see, multiple parameters are passed (i.e. artist and api_key). In ASP.NET MVC, artist would be the controller, getImages the action, but how would I pass multiple parameters to the action?
Is this even possible using the format above?

Parameters are directly supported in MVC by simply adding parameters onto your action methods. Given an action like the following:
public ActionResult GetImages(string artistName, string apiKey)
MVC will auto-populate the parameters when given a URL like:
/Artist/GetImages/?artistName=cher&apiKey=XXX
One additional special case is parameters named "id". Any parameter named ID can be put into the path rather than the querystring, so something like:
public ActionResult GetImages(string id, string apiKey)
would be populated correctly with a URL like the following:
/Artist/GetImages/cher?apiKey=XXX
In addition, if you have more complicated scenarios, you can customize the routing rules that MVC uses to locate an action. Your global.asax file contains routing rules that can be customized. By default the rule looks like this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
If you wanted to support a url like
/Artist/GetImages/cher/api-key
you could add a route like:
routes.MapRoute(
"ArtistImages", // Route name
"{controller}/{action}/{artistName}/{apikey}", // URL with parameters
new { controller = "Home", action = "Index", artistName = "", apikey = "" } // Parameter defaults
);
and a method like the first example above.

Starting with MVC 5, you can also use Attribute Routing to move the URL parameter configuration to your controllers.
A detailed discussion is available here:
http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx
Summary:
First you enable attribute routing
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
}
}
Then you can use attributes to define parameters and optionally data types
public class BooksController : Controller
{
// eg: /books
// eg: /books/1430210079
[Route("books/{isbn?}")]
public ActionResult View(string isbn)

You can pass arbitrary parameters through the query string, but you can also set up custom routes to handle it in a RESTful way:
http://ws.audioscrobbler.com/2.0/?method=artist.getimages&artist=cher&
api_key=b25b959554ed76058ac220b7b2e0a026
That could be:
routes.MapRoute(
"ArtistsImages",
"{ws}/artists/{artist}/{action}/{*apikey}",
new { ws = "2.0", controller="artists" artist = "", action="", apikey="" }
);
So if someone used the following route:
ws.audioscrobbler.com/2.0/artists/cher/images/b25b959554ed76058ac220b7b2e0a026/
It would take them to the same place your example querystring did.
The above is just an example, and doesn't apply the business rules and constraints you'd have to set up to make sure people didn't 'hack' the URL.

Related

URL Routing MVC 5 Asp.net

I know about routing in MVC. I added a new MapRoute under RegisterRoute method in RouteConfig.cs class and successfully called my function with the URL http://localhost:53363/package/PackageDetail/mypackage/5.
However, my question is do i have to add different Map Routes for every method or is there any better way ? Like in PackageController class you can see i have two methods one methods takes PackageId and PackageName and the other takes only PackageId. So do i have to register different Map Routes or not ?
RouteConfig
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Package",
url: "Package/PackageDetail/{packageName}/{packageId}",
defaults: new { controller = "Package", action = "PackageDetail", packageName = UrlParameter.Optional, packageId = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
PackageController.cs :
[HttpGet]
public ActionResult PackageListing(int packageId = 0)
{
return View();
}
[HttpGet]
public ActionResult PackageDetail(string packageName = "", int packageId = 0)
{
return View();
}
Despite the fact that Muhammed's answer will work, it is very repetitive, especially if you're using the same style of routes for multiple types.
There are a few things to consider before deciding upon a single approach to routing. The main one is why have both the name and ID in the route? If you want a more SEO friendly URL structure, don't bother with the ID at all.
If you have multiple products within the same type that have identical names, then there's no point in including the name as part of the URL since that won't get a user where they want to go by itself. In that event, just leave the original route.
However, if you have several different controllers (or actions) with a similar name/id structure for the routes, you'll be far better served with making your custom route more generic.
routes.MapRoute(
name: "NameAndId",
url: "{controller}/{action}/{name}/{id:int}",
defaults: new
{
controller = "Package",
action = "PackageDetail",
name = UrlParameter.Optional,
id = UrlParameter.Optional
});
Keep this above the default route, and this will redirect not just
/Package/PackageDetail/Deluxe/5
but also allow you to have stuff like this:
/Meals/Menu/Dinner/3
That may not necessarily be applicable for you in this project, but since you're learning MVC, this is a good skill to pick up. The more generic you're able to maintain your route definitions, the less you'll need to repeat it. Of course, if this is a one-time special route, there's nothing wrong with using the attributes.
Also to answer your final question, you do not need to create another custom route, because your PackageListing method will be routed through the default route that was provided when you created your project.
If you want to override default route url and generate custom url then you need to register route in route config file.
You can pass Package name and package Id as below.
http://sitename/Package/PackageListing?packageId=1
http://sitename/Package/PackageDetail?packageName=packagename&packageId=1
but if you want to generate URL as below than you need to add route in route.config file.
http://sitename/Package/PackageListing/1
http://sitename/Package/PackageDetail/packageName/1

Using Both of Attribute and Convention Routing

Is there a way to use Convention and Attribute Routing together?
I want to call an action method with the real name of method and controller when I defined the attribute routing.
The mapping method is calling at startup:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();///
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Here is the controller:
[RoutePrefix("d")]
[Route("{action=index}")]
public class DefaultController : Controller
{
[Route]
public ActionResult Index()
{
return View();
}
[Route("f")]
public ActionResult Foo()
{
return View();
}
}
I can reach to Foo action method with /d/f url. But when I try this url: /Default/Foo, the 404 error occurs. Actually it throws the action not found exception which it says like A public action method 'Foo' was not found on controller 'Namespace...DefaultController'.
I checked the source code of asp.net mvc and I saw these lines:
if (controllerContext.RouteData.HasDirectRouteMatch())
{
////////
}
else
{
ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
return actionDescriptor;
}
It checks if there is a direct route or not, which the /Default/Foo route is not a direct route so it should act as convention routing which is registered at startup as {controller}/{action}/{id}. But it doesn't find the action with controllerDescriptor.FindAction method and it throws the exception.
Is this a bug or cant I use both routing methods together? Or are there any workaround to use both?
Edit
I debugged into mvc source code, and I saw these lines:
namespace System.Web.Mvc
{
// Common base class for Async and Sync action selectors
internal abstract class ActionMethodSelectorBase
{
private StandardRouteActionMethodCache _standardRouteCache;
protected void Initialize(Type controllerType)
{
ControllerType = controllerType;
var allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);
ActionMethods = Array.FindAll(allMethods, IsValidActionMethod);
// The attribute routing mapper will remove methods from this set as they are mapped.
// The lookup tables are initialized lazily to ensure that direct routing's changes are respected.
StandardRouteMethods = new HashSet<MethodInfo>(ActionMethods);
}
The last comments about attribute routing explains why this problem happens. The Attribute routing removes StandardRouteMethods when you call MapMvcAttributeRoutes.
I'm still seeking a workaround.
I want to call an action method with the real name of method and controller when I defined the attribute routing.
It seems like you are going pretty far in the wrong direction if all you want is to call the application knowing the controller and action names. If you have the name of the controller and action (area and other route values) you can use them to get the URL to use quite easily.
var url = Url.Action("Index", "Default");
// returns /d/f
If this works for your usage, then you don't have to have a duplicate set of route mappings at all.
NOTE: Creating 2 URLs to the same page is not SEO friendly (unless you use the canonical tag). In fact, many question here on SO are about removing the duplicate routes from the route table. It seems like they did us all a favor by removing the duplicate routes so we don't have to ignore them all manually.
Alternatives
One possible workaround is to add 2 route attributes to the action method (I don't believe it can be done without removing the RoutePrefix from the controller).
[Route("d/f")]
[Route("Default/Foo")]
public ActionResult Foo()
{
return View();
}
Another possible workaround - don't use attribute routing for all or part of your application. Attribute routing only supports a subset of convention-based routing features, but is useful in some specific scenarios. This does not appear to be one of those scenarios.

Does an existing action get routed to before a mapped route is attempted?

If an action exists on a controller, does asp.net-mvc route to that action before attempting to process any custom mapped routes?
Example.
Say I have the following controller
public class ShopController : Controller
{
public ActionResult Shop(Category category)
{
// returns some result
}
public ActionResult CartItemCount()
{
// returns some result
}
}
And I have registered the following route in my route collection:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Shop",
"Shop/{category}",
new { controller = "Shop", action = "Shop", category = UrlParameter.Optional } // Parameter defaults
);
}
What would happen if I had the following URL?
http://www.example.com/Shop/CartItemCount
Edit
I confused myself by thinking that the name of the mapped route was related to how it was processed. That is not the case, when it comes to url matching the name of the route does not matter.
As it turns out I had another route defined just above the one I gave in the example. This route, though named differently, was getting matched. I didn't even think to check it because, as I said, I thought the name given to a route somehow impacted the matching.
The routes decide which action to use. If there are no routes defined, you wont hit an action even if it exists. Install RouteDebugger and fire off your url. It will tell you ALL routes that it matches and which one it has actually used.

Passing parameters through the url using MVC?

I know you can pass in parameters via urls like .com/MyPage/?controlID=5 but how can you do it with something like .com/MyPage/5? Thus not requiring the variable name or a question mark.
You would define a custom route, or use the model binding to get the intended effect. In your case, the route would be something like:
routes.Add("someRoute",
"{controller}/{action}/{controlId}",
new { controller = "Home", action = "Index", controlId = UrlParameter.Optional }
);
public ActionResult Index(int? controlId)
{
}
Now, the only "gotcha" with this route is that if you also have the default route specified, these two routes will be in contention and the first one you have defined will win. If there is some form of differentiating value (say, that controlId always matches some kind of pattern), then you can always add a HttpRouteConstraint to the route to differentiate your new route from the default route.
Alternatively, you can rename the parameter on your action method, if you are still using the default route, to be id, and change your query string key to 'id':
public ActionResult Index(int? id)
{
// Do Stuff
}
Create a method in MyPageController:
public ActionResult Index (int id)
{
}
That will work with the default routes

Change URL of action in mvc

Is there a way to change the URL of a given action in mvc without changing the action or controller called?
If so, how would this be done on the following MapRoute:
routes.MapRoute(
"Estate.CloseDeal",
"Estate/CloseDeal/{groupId}/{paymentType}/{mortgageValue}/{province}",
new { controller = "Estate", action = "CloseDeal" },
new { groupId = "\\d+", paymentType = "\\d+", mortgageValue = "\\d+", province = "\\d+" }
);
The desired URL is: ".../estate-support/deal-closing/...". Currently it displays as ".../Estate/CloseDeal/..."
The button linking to this action looks like:
<button detail="#Url.Action("CloseDeal", new { groupId = info.GroupId })" class="orange">
EDIT 1:
Tried changing to:
routes.MapRoute(
"Estate.CloseDeal",
"estate-support/deal-closing/{groupId}/{paymentType}/{mortgageValue}/{province}",
new { controller = "Estate", action = "CloseDeal" },
new { groupId = "\\d+", paymentType = "\\d+", mortgageValue = "\\d+", province = "\\d+" }
);
This returned error: The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.
Edit 2:
Changing the second string worked for all routes but this one - the difference being, this route has additional parameters (groupID, paymentType etc.).
Just replace "Estate/CloseDeal" in the second string with "estate-support/deal-closing" - should work fine.
In this particular case it's this easy because the route is not parameterised over the controller and action names - i.e. the route doesn't have "{controller}/{action}" in it.
You can also apply a route directly to an action (as shown below) or controller class itself using RouteAttribute
// Your controller class
// (You could add a Route attribute here)]
public class Estate
{
// Directly apply a route (or 2?!) to this action
[Route("estate-support/deal-closing")]
[Route("clinched")] // You can add multiple routes if required
public ActionResult CloseDeal()
{
...
}
// And of course you can parameters to the route too, such as:
[Route("customers/{customerId}/orders/{orderId}")]
public ActionResult GetOrderByCustomer(int customerId, int orderId)
{
...
}
...
}
For this to work you need to enable it by calling MapMvcAttributeRoutes in RouteConfig.cs:
public static void RegisterRoutes(RouteCollection routes)
{
// Enable mapping by attibute in the controller classes
routes.MapMvcAttributeRoutes();
...
}
More info here from learn.microsoft.com: Attribute Routing in ASP.NET
and here: C# Corner - route attribute in MVC
You need either to update parameters of Url.Action calls or make you route that you want to be rendered to match the parameter of Url.Action call (i.e. name should match if using Url>Action override that uses name).
Note that you may want to map old url to new one with adding route that will simply redirect to new one if you expect people could added old Urls to favorites.

Categories