ASP.net MVC3 Multilingual Route Rewriting - asp.net-mvc-3

Is there any good method to create route rewriting for a multilingual web application?
The URL schema should be the following
http://<Domainname>/{Language}/{Controller}/{Action}/{Id}
but URLs without the Language part should also be supported, but they should not just map to the controllers directly but generate a redirect response.
The important thing here is that the redirect should not be hard coded to a specific language but be determined based on factors like the users preferred language if possible.
Note: The process of determining the correct language is not the problem, just how to do the non static rewriting.
Thanks

I managed that with following routes;
routes.MapRoute(
"Default", // Route name
"{language}/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", language = "tr", id = UrlParameter.Optional }, // Parameter defaults
new { language = #"(tr)|(en)" }
);
I handle the culture by overriding the GetControllerInstance() method of DefaultControllerFactory. the example is below;
public class NinjectControllerFactory : DefaultControllerFactory {
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType) {
//Get the {language} parameter in the RouteData
string UILanguage;
if (requestContext.RouteData.Values["language"] == null) {
UILanguage = "tr";
}
else {
UILanguage = requestContext.RouteData.Values["language"].ToString();
}
//Get the culture info of the language code
CultureInfo culture = CultureInfo.CreateSpecificCulture(UILanguage);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
return base.GetControllerInstance(requestContext, controllerType);
}
}
and register it on the global.asax;
protected void Application_Start() {
//other things here
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
}

Related

Custom MVC routing based on URL stored in database

I'm trying to add some custom routing logic based on url's stored in a database for mvc. (CMS Like), I think its fairly basic, but I feel like i'm not really getting anywhere.
Basically a user may type url's such as:
www.somesite.com/categorya/categoryb/categoryf/someitem
www.somesite.com/about/someinfo
In the database these items are stored, along with the type they are, i.e. a normal page, or a product page.
Depending on this I then want to actually hit a different 'action' method, i.e. I would like the above to hit the methods:
PageController/Product
PageController/Normal
These actions then load the content for this page and display the same view (product view, or a normal view).
Using the normal way of routing won't work, since I could potentially have things like;
cata/producta
cata/catb/catc/catd/cate/catf/producta
Now i've been looking here : ASP.NET MVC custom routing for search
And trying to use this as a basis, but how do I actually 'change' my action method I want to hit within the InvokeActionMethod call?
Using MVC 3.0 btw.
Thanks for any help/suggestions
Final Solution:
Global.asax
routes.MapRoute(
"Default",
"{*path}",
new { controller = "Page", action = "NotFound", path= "Home" }
).RouteHandler = new ApplicationRouteHandler();
Route Handlers
public class ApplicationRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new ApplicationHandler(requestContext);
}
}
public class ApplicationHandler : MvcHandler, IRequiresSessionState
{
public ApplicationHandler(RequestContext requestContext)
: base(requestContext)
{
}
protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
{
var url = RequestContext.RouteData.Values["path"].ToString();
var page = SomePageService.GetPageByUrl(url);
if (page == null)
{
RequestContext.RouteData.Values["Action"] = "NotFound";
}
else
{
RequestContext.RouteData.Values["Action"] = page.Action;
RequestContext.RouteData.Values["page"] = page;
}
return base.BeginProcessRequest(httpContext, callback, state);
}
}
Maybe not an exact solution for your situation, but I've recently had to handle something similar so this might point you in the right direction.
What I did was setup a simple route in Global.asax with a catch-all parameter which calls a custom RouteHandler class.
// Custom MVC route
routes.MapRoute(
"Custom",
"{lang}/{*path}",
new { controller = "Default", action = "Index" },
new { lang = #"fr|en" }
).RouteHandler = new ApplicationRouteHandler();
ApplicationRouteHandler.cs :
public class ApplicationRouteHandler : IRouteHandler
{
/// <summary>
/// Provides the object that processes the request.
/// </summary>
/// <param name="requestContext">An object that encapsulates information about the request.</param>
/// <returns>
/// An object that processes the request.
/// </returns>
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
string path = requestContext.RouteData.Values["path"] as string;
// attempt to retrieve controller and action for current path
Page page = GetPageData(path);
// Method that returns a 404 error
if (page == null)
return SetupErrorHandler(requestContext, "ApplicationRouteHandler");
// Assign route values to current requestContext
requestContext.RouteData.Values["controller"] = page.Controller;
requestContext.RouteData.Values["action"] = page.Action;
return new MvcHandler(requestContext);
}
}
Obviously the way you retrieve the action and controller names from your database will probably be much different than mine, but this should give you an idea.

