Asp.Net MVC Routing not working as expected in 5.1 - asp.net-mvc

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);
}
}

Related

In ASP.NET MVC, why do I get 404 errors after Publishing my website?

I'm still new to ASP.NET MVC and I'm struggling a little with the routing.
Using the ASP.NET development server (running directly from Visual Studio), my application can find its views without any problems. The standard ASP.NET URL is used - http://localhost:1871/InterestingLink/Register
However, when I publish my site to IIS and access it via http://localhost/MyFancyApplication/InterestingLink/Register, I get a 404 error.
Any suggestions on what might be wrong?
More info...
This is what my global.asax file looks like (standard):
public class MvcApplication : 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 = "Home", action = "Index", id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
My controller is also very simple:
public class InterestingLinkController : Controller
{
public ActionResult Register()
{
return View("Register");
}
}
I figured out what was wrong. The problem was actually that IIS5 (in Windows XP) does not fire up ASP.NET when the URL does not contain a .ASPX. The easiest way to get around this is to add a '.aspx' to your controller section in global.asax. For example:
routes.MapRoute(
"Default",
"{controller}.aspx/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
Not pretty, but it will do.
Lots of things could be wrong:
Is the IIS Virtual Directory & Application set correctly?
Is the ASP.NET application being called at all? (Add some logging/breakpoiont in Application_Start and Application_BeginRequest)
Just for a start. You are going to have to apply the usual debugging approaches.
(To avoid issues like this, I rarely use the development server and just use IIS the whole time: most difficult thing is remembering to run VS elevated every time.)

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.

ASP.NET MVC QueryString defaults overriding supplied values?

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 });

Is it possible to run ASP.NET MVC routes in different AppDomains?

