Can my MVC2 app specify route constraints on Query String parameters? - asp.net-mvc-2

My MVC2 app uses a component that makes subsequent AJAX calls back to the same action, which causes all kinds of unnecessary data access and processing on the server. The component vendor suggests I re-route those subsequent requests to a different action. The subsequent requests differ in that they have a particular query string, and I want to know whether I can put constraints on the query string in my route table.
For example, the initial request comes in with a URL like http://localhost/document/display/1. This can be handled by the default route. I want to write a custom route to handle URLs like http://localhost/document/display/1?vendorParam1=blah1&script=blah.js and http://localhost/document/display/1?vendorParam2=blah2&script=blah.js by detecting "vendor" in the URL.
I tried the following, but it throws a System.ArgumentException: The route URL cannot start with a '/' or '~' character and it cannot contain a '?' character.:
routes.MapRoute(
null,
"Document/Display/{id}?{args}",
new { controller = "OtherController", action = "OtherAction" },
new RouteValueDictionary { { "args", "vendor" } });
Can I write a route that takes the query string into account? If not, do you have any other ideas?
Update: Put simply, can I write routing constraints such that http://localhost/document/display/1 is routed to the DocumentController.Display action but http://localhost/document/display/1?vendorParam1=blah1&script=blah.js is routed to the VendorController.Display action? Eventually, I would like any URL whose query string contains "vendor" to be routed to the VendorController.Display action.
I understand the first URL can be handled by the default route, but what about the second? Is it possible to do this at all? After lots of trial and error on my part, it looks like the answer is "No".

QueryString parameters can be used in constraints, although it's not supported by default. Here you can find an article describing how to implement this in ASP.NET MVC 2.
As it is in Dutch, here's the implementation. Add an 'IRouteConstraint' class:
public class QueryStringConstraint : IRouteConstraint
{
private readonly Regex _regex;
public QueryStringConstraint(string regex)
{
_regex = new Regex(regex, RegexOptions.IgnoreCase);
}
public bool Match (HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// check whether the paramname is in the QS collection
if(httpContext.Request.QueryString.AllKeys.Contains(parameterName))
{
// validate on the given regex
return _regex.Match(httpContext.Request.QueryString[parameterName]).Success;
}
// or return false
return false;
}
}
Now you can use this in your routes:
routes.MapRoute("object-contact",
"{aanbod}",
/* ... */,
new { pagina = new QueryStringConstraint("some|constraint") });

You don't need a route for this. It is already handled by the default model binder. Query string parameters will be automatically bound to action arguments:
public ActionResult Foo(string id, string script, string vendorname)
{
// the id parameter will be bound from the default route token
// script and vendorname parameters will be bound from the request string
...
}
UPDATE:
If you don't know the name of the query string parameters that will be passed you could loop through them:
foreach (string key in Request.QueryString.Keys)
{
string value = Request.QueryString[key];
}