Migrate custom route to ASP.NET MVC custom route to support List<string>

I have a URL in a Django based web app that looks similar to this:
/market/prices/2011-05-01/min/stocks/msft/dell/appl/
The application is being rewritten in ASP.NET MVC 3. I need to maintain the URL.
The crux of the problem is that I to support the multiple stock ticker symbols separated by forward slashes.
I want a custom route that looks like this:
routes.MapRoute(
"Stocks",
"{queryDate}/{minOrMax}/stocks/{listOfStocksSeparatedByForwardSlash}",
new { controller = "Market", action = "Prices" }
);
The controller would look something like:
public ActionResult Prices(string queryDate, string minOrMax, ICollection<string> listOfStocksSeparatedByForwardSlash) {
var model = repository.List(queryDate, minOrMax, listOfStocksSeparatedByForwardSlash);
return View(model );
}
My current solution is as follows:
routes.MapRoute(
"Stocks",
"{queryDate}/{minOrMax}/stocks/{*listOfStocksSeparatedByForwardSlash}",
new { controller = "Market", action = "Prices" }
);
public ActionResult Prices(string queryDate, string minOrMax, string listOfStocksSeparatedByForwardSlash) {
var list = listOfStocksSeparatedByForwardSlash.Split('/').ToList();
var model = repository.List(queryDate, minOrMax, list);
return View(model );
}
Although this works, I'm interested to know if there is a better way to do this?
Okay, this is an option, although I think your approach is easier.
You can provide a RouteHandler attached to a route, like so:
routes.MapRoute(
name: "Test",
url: "Test/{someDate}/{*tickerSymbols}",
defaults: new { controller = "Home", action = "Test" }).RouteHandler = new SlashSeparatedTrailingParametersRouteHandler("tickerSymbols", "tickers");
with the route handler being
public class SlashSeparatedTrailingParametersRouteHandler : IRouteHandler
{
private readonly string catchallParameterName;
private readonly string actionTargetParameter;
public SlashSeparatedTrailingParametersRouteHandler(string catchallParameterName, string actionTargetParameter)
{
this.catchallParameterName = catchallParameterName;
this.actionTargetParameter = actionTargetParameter;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
if (requestContext == null)
{
throw new ArgumentNullException("requestContext");
}
IRouteHandler handler = new MvcRouteHandler();
var vals = requestContext.RouteData.Values;
vals[this.actionTargetParameter] = vals[this.catchallParameterName].ToString().Split('/');
return handler.GetHttpHandler(requestContext);
}
}
If this is your controller action:
[HttpGet]
public ActionResult Test(DateTime someDate, string[] tickers)
{
if (tickers == null)
{
throw new ArgumentNullException("tickers");
}
return Content(string.Format("{0} and {1}", someDate, tickers.Length.ToString(CultureInfo.InvariantCulture)));
}
and this your post:
http://localhost/Test/12-06-2012/Foo/Bar
then your output is:
12/6/2012 12:00:00 AM and 2
On the elegance this this improves the parameter on the action method at the expense of having to write your own route handler.

ASP.net MVC routing with optional first parameter

