How to change route to username after logged in? - c#

Before user login, the route is:
localhost:54274/Home
localhost:54274/Home/About
localhost:54274/Home/Contact
localhost:54274/Home/Login
localhost:54274/Home/Register
Once after user logged in, the route is:
1. localhost:54274/Project
2. localhost:54274/Project/Create
3. localhost:54274/Project/Edit/1
4. localhost:54274/Project/Delete/2
5. localhost:54274/Project/1/Requirement
6. localhost:54274/Project/1/Requirement/Create
7. localhost:54274/Project/1/Requirement/Edit/3
8. localhost:54274/Project/1/Requirement/Delete/4
I want the route changed to the username once after user logged in. For example, the username is hendyharf.
1. localhost:54274/hendyharf/Project
2. localhost:54274/hendyharf/Project/Create
3. localhost:54274/hendyharf/Project/Edit/1
4. localhost:54274/hendyharf/Project/Delete/2
5. localhost:54274/hendyharf/Project/1/Requirement
6. localhost:54274/hendyharf/Project/1/Requirement/Create
7. localhost:54274/hendyharf/Project/1/Requirement/Edit/3
8. localhost:54274/hendyharf/Project/1/Requirement/Delete/4
The controller for my project are only 3 controllers: HomeController, ProjectController, and RequirementController
My RouteConfig.cs is still in a default
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
How should I supposed to do to make the route change to username?

You need to add a route to cover the case that has a user name.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Username_Default",
url: "{username}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { username = new OwinUsernameConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
But for that to work right, you will either need to add a literal string to your URL to identify the segment as username (i.e. username-{username}\) or you will need to make a constraint that only allows the user names that are in the database. Here is an example of the latter:
using MvcUsernameInUrl.Models;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.Caching;
using System.Web.Routing;
namespace MvcUsernameInUrl
{
public class OwinUsernameConstraint : IRouteConstraint
{
private object synclock = new object();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (parameterName == null)
throw new ArgumentNullException("parameterName");
if (values == null)
throw new ArgumentNullException("values");
object value;
if (values.TryGetValue(parameterName, out value) && value != null)
{
string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
return this.GetUsernameList(httpContext).Contains(valueString);
}
return false;
}
private IEnumerable<string> GetUsernameList(HttpContextBase httpContext)
{
string key = "UsernameConstraint.GetUsernameList";
var usernames = httpContext.Cache[key];
if (usernames == null)
{
lock (synclock)
{
usernames = httpContext.Cache[key];
if (usernames == null)
{
// Retrieve the list of usernames from the database
using (var db = ApplicationDbContext.Create())
{
usernames = (from users in db.Users
select users.UserName).ToList();
}
httpContext.Cache.Insert(
key: key,
value: usernames,
dependencies: null,
absoluteExpiration: Cache.NoAbsoluteExpiration,
slidingExpiration: TimeSpan.FromSeconds(15),
priority: CacheItemPriority.NotRemovable,
onRemoveCallback: null);
}
}
}
return (IEnumerable<string>)usernames;
}
}
}
NOTE: I strongly recommend using caching for this as in the example, since route constraints run on every request and it is not good to hit the database on every request. The downside of this is that it takes up to 15 seconds for the username to become active after it is registered. You could potentially get around this by updating the cache (in a thread-safe way) when a new account is registered in addition to adding the record to the database, which would make it available immediately in the route constraint.
Then it is simply a matter of doing a 302 redirect when the user logs in. You could potentially do that in a global filter.
using System.Web;
using System.Web.Mvc;
namespace MvcUsernameInUrl
{
public class RedirectLoggedOnUserFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var routeValues = filterContext.RequestContext.RouteData.Values;
bool isLoggedIn = filterContext.HttpContext.User.Identity.IsAuthenticated;
bool requestHasUserName = routeValues.ContainsKey("username");
if (isLoggedIn && !requestHasUserName)
{
var userName = filterContext.HttpContext.User.Identity.Name;
// Add the user name as a route value
routeValues.Add("username", userName);
filterContext.Result = new RedirectToRouteResult(routeValues);
}
else if (!isLoggedIn && requestHasUserName)
{
// Remove the user name as a route value
routeValues.Remove("username");
filterContext.Result = new RedirectToRouteResult(routeValues);
}
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
// Do nothing
}
}
}
Usage
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new RedirectLoggedOnUserFilter());
filters.Add(new HandleErrorAttribute());
}
}
MVC will automatically reuse route values from the request when genrating URLs, so there is no need to change any of your ActionLinks to include username.
Here is a working demo on GitHub using MVC5, OWIN, and ASP.NET Identity.

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.

