ASP.NET MVC QueryString defaults overriding supplied values? - asp.net-mvc

Using ASP.NET MVC Preview 5 (though this has also been tried with the Beta), it appears that querystring defaults in a route override the value that is passed in on the query string. A repro is to write a controller like this:
public class TestController : Controller
{
public ActionResult Foo(int x)
{
Trace.WriteLine(x);
Trace.WriteLine(this.HttpContext.Request.QueryString["x"]);
return new EmptyResult();
}
}
With route mapped as follows:
routes.MapRoute(
"test",
"Test/Foo",
new { controller = "Test", action = "Foo", x = 1 });
And then invoke it with this relative URI:
/Test/Foo?x=5
The trace output I see is:
1
5
So in other words the default value that was set up for the route is always passed into the method, irrespective of whether it was actually supplied on the querystring. Note that if the default for the querystring is removed, i.e. the route is mapped as follows:
routes.MapRoute(
"test",
"Test/Foo",
new { controller = "Test", action = "Foo" });
Then the controller behaves as expected and the value is passed in as the parameter value, giving the trace output:
5
5
This looks to me like a bug, but I would find it very surprising that a bug like this could still be in the beta release of the ASP.NET MVC framework, as querystrings with defaults aren't exactly an esoteric or edge-case feature, so it's almost certainly my fault. Any ideas what I'm doing wrong?

The best way to look at ASP.NET MVC with QueryStrings is to think of them as values that the route does not know about. As you found out, the QueryString is not part of the RouteData, therefore, you should keep what you are passing as a query string separate from the route values.
A way to work around them is to create default values yourself in the action if the values passed from the QueryString are null.
In your example, the route knows about x, therefore your url should really look like this:
/Test/Foo or /Test/Foo/5
and the route should look like this:
routes.MapRoute("test", "Test/Foo/{x}", new {controller = "Test", action = "Foo", x = 1});
To get the behavior you were looking for.
If you want to pass a QueryString value, say like a page number then you would do this:
/Test/Foo/5?page=1
And your action should change like this:
public ActionResult Foo(int x, int? page)
{
Trace.WriteLine(x);
Trace.WriteLine(page.HasValue ? page.Value : 1);
return new EmptyResult();
}
Now the test:
Url: /Test/Foo
Trace:
1
1
Url: /Test/Foo/5
Trace:
5
1
Url: /Test/Foo/5?page=2
Trace:
5
2
Url: /Test/Foo?page=2
Trace:
1
2
Hope this helps clarify some things.

One of my colleagues found a link which indicates that this is by design and it appears the author of that article raised an issue with the MVC team saying this was a change from earlier releases. The response from them was below (for "page" you can read "x" to have it relate to the question above):
This is by design. Routing does not
concern itself with query string
values; it concerns itself only with
values from RouteData. You should
instead remove the entry for "page"
from the Defaults dictionary, and in
either the action method itself or in
a filter set the default value for
"page" if it has not already been set.
We hope to in the future have an
easier way to mark a parameter as
explicitly coming from RouteData, the
query string, or a form. Until that is
implemented the above solution should
work. Please let us know if it
doesn't!
So it appears that this behaviour is 'correct', however it is so orthogonal to the principle of least astonishment that I still can't quite believe it.
Edit #1: Note that the post details a method of how to provide default values, however this no longer works as the ActionMethod property he uses to access the MethodInfo has been removed in the latest version of ASP.NET MVC. I'm currently working on an alternative and will post it when done.
Edit #2: I've updated the idea in the linked post to work with the Preview 5 release of ASP.NET MVC and I believe it should also work with the Beta release though I can't guarantee it as we haven't moved to that release yet. It's so simple that I've just posted it inline here.
First there's the default attribute (we can't use the existing .NET DefaultValueAttribute as it needs to inherit from CustomModelBinderAttribute):
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class DefaultAttribute : CustomModelBinderAttribute
{
private readonly object value;
public DefaultAttribute(object value)
{
this.value = value;
}
public DefaultAttribute(string value, Type conversionType)
{
this.value = Convert.ChangeType(value, conversionType);
}
public override IModelBinder GetBinder()
{
return new DefaultValueModelBinder(this.value);
}
}
The the custom binder:
public sealed class DefaultValueModelBinder : IModelBinder
{
private readonly object value;
public DefaultValueModelBinder(object value)
{
this.value = value;
}
public ModelBinderResult BindModel(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
var queryValue = request .QueryString[bindingContext.ModelName];
return string.IsNullOrEmpty(queryValue)
? new ModelBinderResult(this.value)
: new DefaultModelBinder().BindModel(bindingContext);
}
}
And then you can simply apply it to the method parameters that come in on the querystring, e.g.
public ActionResult Foo([Default(1)] int x)
{
// implementation
}
Works like a charm!