I need to provide following functionality for one of the web sites.
http://www.example.com/[sponsor]/{controller}/{action}
Depending on the [sponsor], the web page has to be customized.
I tried combination of registering the routes with Application_Start and Session_Start but not able to get it working.
public static void RegisterRoutes(RouteCollection routes, string sponsor)
{
if (routes[sponsor] == null)
{
routes.MapRoute(
sponsor, // Route name
sponsor + "/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
}
Also, the default behavior without [sponsor] should also function.
Can someone please let me know if it is technically feasible to have an optional first parameter in the MVC3 URL. If yes, please share the implementation. Thank you.
Updated Code
After making the changes as suggested by Sergey Kudriavtsev, the code works when value is given.
If name is not provided then MVC does not route to the controller/action.
Note that this works only for the home controller (both and non-sponsor). For other controllers/actions, even when sponsor parameter is specified it is not routing.
Please suggest what has to be modified.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"SponsorRoute",
"{sponsor}/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
"NonSponsorRoute",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional, sponsor = string.Empty }
);
}
Action Method
public ActionResult Index(string sponsor)
{
}
In your case sponsor should not be treated as a constant part of URL, but as a variable part.
In Global.asax:
public static void RegisterRoutes(RouteCollection routes)
{
...
routes.MapRoute(
"SponsorRoute",
"{sponsor}/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
"NonSponsorRoute",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional, sponsor=string.Empty }
);
...
}
In your controllers, for example, HomeController.cs:
namespace YourWebApp.Controllers
{
public class HomeController : Controller
{
public ActionResult Index(string sponsor)
{
// Here you can do any pre-processing depending on sponsor value, including redirects etc.
}
...
}
}
Note that type of this parameter will always be System.String and the name of route template component {sponsor} must exactly match the name of action parameter string sponsor in your controllers.
UPD: Added second route for non-sponsor case.
Please note that such setup will complicate your logic, because you might confuse different urls, for example URL
http://www.example.com/a/b/c
could be matched by both routes: first one will have sponsor=a, controller=b and action=c; second one will have controller=a, action=b and id=c.
This situation can be avoided if you specify more strict requirements to URLs - for example, you may want IDs to be numerical only. Restrictions are specified in fourth parameter of routes.MapRoute() function.
Another approach for disambiguation is specifying separate routes for all of your controllers (usually you won't have much of them in your app) before generic route for sponsors.
UPD:
Most straightforward yet least maintainable way to distinguish between sponsor and non-sponsor routes is specifying controller-specific routes, like this:
public static void RegisterRoutes(RouteCollection routes)
{
...
routes.MapRoute(
"HomeRoute",
"Home/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional, sponsor=string.Empty }
);
routes.MapRoute(
"AccountRoute",
"Account/{action}/{id}", // URL with parameters
new { controller = "Account", action = "Index", id = UrlParameter.Optional, sponsor=string.Empty }
);
...
routes.MapRoute(
"SponsorRoute",
"{sponsor}/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
...
}
Note that here all controller-specific routes must be added before SponsorRoute.
More complex yet more clean way is implementing RouteConstraints for sponsor and controller names as described in answer from #counsellorben.
In my case, I've resolved this issue using the following two routers:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "MultiCulture",
url: "{culture}/{controller}/{action}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { culture = new CultureConstraint(CultureFactory.All.Select(item => item.UrlPrefix).ToArray()) }
).RouteHandler = new MultiCultureMvcRouteHandler();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}",
defaults: new { controller = "Home", action = "Index" }
);
}
}
Where CultureConstraint class looks like below:
public class CultureConstraint : IRouteConstraint
{
private readonly string[] values;
public CultureConstraint(params string[] values)
{
this.values = values;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary routeValues, RouteDirection routeDirection)
{
string value = routeValues[parameterName].ToString();
return this.values.Contains(value);
}
}
And MultiCultureMvcRouteHandler like this:
public class MultiCultureMvcRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
{
var culture = CultureManager.GetCulture(requestContext.RouteData);
if (culture != null)
{
var cultureInfo = new CultureInfo(culture.Name);
Thread.CurrentThread.CurrentUICulture = cultureInfo;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureInfo.Name);
}
return base.GetHttpHandler(requestContext);
}
}
In addition to adding a second route before the default route, as Sergey said in his answer, you also must add a RouteConstraint to the initial route, to enforce that the {sponsor} token is the name of a valid sponsor.
You can use the RouteConstraint in this answer: Asp.Net Custom Routing and custom routing and add category before controller
Remember that you must also enforce a rule that a sponsor name cannot be the same as any of your controller names.
i will show you in simple example you don't have to change in Route.config.cs
only you have to do in Route.config.cs just put in
Optional URI Parameters First and Default Values
Route.config.cs
routes.MapMvcAttributeRoutes();
Controller
[Route("{Name}/Controller/ActionName")]
public ActionResult Details(string Name)
{
// some code here
return View();
}
Results
localhost:2345/Name/controllername/actionname/id(optional)