Constraints in route

So I have ASP.NET MVC application. I would like to configure its routes. Here is my RouteConfig's code:
public static void Register(RouteCollection routes, bool useAttributes = true)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
This route works fine. Besides I have an area in my application and try to configure its routes too. It is my area registration code:
public override void RegisterArea(AreaRegistrationContext context)
{
try
{
context.MapRoute(
name: "SiteSettings_Controller",
url: "SiteSettings/{controller}/{action}/{id}",
defaults: new {action = "Index", id = UrlParameter.Optional,
// here I tried to use #"(UserManagement|Tools|Settings)"
//as constraint but it takes no effect
constraints: new {controller = "UserManagement|Tools|Settings" }
);
}
catch (Exception e)
{
// here I get InvalidOperationException ""
}
}
I would like to restrict controllers in SiteSettingsArea's route but when I go to "localhost/SiteSettings/UserManagement" url I get InvalidOperationException with message "No route in the route table matches the supplied values". I believe that this url corresponds to SiteSettings_Controller route but obviously I am wrong. How could I limit controllers in the route properly?
If you search your codebase for SiteSettings_Controller does it appear anywhere else?
The below code certainly worked for me when I just tested it.
using System;
using System.Web.Mvc;
namespace WebApplication1.Areas.SiteSettings
{
public class SiteSettingsAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "SiteSettings";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
name: "SiteSettings_Controller",
url: "SiteSettings/{controller}/{action}/{id}",
defaults: new
{
action = "Index",
id = UrlParameter.Optional
},
constraints: new { controller = "UserManagement|Tools|Settings" }
);
}
}
}

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

How to set up specific routing in mvc3 web application

I have problem with routing. I have many pages on my site generated dynamically from database.
First thing which I want to accomplish is to route to these pages like that:
"How to repair a car"
www.EXAMPLE.com/How-to-repair-a-car
For now it works like that: www.EXAMPLE.com/Home/Index/How-to-repair-a-car
Secondly my default page have to be like that: www.EXAMPLE.com
On the Start Page will be news with pagging, so if someone click for instance in the "page 2" button, the address should looks: www.EXAMPLE.com/page =2
CONCLUSION:
default page -> www.EXAMPLE.com (with page = 0)
default page with specific page of news -> www.EXAMPLE.com/page=12
article page -> www.EXAMPLE.com/How-to-repair-car (without parameter 'page') routing sholud point to article or error404
PS: sorry for my english
Try to create route for articles in routing config, like this:
Routing config:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null, "{article}",
new {controller = "Home", action = "Article" });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
HomeController:
public class HomeController : Controller
{
public ActionResult Index(int? page)
{
var definedPage = page ?? 0;
ViewBag.page = "your page is " + definedPage;
return View();
}
public ActionResult Article(string article)
{
ViewBag.article = article;
return View();
}
}
/?page=10 - works
/How-to-repair-car - works
That approach excellent works.
Here is a basic routing example for www.example.com/How-to-repair-car
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace Tipser.Web
{
public class MyMvcApplication : HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"ArticleRoute",
"{articleName}",
new { Controller = "Home", Action = "Index", articleName = UrlParameter.Optional },
new { userFriendlyURL = new ArticleConstraint() }
);
}
public class ArticleConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var articleName = values["articleName"] as string;
//determine if there is a valid article
if (there_is_there_any_article_matching(articleName))
return true;
return false;
}
}
}
}

Change route on runtime in MVC4

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.

Categories

Resources