This post is old, but couldn't you write a route before your default route
this would only catch routes with "vendor" in the args
routes.MapRoute(
null,
"Document/Display/{id}?{args}",
new { controller = "VendorController", action = "OtherAction" },
new {args=#".*(vendor).*"}//believe this is correct regex to catch "vendor" anywhere in the args
);
And This would catch the rest
routes.MapRoute(
null,
"Document/Display/{id}?{args}",
new { controller = "DisplayController", action = "OtherAction" }
);
Haven't tried this and I am a novice to MVC but I believe this should work?? From what I understand if the constraint doesn't match the route isn't used. So it would test the next route. Since your next route doesn't use any constraint on the args, it should, match the route.
I tried this out and it worked for me.

Related

Grails rest-api app to handle multiple params

Using Grails 3.1.3, I created a rest-api so that I am able to capture GET requests that not only query for one parameter, but multiple if needed. I don't know how to code this correctly inside the UrlMappings file. Here are the details.
Domain class:
class ProdDetail {
Integer pid
String title
String category
Integer year
}
And some of these inside the BootStrap:
new ProdDetail(pid:'101', title:'No Highway', author:'Nevil Shute', category:'fiction', year:1948).save(failOnError:true)
new ProdDetail(pid:'214', title:'In the Country of Men', author:'Hisham Matar', category:'misery', year:2007).save(failOnError:true)
Controller:
protected List<ProdDetail> listAllResources(Map params) {
println params
try {
ProdDetail.where {
if (params.category && params.maxYear) {
category == params.category && year <= params.int('maxYear')
} else if (params.category) {
category == params.category
} else if (params.maxYear) {
year <= params.int('maxYear')
} else {
pid > 0
}
}.list()
} catch (Exception e) {
[]
}
}
UrlMappings:
static mappings = {
"/prodDetails"(resources:'prodDetail')
"/prodDetails/category/$category?"(controller:'prodDetail', action:'index')
"/prodDetails/yearUnder/$maxYear?"(controller:'prodDetail', action:'index')
// the line below is not right I think, what's the correct format?
"/prodDetails/combo/$category?&$maxYear?"(controller:'prodDetail', action:'index')
}
Now, where as these two curls would work:
curl localhost:8080/prodDetails/category/misery
curl localhost:8080/prodDetails/yearUnder/2007
This one fails to go into the desired clause in the controller to detect both params:
curl localhost:8080/prodDetails/combo/?category=misery&maxYear=2007
It just detects 'category' but not the 'maxYear' which it considers as 'null'.
How can I cater for such a curl please?
It kind of depends on what you want your URLs to look like, but assuming you want your requests to look like this:
http://localhost:8080/prodDetails/combo/misery?maxYear=2007&title=common
The UrlMappings should look like
static mappings = {
"/prodDetails/combo/$category"(controller:'prodDetail', action:'index')
}
Then the params object in the controller should have both whatever's in the place of $category, in this example misery, and the other parameters after the ? as well.
If you want the parameters to be in the path you can do this:
static mappings = {
"/prodDetails/combo/$category/$title/$maxYear"(controller:'prodDetail', action:'index')
}
And the request would then be:
http://localhost:8080/prodDetails/combo/misery/common/2007
One other option would be to use a command object. So if you had:
static mappings = {
"/prodDetails/combosearch"(controller:'prodDetail', action:'comboSearch')
}
And then created an object beside the controller called ComboSearchCommand.groovy that looked like:
import grails.validation.Validateable
class ComboSearchCommand implements Validetable {
String category
String title
int maxYear
static constraints = {
category blank: false, nullable: true
title blank: false, nullable: true
maxYear blank: false, nullable: true
}
}
(Which you can do validation on just like a domain object)
And then in your controller you have the method take the command object instead of params
protected List<ProdDetail> comboSearch(ComboSearchCommand command) {
println command.category
}
Then your URL would be
http://localhost:8080/prodDetails/combosearch?category=misery&maxYear=2007&title=common
And the parameters will bind to the command object.
I've used that quite a bit, you can share validations or have your command object inherit validations from domain objects, lots of flexibility.
https://grails.github.io/grails-doc/latest/guide/single.html#commandObjects
You don't need to specify the parameters in UrlMappings if those params are not part of the URL:
No need of this:
"/prodDetails/combo/$category&?$maxYear?"(controller:'prodDetail', action:'index')
Yes you need this to match the URL to a controller/action (but remove the ?)
"/prodDetails/yearUnder/$maxYear?"(controller:'prodDetail', action:'index')
Also, you don't need Map params in listAllResources(Map params)
"params" is an injected property of controllers, the println params will work OK with: listAllResources()
What I would do is to define:
listAllResources(String category, int maxYear, ...) where ... are all the params that action can receive, most would be optional, so you will receive a null value if not included in your request.
Remember: UrlMappings are to map URLs to controller/actions, and you have the same controller/action, so I would remove all the mappings and process the optional parameters in the action just checking which are null or not.
Edit (considering comments)
Q: the method is not overloaded to handle params like that
A: methods are dynamic, this is Grails / Groovy, not Java. It will call the action method even if all the params are null. I would recommend you to go through the Grails controller documentation in detail.
Q: found that the listAllResources method was never called
A: remove the protected keyword from the action, only subclasses would be able to invoke that method. Also, you can add an UrlMapping to avoid users to invoke that URL (match the URL and return 404 Not Available or something like that)
Q: I want to handle a GET request like this localhost:8080/prodDetails/combo?category=misery&year=2016&title=commonTitle, how exactly should the i) entry in UrlMappings, and ii) the listAllResources method look like?
A:
static mappings = {
// if "compo" comes in the action portion, map that to the listAllResources method
// as I said, if all parameters comes in the query string, no further actions are needed, if you need parameters to be part of the URL path, then you need to play with the $xxxx in the URL
"/prodDetails/combo"(controller:'prodDetail', action:'listAllResources')
}
def listAllResources()
{
println params
// logic here
render "OK"
}
Check:
https://grails.github.io/grails-doc/latest/ref/Controllers/params.html
https://grails.github.io/grails-doc/latest/ref/Controllers/render.html
How does grails pass arguments to controller methods?

Alter MVC Routing with dynamic prefix while maintaining backwards url compatibility

