Change route on runtime in MVC4 - c#

Currently when I enter /ru/ it will change to Russian language but then if I go to / it will display Russian resources (meaning cookie/locale is set), however it won't redirect to /ru/. I can't really just redirect it since, for example if user is on /en/item/32 and he changes language to Russian he needs to be redirected to /ru/item/32, not /ru.
My [Localization] data-annotation function that checks cookies
public class Localization : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.RouteData.Values["lang"] != null && !string.IsNullOrWhiteSpace(filterContext.RouteData.Values["lang"].ToString()))
{
// Set from route data
var lang = filterContext.RouteData.Values["lang"].ToString();
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
}
else
{
// Set from cookie
var cookie = filterContext.HttpContext.Request.Cookies["lang"];
var langHeader = string.Empty;
if (cookie != null)
{
langHeader = cookie.Value;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
}
else
{
// Cookie does not exist, set default
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
}
filterContext.RouteData.Values["lang"] = langHeader;
}
// Update cookie
HttpCookie _cookie = new HttpCookie("lang", Thread.CurrentThread.CurrentUICulture.Name);
_cookie.Expires = DateTime.Now.AddYears(1);
filterContext.HttpContext.Response.SetCookie(_cookie);
base.OnActionExecuting(filterContext);
}
}
And my routes are configured like so
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Localization",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "ORMebeles.Controllers" }
);
/*
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "ORMebeles.Controllers" }
);
*/
}
}
So how can I just inject the {lang} attribute to route and make it stick there?

The trouble you've got here is that OnActionExecuting runs after the incoming request has been evaluated against your routes. So setting filterContext.RouteData.Values["lang"] = langHeader; has no effect in this scenario.
There's a good (MVC 2) example here of how to set up a multi-language site, although this example does not use a lang variable in the url - about halfway down they set up ActionLinks to an action that changes the culture and then redirects back to the previous page.
If you need to keep the lang variable in the URL, you could set up the links to point at the current page, but change the lang parameter, using an HtmlHelper like this:
public static MvcHtmlString ChangeLanguageLink(this HtmlHelper html, string text, string lang)
{
var currentController = html.ViewContext.RouteData.GetRequiredString("controller");
var currentAction = html.ViewContext.RouteData.GetRequiredString("action");
return html.ActionLink(text, currentAction, currentController, new { lang }, null);
}
This should use your route to produce a link that sends the user to the same action but with a different lang - and your Localization filter can set the current culture accordingly.
If there is no lang specified in the url and you need to redirect, then you can set the filterContxt.Result to a RedirectResult with similar parameters as described above.

Related

ASP.NET MVC: How to get route values [duplicate]

This question already has answers here:
ASP.NET MVC 5 culture in route and url
(2 answers)
Closed 5 years ago.
I need to get the value of language parameter i tried this code
if(HttpContext.Current.Request.RequestContext.RouteData.Values["language"] == null)
{
HttpContext.Current.Request.RequestContext.RouteData.Values["language"] = "en-US";
}
the above code makes the url like the following which is very good
http://localhost:25576/en-US/Home
the problem is when the user enters http://localhost:25576/Home (without en-US)
the value of HttpContext.Current.Request.RequestContext.RouteData.Values["language"] becomes "Home"
My question is how to get the real value of language parameter if the user removes en-US or if the user enters http://localhost:25576/Home
RouteConfig
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "DefaultLocalized",
url: "{language}/{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional,
language = ""
}
);
}
You can create a new ActionFilterAttribute for that purpose:
public class LocalizationAttribute : ActionFilterAttribute
{
private readonly string _defaultLanguage = "en-US";
public LocalizationAttribute(string defaultLanguage = null)
{
this._defaultLanguage = defaultLanguage ?? this._defaultLanguage;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var language = filterContext.RouteData.Values["language"] as string ?? this._defaultLanguage;
if (language != this._defaultLanguage)
{
try
{
Thread.CurrentThread.CurrentCulture =
Thread.CurrentThread.CurrentUICulture =
CultureInfo.CreateSpecificCulture(language);
}
catch
{
throw new NotSupportedException($"Invalid language code '{language}'.");
}
}
}
}
You also need to register that ActionFilter as part of the GlobalFilter.
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new LocalizationAttribute("en-US"), 0);
}
}
The token of "language" is "null" or "empty" in your codes, if a user's request ignored this. So you MUST assign it as a default value, such as "en-US".
Take this as an example:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "DefaultLocalized",
url: "{language}/{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional,
language = "en-US"
}
);
}
And please remove the language assignment snippet of codes you've shown to us above, it can be TOTALLY done through the default route config.