I am having problems with thinking up a solution for the following. I got a blog which I recently upgraded from web forms to MVC. The blog is avalible in both swedish and english on two different domains and are running in the same web site in IIS.
The problem is that I would like language specific urls on the both sites, like this:
English: http://codeodyssey.com/archive/2009/1/15/code-odyssey-the-next-chapter
Swedish: http://codeodyssey.se/arkiv/2009/1/15/code-odyssey-nasta-kapitel
At the moment I have made this to work by registering the RouteTable on each request depending on which domain is called. My Global.asax Looks something like this (not the whole code):
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
string archiveRoute = "archive";
if (Thread.CurrentThread.CurrentUICulture.ToString() == "sv-SE")
{
archiveRoute = "arkiv";
}
routes.MapRoute(
"BlogPost",
archiveRoute+"/{year}/{month}/{day}/{slug}",
new { controller = "Blog", action = "ArchiveBySlug" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
routes.MapRoute(
"404-PageNotFound",
"{*url}",
new { controller = "Error", action = "ResourceNotFound" }
);
}
void Application_BeginRequest(object sender, EventArgs e)
{
//Check whcih domian the request is made for, and store the Culture
string currentCulture = HttpContext.Current.Request.Url.ToString().IndexOf("codeodyssey.se") != -1 ? "sv-SE" : "en-GB";
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(currentCulture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(currentCulture);
RouteTable.Routes.Clear();
RegisterRoutes(RouteTable.Routes);
Bootstrapper.ConfigureStructureMap();
ControllerBuilder.Current.SetControllerFactory(
new CodeOdyssey.Web.Controllers.StructureMapControllerFactory()
);
}
protected void Application_Start()
{
}
This works at the moment but I know it not a great solution. I have been getting a "Item has already been added. Key in dictionary" error when stating up this app and it does not seems stable at times.
I would like to only set up my routes in the Application_Start as they should and not having to clear them on every request like I am doing now. Problem is that the request object does not exist and I have no way of knowing which of the language specific routes I should register.
Been reading about the AppDomain but could not find many examples on how to use it on a web site. I'we been thinking to star something like this:
protected void Application_Start()
{
AppDomain.CreateDomain("codeodyssey.se");
AppDomain.CreateDomain("codeodyssey.com");
}
Then registring each web sites routes in each app domain and send the requests to one of them based on the url. Can't find any examples on how to work with AppDomains in this manner.
Am I completely off track? Or is there a better solution for this?
The ASP.Net runtime manages AppDomains for you, so its probably not a good idea to create AppDomains in your code.
However, if you can, I would suggest creating multiple IIS Applications (one for http://codeodyssey.com and one for http://codeodyssey.se). Point both applications at the same directory on disk. This will give you the two AppDomains you are looking for.
Then, in your Application_Start code, you can check the domain and build routes accordingly.

Asp.Net MVC: How do I enable dashes in my urls?

I'd like to have dashes separate words in my URLs. So instead of:
/MyController/MyAction
I'd like:
/My-Controller/My-Action
Is this possible?
You can use the ActionName attribute like so:
[ActionName("My-Action")]
public ActionResult MyAction() {
return View();
}
Note that you will then need to call your View file "My-Action.cshtml" (or appropriate extension). You will also need to reference "my-action" in any Html.ActionLink methods.
There isn't such a simple solution for controllers.
Edit: Update for MVC5
Enable the routes globally:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcAttributeRoutes();
// routes.MapRoute...
}
Now with MVC5, Attribute Routing has been absorbed into the project. You can now use:
[Route("My-Action")]
On Action Methods.
For controllers, you can apply a RoutePrefix attribute which will be applied to all action methods in that controller:
[RoutePrefix("my-controller")]
One of the benefits of using RoutePrefix is URL parameters will also be passed down to any action methods.
[RoutePrefix("clients/{clientId:int}")]
public class ClientsController : Controller .....
Snip..
[Route("edit-client")]
public ActionResult Edit(int clientId) // will match /clients/123/edit-client
You could create a custom route handler as shown in this blog:
http://blog.didsburydesign.com/2010/02/how-to-allow-hyphens-in-urls-using-asp-net-mvc-2/
public class HyphenatedRouteHandler : MvcRouteHandler{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
...and the new route:
routes.Add(
new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Default", action = "Index", id = "" }),
new HyphenatedRouteHandler())
);
A very similar question was asked here: ASP.net MVC support for URL's with hyphens
I've developed an open source NuGet library for this problem which implicitly converts EveryMvc/Url to every-mvc/url.
Uppercase urls are problematic because cookie paths are case-sensitive, most of the internet is actually case-sensitive while Microsoft technologies treats urls as case-insensitive. (More on my blog post)
NuGet Package: https://www.nuget.org/packages/LowercaseDashedRoute/
To install it, simply open the NuGet window in the Visual Studio by right clicking the Project and selecting NuGet Package Manager, and on the "Online" tab type "Lowercase Dashed Route", and it should pop up.
Alternatively, you can run this code in the Package Manager Console:
Install-Package LowercaseDashedRoute
After that you should open App_Start/RouteConfig.cs and comment out existing route.MapRoute(...) call and add this instead:
routes.Add(new LowercaseDashedRoute("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
new DashedRouteHandler()
)
);
That's it. All the urls are lowercase, dashed, and converted implicitly without you doing anything more.
Open Source Project Url: https://github.com/AtaS/lowercase-dashed-route
Here's what I did using areas in ASP.NET MVC 5 and it worked liked a charm. I didn't have to rename my views, either.
In RouteConfig.cs, do this:
public static void RegisterRoutes(RouteCollection routes)
{
// add these to enable attribute routing and lowercase urls, if desired
routes.MapMvcAttributeRoutes();
routes.LowercaseUrls = true;
// routes.MapRoute...
}
In your controller, add this before your class definition:
[RouteArea("SampleArea", AreaPrefix = "sample-area")]
[Route("{action}")]
public class SampleAreaController: Controller
{
// ...
[Route("my-action")]
public ViewResult MyAction()
{
// do something useful
}
}
The URL that shows up in the browser if testing on local machine is: localhost/sample-area/my-action. You don't need to rename your view files or anything. I was quite happy with the end result.
After routing attributes are enabled you can delete any area registration files you have such as SampleAreaRegistration.cs.
This article helped me come to this conclusion. I hope it is useful to you.
Asp.Net MVC 5 will support attribute routing, allowing more explicit control over route names. Sample usage will look like:
[RoutePrefix("dogs-and-cats")]
public class DogsAndCatsController : Controller
{
[HttpGet("living-together")]
public ViewResult LivingTogether() { ... }
[HttpPost("mass-hysteria")]
public ViewResult MassHysteria() { }
}
To get this behavior for projects using Asp.Net MVC prior to v5, similar functionality can be found with the AttributeRouting project (also available as a nuget). In fact, Microsoft reached out to the author of AttributeRouting to help them with their implementation for MVC 5.
You could write a custom route that derives from the Route class GetRouteData to strip dashes, but when you call the APIs to generate a URL, you'll have to remember to include the dashes for action name and controller name.
That shouldn't be too hard.
You can define a specific route such as:
routes.MapRoute(
"TandC", // Route controllerName
"CommonPath/{controller}/Terms-and-Conditions", // URL with parameters
new {
controller = "Home",
action = "Terms_and_Conditions"
} // Parameter defaults
);
But this route has to be registered BEFORE your default route.
If you have access to the IIS URL Rewrite module ( http://blogs.iis.net/ruslany/archive/2009/04/08/10-url-rewriting-tips-and-tricks.aspx ), you can simply rewrite the URLs.
Requests to /my-controller/my-action can be rewritten to /mycontroller/myaction and then there is no need to write custom handlers or anything else. Visitors get pretty urls and you get ones MVC can understand.
Here's an example for one controller and action, but you could modify this to be a more generic solution:
<rewrite>
<rules>
<rule name="Dashes, damnit">
<match url="^my-controller(.*)" />
<action type="Rewrite" url="MyController/Index{R:1}" />
</rule>
</rules>
</rewrite>
The possible downside to this is you'll have to switch your project to use IIS Express or IIS for rewrites to work during development.
I'm still pretty new to MVC, so take it with a grain of salt. It's not an elegant, catch-all solution but did the trick for me in MVC4:
routes.MapRoute(
name: "ControllerName",
url: "Controller-Name/{action}/{id}",
defaults: new { controller = "ControllerName", action = "Index", id = UrlParameter.Optional }
);

Resources