As it is not, I have a site where you must come in on a single url and a cookie is set to track which customer you are affiliated with. I want to change this so that certain controllers only use a url like this:
/{friendlyName}/{controller}/{index}/{id}
that friendly name is unique and lets me select the correct customer without using the cookie kludge.
I have controllers: Home, Redirect that I do not want the friendly name part of (and possibly more).
I have a few others that fit this category that I would like to move into their own areas. How can I not include the areas as valid friendly names? For instance, I have a controller that services up content in an iframe called Framed. currently, a url for this looks like /Framed/action/id. I could put this in an area called Framed with a controller the same name as the action, and I should still be able to maintain the same url.
For the controller Error I want the friendly name to be optional
I have other controllers that I want the friendly name to be required: SignIn, SignOut, Account
Once I have the routing, the problem is altering the code so that my redirects maintain the friendlyurl. Any ideas on how to do that?
My problem is just coming up with a good plan of attack on how to change the routing of my site. I must maintain backwards compatibility of some of the urls - namely anything I don't want the friendly url part of, including the controllers I discussed slitting into their own areas. I'm looking for any good suggests on how to lay this out and go about altering the changes.
To accomplish your objectives, you will need a combination of routes and RouteConstraints. Also, you will need to enforce rules that a friendlyName is unique, and is different from the names of any controllers or areas.
The following routes should be sufficient in RegisterRoutes() in Global.asax.cs:
routes.MapRoute(
"WithFriendlyName",
"{friendlyName}/{controller}/{index}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { friendlyName = new MustBeFriendlyName() }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = new MustNotRequireFriendlyName() }
);
The RouteConstraints should look something like this:
using System;
using System.Web;
using System.Web.Routing;
namespace Examples.Extensions
{
public class MustBeFriendlyName : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this is a valid friendlyName
// MUST BE CERTAIN friendlyName DOES NOT MATCH ANY
// CONTROLLER NAMES OR AREA NAMES
var _db = new DbContext();
return _db.FriendlyNames.FirstOrDefault(x => x.FriendlyName.ToLowerInvariant() ==
values[parameterName].ToString().ToLowerInvariant()) != null;
}
}
public class MustNotRequireFriendlyName : IRouteConstraint
{
private const string controllersRequiringFriendlyNames =
"SignIn~SignOut~Account";
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this controller does NOT require a friendlyName
return controllersRequiringFriendlyNames.ToLowerInvariant()
.Contains(values[parameterName].ToString().ToLowerInvariant());
}
}
}
This should get you started.
As far as the URLs generated by your redirects, if the routing is set up correctly, the URL generation should follow, so that the only changes you are likely to need are those to insure {friendlyName} is being passed.
You probably will have to add some additional routes and constraints as you get further into your changes.
Just wanted to add to this, as the optional prefix been biting me for the past couple of days. While I want to use the solution provided by #counsellorben, I also needed to be able to address the routes by the same name, which is impossible when using 2 routes.
It took me some headscratching, but finally the solution actually seemed very simple. I just needed to create an intermediate aggregate route:
public class AggregateRoute : RouteBase
{
private readonly RouteBase[] _routes;
public AggregateRoute(params RouteBase[] routes)
{
_routes = routes;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData routeData = null;
foreach (var route in _routes)
{
routeData = route.GetRouteData(httpContext);
if (routeData != null) break;
}
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData virtualPath = null;
foreach (var route in _routes)
{
virtualPath = route.GetVirtualPath(requestContext, values);
if (virtualPath != null) break;
}
return virtualPath;
}
}
This allows me to do:
routes.Add(
"RouteName",
new AggregateRoute(
new Route("{path}", new MvcRouteHandler()),
new Route("{prefix}/{path}", new MvcRouteHandler())
)
);
Which enables resolving either route by the same name, which is impossible when adding both routes separately:
Url.RouteLink("RouteName", new RouteValueDictionary{
new{path="some-path"}});
Url.RouteLink("RouteName", new RouteValueDictionary{
new{path="some-prefix/some-path"}});

Optional route parameters and action selection