Asp.net Mvc Route By Only Slug

I'm using ASP.NET MVC 4 with C#. I'm using areas and it's named like "Admin"
Here is my route config;
public static class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(name: "PageBySlug",
url: "{slug}",
defaults: new {controller = "Home", action = "RenderPage"},
constraints: new {slug = ".+"});
routes.MapRoute(name: "Default",
url: "{controller}/{action}/{id}",
defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional},
namespaces: new[] { "Web.Frontend.Controllers.Controllers" });
}
}
I generated frontend page links like; "products/apple-iphone"
So I want to call them like this.
But the error is: The code can't get the controller / action method.
I used frontend page links like;
#Html.ActionLink(linkItem.Title, "RenderPage", routeValues: new {controller = "Home", slug = linkItem.PageSlug})
#Html.RouteLink(linkItem.Title, routeName: "PageBySlug", routeValues: new { controller = "Home", action = "RenderPage", slug = linkItem.PageSlug })
#linkItem.Title
#linkItem.Title
They are rendering url links like; http://localhost:1231/products/apple-iphone
It's like what I want. But when I click any link, asp.net mvc gives me this error:
Server Error in '/' Application.
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /products/apple-iphone
Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.6.1069.1
Here is my controller;
namespace Web.Frontend.Controllers
{
public class HomeController : BaseFrontendController
{
public ActionResult Index()
{
return View();
}
public ActionResult RenderPage(string slug)
{
return View();
}
}
}
So how can I catch every link request like this combined slug and turn my coded view ?
The problem is, When you request products/iphone, the routing engine don't know whether you meant the slug "products/iphone" or the controller "products" and action method "iphone".
You can write a custom route constraint to take care of this. This constraint will check whether the slug part of the urls is a valid controller or not, if yes,the controller action will be executed.
public class SlugConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
var asm = Assembly.GetExecutingAssembly();
//Get all the controller names
var controllerTypes = (from t in asm.GetExportedTypes()
where typeof(IController).IsAssignableFrom(t)
select t.Name.Replace("Controller", ""));
var slug = values["slug"];
if (slug != null)
{
if (controllerTypes.Any(x => x.Equals(slug.ToString(),
StringComparison.OrdinalIgnoreCase)))
{
return false;
}
else
{
var c = slug.ToString().Split('/');
if (c.Any())
{
var firstPart = c[0];
if (controllerTypes.Any(x => x.Equals(firstPart,
StringComparison.OrdinalIgnoreCase)))
{
return false;
}
}
}
return true;
}
return false;
}
}
Now use this route constraint when you register your custom route definition for the slug. make sure you use {*slug} in the route pattern. The * indicates it is anything(Ex : "a/b/c")(Variable number of url segments- more like a catch all)
routes.MapRoute(name: "PageBySlug",
url: "{*slug}",
defaults: new { controller = "Home", action = "RenderPage" },
constraints: new { slug = new SlugConstraint() }
, namespaces: new string[] { "Web.Frontend.Controllers.Controllers" });
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
, new string[] { "Web.Frontend.Controllers.Controllers" });
you can provide only this type of link
#linkItem.Title
Because Routetable find your route using Route name provided by you. so controller name and action name is not necessary.

ASP.NET MVC 5 culture in route and url