ASP.NET MVC language change link

I have an ASP.NET MVC site that it's in two languages using Resources. To allow the server to present the site in the apropiate language (depending on the one that's configured in the user's browser) I put the following in my web.config:
<globalization culture="es-es" uiCulture="auto" />
How can I add a link to change the uiCulture? I want to store the selection in a cookie and if it's not present, then fall back to the browser configuration... Is it possible?
You may take a look at the following guide. It uses Session to store the current user language preference but the code could be very easily tweaked in order to use a cookie. The idea is that you will have a controller action:
public ActionResult ChangeCulture(string lang, string returnUrl)
{
var langCookie = new HttpCookie("lang", lang)
{
HttpOnly = true
};
Response.AppendCookie(langCookie);
return Redirect(returnUrl);
}
and then in Global.asax you could subscribe for the Application_AcquireRequestState event in order to set the current thread culture based on the value of the cookie:
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
var langCookie = HttpContext.Current.Request.Cookies["lang"];
if (langCookie != null)
{
var ci = new CultureInfo(langCookie.Value);
//Checking first if there is no value in session
//and set default language
//this can happen for first user's request
if (ci == null)
{
//Sets default culture to english invariant
string langName = "en";
//Try to get values from Accept lang HTTP header
if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0)
{
//Gets accepted list
langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2);
}
langCookie = new HttpCookie("lang", langName)
{
HttpOnly = true
};
HttpContext.Current.Response.AppendCookie(langCookie);
}
//Finally setting culture for each request
Thread.CurrentThread.CurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = ci;
//The line below creates issue when using default culture values for other
//cultures for ex: NumericSepratore.
//Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
Now this being said using cookies and session to store current language is not SEO friendly. What I prefer doing when I need a localized application is to use a special route which will contain the language:
routes.MapRoute(
"Default",
"{lang}/{controller}/{action}/{id}",
new
{
lang = "en-US",
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}
);
and then prefix all my urls with the language. This provides unique urls for different languages so that robots can properly index all content. Now all that's left is to modify the Application_AcquireRequestState method so that it uses the lang token of the url and based on its value set the proper Thread.CurrentThread.CurrentUICulture and Thread.CurrentThread.CurrentCulture.
And now when you wanted to change the language you would simply generate the proper link:
#Html.ActionLink("Page index en français", "index", new { lang = "fr-FR" })
An alternative and I feel it is more flexible
protected override void ExecuteCore()
{
if (RouteData.Values["lang"] != null && !string.IsNullOrWhiteSpace(RouteData.Values["lang"].ToString()))
{
SetCulture(RouteData.Values["lang"].ToString());
}
else
{
var cookie = HttpContext.Request.Cookies["myappculture"];
if (cookie != null)
{ SetCulture(cookie.Value); }
else
{ SetCulture(HttpContext.Request.UserLanguages[0]);}
}
base.ExecuteCore();
}
public ActionResult ChangeCulture(string lang, string returnUrl)
{
SetCulture(lang);
// Little house keeping
Regex re = new Regex("^/\\w{2,3}(-\\w{2})?");
returnUrl = re.Replace(returnUrl,"/" + lang.ToLower());
return Redirect(returnUrl);
}
private void SetCulture(string lang)
{
CultureInfo ci = new CultureInfo(lang);
System.Threading.Thread.CurrentThread.CurrentUICulture = ci;
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
// Force a valid culture in the URL
RouteData.Values["lang"] = lang;
// save the location into cookie
HttpCookie _cookie = new HttpCookie("myappculture", Thread.CurrentThread.CurrentUICulture.Name);
_cookie.Expires = DateTime.Now.AddYears(1);
HttpContext.Response.SetCookie(_cookie);
}
In the view
I kept the resource in a different project as follows
If you use the App_GloabalResources to store your resx language files, all you have to do is add a drop down which changes the current thread's UI Culture and this will automatically select the right resx language file to display.
App_GloabalResources is not the right place the resources when it comes to MVC programmering. See http://buildingwebapps.blogspot.no/2012/05/right-way-to-use-resource-files-for.html