I think the reason querystring parameters do not override the defaults is to stop people hacking the url.
Someone could use a url whose querystring included controller, action or other defaults you didn't want them to change.
I've dealt with this problem by doing what #Dale-Ragan suggested and dealing with it in the action method. Works for me.

I thought the point with Routing in MVC is to get rid of querystrings. Like this:
routes.MapRoute(
"test",
"Test/Foo/{x}",
new { controller = "Test", action = "Foo", x = 1 });

Related

RequestContext - RouteData does not contain action

So i've created my own ControllerFactory and i'm overloading GetControllerSessionBehavior in order to extend the MVC behavior.
To do my custom work i have to use reflection on the called action. However i've stumbled upon a weird issue - i can't retrieve the action by accessing RequestContext.RouteData
While setting up a reproduction sample for this i was not able to reproduce the error.
Is anyone aware of possible reasons for this or knows how to retrieve the action by calling a method with the request context other than this?
public class CustomControllerFactory : DefaultControllerFactory
{
protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
{
if (!requestContext.RouteData.Values.ContainsKey("action"))
return base.GetControllerSessionBehavior(requestContext, controllerType);
var controllerAction = requestContext.RouteData.Values["action"];
var action = controllerAction.ToString();
var actionMethod = controllerType.GetMember(action, MemberTypes.Method, BindingFlags.Instance | BindingFlags.Public).FirstOrDefault();
if(actionMethod == null)
return base.GetControllerSessionBehavior(requestContext, controllerType);
var cattr = actionMethod.GetCustomAttribute<SessionStateActionAttribute>();
if (cattr != null)
return cattr.Behavior;
return base.GetControllerSessionBehavior(requestContext, controllerType);
}
}
Action which i can call just fine but can't access the action name of within my controller factory:
[Route("Open/{createModel:bool?}/{tlpId:int}/{siteId:int?}")]
public ActionResult Open(int tlpId, int? siteId, bool? createModel = true)
{
}
Any ideas welcome.
Update:
The problem seems to be related to attribute routing. While it's working fine in repro it doesn't work in production for me.
Found this along the way - Once this is answered i'll have my proper solution too i guess.
Update 2:
Interesting. Reproduction MVC Version 5.0.0.0, Production 5.2.2. Possible introduction of bug?
I can confirm that there was a breaking change to attribute routing between 5.0.0 and 5.1.1. I reported the issue here. However, for my use case Microsoft was able to provide an acceptable workaround.
On the other hand, the problem you are bumping into looks like another culprit. For attribute routing, the route values are stored in a nested route key named MS_DirectRouteMatches. I am not sure exactly which version that changed in, but I know it happened v5+.
So, to fix your issue, you will need to check for the existence of a nested RouteData collection, and use instead of the normal RouteData in the case it exists.
var routeData = requestContext.RouteData;
if (routeData.Values.ContainsKey("MS_DirectRouteMatches"))
{
routeData = ((IEnumerable<RouteData>)routeData.Values["MS_DirectRouteMatches"]).First();
}
var controllerAction = routeData.Values["action"];
var action = controllerAction.ToString();
BTW - In the linked question you provided, the asker assumed that there is a possibility where a request can match more than one route. But that is not possible - a request will match 0 or 1 route, but never more than one.

Asp.Net MVC Routing not working as expected in 5.1