I've translated my mvc website, which is working great. If I select another language (Dutch or English) the content gets translated.
This works because I set the culture in the session.
Now I want to show the selected culture(=culture) in the url.
If it is the default language it should not be showed in the url, only if it is not the default language it should show it in the url.
e.g.:
For default culture (dutch):
site.com/foo
site.com/foo/bar
site.com/foo/bar/5
For non-default culture (english):
site.com/en/foo
site.com/en/foo/bar
site.com/en/foo/bar/5
My problem is that I always see this:
site.com/nl/foo/bar/5
even if I clicked on English (see _Layout.cs). My content is translated in English but the route parameter in the url stays on "nl" instead of "en".
How can I solve this or what am I doing wrong?
I tried in the global.asax to set the RouteData but doesn't help.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
routes.LowercaseUrls = true;
routes.MapRoute(
name: "Errors",
url: "Error/{action}/{code}",
defaults: new { controller = "Error", action = "Other", code = RouteParameter.Optional }
);
routes.MapRoute(
name: "DefaultWithCulture",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { culture = "[a-z]{2}" }
);// or maybe: "[a-z]{2}-[a-z]{2}
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Global.asax.cs:
protected void Application_Start()
{
MvcHandler.DisableMvcResponseHeader = true;
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
if (HttpContext.Current.Session != null)
{
CultureInfo ci = (CultureInfo)this.Session["Culture"];
if (ci == null)
{
string langName = "nl";
if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0)
{
langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2);
}
ci = new CultureInfo(langName);
this.Session["Culture"] = ci;
}
HttpContextBase currentContext = new HttpContextWrapper(HttpContext.Current);
RouteData routeData = RouteTable.Routes.GetRouteData(currentContext);
routeData.Values["culture"] = ci;
Thread.CurrentThread.CurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
_Layout.cs (where I let user change language)
// ...
<ul class="dropdown-menu" role="menu">
<li class="#isCurrentLang("nl")">#Html.ActionLink("Nederlands", "ChangeCulture", "Culture", new { lang = "nl", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "nl" })</li>
<li class="#isCurrentLang("en")">#Html.ActionLink("English", "ChangeCulture", "Culture", new { lang = "en", returnUrl = this.Request.RawUrl }, new { rel = "alternate", hreflang = "en" })</li>
</ul>
// ...
CultureController: (=where I set the Session that I use in GlobalAsax to change the CurrentCulture and CurrentUICulture)
public class CultureController : Controller
{
// GET: Culture
public ActionResult Index()
{
return RedirectToAction("Index", "Home");
}
public ActionResult ChangeCulture(string lang, string returnUrl)
{
Session["Culture"] = new CultureInfo(lang);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
}
There are several issues with this approach, but it boils down to being a workflow issue.
You have a CultureController whose only purpose is to redirect the user to another page on the site. Keep in mind RedirectToAction will send an HTTP 302 response to the user's browser, which will tell it to lookup the new location on your server. This is an unnecessary round-trip across the network.
You are using session state to store the culture of the user when it is already available in the URL. Session state is totally unnecessary in this case.
You are reading the HttpContext.Current.Request.UserLanguages from the user, which might be different from the culture they requested in the URL.
The third issue is primarily because of a fundamentally different view between Microsoft and Google about how to handle globalization.
Microsoft's (original) view was that the same URL should be used for every culture and that the UserLanguages of the browser should determine what language the website should display.
Google's view is that every culture should be hosted on a different URL. This makes more sense if you think about it. It is desirable for every person who finds your website in the search results (SERPs) to be able to search for the content in their native language.
Globalization of a web site should be viewed as content rather than personalization - you are broadcasting a culture to a group of people, not an individual person. Therefore, it typically doesn't make sense to use any personalization features of ASP.NET such as session state or cookies to implement globalization - these features prevent search engines from indexing the content of your localized pages.
If you can send the user to a different culture simply by routing them to a new URL, there is far less to worry about - you don't need a separate page for the user to select their culture, simply include a link in the header or footer to change the culture of the existing page and then all of the links will automatically switch to the culture the user has chosen (because MVC automatically reuses route values from the current request).
Fixing the Issues
First of all, get rid of the CultureController and the code in the Application_AcquireRequestState method.
CultureFilter
Now, since culture is a cross-cutting concern, setting the culture of the current thread should be done in an IAuthorizationFilter. This ensures the culture is set before the ModelBinder is used in MVC.
using System.Globalization;
using System.Threading;
using System.Web.Mvc;
public class CultureFilter : IAuthorizationFilter
{
private readonly string defaultCulture;
public CultureFilter(string defaultCulture)
{
this.defaultCulture = defaultCulture;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var values = filterContext.RouteData.Values;
string culture = (string)values["culture"] ?? this.defaultCulture;
CultureInfo ci = new CultureInfo(culture);
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
You can set the filter globally by registering it as a global filter.
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CultureFilter(defaultCulture: "nl"));
filters.Add(new HandleErrorAttribute());
}
}
Language Selection
You can simplify the language selection by linking to the same action and controller for the current page and including it as an option in the page header or footer in your _Layout.cshtml.
#{
var routeValues = this.ViewContext.RouteData.Values;
var controller = routeValues["controller"] as string;
var action = routeValues["action"] as string;
}
<ul>
<li>#Html.ActionLink("Nederlands", #action, #controller, new { culture = "nl" }, new { rel = "alternate", hreflang = "nl" })</li>
<li>#Html.ActionLink("English", #action, #controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" })</li>
</ul>
As mentioned previously, all other links on the page will automatically be passed a culture from the current context, so they will automatically stay within the same culture. There is no reason to pass the culture explicitly in those cases.
#ActionLink("About", "About", "Home")
With the above link, if the current URL is /Home/Contact, the link that is generated will be /Home/About. If the current URL is /en/Home/Contact, the link will be generated as /en/Home/About.
Default Culture
Finally, we get to the heart of your question. The reason your default culture is not being generated correctly is because routing is a 2-way map and regardless of whether you are matching an incoming request or generating an outgoing URL, the first match always wins. When building your URL, the first match is DefaultWithCulture.
Normally, you can fix this simply by reversing the order of the routes. However, in your case that would cause the incoming routes to fail.
So, the simplest option in your case is to build a custom route constraint to handle the special case of the default culture when generating the URL. You simply return false when the default culture is supplied and it will cause the .NET routing framework to skip the DefaultWithCulture route and move to the next registered route (in this case Default).
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;
public class CultureConstraint : IRouteConstraint
{
private readonly string defaultCulture;
private readonly string pattern;
public CultureConstraint(string defaultCulture, string pattern)
{
this.defaultCulture = defaultCulture;
this.pattern = pattern;
}
public bool Match(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration &&
this.defaultCulture.Equals(values[parameterName]))
{
return false;
}
else
{
return Regex.IsMatch((string)values[parameterName], "^" + pattern + "$");
}
}
}
All that is left is to add the constraint to your routing configuration. You also should remove the default setting for culture in the DefaultWithCulture route since you only want it to match when there is a culture supplied in the URL anyway. The Default route on the other hand should have a culture because there is no way to pass it through the URL.
routes.LowercaseUrls = true;
routes.MapRoute(
name: "Errors",
url: "Error/{action}/{code}",
defaults: new { controller = "Error", action = "Other", code = UrlParameter.Optional }
);
routes.MapRoute(
name: "DefaultWithCulture",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
AttributeRouting
NOTE: This section applies only if you are using MVC 5. You can skip this if you are using a previous version.
For AttributeRouting, you can simplify things by automating the creation of 2 different routes for each action. You need to tweak each route a little bit and add them to the same class structure that MapMvcAttributeRoutes uses. Unfortunately, Microsoft decided to make the types internal so it requires Reflection to instantiate and populate them.
RouteCollectionExtensions
Here we just use the built in functionality of MVC to scan our project and create a set of routes, then insert an additional route URL prefix for the culture and the CultureConstraint before adding the instances to our MVC RouteTable.
There is also a separate route that is created for resolving the URLs (the same way that AttributeRouting does it).
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Mvc.Routing;
using System.Web.Routing;
public static class RouteCollectionExtensions
{
public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object constraints)
{
MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(constraints));
}
public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary constraints)
{
var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc");
var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc");
FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance);
var subRoutes = Activator.CreateInstance(subRouteCollectionType);
var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes);
// Add the route entries collection first to the route collection
routes.Add((RouteBase)routeEntries);
var localizedRouteTable = new RouteCollection();
// Get a copy of the attribute routes
localizedRouteTable.MapMvcAttributeRoutes();
foreach (var routeBase in localizedRouteTable)
{
if (routeBase.GetType().Equals(routeCollectionRouteType))
{
// Get the value of the _subRoutes field
var tempSubRoutes = subRoutesInfo.GetValue(routeBase);
// Get the PropertyInfo for the Entries property
PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries");
if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)))
{
foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes))
{
var route = routeEntry.Route;
// Create the localized route
var localizedRoute = CreateLocalizedRoute(route, urlPrefix, constraints);
// Add the localized route entry
var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute);
AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry);
// Add the default route entry
AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry);
// Add the localized link generation route
var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute);
routes.Add(localizedLinkGenerationRoute);
// Add the default link generation route
var linkGenerationRoute = CreateLinkGenerationRoute(route);
routes.Add(linkGenerationRoute);
}
}
}
}
}
private static Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints)
{
// Add the URL prefix
var routeUrl = urlPrefix + route.Url;
// Combine the constraints
var routeConstraints = new RouteValueDictionary(constraints);
foreach (var constraint in route.Constraints)
{
routeConstraints.Add(constraint.Key, constraint.Value);
}
return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);
}
private static RouteEntry CreateLocalizedRouteEntry(string name, Route route)
{
var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized";
return new RouteEntry(localizedRouteEntryName, route);
}
private static void AddRouteEntry(Type subRouteCollectionType, object subRoutes, RouteEntry newEntry)
{
var addMethodInfo = subRouteCollectionType.GetMethod("Add");
addMethodInfo.Invoke(subRoutes, new[] { newEntry });
}
private static RouteBase CreateLinkGenerationRoute(Route innerRoute)
{
var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc");
return (RouteBase)Activator.CreateInstance(linkGenerationRouteType, innerRoute);
}
}
Then it is just a matter of calling this method instead of MapMvcAttributeRoutes.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Call to register your localized and default attribute routes
routes.MapLocalizedMvcAttributeRoutes(
urlPrefix: "{culture}/",
constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") }
);
routes.MapRoute(
name: "DefaultWithCulture",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "nl", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Default culture fix
Incredible post by NightOwl888. There is something missing though - the normal(not localized) URL-generation attribute routes, which get added through reflection, also need a default culture parameter, otherwise you get a query parameter in the URL.
?culture=nl
In order to avoid this, these changes must be made:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Routing;
using System.Web.Routing;
namespace Endpoints.WebPublic.Infrastructure.Routing
{
public static class RouteCollectionExtensions
{
public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, object defaults, object constraints)
{
MapLocalizedMvcAttributeRoutes(routes, urlPrefix, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints));
}
public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string urlPrefix, RouteValueDictionary defaults, RouteValueDictionary constraints)
{
var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc");
var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc");
FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance);
var subRoutes = Activator.CreateInstance(subRouteCollectionType);
var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes);
// Add the route entries collection first to the route collection
routes.Add((RouteBase)routeEntries);
var localizedRouteTable = new RouteCollection();
// Get a copy of the attribute routes
localizedRouteTable.MapMvcAttributeRoutes();
foreach (var routeBase in localizedRouteTable)
{
if (routeBase.GetType().Equals(routeCollectionRouteType))
{
// Get the value of the _subRoutes field
var tempSubRoutes = subRoutesInfo.GetValue(routeBase);
// Get the PropertyInfo for the Entries property
PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries");
if (entriesInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)))
{
foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(tempSubRoutes))
{
var route = routeEntry.Route;
// Create the localized route
var localizedRoute = CreateLocalizedRoute(route, urlPrefix, constraints);
// Add the localized route entry
var localizedRouteEntry = CreateLocalizedRouteEntry(routeEntry.Name, localizedRoute);
AddRouteEntry(subRouteCollectionType, subRoutes, localizedRouteEntry);
// Add the default route entry
AddRouteEntry(subRouteCollectionType, subRoutes, routeEntry);
// Add the localized link generation route
var localizedLinkGenerationRoute = CreateLinkGenerationRoute(localizedRoute);
routes.Add(localizedLinkGenerationRoute);
// Add the default link generation route
//FIX: needed for default culture on normal attribute route
var newDefaults = new RouteValueDictionary(defaults);
route.Defaults.ToList().ForEach(x => newDefaults.Add(x.Key, x.Value));
var routeWithNewDefaults = new Route(route.Url, newDefaults, route.Constraints, route.DataTokens, route.RouteHandler);
var linkGenerationRoute = CreateLinkGenerationRoute(routeWithNewDefaults);
routes.Add(linkGenerationRoute);
}
}
}
}
}
private static Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints)
{
// Add the URL prefix
var routeUrl = urlPrefix + route.Url;
// Combine the constraints
var routeConstraints = new RouteValueDictionary(constraints);
foreach (var constraint in route.Constraints)
{
routeConstraints.Add(constraint.Key, constraint.Value);
}
return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);
}
private static RouteEntry CreateLocalizedRouteEntry(string name, Route route)
{
var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized";
return new RouteEntry(localizedRouteEntryName, route);
}
private static void AddRouteEntry(Type subRouteCollectionType, object subRoutes, RouteEntry newEntry)
{
var addMethodInfo = subRouteCollectionType.GetMethod("Add");
addMethodInfo.Invoke(subRoutes, new[] { newEntry });
}
private static RouteBase CreateLinkGenerationRoute(Route innerRoute)
{
var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc");
return (RouteBase)Activator.CreateInstance(linkGenerationRouteType, innerRoute);
}
}
}
And to attribute routes registration:
RouteTable.Routes.MapLocalizedMvcAttributeRoutes(
urlPrefix: "{culture}/",
defaults: new { culture = "nl" },
constraints: new { culture = new CultureConstraint(defaultCulture: "nl", pattern: "[a-z]{2}") }
);
Better solution
And actually, after some time, I needed to add url translation, so i digged in more, and it appears there is no need to do the reflection hacking described. The ASP.NET guys thought about it, there is much cleaner solution - instead you can extend a DefaultDirectRouteProvider like this:
public static class RouteCollectionExtensions
{
public static void MapLocalizedMvcAttributeRoutes(this RouteCollection routes, string defaultCulture)
{
var routeProvider = new LocalizeDirectRouteProvider(
"{culture}/",
defaultCulture
);
routes.MapMvcAttributeRoutes(routeProvider);
}
}
class LocalizeDirectRouteProvider : DefaultDirectRouteProvider
{
ILogger _log = LogManager.GetCurrentClassLogger();
string _urlPrefix;
string _defaultCulture;
RouteValueDictionary _constraints;
public LocalizeDirectRouteProvider(string urlPrefix, string defaultCulture)
{
_urlPrefix = urlPrefix;
_defaultCulture = defaultCulture;
_constraints = new RouteValueDictionary() { { "culture", new CultureConstraint(defaultCulture: defaultCulture) } };
}
protected override IReadOnlyList<RouteEntry> GetActionDirectRoutes(
ActionDescriptor actionDescriptor,
IReadOnlyList<IDirectRouteFactory> factories,
IInlineConstraintResolver constraintResolver)
{
var originalEntries = base.GetActionDirectRoutes(actionDescriptor, factories, constraintResolver);
var finalEntries = new List<RouteEntry>();
foreach (RouteEntry originalEntry in originalEntries)
{
var localizedRoute = CreateLocalizedRoute(originalEntry.Route, _urlPrefix, _constraints);
var localizedRouteEntry = CreateLocalizedRouteEntry(originalEntry.Name, localizedRoute);
finalEntries.Add(localizedRouteEntry);
originalEntry.Route.Defaults.Add("culture", _defaultCulture);
finalEntries.Add(originalEntry);
}
return finalEntries;
}
private Route CreateLocalizedRoute(Route route, string urlPrefix, RouteValueDictionary constraints)
{
// Add the URL prefix
var routeUrl = urlPrefix + route.Url;
// Combine the constraints
var routeConstraints = new RouteValueDictionary(constraints);
foreach (var constraint in route.Constraints)
{
routeConstraints.Add(constraint.Key, constraint.Value);
}
return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);
}
private RouteEntry CreateLocalizedRouteEntry(string name, Route route)
{
var localizedRouteEntryName = string.IsNullOrEmpty(name) ? null : name + "_Localized";
return new RouteEntry(localizedRouteEntryName, route);
}
}
There is a solution based on this, including url translation here: https://github.com/boudinov/mvc-5-routing-localization