RouteHandler vs ControllerFactory

new to asp.net mvc (using v3 + razor) and am wondering how to best solve a problem with creating dynamic routes based on a database. Essentially, the main site navigation will be entered into a database and I want to load them up as routes. i.e. - Load Category list from database, then append the routes to the routing engine if possible...
mysite.com/cars
mysite.com/televisions
mysite.com/computers
etc....
Each category after the slash comes from the db, but, there are regular entries like /about and /contactus that will not be in the database and have been statically entered in the global.asax... my question is:
For the dynamic database URLs should I use a custom RouteHandler or pehaps create a ControllerFactory that will match and handle the requests for the entries loaded from the database. Is it possible to have the DefaultControllerFactory handle the routing if my RouteHandler or CustomControllerFactory don't find the route in the list from the database? Thanks for any help, very first project with this so I'm not sure what the best route is ;) no pun intended...
Update:
Tried using a route constraint that pulls from the database but it conflicts with the default route now... here is my custom constraint and routes:
public class CategoryListConstraint : IRouteConstraint
{
public CategoryListConstraint()
{
var repo = new Repository<Topic>();
var cats = repo.All();
var values = new List<string>();
foreach (var c in cats)
{
values.Add(c.URI.Replace("/", "").Replace("?", ""));
}
this._values = values.ToArray<string>();
}
private string[] _values;
public bool Match(HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
// Get the value called "parameterName" from the
// RouteValueDictionary called "value"
string value = values[parameterName].ToString();
// Return true is the list of allowed values contains
// this value.
return _values.Contains(value);
}
}
and here are the routes:
Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Categories",
"{category}/{*values}",
new { controller = "Category", action = "List" },
new CategoryListConstraint()
);
Routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
The home page www.mysite.com loads using the Default route. All the URLs that match the constraint list are loaded by the category route... but if I have the www.mysite.com/admin or www.mysite.com/aboutus these are getting picked up by the Categories route even though the values are not in the constraint list. Confused...
What about something like this?
Categories controller:
public ActionResult List(string category)
{
var products = _repo.Get(category); // however you are getting your data
return View(products);
}
Routes
routers.MapRoute(
"About",
"About",
new { controller = "Home", action = "About" });
//... other static routes
routes.MapRoute(
"CategoriesList",
"{id}",
new { controller = "Categories", action = "List" },
new { id = #"\w+" });
The incoming URL is tested against each Route rule to see if it matches - and if a Route rule matches then that rule (and its associated RouteHandler) is the one that is used to process the request (and all subsequent rules are ignored). This means that you want to typically structure your routing Rules in a "most specific to least specific" order
source
Found the exact solution I was looking for. Code is below. I managed to avoid using Controller Factories or implementing a custom IRouteHandler by using extending the RouteBase class which worked perfectly and allows me to pass control down to the default mvc route is something specific isn't hit. BTW - constraints ended up not working properly as the broke the controllers associated with the default route (although the default route was getting hit)
public class CustomRoutingEngine : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeHandler = new MvcRouteHandler();
var currentRoute = new Route("{controller}/{*URI}", routeHandler);
var routeData = new RouteData(currentRoute, routeHandler);
// implement caching here
var list = GetConstraintList();
// set your values dynamically here
routeData.Values["controller"] = "Category";
// or
routeData.Values.Add("action", "List");
// return the route, or null to have it passed to the next routing engine in the list
var url = Util.StripSlashOnFrontAndBack(httpContext.Request.Path.ToLower()).Split('/')[0];
if (list.Contains(url))
return routeData;
return null; // have another route handle the routing
}
protected List<string> GetConstraintList()
{
using (var repo = new RavenRepository<Topic>())
{
var tops = repo.Query().Where(x => x.Hidden == false).ToList()
.Select(x=>x.Name.ToLower());
List<string> list = new List<string>();
list.AddRange(tops);
repo.Dispose();
return list ?? new List<string>();
}
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//implement this to return url's for routes, or null to just pass it on
return null;
}
}
Then my register routes method looks like so:
Routes.Clear();
// Set Defaults
Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.Add(new App.Helpers.CustomRoutingEngine());
Routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);

Resources