I need to add the culture to the url to support localization in my asp.net mvc application with url's like: sample.com/en/about
sample.com/en/product/2342
I recently upgraded my app from MVC 5.0 to 5.1 but the routing did not work as expected so I created a fresh asp.net mvc 5.0 test application and got the culture to show up in the url in a matter of minutes. However as soon as I upgrade this test application to MVC 5.1 the culture is no longer generated in links and if you manually type it into the url you get a 404 error.
I zipped up my 5.0 and 5.1 test applications here. I need help understanding why this doesn't work in MVC 5.1 and how to correct it. Perhaps my understanding of routing is flawed or this is a legitimate bug with 5.1?
In this test application the Home/About action has a routing attribute applied to it [Route("about")] and it's expected that when the link for that route is generated it should be localhost/en/about but instead it's just localhost/about. If you type localhost/en/about into the address bar you'll get a 404 error in the Mvc 5.1 test application.
Here is the relevant code that does work in MVC 5.0:
public class RouteConfig
{
private const string STR_Culture = "culture";
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LowercaseUrls = true;
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { culture = "en", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
foreach (var item in routes)
{
// this works in MVC 5.0
if (item is Route)
{
var route = item as Route;
if (route.Url.IndexOf("{" + STR_Culture + "}") == -1)
route.Url = String.Format("{{{0}}}/{1}", STR_Culture, route.Url);
//AddCulture(route.Defaults);
}
}
}
private static void AddCulture(RouteValueDictionary dictionary)
{
if (dictionary == null)
dictionary = new RouteValueDictionary();
if (dictionary.ContainsKey(STR_Culture) == false)
dictionary.Add(STR_Culture, "en");
}
}
Ok, figured this out. MVC 5.1 has introduced breaking changes. In the code above there is a foreach loop that dynamically changes all routing urls to append the "{culture}/" placeholder. e.g the route about becomes {culture}/about and so on.
This works in 5.0 because routes are of type System.Web.Routing.Route. In 5.1 they have introduced a bunch of additional classes. One of which is called LinkGenerationRoute that is used for all routes applied through attribute routing. This class holds on to a private readonly reference of the original Route that was made during the initial call to routes.MapMvcAttributeRoutes(); that registers attribute based routes. Then this class clones that Route by sending its individual properties to the base class that it inherits from: Route.
In the foreach loop I'm effectively modifying the base classe's Url but NOT the internally referenced Route object that LinkGenerationRoute is holding on to. The effect is that there are now two instances of the Route inside the framework and we only have the ability to modify the base one after its created. Unfortunately the internal Route (_innerRoute) is used for getting the virtual path thus causing links to be generated incorrectly because it cannot be modified after its created.
Looks like the only way is to manually add this placeholder in every route definition. e.g.
[Route("{culture}/about")], [Route("{culture}/contact")], [Route("{culture}/product/{productId:int}")] and so on.
At the end of the day I see no point to holding an internal reference to the Route in this class. The current instance should be used. e.g. this.GetVirtualPath(requestContext, values);
internal class LinkGenerationRoute : Route
{
private readonly Route _innerRoute; // original route cannot be modified
public LinkGenerationRoute(Route innerRoute)
: base(innerRoute.Url, innerRoute.Defaults, innerRoute.Constraints, innerRoute.DataTokens,
innerRoute.RouteHandler) // original route is effectively cloned by sending individual properties to base class
{
if (innerRoute == null)
{
throw Error.ArgumentNull("innerRoute");
}
_innerRoute = innerRoute;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// Claims no routes
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
// internal route is used for getting the virtual path. fail..
return _innerRoute.GetVirtualPath(requestContext, values);
}
}

ASP.Net MVC Routing/Using Dynamic Actions

Ok so I am trying to build a controller that has the following action methods
public ActionResult ExecuteStep_1a
public ActionResult ExecuteStep_1b
public ActionResult ExecuteStep_2
Etc...
Is there a way to define a route that uses a get parameter concatenated to the action name? So for instance the URL would be /step/ExecuteStep_1a. I tried defining a route with URL equal to:
{controller}/{action}_{number}
with no success. I tried a few other permutations again with no results. If someone could point me in the right direction I'd appreciate it. Oh I set action equal to ExecuteResult_ with the default if that adds to my explanation any.
You can use root Action and them use reflection like that:
{controller}/{action}/{step}
public ActionResult ExecuteStep(string step){
try {
Type thisType = this.GetType();
MethodInfo theMethod = thisType.GetMethod("ExecuteStep_" + step);
return theMethod.Invoke(this, null);
}
catch {}
}
But there is some speed limitation, if you using Reflection.

ASP.Net MVC routing legacy URLs passing querystring Ids to controller actions