Dynamic Routes from database for ASP.NET MVC CMS

Basically I have a CMS backend I built using ASP.NET MVC and now I'm moving on to the frontend site and need to be able to load pages from my CMS database, based on the route entered.
So if the user enters example.com/students/information, MVC would look in the pages table to see if a page exists that has a permalink that matches students/information, if so it would redirect to the page controller and then load the page data from the database and return it to the view for display.
So far I have tried to have a catch all route, but it only works for two URL segments, so /students/information, but not /students/information/fall. I can't find anything online on how to accomplish this, so I though I would ask here, before I find and open source ASP.NET MVC CMS and dissect the code.
Here is the route configuration I have so far, but I feel there is a better way to do this.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Default route to handle core pages
routes.MapRoute(null,"{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new { controller = "Index" }
);
// CMS route to handle routing to the PageController to check the database for the route.
var db = new MvcCMS.Models.MvcCMSContext();
//var page = db.CMSPages.Where(p => p.Permalink == )
routes.MapRoute(
null,
"{*.}",
new { controller = "Page", action = "Index" }
);
}
If anybody can point me in the right direction on how I would go about loading CMS pages from the database, with up to three URL segments, and still be able to load core pages, that have a controller and action predefined.
You can use a constraint to decide whether to override the default routing logic.
public class CmsUrlConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var db = new MvcCMS.Models.MvcCMSContext();
if (values[parameterName] != null)
{
var permalink = values[parameterName].ToString();
return db.CMSPages.Any(p => p.Permalink == permalink);
}
return false;
}
}
use it in route definition like,
routes.MapRoute(
name: "CmsRoute",
url: "{*permalink}",
defaults: new {controller = "Page", action = "Index"},
constraints: new { permalink = new CmsUrlConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Now if you have an 'Index' action in 'Page' Controller like,
public ActionResult Index(string permalink)
{
//load the content from db with permalink
//show the content with view
}
all urls will be caught by the first route and be verified by the constraint.
if the permalink exists in db the url will be handled by Index action in Page controller.
if not the constraint will fail and the url will fallback to default route(i dont know if you have any other controllers in the project and how you will decide your 404 logic).
EDIT
To avoid re querying the cms page in the Index action in Page controller, one can use the HttpContext.Items dictionary, like
in the constraint
var db = new MvcCMS.Models.MvcCMSContext();
if (values[parameterName] != null)
{
var permalink = values[parameterName].ToString();
var page = db.CMSPages.Where(p => p.Permalink == permalink).FirstOrDefault();
if(page != null)
{
HttpContext.Items["cmspage"] = page;
return true;
}
return false;
}
return false;
then in the action,
public ActionResult Index(string permalink)
{
var page = HttpContext.Items["cmspage"] as CMSPage;
//show the content with view
}
I use simpler approach that doesn't require any custom router handling.
Simply create a single/global Controller that handles a few optional parameters, then process those parameters as you like:
//Route all traffic through this controller with the base URL being the domain
[Route("")]
[ApiController]
public class ValuesController : ControllerBase
{
//GET api/values
[HttpGet("{a1?}/{a2?}/{a3?}/{a4?}/{a5?}")]
public ActionResult<IEnumerable<string>> Get(string a1 = "", string a2 = "", string a3 = "", string a4 = "", string a5 = "")
{
//Custom logic processing each of the route values
return new string[] { a1, a2, a3, a4, a5 };
}
}
Sample output at example.com/test1/test2/test3
["test1","test2","test3","",""]

Can't figure out why this MVC2 route doesn't work

I'm trying to create an ASP.NET MVC2 route with a regular expression constraint to filter language names (like en-us, pt-br) but unfortunately it doesn't work. Have a look:
routes.MapRoute(
"Culture", // Route name
"{culture}", // URL with parameters
new { controller = "Home", action = "Culture" }, // Parameter defaults
new { culture = #"^[a-z]{2}-[a-z]{2}$" }
);
Does anyone have any idea?
Edit: The url i'm testing is http://localhost/en-us
case sensitive perhaps?
"en-US"
So you need:
new { culture = #"^[a-z]{2}-[A-Z]{2}$" }
But use this one to make it case insensitive:
new { culture = #"^[a-zA-Z]{2}-[a-zA-Z]{2}$" }
I don't know why it doesn't work in your case but here's what works:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Culture",
"{culture}",
new { controller = "Home", action = "Culture" },
new { culture = #"^[a-z]{2}-[a-z]{2}$" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Controller:
public class HomeController : Controller
{
public ActionResult Culture(string culture)
{
return View();
}
}
URL: http://example.com/en-us invokes successfully the Culture action on HomeController and passes en-us in the culture parameter.

Categories

Resources