I use the default route definition:
{controller}/{action}/{id}
where id = UrlParameter.Optional. As much as I understand it this means when id is not being part of the URL this route value will not exists in the RouteValues dictionary.
So this also seems perfectly possible (both GET):
public ActionResult Index() { ... } // handle URLs: controller/action
public ActionResult Index(int id) { ... } // handle URLs: controller/action/id
When id is missing the first action would be executed, but when id is present, the second one would execute. Fine, but it doesn't work. It can't resolve actions.
How can I accomplish this?
I'm thinking of writing a custom action method selector attribute like:
[RequiresRouteValue(string valueName)]
This would make it possible to use this kind of action methods. But is this the only way of doing it?
Is there something built-in I can hang on to?
Use either:
[HttpGet]
public ActionResult Index() { ... } // handle URLs: controller/action
[HttpPost]
public ActionResult Index(int id) { ... } // handle URLs: controller/action/id
Or just have one with a nullable param:
public ActionResult Index(int? id) { ... } // handles both instances
EDIT:
Would something like this work?
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/", // URL with parameters
new { controller = "Login", action = "Index" } // Parameter defaults
);
routes.MapRoute(
"DefaultWithValue", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Login", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
Well from the exception that action can't be determines is pretty clear that actions are resolved first then data binder comes into play and examines action's parameters and tries to data bind values to them. Makes perfect sense.
This makes perfect sense. There would be no point in first trying to data bind values to all possible types and see what we get and then look for an appropriate action. That would be next to impossible.
So. Since action selection is the problem here I guess the best (and only) way to solve this (if I don't want to use a multifaceted single action method) is to write a custom action method selector attribute.
You can read all the details and get the code on my blog:
Improving Asp.net MVC maintainability and RESTful conformance

ASP.NET Mvc - nullable parameters and comma as separator

How should I define route in my global.asax to be able use nullable parameters and coma as separator?
I'm trying to implement routing rule for my search users page like
"{Controller}/{Action},{name},{page},{status}"
Full entry from the Global.asax:
routes.MapRoute(
"Search",
"{controller}/{action},{name},{page},{status}",
new { controller = "User", action = "Find",
name = UrlParameter.Optional,
page = UrlParameter.Optional,
status = UrlParameter.Optional }
);
Routine defined like above works fine when I'm entering all parameters, but when some parameters are equal to null routing fails (for example "user/find,,,")
According to Clicktricity comment bellow - the singature of action method that handes the request:
public ActionResult Find(string userName, int? page, int? status)
{
// [...] some actions to handle the request
}
On the beginning I was testing the route by VS debugger, now I'm using route debugger described on Phil's Haack blog. The tool confirm - that routing with null values is not properly handled (or I'm doing something wrong ;) )
As far as I know .Net routing doesn't let you do multiple nullable parameters like that. Multiple parameters will only work if they are missing working backwards from the end and with the separator also missing so you'd get matches on
user/find,bob,2,live
user/find,bob,2
user/find,bob
user/find
It'd be a lot easier to use querystrings for what you're trying to do.
Edit based on comment:
If this is necessary then you could try doing it this way (though it's not a nice approach)
Change your path to match
{Controller}/{Action},{*parameters}
Make sure to put a constraint on the action and controller so this is limited to as few as possible.
Rename each action that would take your full list to something else, adding a standard prefix to each one would be the cleanest way, and add the [NonAction] attribute. Add a new method with the original name that takes a string, this string is a comma separated string of your variables. In this method split the string and return the original action passing in the values from the split.
So you go from:
public ActionResult Find(string name, int page, string status){
//Do stuff here
return View(result);
}
To
public ActionResult Find(string parameters){
string name;
int? page;
string status;
//split parameters and parse into variables
return FindAction(name, page, status);
}
[NonAction]
public ActionResult FindAction(string parameters){
//Do what you did in your previous Find action
return View(results);
}

ASP.NET MVC Route Default values

i defined two routes in global.asax like below
context.MapRoute("HomeRedirect", "",
new
{
controller = "Home",
action = "redirect"
});
context.MapRoute("UrlResolver", "{culture}/some",
new
{
culture = "en-gb",
controller = "someController",
action = "someAction"
},
new
{
culture = new CultureRouteConstraint()
});
according to above definition, when user request mysite.com/ redirect action of HomeController should be called and in that:
public class HomeController : Controller
{
public ActionResult Redirect()
{
return RedirectToRoute("UrlResolver");
}
}
i want to redirect user to second defined route on above, so also i specified default values for that and some Constraint for each of those. but when RedirectToRoute("UrlResolver") turns, no default values passed to routeConstraints on second route and No route in the route table matches the supplied values shows.
update
my CultureRouteConstraint:
public class CultureRouteConstraint : IRouteConstraint
{
bool IRouteConstraint.Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
try
{
var parameter = values[parameterName] as string;
return (someCondition(parameter));
}
catch
{
return false;
}
}
}
now values parameter haven't culture key/value, but route parameter have that.
The actual route specified for your second rule is "{culture}/some", so your redirect would have to be, for example, to "/EN-us/some" rather than to the name of the rule.
The implementation of HomeController.Redirect() doesn't seem to add any additional value, so why include the route named "HomeRedirect" at all?
How about just deleting it and letting your route named "UrlResolver" handle the requests. You can configure the defaults as you like.
You might add a "catch all" route below the "UrlResolver" route to catch cases where the CultureRouteConstraint() doesn't match the given URL.