I am trying to add support for different languages to existing MVC 3 application. So far my links were
oldUrl -> myapp.com/Item/Details/5/some-title
And there are links to the website from different places on the web. But now I want to change the URL so the language is included:
newUrl -> kmyapp.com/en/Item/Details/5/some-title
However I want the oldUrl links to be valid. And the question is how do I do this... The easiest way would be to add another route, but I already have too many of them and it is getting kind of ugly, and when I create urls from a page that doesn't have the language the links also don't have the language so I have to do something else then.
Another way would be to override something that stands just before MVC tries to map the request to a specific route, but has already parsed the request url and filled the routeValues dictionary. So I can just check for the language and if it is not included in the route parameters I can add it on my own and call the implementation of the parent class to continue. Is that possible? What should I look at - UrlRoutingModule, MvcHttpHandler or something else? What can I override and how to tell MVC to use my version?
Any other ideas would be also appreciated!
There are of course many solutions. I'm going to show you two:
one that you control in your application
one that you control outside of your application (on IIS)
Solution one - Asp.net MVC routing
Provide routing that covers old and new routing:
routes.MapRoute(
"New",
"{lang}/{controller}/{action}/{id}",
new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional },
new { lang = "en|de|it|es|fr" }
);
routes.MapRoute(
"NewEx",
"{lang}/{controller}/{action}/{id}/{title}",
new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional, title = UrlParameter.Optional },
new { lang = "en|de|it|es|fr" }
);
routes.MapRoute(
"Old",
"{controller}/{action}/{id}",
new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
"OldEx",
"{controller}/{action}/{id}/{title}",
new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional, title = UrlParameter.Optional }
);
As you can see I've provided language default for the old routes as well since it's not present in the URL. Whether you want that or not is your own decision but this kind of routing makes it possible to not duplicate your controller actions. In any case you'd have to define a default language which can be provided this way.
There's a bigger question whether you still want to support old URLs or you'd rather redirect them (HTTP Redirect Permanent Status 301).
Permanent redirection
Change old routes to:
routes.MapRoute(
"Old",
"{controllerOld}/{actionOld}/{idOld}",
new { controller = "Redirect", action = "Permanent", id = UrlParameter.Optional }
);
routes.MapRoute(
"OldEx",
"{controllerOld}/{actionOld}/{idOld}/{titleOld}",
new { controller = "Redirect", action = "Permanent", id = UrlParameter.Optional, title = UrlParameter.Optional }
);
Then write a controller class that does redirection:
public class RedirectController : Controller
{
public ActionResult Permanent(string controllerOld, string actionOld, string idOld, string titleOld)
{
return RedirectToRoutePermanent(new {
lang = "en",
controller = controllerOld,
action = actionOld,
id = idOld,
title = titleOld
});
}
}
Solution two - IIS URL rewriting module
This solution relies on IIS URL Rewriting module, where you could rewrite any requests without language selection to your preferred default.
I'm not going to wrote how URL Rewriting works here, because there're plenty of web resources with detailed info about that.
Another way would be to override something that stands just before MVC tries to map the request to a specific route, but has already parsed the request url and filled the routeValues dictionary. So I can just check for the language and if it is not included in the route parameters I can add it on my own and call the implementation of the parent class to continue. Is that possible?
Yes It is possible.
You can Override GetRouteData in RouteBase which will have URL details in it.
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
}
and in global.aspx add below code.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new MyUrlRoute()); // Add before your default Routes
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
For more detailed implementation - refer blog
url-manipulation-implementing-routebase.html
Related
I am writing an MVC 4 application which has controllers and many lengthy action names, and they create a lengthy URL which is not easy to remember.
Any idea of how we can implement short URLs (Preferably, without affecting routing) in MVC 4? May be using custom attributes?
Actually, you can specify your routes in RouteConfig.cs. Here is the code from my application:
routes.MapRoute("SignUp", "signup", new { controller = "SignUp", action = "Index" });
routes.MapRoute("SignOut", "signout", new { controller = "Login", action = "SignOut" });
routes.MapRoute("Login", "login", new { controller = "Login", action = "Login" });
Second parameter here (signup, signout, login) are the short urls. If you want something more, you can specify your routes like this:
routes.MapRoute("SetNewPassword", "set-new-password/{userId}/{passwordResetKey}", new { controller = "SetNewPassword", action = "Index" });
The url here is something like /set-new-password/123/blabla
New routes don't affect default routes. Just make sure you have this default line at the end:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Btw, you can use handy route debugger from Phil Haack. It's really easy to set up and use (you just need to comment/uncomment one single line in your Global.asax.cs). It's absolutely must have tool for every Asp.Net MVC developer.
May be a "catchall" routing will help you.
You could set up a catch-all type route, to direct all /something requests to a specific action and controller, something like:
routes.MapRoute(
"ShortUrls",
"{name}",
new {controller = "ShortUrl", action = "Index", name = UrlParameter.Optional}
);
(depending on how the rest of your routing is set up, you probably don't want to do it exactly like this as it will likely cause you some serious routing headaches - but this works here for the sake of simplicity)
Then just have your action redirect to the desired URL, based on the specified value:
public class ShortUrlController : Controller
{
//
// GET: /ShortUrl/
public ActionResult Index(string name)
{
var urls = new Dictionary<string, string>();
urls.Add("disney", "http://..your lengthy url");
urls.Add("scuba", "http://another..lengthy url");
return Redirect(urls[name]);
}
}
I have an MVC website which used to use URLs in the standard format of: Controller/Action.
Recently, I have changed it to: Site/Controller/Action.
The problem is, there are several links to my site out there which follow the old format, and I want to redirect them accordingly.
for example: mydomain.com/Home/CustomerSearch now should go to mydomain.com/Online/Home/CustomerSearch
whereas: mydomain.com/AffiliatesHome/CustomerSearch now should go to mydomain.com/Affiliate/AffiliatesHome/CustomerSearch
How can I get it to handle the redirecting by putting in the extra routing, depending on the link they came in by?
The current routing I am using is:
routes.MapRoute(
"Default", // Route name
"{site}/{controller}/{action}/{id}",
new {site="IS", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Since I do not really see an schema in your old to new URL mapping I would suggest to add routes that match the old Controller/Action Schema and map them to the new Site/Controller/Action route schema.
So you could add the following routes
routes.MapRoute(
"LegacyHome",
"Home/{action}/{id}",
new { site="Online", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
"LegacyAffiliates",
"AffiliatesHome/{action}/{id}",
new { site="Affiliate", controller = "AffiliatesHome", action = "Index", id = UrlParameter.Optional }
);
From an SEO standpoint this is not ideal because you have different URLs for the same page. A permanent redirect via status code 301 and the new URL passed in the location is better suited.
You could build a redirect controller and use the legacy routes to map legacy URLs to the redirect controller somehow like this
routes.MapRoute(
"LegacyHome",
"Home/{newAction}/{id}",
new { controller = "Redirect", action = "Redirect", newSite = "Online", newController="Home", newAction = "Index", id = UrlParameter.Optional }
);
Code of the redirect controller
public class RedirectController : Controller
{
public ActionResult Redirect(string newSite, string newController, string newAction)
{
var routeValues = new RouteValueDictionary(
new
{
site = newSite,
controller = newController,
action = newAction
});
if (RouteData.Values["id"] != null)
{
routeValues.Add("id", RouteData.Values["id"]);
}
return RedirectToRoutePermanent(routeValues);
}
}
I have a website with urls like this :
www.mysite.com/controller/action/id
I need to create urls for others languages (english), so I have used ALEX ADAMYAN implementation of the MultiCultureMvcRouteHandler. So now I have urls like this :
www.mysite.com/en/controller/action/id
www.mysite.com/fr/controller/action/id
but I need to keep my old urls has default one, ie to have :
www.mysite.com/en/controller/action/id
www.mysite.com/controller/action/id
So I'm wondering how I can modify the routes, in his implementation, alex loops the routes and add the en/fr parameter to all routes so it overwrites former routes, then former urls are dropped, what I want to avoid. If I duplicate all the routes to keep one with fr/en and one without it doesn't works, may be this is because the order is lost in the route collection ?
I solved this problem looping through all my languages:
public static List<Language> Languages;
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
LanguageRepository langRepo = new LanguageRepository();
Languages = langRepo.GetAllLanguages();
foreach (Language language in Languages)
{
routes.MapRoute(
"Localization_" + language.LanguageAbbreviation,
language.LanguageAbbreviation + "/{controller}/{action}/{id}",
new { lang = language.LanguageAbbreviation, controller = "Home", action = "Index", id = UrlParameter.Optional });
}
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new {lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional });
}
Where langabbreviation likes "en" or "ru".
The last route is for "default" "withoutlanguage" url like "www.site.com/controller/action" and you need to set up the default lang abbr for it ("en" in my case).
Hope it will help.
I have the following route:
routes.MapRoute(
"Property",
"{language}/property/{propertyUrlId}",
new { controller = "PropertyDetails", action = "Property" }
This is the Controller that should be called for that route:
public class PropertyDetailsController : Controller
{
public ActionResult Property(string language, string propertyUrlId)
{
etc.
And the following URL that should use that route:
http://domain.com/en-us/property/3
Instead, I get 404. Any ideas why?
Here are my routes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Property",
"property/{propertyUrlId}",
//new { controller = "PropertyDetails", action = "Property" }, new { language = #"[a-zA-Z]{2}-[a-zA-Z]{2}" }
new { controller = "PropertyDetails", action = "Property" }
);
}
Didn't work with language, or with language/country, either.
You most likely have registered the default route before your Property route. Default route typically looks like this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
Just register your Property route BEFORE this default route and it will work.
Why it fails? (Assuming you are indeed registering default route first)
en-us -> is interpreted as controller
property -> is interpreted as action
Since you don't have a en-usController with a Property action -> 404
Use "en-us" as a segment of the URL is completely fine. I guess you have registered other routes as well. Try to bring this route to the top of others and at least on top of the default route.
I have tested the scenario, it works just fine for me.
Considering that you want to have the structure of the url as:
http://domain.com/en-us/property/3
use this routing:
routes.MapRoute(
"Property", // Route name
"{language}/property/{propertyUrlId}", // URL with parameters
new { controller = "PropertyDetails", action = "Property", propertyUrlId = UrlParameter.Optional } // Parameter defaults
);
if there is a default routing in your Global.asax file, like this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
Put the routint above this block of code.
And your Controller Action should look like this:
public ActionResult Property(int propertyUrlId)
{
return View();
}
First of all, there is no reason to break {language} apart into two chunks in the route. As some of you stated, this is fine:
routes.MapRoute(
"Property",
"{language}/property/{propertyUrlId}",
new { controller = "PropertyDetails", action = "Property" }
Second, I omitted some information which was crucial to the solving of this problem. It didn't occur to me to include this in my problem description, as I didn't know there was any relationship. The MVC project is in a solution which also contains a website (non-MVC) which is using the Sitecore CMS as its datastore. Sitecore was stripping out the language segment of the URL and storing it, itself. Once I learned that this was happening, I was able to deal with the problem.
I appreciate all the input, and I apologize for the confusion.
I'm looking to make a really simple route in my ASP.NET MVC 2.0 website. I've been googling for help but all the examples I can find are for really complex routing.
Basically I want all the pages in my Home Controller to resolve after the domain as opposed to /Home/
For example I want http://www.MyWebsite.com/Home/LandingPage/
To become http://www.MyWebsite.com/LandingPage/
But only for the Home controller, I want the rest of my controllers to function as normal.
I thought about creating a controller for each and just using an index, but we need lots of landing pages for our marketing like this and it would quickly make the site loaded with controllers for a single page each, which is less than ideal.
One way to do this would be to have a separate route for each landing page. Another way would be to have a single route with a constraint that matches each landing page (and nothing else).
routes.MapRoute(
"LandingPage1"
"landingpage1/{id}",
new { controller = "home", action = "landingpage", id = UrlParameter.Optional } );
routes.MapRoute(
"LandingPage2"
"landingpage2/{id}",
new { controller = "home", action = "landingpage2", id = UrlParameter.Optional } );
Note that you could probably do this with a bit of reflection as well (untested).
foreach (var method on typeof(HomeController).GetMethods())
{
if (method.ReturnType.IsInstanceOf(typeof(ActionResult)))
{
routes.MapRoute(
method.Name,
method.Name + "/{id}",
new { controller = "home", action = method.Name, id = UrlParameter.Optional } );
}
}
The RouteConstraint solution would be similar except that you'd have a single route with a custom constraint that evaluated whether the appropriate route value matched one of the methods on the HomeController and, if so, replaced the controller and action with "home" and the matched value.
routes.MapRoute(
"LandingPage",
"{action}/{id}",
new { controller = "home", action = "index", id = UrlParameter.Optional },
new LandingPageRouteConstraint()
);
public LandingPageRouteContstraint : IRouteConstraint
{
public bool Match
(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
// simplistic, you'd also likely need to check that it has the correct return
// type, ...
return typeof(HomeController).GetMethod( values.Values["action"] ) != null;
}
}
Note that the route per page mechanism, even if you use reflection, is done only once. From then on you do a simple look up each time. The RouteConstraint mechanism will use reflection each time to see if the route matches (unless it caches the results, which I don't think it does).
I think you are missing the default route.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
So, when you type www.mywebsite.com, the controller, action, and id parameters would have the following values:
controller : Home
action: Index
id : ""