We're currently running on IIS6, but hoping to move to IIS 7 soon.
We're moving an existing web forms site over to ASP.Net MVC. We have quite a few legacy pages which we need to redirect to the new controllers. I came across this article which looked interesting:
http://blog.eworldui.net/post/2008/04/ASPNET-MVC---Legacy-Url-Routing.aspx
So I guess I could either write my own route handler, or do my redirect in the controller. The latter smells slightly.
However, I'm not quite sure how to handle the query string values from the legacy urls which ideally I need to pass to my controller's Show() method. For example:
Legacy URL:
/Artists/ViewArtist.aspx?Id=4589
I want this to map to:
ArtistsController Show action
Actually my Show action takes the artist name, so I do want the user to be redirected from the Legacy URL to /artists/Madonna
Thanks!
depending on the article you mentioned, these are the steps to accomplish this:
1-Your LegacyHandler must extract the routes values from the query string(in this case it is the artist's id)
here is the code to do that:
public class LegacyHandler:MvcHandler
{
private RequestContext requestContext;
public LegacyHandler(RequestContext requestContext) : base(requestContext)
{
this.requestContext = requestContext;
}
protected override void ProcessRequest(HttpContextBase httpContext)
{
string redirectActionName = ((LegacyRoute) RequestContext.RouteData.Route).RedirectActionName;
var queryString = requestContext.HttpContext.Request.QueryString;
foreach (var key in queryString.AllKeys)
{
requestContext.RouteData.Values.Add(key, queryString[key]);
}
VirtualPathData path = RouteTable.Routes.GetVirtualPath(requestContext, redirectActionName,
requestContext.RouteData.Values);
httpContext.Response.Status = "301 Moved Permanently";
httpContext.Response.AppendHeader("Location", path.VirtualPath);
}
}
2- you have to add these two routes to the RouteTable where you have an ArtistController with ViewArtist action that accept an id parameter of int type
routes.Add("Legacy", new LegacyRoute("Artists/ViewArtist.aspx", "Artist", new LegacyRouteHandler()));
routes.MapRoute("Artist", "Artist/ViewArtist/{id}", new
{
controller = "Artist",
action = "ViewArtist",
});
Now you can navigate to a url like : /Artists/ViewArtist.aspx?id=123
and you will be redirected to : /Artist/ViewArtist/123
I was struggling a bit with this until I got my head around it. It was a lot easier to do this in a Controller like Perhentian did then directly in the route config, at least in my situation since our new URLs don't have id in them. The reason is that in the Controller I had access to all my repositories and domain objects. To help others this is what I did:
routes.MapRoute(null,
"product_list.aspx", // Matches legacy product_list.aspx
new { controller = "Products", action = "Legacy" }
);
public ActionResult Legacy(int catid)
{
MenuItem menuItem = menu.GetMenuItem(catid);
return RedirectPermanent(menuItem.Path);
}
menu is an object where I've stored information related to menu entries, like the Path which is the URL for the menu entry.
This redirects from for instance
/product_list.aspx?catid=50
to
/pc-tillbehor/kylning-flaktar/flaktar/170-mm
Note that RedirectPermanent is MVC3+. If you're using an older version you need to create the 301 manually.

MVC User Controls + ViewData

Hi im new to MVC and I've fished around with no luck on how to build MVC User Controls that have ViewData returned to them. I was hoping someone would post a step by step solution on how to approach this problem. If you could make your solution very detailed that would help out greatly.
Sorry for being so discrete with my question, I would just like to clarify that what Im ultimatly trying to do is pass an id to a controller actionresult method and wanting to render it to a user control directly from the controller itself. Im unsure on how to begin with this approach and wondering if this is even possible. It will essentially in my mind look like this
public ActionResult RTest(int id){
RTestDataContext db = new RTestDataContext();
var table = db.GetTable<tRTest>();
var record = table.SingleOrDefault(m=> m.id = id);
return View("RTest", record);
}
and in my User Control I would like to render the objects of that record and thats my issue.
If I understand your question, you are trying to pass ViewData into the user control. A user control is essentially a partial view, so you would do this:
<% Html.RenderPartial("someUserControl.ascx", viewData); %>
Now in your usercontrol, ViewData will be whatever you passed in...
OK here it goes --
We use Json data
In the aspx page we have an ajax call that calls the controller. Look up the available option parameters for ajax calls.
url: This calls the function in the class.(obviously) Our class name is JobController, function name is updateJob and it takes no parameters. The url drops the controllerPortion from the classname. For example to call the updateJob function the url would be '/Job/UpdateJob/'.
var data = {x:1, y:2};
$.ajax({
data: data,
cache: false,
url: '/ClassName/functionName/parameter',
dataType: "json",
type: "post",
success: function(result) {
//do something
},
error: function(errorData) {
alert(errorData.responseText);
}
}
);
In the JobController Class:
public ActionResult UpdateJob(string id)
{
string x_Value_from_ajax = Request.Form["x"];
string y_Value_from_ajax = Request.Form["y"];
return Json(dataContextClass.UpdateJob(x_Value_from_ajax, y_Value_from_ajax));
}
We have a Global.asax.cs page that maps the ajax calls.
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "EnterTime", action = "Index", id = "" } // Parameter defaults (EnterTime is our default controller class, index is our default function and it takes no parameters.)
);
}
}
I hope this gets you off to a good start.
Good luck
I am pretty sure view data is accessible inside user controls so long as you extend System.Web.Mvc.ViewUserControl and pass it in. I have a snippet of code:
<%Html.RenderPartial("~/UserControls/CategoryChooser.ascx", ViewData);%>
and from within my CategoryChooser ViewData is accessible.
Not sure if I understand your problem completely, but here's my answer to "How to add a User Control to your ASP.NET MVC Project".
In Visual Studio 2008, you can choose Add Item. In the categories at the left side, you can choose Visual C# > Web > MVC. There's an option MVC View User Control. Select it, choose a name, select the desired master page and you're good to go.

Resources